diff --git a/Casper.Network.SDK.Test/CESJsonSerializerTest.cs b/Casper.Network.SDK.Test/CESJsonSerializerTest.cs
new file mode 100644
index 0000000..7268ea3
--- /dev/null
+++ b/Casper.Network.SDK.Test/CESJsonSerializerTest.cs
@@ -0,0 +1,284 @@
+using System.Numerics;
+using System.Text.Json;
+using Casper.Network.SDK.CES;
+using Casper.Network.SDK.Types;
+using NUnit.Framework;
+using Org.BouncyCastle.Utilities.Encoders;
+
+namespace NetCasperTest
+{
+ ///
+ /// Verifies that and round-trip
+ /// correctly through System.Text.Json serialization/deserialization using the
+ /// [JsonPropertyName] and [JsonConstructor] annotations added to those classes.
+ ///
+ public class CESJsonSerializerTest
+ {
+ // ── shared fixtures (same as CESParserTest) ───────────────────────────
+ //
+ // Schema: one event "Transfer" with fields amount (U512) and sender (String)
+ private static readonly string SchemaHex = string.Concat(
+ "01000000",
+ "08000000",
+ "5472616e73666572",
+ "02000000",
+ "06000000", "616d6f756e74", "08",
+ "06000000", "73656e646572", "0a"
+ );
+
+ // event_Transfer(amount=100, sender="Alice")
+ private static readonly string EventHex = string.Concat(
+ "0e000000",
+ "6576656e745f5472616e73666572",
+ "01", "64",
+ "05000000", "416c696365"
+ );
+
+ private const string ContractHash = "hash-aabbccddaabbccddaabbccddaabbccddaabbccddaabbccddaabbccddaabbccdd";
+ private const string ContractPkgHash = "contract-package-hash-1122334411223344112233441122334411223344112233441122334411223344";
+
+ // ── helpers ───────────────────────────────────────────────────────────
+
+ private static CESContractSchema BuildAnnotatedSchema() =>
+ new CESContractSchema(CESContractSchema.ParseSchema(Hex.Decode(SchemaHex)).Events)
+ {
+ ContractHash = ContractHash,
+ ContractPackageHash = ContractPkgHash,
+ };
+
+ private static CESEvent BuildAnnotatedEvent()
+ {
+ var schema = BuildAnnotatedSchema();
+ var evt = CESEvent.ParseEvent(Hex.Decode(EventHex), schema);
+ return new CESEvent(evt.Name, evt.Fields)
+ {
+ ContractHash = ContractHash,
+ ContractPackageHash = ContractPkgHash,
+ TransformId = 3,
+ EventId = "42",
+ };
+ }
+
+ // ── CESEventSchemaField ───────────────────────────────────────────────
+
+ [Test]
+ public void SchemaField_RoundTrip_PreservesNameAndCLType()
+ {
+ var schema = CESContractSchema.ParseSchema(Hex.Decode(SchemaHex));
+ var field = schema.Events["Transfer"].Fields[0];
+
+ var json = JsonSerializer.Serialize(field);
+ var restored = JsonSerializer.Deserialize(json);
+
+ Assert.AreEqual(field.Name, restored.Name);
+ Assert.AreEqual(field.CLTypeInfo.Type, restored.CLTypeInfo.Type);
+ }
+
+ [Test]
+ public void SchemaField_Serializes_SnakeCasePropertyNames()
+ {
+ var schema = CESContractSchema.ParseSchema(Hex.Decode(SchemaHex));
+ var field = schema.Events["Transfer"].Fields[0];
+
+ var json = JsonSerializer.Serialize(field);
+
+ Assert.IsTrue(json.Contains("\"name\""));
+ Assert.IsTrue(json.Contains("\"cl_type\""));
+ }
+
+ // ── CESEventSchema ────────────────────────────────────────────────────
+
+ [Test]
+ public void EventSchema_RoundTrip_PreservesEventNameAndFields()
+ {
+ var schema = CESContractSchema.ParseSchema(Hex.Decode(SchemaHex));
+ var eventSchema = schema.Events["Transfer"];
+
+ var json = JsonSerializer.Serialize(eventSchema);
+ var restored = JsonSerializer.Deserialize(json);
+
+ Assert.AreEqual("Transfer", restored.EventName);
+ Assert.AreEqual(2, restored.Fields.Count);
+ Assert.AreEqual("amount", restored.Fields[0].Name);
+ Assert.AreEqual(CLType.U512, restored.Fields[0].CLTypeInfo.Type);
+ Assert.AreEqual("sender", restored.Fields[1].Name);
+ Assert.AreEqual(CLType.String, restored.Fields[1].CLTypeInfo.Type);
+ }
+
+ [Test]
+ public void EventSchema_Serializes_SnakeCasePropertyNames()
+ {
+ var schema = CESContractSchema.ParseSchema(Hex.Decode(SchemaHex));
+ var eventSchema = schema.Events["Transfer"];
+
+ var json = JsonSerializer.Serialize(eventSchema);
+
+ Assert.IsTrue(json.Contains("\"event_name\""));
+ Assert.IsTrue(json.Contains("\"fields\""));
+ }
+
+ // ── CESContractSchema ─────────────────────────────────────────────────
+
+ [Test]
+ public void ContractSchema_RoundTrip_PreservesEvents()
+ {
+ var schema = BuildAnnotatedSchema();
+
+ var json = JsonSerializer.Serialize(schema);
+ var restored = JsonSerializer.Deserialize(json);
+
+ Assert.IsNotNull(restored);
+ Assert.IsTrue(restored.TryGetEventSchema("Transfer", out var evt));
+ Assert.AreEqual("Transfer", evt.EventName);
+ Assert.AreEqual(2, evt.Fields.Count);
+ }
+
+ [Test]
+ public void ContractSchema_RoundTrip_PreservesContractHash()
+ {
+ var schema = BuildAnnotatedSchema();
+
+ var json = JsonSerializer.Serialize(schema);
+ var restored = JsonSerializer.Deserialize(json);
+
+ Assert.AreEqual(ContractHash, restored.ContractHash);
+ }
+
+ [Test]
+ public void ContractSchema_RoundTrip_PreservesContractPackageHash()
+ {
+ var schema = BuildAnnotatedSchema();
+
+ var json = JsonSerializer.Serialize(schema);
+ var restored = JsonSerializer.Deserialize(json);
+
+ Assert.AreEqual(ContractPkgHash, restored.ContractPackageHash);
+ }
+
+ [Test]
+ public void ContractSchema_Serializes_SnakeCasePropertyNames()
+ {
+ var schema = BuildAnnotatedSchema();
+
+ var json = JsonSerializer.Serialize(schema);
+
+ Assert.IsTrue(json.Contains("\"events\""));
+ Assert.IsTrue(json.Contains("\"contract_hash\""));
+ Assert.IsTrue(json.Contains("\"contract_package_hash\""));
+ }
+
+ [Test]
+ public void ContractSchema_WithNullHashes_RoundTripPreservesNulls()
+ {
+ var schema = CESContractSchema.ParseSchema(Hex.Decode(SchemaHex));
+
+ var json = JsonSerializer.Serialize(schema);
+ var restored = JsonSerializer.Deserialize(json);
+
+ Assert.IsNull(restored.ContractHash);
+ Assert.IsNull(restored.ContractPackageHash);
+ }
+
+ // ── CESEvent ──────────────────────────────────────────────────────────
+
+ [Test]
+ public void CESEvent_RoundTrip_PreservesName()
+ {
+ var evt = BuildAnnotatedEvent();
+
+ var json = JsonSerializer.Serialize(evt);
+ var restored = JsonSerializer.Deserialize(json);
+
+ Assert.AreEqual("Transfer", restored.Name);
+ }
+
+ [Test]
+ public void CESEvent_RoundTrip_PreservesFieldCount()
+ {
+ var evt = BuildAnnotatedEvent();
+
+ var json = JsonSerializer.Serialize(evt);
+ var restored = JsonSerializer.Deserialize(json);
+
+ Assert.AreEqual(2, restored.Fields.Count);
+ }
+
+ [Test]
+ public void CESEvent_RoundTrip_PreservesU512Field()
+ {
+ var evt = BuildAnnotatedEvent();
+
+ var json = JsonSerializer.Serialize(evt);
+ var restored = JsonSerializer.Deserialize(json);
+
+ var amount = restored["amount"];
+ Assert.IsNotNull(amount);
+ Assert.AreEqual(CLType.U512, amount.TypeInfo.Type);
+ Assert.AreEqual(new BigInteger(100), amount.ToBigInteger());
+ }
+
+ [Test]
+ public void CESEvent_RoundTrip_PreservesStringField()
+ {
+ var evt = BuildAnnotatedEvent();
+
+ var json = JsonSerializer.Serialize(evt);
+ var restored = JsonSerializer.Deserialize(json);
+
+ var sender = restored["sender"];
+ Assert.IsNotNull(sender);
+ Assert.AreEqual(CLType.String, sender.TypeInfo.Type);
+ Assert.AreEqual("Alice", sender.ToString());
+ }
+
+ [Test]
+ public void CESEvent_RoundTrip_PreservesContextFields()
+ {
+ var evt = BuildAnnotatedEvent();
+
+ var json = JsonSerializer.Serialize(evt);
+ var restored = JsonSerializer.Deserialize(json);
+
+ Assert.AreEqual(ContractHash, restored.ContractHash);
+ Assert.AreEqual(ContractPkgHash, restored.ContractPackageHash);
+ Assert.AreEqual(3, restored.TransformId);
+ Assert.AreEqual("42", restored.EventId);
+ }
+
+ [Test]
+ public void CESEvent_Serializes_SnakeCasePropertyNames()
+ {
+ var evt = BuildAnnotatedEvent();
+
+ var json = JsonSerializer.Serialize(evt);
+
+ Assert.IsTrue(json.Contains("\"name\""));
+ Assert.IsTrue(json.Contains("\"fields\""));
+ Assert.IsTrue(json.Contains("\"contract_hash\""));
+ Assert.IsTrue(json.Contains("\"contract_package_hash\""));
+ Assert.IsTrue(json.Contains("\"transform_id\""));
+ Assert.IsTrue(json.Contains("\"event_id\""));
+ }
+
+ [Test]
+ public void CESEvent_FieldsSerializeAsArrayOfPairs()
+ {
+ var evt = BuildAnnotatedEvent();
+
+ // Each field must serialize as ["fieldName", {CLValue}], not as an object.
+ var json = JsonSerializer.Serialize(evt);
+ using var doc = JsonDocument.Parse(json);
+ var fields = doc.RootElement.GetProperty("fields");
+
+ Assert.AreEqual(JsonValueKind.Array, fields.ValueKind);
+
+ var first = fields[0];
+ Assert.AreEqual(JsonValueKind.Array, first.ValueKind);
+ Assert.AreEqual("amount", first[0].GetString());
+ Assert.AreEqual(JsonValueKind.Object, first[1].ValueKind);
+
+ var second = fields[1];
+ Assert.AreEqual("sender", second[0].GetString());
+ }
+ }
+}
diff --git a/Casper.Network.SDK.Test/CESParserGetEventsTest.cs b/Casper.Network.SDK.Test/CESParserGetEventsTest.cs
new file mode 100644
index 0000000..1d62e00
--- /dev/null
+++ b/Casper.Network.SDK.Test/CESParserGetEventsTest.cs
@@ -0,0 +1,674 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Numerics;
+using Casper.Network.SDK.ByteSerializers;
+using Casper.Network.SDK.CES;
+using Casper.Network.SDK.Types;
+using NUnit.Framework;
+using Org.BouncyCastle.Utilities.Encoders;
+
+namespace NetCasperTest
+{
+ [TestFixture]
+ public class CESParserGetEventsTest
+ {
+ // ─── shared schema / event fixtures ────────────────────────────────────
+ //
+ // Schema: one event "Transfer" with fields amount (U512) and sender (String)
+
+ private static readonly string SchemaHex = string.Concat(
+ "01000000", // 1 event
+ "08000000", // len("Transfer") = 8
+ "5472616e73666572", // "Transfer"
+ "02000000", // 2 fields
+ "06000000", // len("amount") = 6
+ "616d6f756e74", // "amount"
+ "08", // CLType.U512
+ "06000000", // len("sender") = 6
+ "73656e646572", // "sender"
+ "0a" // CLType.String
+ );
+
+ // event_Transfer(amount=100, sender="Alice")
+ // U512(100) → length byte 0x01 + value 0x64
+ // String → 4-byte LE length + UTF-8
+ private static readonly string EventHex = string.Concat(
+ "0e000000", // CLString len = 14
+ "6576656e745f5472616e73666572", // "event_Transfer"
+ "01", "64", // U512(100)
+ "05000000", "416c696365" // String("Alice")
+ );
+
+ // event_Transfer(amount=200, sender="Bob")
+ private static readonly string EventBobHex = string.Concat(
+ "0e000000", // CLString len = 14
+ "6576656e745f5472616e73666572", // "event_Transfer"
+ "01", "c8", // U512(200)
+ "03000000", "426f62" // String("Bob")
+ );
+
+ // Schema for a second contract: one event "Mint" with field recipient (U512)
+ private static readonly string SchemaHex2 = string.Concat(
+ "01000000", // 1 event
+ "04000000", // len("Mint") = 4
+ "4d696e74", // "Mint"
+ "01000000", // 1 field
+ "09000000", // len("recipient") = 9
+ "726563697069656e74", // "recipient"
+ "08" // CLType.U512
+ );
+
+ // event_Mint(recipient=200)
+ private static readonly string EventMintHex = string.Concat(
+ "0a000000", // CLString len = 10
+ "6576656e745f4d696e74", // "event_Mint"
+ "01", "c8" // U512(200)
+ );
+
+ // 32-byte seeds (hex-encoded, 64 chars each)
+ private const string SeedHex =
+ "aabbccddaabbccddaabbccddaabbccddaabbccddaabbccddaabbccddaabbccdd";
+
+ private const string SeedHex2 =
+ "1122334411223344112233441122334411223344112233441122334411223344";
+
+ // 32-byte dictionary global-state key hashes (different from the seeds)
+ private const string DictKeyHex =
+ "deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef";
+
+ private const string DictKeyHex2 =
+ "cafebabecafebabecafebabecafebabecafebabecafebabecafebabecafebabe";
+
+ // ─── helpers ───────────────────────────────────────────────────────────
+
+ private static CESContractSchema ParseSchema(string schemaHex) =>
+ CESContractSchema.ParseSchema(Hex.Decode(schemaHex));
+
+ private static CESContractSchema MakeWatchedSchema(string schemaHex, string seedHex) =>
+ new CESContractSchema(CESContractSchema.ParseSchema(Hex.Decode(schemaHex)).Events)
+ {
+ EventsURef = new URef($"uref-{seedHex}-000")
+ };
+
+ ///
+ /// Builds the raw bytes that expects.
+ /// Format:
+ /// [4B outer length] [CLValue blob (length + data + type)] [4B seed len] [seed bytes]
+ /// [4B key len] [key bytes]
+ /// The event payload is wrapped as a CLValue.ByteArray so that
+ /// dict.Value equals the original bytes.
+ ///
+ private static byte[] BuildDictBytes(byte[] eventPayload, byte[] seedBytes, string itemKey)
+ {
+ // CLValueByteSerializer.ToBytes produces the exact bytes that FromReader consumes:
+ // [4B data length] [N data bytes] [type bytes]
+ var clValueBlob = new CLValueByteSerializer().ToBytes(CLValue.ByteArray(eventPayload));
+
+ using var ms = new MemoryStream();
+
+ // outer length = total bytes that FromReader() will read
+ WriteU32LE(ms, (uint)clValueBlob.Length);
+ ms.Write(clValueBlob, 0, clValueBlob.Length);
+
+ // seed: 4-byte LE count + raw bytes
+ WriteU32LE(ms, (uint)seedBytes.Length);
+ ms.Write(seedBytes, 0, seedBytes.Length);
+
+ // item key: 4-byte LE count + UTF-8
+ var keyBytes = System.Text.Encoding.UTF8.GetBytes(itemKey);
+ WriteU32LE(ms, (uint)keyBytes.Length);
+ ms.Write(keyBytes, 0, keyBytes.Length);
+
+ return ms.ToArray();
+ }
+
+ private static void WriteU32LE(MemoryStream ms, uint value)
+ {
+ var bytes = BitConverter.GetBytes(value);
+ if (!BitConverter.IsLittleEndian) Array.Reverse(bytes);
+ ms.Write(bytes, 0, bytes.Length);
+ }
+
+ ///
+ /// Creates a that looks exactly like a CES dictionary write.
+ ///
+ private static Transform MakeCESTransform(
+ string dictKeyHex, byte[] eventPayload, byte[] seedBytes, string itemKey = "1")
+ {
+ var clValue = new CLValue(
+ BuildDictBytes(eventPayload, seedBytes, itemKey),
+ new CLTypeInfo(CLType.Any));
+
+ return new Transform
+ {
+ Key = GlobalStateKey.FromString($"dictionary-{dictKeyHex}"),
+ Kind = new WriteTransformKind { Value = new StoredValue { CLValue = clValue } }
+ };
+ }
+
+ // ─── guard tests ───────────────────────────────────────────────────────
+
+ [Test]
+ public void GetEvents_NullTransforms_ThrowsArgumentNullException()
+ {
+ Assert.Throws(() =>
+ CESParser.GetEvents(null, new List()));
+ }
+
+ [Test]
+ public void GetEvents_NullWatchedContracts_ThrowsArgumentNullException()
+ {
+ Assert.Throws(() =>
+ CESParser.GetEvents(new List(), null));
+ }
+
+ // ─── empty inputs ──────────────────────────────────────────────────────
+
+ [Test]
+ public void GetEvents_EmptyTransforms_ReturnsEmptyList()
+ {
+ var result = CESParser.GetEvents(
+ new List(),
+ new List { MakeWatchedSchema(SchemaHex, SeedHex) });
+
+ Assert.IsNotNull(result);
+ Assert.AreEqual(0, result.Count);
+ }
+
+ [Test]
+ public void GetEvents_EmptyWatchedContracts_ReturnsEmptyList()
+ {
+ var transform = MakeCESTransform(DictKeyHex, Hex.Decode(EventHex), Hex.Decode(SeedHex));
+
+ var result = CESParser.GetEvents(
+ new List { transform },
+ new List());
+
+ Assert.AreEqual(0, result.Count);
+ }
+
+ // ─── filter: key type ──────────────────────────────────────────────────
+
+ [Test]
+ public void GetEvents_AccountHashKeyTransform_IsSkipped()
+ {
+ var transform = new Transform
+ {
+ Key = GlobalStateKey.FromString(
+ "account-hash-989ca079a5e446071866331468ab949483162588d57ec13ba6bb051f1e15f8b7"),
+ Kind = new WriteTransformKind
+ {
+ Value = new StoredValue
+ {
+ CLValue = new CLValue(
+ BuildDictBytes(Hex.Decode(EventHex), Hex.Decode(SeedHex), "1"),
+ new CLTypeInfo(CLType.Any))
+ }
+ }
+ };
+
+ var result = CESParser.GetEvents(
+ new List { transform },
+ new List { MakeWatchedSchema(SchemaHex, SeedHex) });
+
+ Assert.AreEqual(0, result.Count);
+ }
+
+ [Test]
+ public void GetEvents_HashKeyTransform_IsSkipped()
+ {
+ var transform = new Transform
+ {
+ Key = GlobalStateKey.FromString(
+ "hash-989ca079a5e446071866331468ab949483162588d57ec13ba6bb051f1e15f8b7"),
+ Kind = new WriteTransformKind
+ {
+ Value = new StoredValue
+ {
+ CLValue = new CLValue(
+ BuildDictBytes(Hex.Decode(EventHex), Hex.Decode(SeedHex), "1"),
+ new CLTypeInfo(CLType.Any))
+ }
+ }
+ };
+
+ var result = CESParser.GetEvents(
+ new List { transform },
+ new List { MakeWatchedSchema(SchemaHex, SeedHex) });
+
+ Assert.AreEqual(0, result.Count);
+ }
+
+ // ─── filter: transform kind ────────────────────────────────────────────
+
+ [Test]
+ public void GetEvents_IdentityTransformOnDictionaryKey_IsSkipped()
+ {
+ var transform = new Transform
+ {
+ Key = GlobalStateKey.FromString($"dictionary-{DictKeyHex}"),
+ Kind = new IdentityTransformKind()
+ };
+
+ var result = CESParser.GetEvents(
+ new List { transform },
+ new List { MakeWatchedSchema(SchemaHex, SeedHex) });
+
+ Assert.AreEqual(0, result.Count);
+ }
+
+ [Test]
+ public void GetEvents_AddInt32TransformOnDictionaryKey_IsSkipped()
+ {
+ var transform = new Transform
+ {
+ Key = GlobalStateKey.FromString($"dictionary-{DictKeyHex}"),
+ Kind = new AddInt32TransformKind { Value = 42 }
+ };
+
+ var result = CESParser.GetEvents(
+ new List { transform },
+ new List { MakeWatchedSchema(SchemaHex, SeedHex) });
+
+ Assert.AreEqual(0, result.Count);
+ }
+
+ [Test]
+ public void GetEvents_WriteWithoutCLValue_IsSkipped()
+ {
+ // Write of an Account (or any other StoredValue without CLValue set)
+ var transform = new Transform
+ {
+ Key = GlobalStateKey.FromString($"dictionary-{DictKeyHex}"),
+ Kind = new WriteTransformKind { Value = new StoredValue() }
+ };
+
+ var result = CESParser.GetEvents(
+ new List { transform },
+ new List { MakeWatchedSchema(SchemaHex, SeedHex) });
+
+ Assert.AreEqual(0, result.Count);
+ }
+
+ // ─── filter: seed matching ─────────────────────────────────────────────
+
+ [Test]
+ public void GetEvents_DictionaryWriteWithUnwatchedSeed_IsSkipped()
+ {
+ const string differentSeed =
+ "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef";
+
+ var transform = MakeCESTransform(
+ DictKeyHex, Hex.Decode(EventHex), Hex.Decode(differentSeed));
+
+ var result = CESParser.GetEvents(
+ new List { transform },
+ new List { MakeWatchedSchema(SchemaHex, SeedHex) });
+
+ Assert.AreEqual(0, result.Count);
+ }
+
+ [Test]
+ public void GetEvents_SeedMatchIgnoresAccessRights()
+ {
+ // CLValueDictionary.Parse always creates the seed with AccessRights.NONE.
+ // The watched-contract key is the plain 32-byte hash (no access rights).
+ // GetEvents must still find the match.
+ var transform = MakeCESTransform(DictKeyHex, Hex.Decode(EventHex), Hex.Decode(SeedHex));
+ var watched = new List { MakeWatchedSchema(SchemaHex, SeedHex) };
+
+ var result = CESParser.GetEvents(new List { transform }, watched);
+
+ Assert.AreEqual(1, result.Count);
+ }
+
+ // ─── happy-path: single event ──────────────────────────────────────────
+
+ [Test]
+ public void GetEvents_SingleMatchingTransform_ReturnsSingleEvent()
+ {
+ var transform = MakeCESTransform(DictKeyHex, Hex.Decode(EventHex), Hex.Decode(SeedHex));
+ var watched = new List { MakeWatchedSchema(SchemaHex, SeedHex) };
+
+ var result = CESParser.GetEvents(new List { transform }, watched);
+
+ Assert.AreEqual(1, result.Count);
+ }
+
+ [Test]
+ public void GetEvents_SingleMatchingTransform_CorrectEventName()
+ {
+ var transform = MakeCESTransform(DictKeyHex, Hex.Decode(EventHex), Hex.Decode(SeedHex));
+ var watched = new List { MakeWatchedSchema(SchemaHex, SeedHex) };
+
+ var result = CESParser.GetEvents(new List { transform }, watched);
+
+ Assert.AreEqual("Transfer", result[0].Name);
+ }
+
+ [Test]
+ public void GetEvents_SingleMatchingTransform_CorrectU512Field()
+ {
+ var transform = MakeCESTransform(DictKeyHex, Hex.Decode(EventHex), Hex.Decode(SeedHex));
+ var watched = new List { MakeWatchedSchema(SchemaHex, SeedHex) };
+
+ var result = CESParser.GetEvents(new List { transform }, watched);
+
+ Assert.AreEqual(new BigInteger(100), result[0]["amount"].ToBigInteger());
+ }
+
+ [Test]
+ public void GetEvents_SingleMatchingTransform_CorrectStringField()
+ {
+ var transform = MakeCESTransform(DictKeyHex, Hex.Decode(EventHex), Hex.Decode(SeedHex));
+ var watched = new List { MakeWatchedSchema(SchemaHex, SeedHex) };
+
+ var result = CESParser.GetEvents(new List { transform }, watched);
+
+ Assert.AreEqual("Alice", result[0]["sender"].ToString());
+ }
+
+ // ─── graceful skipping ─────────────────────────────────────────────────
+
+ [Test]
+ public void GetEvents_EventNameAbsentFromSchema_IsSkipped()
+ {
+ // EventMintHex has name "event_Mint", but the watched schema only knows "Transfer".
+ // ParseEvent throws KeyNotFoundException → GetEvents silently skips.
+ var transform = MakeCESTransform(DictKeyHex, Hex.Decode(EventMintHex), Hex.Decode(SeedHex));
+ var watched = new List { MakeWatchedSchema(SchemaHex, SeedHex) };
+
+ var result = CESParser.GetEvents(new List { transform }, watched);
+
+ Assert.AreEqual(0, result.Count);
+ }
+
+ [Test]
+ public void GetEvents_UnparsableDictionaryBytes_IsSkipped()
+ {
+ // Garbage bytes that CLValueDictionary.Parse will fail on → skipped, no exception.
+ var clValue = new CLValue(new byte[] { 0xDE, 0xAD, 0xBE, 0xEF }, new CLTypeInfo(CLType.Any));
+ var transform = new Transform
+ {
+ Key = GlobalStateKey.FromString($"dictionary-{DictKeyHex}"),
+ Kind = new WriteTransformKind { Value = new StoredValue { CLValue = clValue } }
+ };
+ var watched = new List { MakeWatchedSchema(SchemaHex, SeedHex) };
+
+ Assert.DoesNotThrow(() =>
+ {
+ var result = CESParser.GetEvents(new List { transform }, watched);
+ Assert.AreEqual(0, result.Count);
+ });
+ }
+
+ // ─── multiple events ───────────────────────────────────────────────────
+
+ [Test]
+ public void GetEvents_TwoEventsFromSameContract_ReturnsBoth()
+ {
+ // Two dictionary writes with the same seed but different item keys
+ var t1 = MakeCESTransform(DictKeyHex, Hex.Decode(EventHex), Hex.Decode(SeedHex), "1");
+ var t2 = MakeCESTransform(DictKeyHex2, Hex.Decode(EventBobHex), Hex.Decode(SeedHex), "2");
+ var watched = new List { MakeWatchedSchema(SchemaHex, SeedHex) };
+
+ var result = CESParser.GetEvents(new List { t1, t2 }, watched);
+
+ Assert.AreEqual(2, result.Count);
+ }
+
+ [Test]
+ public void GetEvents_TwoEventsFromDifferentContracts_ReturnsBoth()
+ {
+ var t1 = MakeCESTransform(DictKeyHex, Hex.Decode(EventHex), Hex.Decode(SeedHex));
+ var t2 = MakeCESTransform(DictKeyHex2, Hex.Decode(EventMintHex), Hex.Decode(SeedHex2));
+ var watched = new List
+ {
+ MakeWatchedSchema(SchemaHex, SeedHex),
+ MakeWatchedSchema(SchemaHex2, SeedHex2),
+ };
+
+ var result = CESParser.GetEvents(new List { t1, t2 }, watched);
+
+ Assert.AreEqual(2, result.Count);
+ }
+
+ [Test]
+ public void GetEvents_TwoEventsFromDifferentContracts_CorrectFieldValues()
+ {
+ var t1 = MakeCESTransform(DictKeyHex, Hex.Decode(EventHex), Hex.Decode(SeedHex));
+ var t2 = MakeCESTransform(DictKeyHex2, Hex.Decode(EventMintHex), Hex.Decode(SeedHex2));
+ var watched = new List
+ {
+ MakeWatchedSchema(SchemaHex, SeedHex),
+ MakeWatchedSchema(SchemaHex2, SeedHex2),
+ };
+
+ var result = CESParser.GetEvents(new List { t1, t2 }, watched);
+
+ // first event: Transfer – check both fields
+ Assert.AreEqual("Transfer", result[0].Name);
+ Assert.AreEqual(new BigInteger(100), result[0]["amount"].ToBigInteger());
+ Assert.AreEqual("Alice", result[0]["sender"].ToString());
+
+ // second event: Mint – check recipient
+ Assert.AreEqual("Mint", result[1].Name);
+ Assert.AreEqual(new BigInteger(200), result[1]["recipient"].ToBigInteger());
+ }
+
+ // ─── ordering and mixed transforms ────────────────────────────────────
+
+ [Test]
+ public void GetEvents_OrderPreserved_MatchesTransformOrder()
+ {
+ var watched = new List { MakeWatchedSchema(SchemaHex, SeedHex) };
+
+ // Alice (item "1") comes before Bob (item "2") in the transform list
+ var t1 = MakeCESTransform(DictKeyHex, Hex.Decode(EventHex), Hex.Decode(SeedHex), "1");
+ var t2 = MakeCESTransform(DictKeyHex2, Hex.Decode(EventBobHex), Hex.Decode(SeedHex), "2");
+
+ var result = CESParser.GetEvents(new List { t1, t2 }, watched);
+
+ Assert.AreEqual(2, result.Count);
+ Assert.AreEqual("Alice", result[0]["sender"].ToString());
+ Assert.AreEqual("Bob", result[1]["sender"].ToString());
+ }
+
+ [Test]
+ public void GetEvents_MixedTransforms_OnlyMatchingEventsReturned()
+ {
+ // t1: Identity on a dictionary key → skipped (wrong kind)
+ var t1 = new Transform
+ {
+ Key = GlobalStateKey.FromString($"dictionary-{DictKeyHex}"),
+ Kind = new IdentityTransformKind()
+ };
+ // t2: CES event from watched contract → included
+ var t2 = MakeCESTransform(DictKeyHex, Hex.Decode(EventHex), Hex.Decode(SeedHex));
+ // t3: Write of CLValue to an account-hash key → skipped (wrong key type)
+ var t3 = new Transform
+ {
+ Key = GlobalStateKey.FromString(
+ "account-hash-989ca079a5e446071866331468ab949483162588d57ec13ba6bb051f1e15f8b7"),
+ Kind = new WriteTransformKind { Value = new StoredValue { CLValue = CLValue.U32(1) } }
+ };
+ // t4: dictionary write with an unwatched seed → skipped
+ var t4 = MakeCESTransform(DictKeyHex2, Hex.Decode(EventHex), Hex.Decode(SeedHex2));
+
+ var watched = new List { MakeWatchedSchema(SchemaHex, SeedHex) };
+
+ var result = CESParser.GetEvents(new List { t1, t2, t3, t4 }, watched);
+
+ Assert.AreEqual(1, result.Count);
+ Assert.AreEqual("Transfer", result[0].Name);
+ }
+
+ [Test]
+ public void GetEvents_OneValidOneBadParseable_OnlyValidEventReturned()
+ {
+ // t1: valid CES event
+ var t1 = MakeCESTransform(DictKeyHex, Hex.Decode(EventHex), Hex.Decode(SeedHex));
+ // t2: garbled CLValue bytes that will fail CLValueDictionary.Parse → skipped
+ var t2 = new Transform
+ {
+ Key = GlobalStateKey.FromString($"dictionary-{DictKeyHex2}"),
+ Kind = new WriteTransformKind
+ {
+ Value = new StoredValue
+ {
+ CLValue = new CLValue(new byte[] { 0x00, 0x01 }, new CLTypeInfo(CLType.Any))
+ }
+ }
+ };
+ var watched = new List { MakeWatchedSchema(SchemaHex, SeedHex) };
+
+ var result = CESParser.GetEvents(new List { t1, t2 }, watched);
+
+ Assert.AreEqual(1, result.Count);
+ Assert.AreEqual("Transfer", result[0].Name);
+ }
+
+ // ─── TransformId and EventId ───────────────────────────────────────────
+
+ [Test]
+ public void GetEvents_SingleEvent_TransformIdIsIndexInList()
+ {
+ // t0 is a non-matching transform at index 0; t1 is the CES event at index 1.
+ var t0 = new Transform
+ {
+ Key = GlobalStateKey.FromString(
+ "account-hash-989ca079a5e446071866331468ab949483162588d57ec13ba6bb051f1e15f8b7"),
+ Kind = new IdentityTransformKind()
+ };
+ var t1 = MakeCESTransform(DictKeyHex, Hex.Decode(EventHex), Hex.Decode(SeedHex), "7");
+ var watched = new List { MakeWatchedSchema(SchemaHex, SeedHex) };
+
+ var result = CESParser.GetEvents(new List { t0, t1 }, watched);
+
+ Assert.AreEqual(1, result.Count);
+ Assert.AreEqual(1, result[0].TransformId); // index 1 in the list
+ }
+
+ [Test]
+ public void GetEvents_SingleEvent_EventIdMatchesDictItemKey()
+ {
+ var t0 = MakeCESTransform(DictKeyHex, Hex.Decode(EventHex), Hex.Decode(SeedHex), itemKey: "42");
+ var watched = new List { MakeWatchedSchema(SchemaHex, SeedHex) };
+
+ var result = CESParser.GetEvents(new List { t0 }, watched);
+
+ Assert.AreEqual("42", result[0].EventId);
+ }
+
+ [Test]
+ public void GetEvents_MultipleEvents_TransformIdsAreDistinct()
+ {
+ var t0 = MakeCESTransform(DictKeyHex, Hex.Decode(EventHex), Hex.Decode(SeedHex), "1");
+ var t1 = MakeCESTransform(DictKeyHex2, Hex.Decode(EventBobHex), Hex.Decode(SeedHex), "2");
+ var watched = new List { MakeWatchedSchema(SchemaHex, SeedHex) };
+
+ var result = CESParser.GetEvents(new List { t0, t1 }, watched);
+
+ Assert.AreEqual(2, result.Count);
+ Assert.AreEqual(0, result[0].TransformId);
+ Assert.AreEqual(1, result[1].TransformId);
+ }
+
+ [Test]
+ public void GetEvents_MultipleEvents_EventIdsMatchItemKeys()
+ {
+ var t0 = MakeCESTransform(DictKeyHex, Hex.Decode(EventHex), Hex.Decode(SeedHex), "100");
+ var t1 = MakeCESTransform(DictKeyHex2, Hex.Decode(EventBobHex), Hex.Decode(SeedHex), "101");
+ var watched = new List { MakeWatchedSchema(SchemaHex, SeedHex) };
+
+ var result = CESParser.GetEvents(new List { t0, t1 }, watched);
+
+ Assert.AreEqual("100", result[0].EventId);
+ Assert.AreEqual("101", result[1].EventId);
+ }
+
+ // ─── ContractHash / ContractPackageHash propagation ───────────────────
+
+ [Test]
+ public void GetEvents_SchemaWithContractHash_EventCarriesContractHash()
+ {
+ const string contractHash = "hash-aabbccddaabbccddaabbccddaabbccddaabbccddaabbccddaabbccddaabbccdd";
+ var schema = new CESContractSchema(CESContractSchema.ParseSchema(Hex.Decode(SchemaHex)).Events)
+ {
+ ContractHash = contractHash,
+ EventsURef = new URef($"uref-{SeedHex}-000"),
+ };
+ var transform = MakeCESTransform(DictKeyHex, Hex.Decode(EventHex), Hex.Decode(SeedHex));
+ var watched = new List { schema };
+
+ var result = CESParser.GetEvents(new List { transform }, watched);
+
+ Assert.AreEqual(contractHash, result[0].ContractHash);
+ }
+
+ [Test]
+ public void GetEvents_SchemaWithContractPackageHash_EventCarriesContractPackageHash()
+ {
+ const string pkgHash = "contract-package-hash-1122334411223344112233441122334411223344112233441122334411223344";
+ var schema = new CESContractSchema(CESContractSchema.ParseSchema(Hex.Decode(SchemaHex)).Events)
+ {
+ ContractPackageHash = pkgHash,
+ EventsURef = new URef($"uref-{SeedHex}-000"),
+ };
+ var transform = MakeCESTransform(DictKeyHex, Hex.Decode(EventHex), Hex.Decode(SeedHex));
+ var watched = new List { schema };
+
+ var result = CESParser.GetEvents(new List { transform }, watched);
+
+ Assert.AreEqual(pkgHash, result[0].ContractPackageHash);
+ }
+
+ [Test]
+ public void GetEvents_SchemaWithoutHashes_EventHashesAreNull()
+ {
+ var watched = new List { MakeWatchedSchema(SchemaHex, SeedHex) };
+ var transform = MakeCESTransform(DictKeyHex, Hex.Decode(EventHex), Hex.Decode(SeedHex));
+
+ var result = CESParser.GetEvents(new List { transform }, watched);
+
+ Assert.IsNull(result[0].ContractHash);
+ Assert.IsNull(result[0].ContractPackageHash);
+ }
+
+ [Test]
+ public void GetEvents_TwoDifferentContracts_EventsCarryCorrectHashes()
+ {
+ const string hash1 = "hash-aabbccddaabbccddaabbccddaabbccddaabbccddaabbccddaabbccddaabbccdd";
+ const string hash2 = "hash-1122334411223344112233441122334411223344112233441122334411223344";
+
+ var schema1 = new CESContractSchema(CESContractSchema.ParseSchema(Hex.Decode(SchemaHex)).Events)
+ { ContractHash = hash1, EventsURef = new URef($"uref-{SeedHex}-000") };
+ var schema2 = new CESContractSchema(CESContractSchema.ParseSchema(Hex.Decode(SchemaHex2)).Events)
+ { ContractHash = hash2, EventsURef = new URef($"uref-{SeedHex2}-000") };
+
+ var t1 = MakeCESTransform(DictKeyHex, Hex.Decode(EventHex), Hex.Decode(SeedHex));
+ var t2 = MakeCESTransform(DictKeyHex2, Hex.Decode(EventMintHex), Hex.Decode(SeedHex2));
+ var watched = new List { schema1, schema2 };
+
+ var result = CESParser.GetEvents(new List { t1, t2 }, watched);
+
+ Assert.AreEqual(2, result.Count);
+ Assert.AreEqual(hash1, result[0].ContractHash);
+ Assert.AreEqual(hash2, result[1].ContractHash);
+ }
+
+ // ─── ParseEvent in isolation ───────────────────────────────────────────
+
+ [Test]
+ public void ParseEvent_InIsolation_TransformIdIsZeroAndEventIdIsNull()
+ {
+ var schema = ParseSchema(SchemaHex);
+ var evt = CESEvent.ParseEvent(Hex.Decode(EventHex), schema);
+
+ Assert.AreEqual(0, evt.TransformId);
+ Assert.IsNull(evt.TransformKey);
+ Assert.IsNull(evt.EventId);
+ }
+ }
+}
diff --git a/Casper.Network.SDK.Test/CESParserTest.cs b/Casper.Network.SDK.Test/CESParserTest.cs
new file mode 100644
index 0000000..a2605a7
--- /dev/null
+++ b/Casper.Network.SDK.Test/CESParserTest.cs
@@ -0,0 +1,235 @@
+using System;
+using System.Collections.Generic;
+using System.Numerics;
+using System.Runtime;
+using Casper.Network.SDK.CES;
+using Casper.Network.SDK.Types;
+using NUnit.Framework;
+using Org.BouncyCastle.Utilities.Encoders;
+
+namespace NetCasperTest
+{
+ [TestFixture]
+ public class CESParserTest
+ {
+ // Schema binary for one event "Transfer" with two fields:
+ // amount (CLType.U512 = 0x08)
+ // sender (CLType.String = 0x0a)
+ private static readonly string SchemaHex = string.Concat(
+ "01000000", // 1 event
+ "08000000", // len("Transfer") = 8
+ "5472616e73666572", // "Transfer"
+ "02000000", // 2 fields
+ "06000000", // len("amount") = 6
+ "616d6f756e74", // "amount"
+ "08", // CLType.U512
+ "06000000", // len("sender") = 6
+ "73656e646572", // "sender"
+ "0a" // CLType.String
+ );
+
+ // Event binary for event_Transfer(amount=100, sender="Alice")
+ // U512(100) serializes as: 0x01 (length byte) + 0x64 (100 in one byte)
+ // String("Alice") serializes as: 0x05000000 (length) + 0x416c696365
+ private static readonly string EventHex = string.Concat(
+ "0e000000", // len("event_Transfer") = 14
+ "6576656e745f5472616e73666572", // "event_Transfer"
+ "01", // U512 length = 1 byte
+ "64", // U512 value = 100 (0x64)
+ "05000000", // len("Alice") = 5
+ "416c696365" // "Alice"
+ );
+
+ [Test]
+ public void ParseSchema_SingleEvent_CorrectEventName()
+ {
+ var schema = CESContractSchema.ParseSchema(Hex.Decode(SchemaHex));
+
+ Assert.IsTrue(schema.TryGetEventSchema("Transfer", out var eventSchema));
+ Assert.AreEqual("Transfer", eventSchema.EventName);
+ }
+
+ [Test]
+ public void ParseSchema_SingleEvent_CorrectFieldCount()
+ {
+ var schema = CESContractSchema.ParseSchema(Hex.Decode(SchemaHex));
+
+ schema.TryGetEventSchema("Transfer", out var eventSchema);
+ Assert.AreEqual(2, eventSchema.Fields.Count);
+ }
+
+ [Test]
+ public void ParseSchema_SingleEvent_CorrectFieldDefinitions()
+ {
+ var schema = CESContractSchema.ParseSchema(Hex.Decode(SchemaHex));
+
+ schema.TryGetEventSchema("Transfer", out var eventSchema);
+
+ Assert.AreEqual("amount", eventSchema.Fields[0].Name);
+ Assert.AreEqual(CLType.U512, eventSchema.Fields[0].CLTypeInfo.Type);
+
+ Assert.AreEqual("sender", eventSchema.Fields[1].Name);
+ Assert.AreEqual(CLType.String, eventSchema.Fields[1].CLTypeInfo.Type);
+ }
+
+ [Test]
+ public void ParseSchema_UnknownEvent_ReturnsFalse()
+ {
+ var schema = CESContractSchema.ParseSchema(Hex.Decode(SchemaHex));
+
+ Assert.IsFalse(schema.TryGetEventSchema("Mint", out _));
+ }
+
+ [Test]
+ public void ParseEvent_CorrectEventName()
+ {
+ var schema = CESContractSchema.ParseSchema(Hex.Decode(SchemaHex));
+ var evt = CESEvent.ParseEvent(Hex.Decode(EventHex), schema);
+
+ Assert.AreEqual("Transfer", evt.Name);
+ }
+
+ [Test]
+ public void ParseEvent_CorrectFieldCount()
+ {
+ var schema = CESContractSchema.ParseSchema(Hex.Decode(SchemaHex));
+ var evt = CESEvent.ParseEvent(Hex.Decode(EventHex), schema);
+
+ Assert.AreEqual(2, evt.Fields.Count);
+ }
+
+ [Test]
+ public void ParseEvent_U512FieldCorrectValue()
+ {
+ var schema = CESContractSchema.ParseSchema(Hex.Decode(SchemaHex));
+ var evt = CESEvent.ParseEvent(Hex.Decode(EventHex), schema);
+
+ var amountField = evt["amount"];
+ Assert.IsNotNull(amountField);
+ Assert.AreEqual(CLType.U512, amountField.TypeInfo.Type);
+ Assert.AreEqual(new BigInteger(100), amountField.ToBigInteger());
+ }
+
+ [Test]
+ public void ParseEvent_StringFieldCorrectValue()
+ {
+ var schema = CESContractSchema.ParseSchema(Hex.Decode(SchemaHex));
+ var evt = CESEvent.ParseEvent(Hex.Decode(EventHex), schema);
+
+ var senderField = evt["sender"];
+ Assert.IsNotNull(senderField);
+ Assert.AreEqual(CLType.String, senderField.TypeInfo.Type);
+ Assert.AreEqual("Alice", senderField.ToString());
+ }
+
+ [Test]
+ public void ParseEvent_IndexerReturnsNullForMissingField()
+ {
+ var schema = CESContractSchema.ParseSchema(Hex.Decode(SchemaHex));
+ var evt = CESEvent.ParseEvent(Hex.Decode(EventHex), schema);
+
+ Assert.IsNull(evt["nonexistent"]);
+ }
+
+ [Test]
+ public void ParseEvent_UnknownEventName_ThrowsKeyNotFoundException()
+ {
+ var schema = CESContractSchema.ParseSchema(Hex.Decode(SchemaHex));
+
+ // Build event bytes with an event name ("event_Mint") not present in the schema.
+ var mintEventHex = string.Concat(
+ "0a000000", // len("event_Mint") = 10
+ "6576656e745f4d696e74" // "event_Mint"
+ );
+
+ Assert.Throws(
+ () => CESEvent.ParseEvent(Hex.Decode(mintEventHex), schema));
+ }
+
+ [Test]
+ public void ParseSchema_OptionType_CorrectInnerType()
+ {
+ // Schema with one event "Approve" and one Option(PublicKey) field "operator"
+ // CLType.Option = 0x0d, CLType.PublicKey = 0x16
+ var schemaHex = string.Concat(
+ "01000000", // 1 event
+ "07000000", // len("Approve") = 7
+ "417070726f7665", // "Approve"
+ "01000000", // 1 field
+ "08000000", // len("operator") = 8
+ "6f70657261746f72", // "operator"
+ "0d", // CLType.Option
+ "16" // CLType.PublicKey
+ );
+
+ var schema = CESContractSchema.ParseSchema(Hex.Decode(schemaHex));
+
+ schema.TryGetEventSchema("Approve", out var eventSchema);
+ Assert.AreEqual("operator", eventSchema.Fields[0].Name);
+ Assert.AreEqual(CLType.Option, eventSchema.Fields[0].CLTypeInfo.Type);
+
+ var optionType = (CLOptionTypeInfo)eventSchema.Fields[0].CLTypeInfo;
+ Assert.AreEqual(CLType.PublicKey, optionType.OptionType.Type);
+ }
+
+ [Test]
+ public void ParseSchema_MapType_CorrectKeyAndValueTypes()
+ {
+ // Schema with one event "Allowances" and one Map(PublicKey, U256) field "data"
+ // CLType.Map = 0x11, CLType.PublicKey = 0x16, CLType.U256 = 0x07
+ var schemaHex = string.Concat(
+ "01000000", // 1 event
+ "0a000000", // len("Allowances") = 10
+ "416c6c6f77616e636573", // "Allowances"
+ "01000000", // 1 field
+ "04000000", // len("data") = 4
+ "64617461", // "data"
+ "11", // CLType.Map
+ "16", // key: CLType.PublicKey
+ "07" // value: CLType.U256
+ );
+
+ var schema = CESContractSchema.ParseSchema(Hex.Decode(schemaHex));
+
+ schema.TryGetEventSchema("Allowances", out var eventSchema);
+ Assert.AreEqual("data", eventSchema.Fields[0].Name);
+ Assert.AreEqual(CLType.Map, eventSchema.Fields[0].CLTypeInfo.Type);
+
+ var mapType = (CLMapTypeInfo)eventSchema.Fields[0].CLTypeInfo;
+ Assert.AreEqual(CLType.PublicKey, mapType.KeyType.Type);
+ Assert.AreEqual(CLType.U256, mapType.ValueType.Type);
+ }
+
+ [Test]
+ public void ParseSchema_TupleType_CorrectInnerTypes()
+ {
+ // Schema of a CEP-18 contract
+ var schemaHex =
+ "07000000040000004275726e02000000050000006f776e65720b06000000616d6f756e74071100000044656372656173" +
+ "65416c6c6f77616e636504000000050000006f776e65720b070000007370656e6465720b09000000616c6c6f77616e63" +
+ "650707000000646563725f62790711000000496e637265617365416c6c6f77616e636504000000050000006f776e6572"+
+ "0b070000007370656e6465720b09000000616c6c6f77616e63650706000000696e635f627907040000004d696e740200" +
+ "000009000000726563697069656e740b06000000616d6f756e74070c000000536574416c6c6f77616e63650300000005" +
+ "0000006f776e65720b070000007370656e6465720b09000000616c6c6f77616e636507080000005472616e7366657203" +
+ "0000000600000073656e6465720b09000000726563697069656e740b06000000616d6f756e74070c0000005472616e73" +
+ "66657246726f6d04000000070000007370656e6465720b050000006f776e65720b09000000726563697069656e740b06" +
+ "000000616d6f756e7407";
+
+ var schema = CESContractSchema.ParseSchema(Hex.Decode(schemaHex));
+ Assert.IsNotNull(schema);
+ schema.TryGetEventSchema("Transfer", out var eventSchema);
+ Assert.IsNotNull(eventSchema);
+ Assert.AreEqual("Transfer", eventSchema.EventName);
+
+ var evt0 =
+ "0a0000006576656e745f4d696e74011262d06e53125ea098187fb4d1d5b10a7afed48e5e5eef182ed992fc5b10034908000064a7b3b6e00d";
+ var parsedEvt = CESEvent.ParseEvent(Hex.Decode(evt0), schema);
+ Assert.IsNotNull(parsedEvt);
+ Assert.AreEqual("Mint", parsedEvt.Name);
+ var amount = parsedEvt["amount"];
+ Assert.IsNotNull(amount);
+ Assert.IsNotNull(amount.ToBigInteger());
+ Assert.AreEqual("1000000000000000000", amount.ToBigInteger().ToString() );
+ }
+ }
+}
diff --git a/Casper.Network.SDK.Test/CLValueReaderTest.cs b/Casper.Network.SDK.Test/CLValueReaderTest.cs
new file mode 100644
index 0000000..d0467ba
--- /dev/null
+++ b/Casper.Network.SDK.Test/CLValueReaderTest.cs
@@ -0,0 +1,650 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Numerics;
+using Casper.Network.SDK.ByteSerializers;
+using Casper.Network.SDK.Types;
+using NUnit.Framework;
+using Org.BouncyCastle.Utilities.Encoders;
+
+namespace NetCasperTest
+{
+ public class CLValueReaderTest
+ {
+ // ─── helpers ───────────────────────────────────────────────────────────────
+
+ private static CLValueReader ReaderFor(byte[] bytes) =>
+ new CLValueReader(new BinaryReader(new MemoryStream(bytes)));
+
+ // ─── constructor guard ─────────────────────────────────────────────────────
+
+ [Test]
+ public void NullReaderThrowsArgumentNullException()
+ {
+ Assert.Throws(() => new CLValueReader(null));
+ }
+
+ [Test]
+ public void NullTypeInfoThrowsArgumentNullException()
+ {
+ var reader = ReaderFor(Array.Empty());
+ Assert.Throws(() => reader.Read(null));
+ }
+
+ // ─── primitives ────────────────────────────────────────────────────────────
+
+ [Test]
+ public void ReadBoolTest()
+ {
+ foreach (var value in new[] { false, true })
+ {
+ var original = CLValue.Bool(value);
+ var result = ReaderFor(original.Bytes).Read(CLType.Bool);
+
+ Assert.AreEqual(CLType.Bool, result.TypeInfo.Type);
+ Assert.AreEqual(Hex.ToHexString(original.Bytes), Hex.ToHexString(result.Bytes));
+ Assert.AreEqual(value, result.ToBoolean());
+ }
+ }
+
+ [Test]
+ public void ReadI32Test()
+ {
+ foreach (var value in new[] { int.MinValue, -10, 0, 42, int.MaxValue })
+ {
+ var original = CLValue.I32(value);
+ var result = ReaderFor(original.Bytes).Read(CLType.I32);
+
+ Assert.AreEqual(CLType.I32, result.TypeInfo.Type);
+ Assert.AreEqual(Hex.ToHexString(original.Bytes), Hex.ToHexString(result.Bytes));
+ Assert.AreEqual(value, result.ToInt32());
+ }
+ }
+
+ [Test]
+ public void ReadI64Test()
+ {
+ foreach (var value in new[] { long.MinValue, -16L, 0L, 1L, long.MaxValue })
+ {
+ var original = CLValue.I64(value);
+ var result = ReaderFor(original.Bytes).Read(CLType.I64);
+
+ Assert.AreEqual(CLType.I64, result.TypeInfo.Type);
+ Assert.AreEqual(Hex.ToHexString(original.Bytes), Hex.ToHexString(result.Bytes));
+ Assert.AreEqual(value, result.ToInt64());
+ }
+ }
+
+ [Test]
+ public void ReadU8Test()
+ {
+ foreach (var value in new byte[] { 0x00, 0x7F, 0xFF })
+ {
+ var original = CLValue.U8(value);
+ var result = ReaderFor(original.Bytes).Read(CLType.U8);
+
+ Assert.AreEqual(CLType.U8, result.TypeInfo.Type);
+ Assert.AreEqual(Hex.ToHexString(original.Bytes), Hex.ToHexString(result.Bytes));
+ Assert.AreEqual(value, result.ToByte());
+ }
+ }
+
+ [Test]
+ public void ReadU32Test()
+ {
+ foreach (var value in new uint[] { 0, 1, uint.MaxValue })
+ {
+ var original = CLValue.U32(value);
+ var result = ReaderFor(original.Bytes).Read(CLType.U32);
+
+ Assert.AreEqual(CLType.U32, result.TypeInfo.Type);
+ Assert.AreEqual(Hex.ToHexString(original.Bytes), Hex.ToHexString(result.Bytes));
+ Assert.AreEqual(value, result.ToUInt32());
+ }
+ }
+
+ [Test]
+ public void ReadU64Test()
+ {
+ foreach (var value in new ulong[] { 0, 1, ulong.MaxValue })
+ {
+ var original = CLValue.U64(value);
+ var result = ReaderFor(original.Bytes).Read(CLType.U64);
+
+ Assert.AreEqual(CLType.U64, result.TypeInfo.Type);
+ Assert.AreEqual(Hex.ToHexString(original.Bytes), Hex.ToHexString(result.Bytes));
+ Assert.AreEqual(value, result.ToUInt64());
+ }
+ }
+
+ [Test]
+ public void ReadU128Test()
+ {
+ // single-byte value
+ var small = new BigInteger(255);
+ var original = CLValue.U128(small);
+ var result = ReaderFor(original.Bytes).Read(CLType.U128);
+ Assert.AreEqual(CLType.U128, result.TypeInfo.Type);
+ Assert.AreEqual(small, result.ToBigInteger());
+
+ // ulong.MaxValue — multi-byte encoding
+ var large = new BigInteger(ulong.MaxValue);
+ original = CLValue.U128(large);
+ result = ReaderFor(original.Bytes).Read(CLType.U128);
+ Assert.AreEqual(CLType.U128, result.TypeInfo.Type);
+ Assert.AreEqual(Hex.ToHexString(original.Bytes), Hex.ToHexString(result.Bytes));
+ Assert.AreEqual(large, result.ToBigInteger());
+ }
+
+ [Test]
+ public void ReadU256Test()
+ {
+ var value = new BigInteger(ulong.MaxValue);
+ var original = CLValue.U256(value);
+ var result = ReaderFor(original.Bytes).Read(CLType.U256);
+
+ Assert.AreEqual(CLType.U256, result.TypeInfo.Type);
+ Assert.AreEqual(Hex.ToHexString(original.Bytes), Hex.ToHexString(result.Bytes));
+ Assert.AreEqual(value, result.ToBigInteger());
+ }
+
+ [Test]
+ public void ReadU512Test()
+ {
+ var value = new BigInteger(ulong.MaxValue);
+ var original = CLValue.U512(value);
+ var result = ReaderFor(original.Bytes).Read(CLType.U512);
+
+ Assert.AreEqual(CLType.U512, result.TypeInfo.Type);
+ Assert.AreEqual(Hex.ToHexString(original.Bytes), Hex.ToHexString(result.Bytes));
+ Assert.AreEqual(value, result.ToBigInteger());
+ }
+
+ [Test]
+ public void ReadUnitTest()
+ {
+ var original = CLValue.Unit();
+ var result = ReaderFor(original.Bytes).Read(CLType.Unit);
+
+ Assert.AreEqual(CLType.Unit, result.TypeInfo.Type);
+ Assert.AreEqual(0, result.Bytes.Length);
+ }
+
+ // ─── string ────────────────────────────────────────────────────────────────
+
+ [Test]
+ public void ReadStringTest()
+ {
+ var value = "Hello, Casper!";
+ var original = CLValue.String(value);
+ var result = ReaderFor(original.Bytes).Read(CLType.String);
+
+ Assert.AreEqual(CLType.String, result.TypeInfo.Type);
+ Assert.AreEqual(Hex.ToHexString(original.Bytes), Hex.ToHexString(result.Bytes));
+ Assert.AreEqual(value, result.ToString());
+ }
+
+ [Test]
+ public void ReadEmptyStringTest()
+ {
+ var original = CLValue.String(string.Empty);
+ var result = ReaderFor(original.Bytes).Read(CLType.String);
+
+ Assert.AreEqual(CLType.String, result.TypeInfo.Type);
+ Assert.AreEqual(string.Empty, result.ToString());
+ }
+
+ // ─── URef ──────────────────────────────────────────────────────────────────
+
+ [Test]
+ public void ReadURefTest()
+ {
+ var urefStr = "uref-000102030405060708090a0b0c0d0e0f000102030405060708090a0b0c0d0e0f-007";
+ var original = CLValue.URef(urefStr);
+ var result = ReaderFor(original.Bytes).Read(CLType.URef);
+
+ Assert.AreEqual(CLType.URef, result.TypeInfo.Type);
+ Assert.AreEqual(33, result.Bytes.Length);
+ Assert.AreEqual(Hex.ToHexString(original.Bytes), Hex.ToHexString(result.Bytes));
+ Assert.AreEqual(urefStr, result.ToURef().ToString());
+ }
+
+ // ─── PublicKey ─────────────────────────────────────────────────────────────
+
+ [Test]
+ public void ReadPublicKeyEd25519Test()
+ {
+ var publicKey = KeyPair.CreateNew(KeyAlgo.ED25519).PublicKey;
+ var original = CLValue.PublicKey(publicKey);
+ var result = ReaderFor(original.Bytes).Read(CLType.PublicKey);
+
+ Assert.AreEqual(CLType.PublicKey, result.TypeInfo.Type);
+ Assert.AreEqual(33, result.Bytes.Length); // 1 algo + 32 key bytes
+ Assert.AreEqual(Hex.ToHexString(original.Bytes), Hex.ToHexString(result.Bytes));
+ Assert.AreEqual(publicKey.ToAccountHex(), result.ToPublicKey().ToAccountHex());
+ }
+
+ [Test]
+ public void ReadPublicKeySecp256k1Test()
+ {
+ var publicKey = KeyPair.CreateNew(KeyAlgo.SECP256K1).PublicKey;
+ var original = CLValue.PublicKey(publicKey);
+ var result = ReaderFor(original.Bytes).Read(CLType.PublicKey);
+
+ Assert.AreEqual(CLType.PublicKey, result.TypeInfo.Type);
+ Assert.AreEqual(34, result.Bytes.Length); // 1 algo + 33 key bytes
+ Assert.AreEqual(Hex.ToHexString(original.Bytes), Hex.ToHexString(result.Bytes));
+ Assert.AreEqual(publicKey.ToAccountHex(), result.ToPublicKey().ToAccountHex());
+ }
+
+ // ─── Key (GlobalStateKey) ──────────────────────────────────────────────────
+
+ [Test]
+ public void ReadKeyAccountHashTest()
+ {
+ var key = GlobalStateKey.FromString(
+ "account-hash-989ca079a5e446071866331468ab949483162588d57ec13ba6bb051f1e15f8b7");
+ var original = CLValue.Key(key);
+ var typeInfo = new CLKeyTypeInfo(KeyIdentifier.Account);
+ var result = ReaderFor(original.Bytes).Read(typeInfo);
+
+ Assert.AreEqual(CLType.Key, result.TypeInfo.Type);
+ Assert.AreEqual(33, result.Bytes.Length); // 1 tag + 32 hash bytes
+ Assert.AreEqual(Hex.ToHexString(original.Bytes), Hex.ToHexString(result.Bytes));
+ Assert.AreEqual(key.ToString(), result.ToGlobalStateKey().ToString());
+ }
+
+ [Test]
+ public void ReadKeyEraInfoTest()
+ {
+ var key = GlobalStateKey.FromString("era-2034");
+ var original = CLValue.Key(key);
+ var typeInfo = new CLKeyTypeInfo(KeyIdentifier.EraInfo);
+ var result = ReaderFor(original.Bytes).Read(typeInfo);
+
+ Assert.AreEqual(CLType.Key, result.TypeInfo.Type);
+ Assert.AreEqual(9, result.Bytes.Length); // 1 tag + 8 u64 bytes
+ Assert.AreEqual(Hex.ToHexString(original.Bytes), Hex.ToHexString(result.Bytes));
+ Assert.AreEqual(key.ToString(), result.ToGlobalStateKey().ToString());
+ }
+
+ [Test]
+ public void ReadKeyURefTest()
+ {
+ var key = GlobalStateKey.FromString(
+ "uref-000102030405060708090a0b0c0d0e0f000102030405060708090a0b0c0d0e0f-007");
+ var original = CLValue.Key(key);
+ var typeInfo = new CLKeyTypeInfo(KeyIdentifier.URef);
+ var result = ReaderFor(original.Bytes).Read(typeInfo);
+
+ Assert.AreEqual(CLType.Key, result.TypeInfo.Type);
+ Assert.AreEqual(34, result.Bytes.Length); // 1 tag + 33 URef bytes
+ Assert.AreEqual(Hex.ToHexString(original.Bytes), Hex.ToHexString(result.Bytes));
+ Assert.AreEqual(key.ToString(), result.ToGlobalStateKey().ToString());
+ }
+
+ // ─── Option ────────────────────────────────────────────────────────────────
+
+ [Test]
+ public void ReadOptionSomeTest()
+ {
+ var original = CLValue.Option(CLValue.U32(42u));
+ var typeInfo = new CLOptionTypeInfo(CLType.U32);
+ var result = ReaderFor(original.Bytes).Read(typeInfo);
+
+ Assert.AreEqual(CLType.Option, result.TypeInfo.Type);
+ Assert.AreEqual(CLType.U32, ((CLOptionTypeInfo)result.TypeInfo).OptionType.Type);
+ Assert.AreEqual(Hex.ToHexString(original.Bytes), Hex.ToHexString(result.Bytes));
+ Assert.IsTrue(result.IsSome());
+ Assert.AreEqual(0x01, result.Bytes[0]); // Some tag
+ // inner bytes after the tag should match U32(42).Bytes
+ var innerBytes = result.Bytes.Skip(1).ToArray();
+ Assert.AreEqual(Hex.ToHexString(CLValue.U32(42u).Bytes), Hex.ToHexString(innerBytes));
+ }
+
+ [Test]
+ public void ReadOptionNoneTest()
+ {
+ var original = CLValue.OptionNone(CLType.U32);
+ var typeInfo = new CLOptionTypeInfo(CLType.U32);
+ var result = ReaderFor(original.Bytes).Read(typeInfo);
+
+ Assert.AreEqual(CLType.Option, result.TypeInfo.Type);
+ Assert.AreEqual(CLType.U32, ((CLOptionTypeInfo)result.TypeInfo).OptionType.Type);
+ Assert.AreEqual(1, result.Bytes.Length);
+ Assert.AreEqual(0x00, result.Bytes[0]); // None tag
+ Assert.IsTrue(result.IsNone());
+ }
+
+ // ─── List ──────────────────────────────────────────────────────────────────
+
+ [Test]
+ public void ReadListTest()
+ {
+ var original = CLValue.List(new[]
+ {
+ CLValue.U32(1), CLValue.U32(2), CLValue.U32(3), CLValue.U32(4)
+ });
+ var typeInfo = new CLListTypeInfo(CLType.U32);
+ var result = ReaderFor(original.Bytes).Read(typeInfo);
+
+ Assert.AreEqual(CLType.List, result.TypeInfo.Type);
+ Assert.AreEqual(CLType.U32, ((CLListTypeInfo)result.TypeInfo).ListType.Type);
+ Assert.AreEqual(Hex.ToHexString(original.Bytes), Hex.ToHexString(result.Bytes));
+
+ var items = result.ToList();
+ Assert.AreEqual(4, items.Count);
+ Assert.AreEqual(1u, items[0]);
+ Assert.AreEqual(2u, items[1]);
+ Assert.AreEqual(3u, items[2]);
+ Assert.AreEqual(4u, items[3]);
+ }
+
+ [Test]
+ public void ReadEmptyListTest()
+ {
+ var original = CLValue.EmptyList(new CLKeyTypeInfo(KeyIdentifier.Hash));
+ var typeInfo = new CLListTypeInfo(new CLKeyTypeInfo(KeyIdentifier.Hash));
+ var result = ReaderFor(original.Bytes).Read(typeInfo);
+
+ Assert.AreEqual(CLType.List, result.TypeInfo.Type);
+ Assert.AreEqual(Hex.ToHexString(original.Bytes), Hex.ToHexString(result.Bytes));
+ Assert.AreEqual(0, result.ToList().Count);
+ }
+
+ // ─── ByteArray ─────────────────────────────────────────────────────────────
+
+ [Test]
+ public void ReadByteArrayTest()
+ {
+ var data = Hex.Decode("0102030405060708");
+ var original = CLValue.ByteArray(data);
+ var typeInfo = new CLByteArrayTypeInfo(8);
+ var result = ReaderFor(original.Bytes).Read(typeInfo);
+
+ Assert.AreEqual(CLType.ByteArray, result.TypeInfo.Type);
+ Assert.AreEqual(8, result.Bytes.Length);
+ Assert.AreEqual(Hex.ToHexString(original.Bytes), Hex.ToHexString(result.Bytes));
+ Assert.IsTrue(data.SequenceEqual(result.ToByteArray()));
+ }
+
+ // ─── Result ────────────────────────────────────────────────────────────────
+
+ [Test]
+ public void ReadResultOkTest()
+ {
+ var original = CLValue.Ok(CLValue.U8(0xFF), new CLTypeInfo(CLType.String));
+ var typeInfo = new CLResultTypeInfo(CLType.U8, CLType.String);
+ var result = ReaderFor(original.Bytes).Read(typeInfo);
+
+ Assert.AreEqual(CLType.Result, result.TypeInfo.Type);
+ Assert.AreEqual(Hex.ToHexString(original.Bytes), Hex.ToHexString(result.Bytes));
+ Assert.AreEqual(0x01, result.Bytes[0]); // Ok tag
+ var typed = result.ToResult();
+ Assert.IsTrue(typed.Success);
+ Assert.AreEqual(0xFF, typed.Value);
+ }
+
+ [Test]
+ public void ReadResultErrTest()
+ {
+ var original = CLValue.Err(CLValue.String("Error!"), new CLTypeInfo(CLType.Unit));
+ var typeInfo = new CLResultTypeInfo(CLType.Unit, CLType.String);
+ var result = ReaderFor(original.Bytes).Read(typeInfo);
+
+ Assert.AreEqual(CLType.Result, result.TypeInfo.Type);
+ Assert.AreEqual(Hex.ToHexString(original.Bytes), Hex.ToHexString(result.Bytes));
+ Assert.AreEqual(0x00, result.Bytes[0]); // Err tag
+ var typed = result.ToResult();
+ Assert.IsFalse(typed.Success);
+ Assert.AreEqual("Error!", typed.Error);
+ }
+
+ // ─── Map ───────────────────────────────────────────────────────────────────
+
+ [Test]
+ public void ReadMapTest()
+ {
+ var original = CLValue.Map(new Dictionary
+ {
+ { CLValue.String("key1"), CLValue.U32(1) },
+ { CLValue.String("key2"), CLValue.U32(2) }
+ });
+ var typeInfo = new CLMapTypeInfo(CLType.String, CLType.U32);
+ var result = ReaderFor(original.Bytes).Read(typeInfo);
+
+ Assert.AreEqual(CLType.Map, result.TypeInfo.Type);
+ var mapType = (CLMapTypeInfo)result.TypeInfo;
+ Assert.AreEqual(CLType.String, mapType.KeyType.Type);
+ Assert.AreEqual(CLType.U32, mapType.ValueType.Type);
+ Assert.AreEqual(Hex.ToHexString(original.Bytes), Hex.ToHexString(result.Bytes));
+
+ var dict = result.ToDictionary();
+ Assert.AreEqual(2, dict.Count);
+ Assert.AreEqual(1u, dict["key1"]);
+ Assert.AreEqual(2u, dict["key2"]);
+ }
+
+ [Test]
+ public void ReadEmptyMapTest()
+ {
+ var original = CLValue.EmptyMap(CLType.String, new CLKeyTypeInfo(KeyIdentifier.Hash));
+ var typeInfo = new CLMapTypeInfo(CLType.String, new CLKeyTypeInfo(KeyIdentifier.Hash));
+ var result = ReaderFor(original.Bytes).Read(typeInfo);
+
+ Assert.AreEqual(CLType.Map, result.TypeInfo.Type);
+ Assert.AreEqual(Hex.ToHexString(original.Bytes), Hex.ToHexString(result.Bytes));
+ Assert.AreEqual(0, result.ToDictionary().Count);
+ }
+
+ // ─── Tuples ────────────────────────────────────────────────────────────────
+
+ [Test]
+ public void ReadTuple1Test()
+ {
+ var original = CLValue.Tuple1(CLValue.U32(17));
+ var typeInfo = new CLTuple1TypeInfo(CLType.U32);
+ var result = ReaderFor(original.Bytes).Read(typeInfo);
+
+ Assert.AreEqual(CLType.Tuple1, result.TypeInfo.Type);
+ Assert.AreEqual(CLType.U32, ((CLTuple1TypeInfo)result.TypeInfo).Type0.Type);
+ Assert.AreEqual(Hex.ToHexString(original.Bytes), Hex.ToHexString(result.Bytes));
+ Assert.AreEqual(17u, result.ToTuple1().Item1);
+ }
+
+ [Test]
+ public void ReadTuple2Test()
+ {
+ var original = CLValue.Tuple2(CLValue.U32(17), CLValue.U32(127));
+ var typeInfo = new CLTuple2TypeInfo(CLType.U32, CLType.U32);
+ var result = ReaderFor(original.Bytes).Read(typeInfo);
+
+ Assert.AreEqual(CLType.Tuple2, result.TypeInfo.Type);
+ Assert.AreEqual(Hex.ToHexString(original.Bytes), Hex.ToHexString(result.Bytes));
+ var t = result.ToTuple2();
+ Assert.AreEqual(17u, t.Item1);
+ Assert.AreEqual(127u, t.Item2);
+ }
+
+ [Test]
+ public void ReadTuple2MixedTypesTest()
+ {
+ var original = CLValue.Tuple2(CLValue.U32(127), CLValue.String("ABCDE"));
+ var typeInfo = new CLTuple2TypeInfo(CLType.U32, CLType.String);
+ var result = ReaderFor(original.Bytes).Read(typeInfo);
+
+ Assert.AreEqual(CLType.Tuple2, result.TypeInfo.Type);
+ Assert.AreEqual(Hex.ToHexString(original.Bytes), Hex.ToHexString(result.Bytes));
+ var t = result.ToTuple2();
+ Assert.AreEqual(127u, t.Item1);
+ Assert.AreEqual("ABCDE", t.Item2);
+ }
+
+ [Test]
+ public void ReadTuple3Test()
+ {
+ var original = CLValue.Tuple3(CLValue.U32(17), CLValue.U32(127), CLValue.U32(255));
+ var typeInfo = new CLTuple3TypeInfo(CLType.U32, CLType.U32, CLType.U32);
+ var result = ReaderFor(original.Bytes).Read(typeInfo);
+
+ Assert.AreEqual(CLType.Tuple3, result.TypeInfo.Type);
+ Assert.AreEqual(Hex.ToHexString(original.Bytes), Hex.ToHexString(result.Bytes));
+ var t = result.ToTuple3();
+ Assert.AreEqual(17u, t.Item1);
+ Assert.AreEqual(127u, t.Item2);
+ Assert.AreEqual(255u, t.Item3);
+ }
+
+ // ─── nested / compound types ───────────────────────────────────────────────
+
+ [Test]
+ public void ReadOptionSomeListTest()
+ {
+ // Option(Some(List(U32)))
+ var inner = CLValue.List(new[] { CLValue.U32(10), CLValue.U32(20) });
+ var original = CLValue.Option(inner);
+ var typeInfo = new CLOptionTypeInfo(new CLListTypeInfo(CLType.U32));
+ var result = ReaderFor(original.Bytes).Read(typeInfo);
+
+ Assert.AreEqual(CLType.Option, result.TypeInfo.Type);
+ Assert.AreEqual(Hex.ToHexString(original.Bytes), Hex.ToHexString(result.Bytes));
+ Assert.IsTrue(result.IsSome());
+ }
+
+ [Test]
+ public void ReadOptionNoneListTest()
+ {
+ // Option(None) with inner type List(U32)
+ var original = CLValue.OptionNone(new CLListTypeInfo(CLType.U32));
+ var typeInfo = new CLOptionTypeInfo(new CLListTypeInfo(CLType.U32));
+ var result = ReaderFor(original.Bytes).Read(typeInfo);
+
+ Assert.AreEqual(CLType.Option, result.TypeInfo.Type);
+ Assert.IsTrue(result.IsNone());
+ }
+
+ [Test]
+ public void ReadListOfByteArraysTest()
+ {
+ // List(ByteArray(8))
+ var p1 = Hex.Decode("0102030405060708");
+ var p2 = Hex.Decode("090a0b0c0d0e0f00");
+ var original = CLValue.List(new[]
+ {
+ CLValue.ByteArray(p1),
+ CLValue.ByteArray(p2)
+ });
+ var typeInfo = new CLListTypeInfo(new CLByteArrayTypeInfo(8));
+ var result = ReaderFor(original.Bytes).Read(typeInfo);
+
+ Assert.AreEqual(CLType.List, result.TypeInfo.Type);
+ Assert.AreEqual(Hex.ToHexString(original.Bytes), Hex.ToHexString(result.Bytes));
+
+ var items = result.ToList();
+ Assert.AreEqual(2, items.Count);
+ Assert.IsTrue(p1.SequenceEqual(items[0]));
+ Assert.IsTrue(p2.SequenceEqual(items[1]));
+ }
+
+ [Test]
+ public void ReadTuple3MixedComplexTypesTest()
+ {
+ // Tuple3(I32, Bool, URef)
+ var uref = "uref-cdd5422295f6a61e86a4d3229b28dac2e67523c41e2aafed3a041362df7a8432-007";
+ var original = CLValue.Tuple3(
+ CLValue.I32(123),
+ CLValue.Bool(true),
+ CLValue.URef(uref));
+ var typeInfo = new CLTuple3TypeInfo(CLType.I32, CLType.Bool, CLType.URef);
+ var result = ReaderFor(original.Bytes).Read(typeInfo);
+
+ Assert.AreEqual(CLType.Tuple3, result.TypeInfo.Type);
+ Assert.AreEqual(Hex.ToHexString(original.Bytes), Hex.ToHexString(result.Bytes));
+ var t = result.ToTuple3();
+ Assert.AreEqual(123, t.Item1);
+ Assert.AreEqual(true, t.Item2);
+ Assert.AreEqual(uref, t.Item3.ToString());
+ }
+
+ // ─── stream position and multi-value reads ─────────────────────────────────
+
+ [Test]
+ public void ReadSequentialValuesAdvancesStreamTest()
+ {
+ // Pack three values back-to-back into a single MemoryStream and verify
+ // that each Read() advances the position so the next call reads correctly.
+ var u32Bytes = CLValue.U32(42u).Bytes;
+ var boolBytes = CLValue.Bool(true).Bytes;
+ var strBytes = CLValue.String("Casper").Bytes;
+
+ using var ms = new MemoryStream();
+ ms.Write(u32Bytes, 0, u32Bytes.Length);
+ ms.Write(boolBytes, 0, boolBytes.Length);
+ ms.Write(strBytes, 0, strBytes.Length);
+ ms.Position = 0;
+
+ var clValueReader = new CLValueReader(new BinaryReader(ms));
+
+ var v1 = clValueReader.Read(CLType.U32);
+ var v2 = clValueReader.Read(CLType.Bool);
+ var v3 = clValueReader.Read(CLType.String);
+
+ // All bytes consumed — stream at end
+ Assert.AreEqual(ms.Length, ms.Position);
+
+ Assert.AreEqual(42u, v1.ToUInt32());
+ Assert.AreEqual(true, v2.ToBoolean());
+ Assert.AreEqual("Casper", v3.ToString());
+ }
+
+ // ─── bytes match CLValue factory output ────────────────────────────────────
+
+ [Test]
+ public void ReadBytesMatchCLValueFactoryOutputTest()
+ {
+ // CLValueReader.Read() must produce exactly the same Bytes as the
+ // corresponding CLValue factory method — verified against known hex strings
+ // from CLValueByteSerializerTest (the data-only portion, no length/type prefix).
+
+ var serializer = new CLValueByteSerializer();
+
+ void VerifyRoundTrip(CLValue original, CLTypeInfo typeInfo)
+ {
+ var result = ReaderFor(original.Bytes).Read(typeInfo);
+ Assert.AreEqual(Hex.ToHexString(original.Bytes), Hex.ToHexString(result.Bytes),
+ $"Bytes mismatch for CLType {typeInfo.Type}");
+ // Also verify that the result can be re-serialized identically
+ var reEncoded = serializer.ToBytes(result);
+ var originalEncoded = serializer.ToBytes(original);
+ Assert.AreEqual(Hex.ToHexString(originalEncoded), Hex.ToHexString(reEncoded),
+ $"Re-serialized bytes mismatch for CLType {typeInfo.Type}");
+ }
+
+ VerifyRoundTrip(CLValue.Bool(false), CLType.Bool);
+ VerifyRoundTrip(CLValue.I32(-10), CLType.I32);
+ VerifyRoundTrip(CLValue.I64(-16), CLType.I64);
+ VerifyRoundTrip(CLValue.U8(0x7F), CLType.U8);
+ VerifyRoundTrip(CLValue.U32(uint.MaxValue), CLType.U32);
+ VerifyRoundTrip(CLValue.U64(ulong.MaxValue), CLType.U64);
+ VerifyRoundTrip(CLValue.U128(new BigInteger(ulong.MaxValue)), CLType.U128);
+ VerifyRoundTrip(CLValue.U256(new BigInteger(ulong.MaxValue)), CLType.U256);
+ VerifyRoundTrip(CLValue.U512(new BigInteger(ulong.MaxValue)), CLType.U512);
+ VerifyRoundTrip(CLValue.Unit(), CLType.Unit);
+ VerifyRoundTrip(CLValue.String("Hello, Casper!"), CLType.String);
+ VerifyRoundTrip(CLValue.URef("uref-000102030405060708090a0b0c0d0e0f000102030405060708090a0b0c0d0e0f-007"),
+ CLType.URef);
+ VerifyRoundTrip(CLValue.List(new[] { CLValue.U32(1), CLValue.U32(2) }),
+ new CLListTypeInfo(CLType.U32));
+ VerifyRoundTrip(CLValue.ByteArray(Hex.Decode("0102030405060708")),
+ new CLByteArrayTypeInfo(8));
+ VerifyRoundTrip(CLValue.Ok(CLValue.U8(0xFF), new CLTypeInfo(CLType.String)),
+ new CLResultTypeInfo(CLType.U8, CLType.String));
+ VerifyRoundTrip(CLValue.Tuple1(CLValue.U32(17)), new CLTuple1TypeInfo(CLType.U32));
+ VerifyRoundTrip(CLValue.Tuple2(CLValue.U32(17), CLValue.U32(127)),
+ new CLTuple2TypeInfo(CLType.U32, CLType.U32));
+ VerifyRoundTrip(CLValue.Tuple3(CLValue.U32(17), CLValue.U32(127), CLValue.U32(17)),
+ new CLTuple3TypeInfo(CLType.U32, CLType.U32, CLType.U32));
+ }
+ }
+}
diff --git a/Casper.Network.SDK/ByteSerializers/CLValueByteSerializer.cs b/Casper.Network.SDK/ByteSerializers/CLValueByteSerializer.cs
index d4fc8eb..d4577a0 100644
--- a/Casper.Network.SDK/ByteSerializers/CLValueByteSerializer.cs
+++ b/Casper.Network.SDK/ByteSerializers/CLValueByteSerializer.cs
@@ -39,7 +39,7 @@ public CLValue FromReader(BinaryReader reader)
var dataBytes = reader.ReadBytes((int)dataLength);
// read type info recursively
- var typeInfo = CLTypeFromBytes(reader);
+ var typeInfo = reader.ReadCLTypeInfo();
return new CLValue(dataBytes, typeInfo);
}
diff --git a/Casper.Network.SDK/CES/CESEvent.cs b/Casper.Network.SDK/CES/CESEvent.cs
new file mode 100644
index 0000000..5f359fa
--- /dev/null
+++ b/Casper.Network.SDK/CES/CESEvent.cs
@@ -0,0 +1,191 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+using Casper.Network.SDK.Types;
+using Casper.Network.SDK.Utils;
+
+namespace Casper.Network.SDK.CES
+{
+ ///
+ /// A fully parsed CES event containing the event name, its typed fields, and, optionally, the
+ /// execution-context metadata of the event.
+ ///
+ public class CESEvent
+ {
+ [JsonPropertyName("name")]
+ public string Name { get; }
+
+ [JsonPropertyName("fields")]
+ [JsonConverter(typeof(NamedArgListConverter))]
+ public IReadOnlyList Fields { get; }
+
+ ///
+ /// The contract hash of the emitting contract (e.g. "hash-abc…def").
+ /// Set by ; null when parsed in isolation.
+ ///
+ [JsonPropertyName("contract_hash")]
+ public string ContractHash { get; init; }
+
+ ///
+ /// The contract-package hash of the emitting contract.
+ /// Set by ; null when parsed in isolation.
+ ///
+ [JsonPropertyName("contract_package_hash")]
+ public string ContractPackageHash { get; init; }
+
+ ///
+ /// The key in the global state that stores the event (dictionary item)
+ /// Set by ; null when parsed in isolation.
+ ///
+ public GlobalStateKey TransformKey { get; init; }
+
+ ///
+ /// Zero-based index of the inside the execution-result
+ /// effect list from which this event was extracted.
+ /// Set by ; defaults to 0 when the event
+ /// is parsed in isolation via .
+ ///
+ [JsonPropertyName("transform_id")]
+ public int TransformId { get; init; }
+
+ ///
+ /// The string key that identifies this entry inside the contract's __events
+ /// dictionary (i.e. the sequential event counter emitted by the contract).
+ /// Set by ; null when parsed in isolation.
+ ///
+ [JsonPropertyName("event_id")]
+ public string EventId { get; init; }
+
+ [JsonConstructor]
+ public CESEvent(string name, IReadOnlyList fields)
+ {
+ Name = name;
+ Fields = fields;
+ }
+
+ ///
+ /// Returns the CLValue of the field with the given name, or null if not found.
+ ///
+ public CLValue this[string fieldName] =>
+ Fields.FirstOrDefault(f => f.Name == fieldName)?.Value;
+
+ // ─────────────────────────────────────────────────────────────────────
+ // Parse
+ // ─────────────────────────────────────────────────────────────────────
+
+ ///
+ /// Parses a single CES event from its raw bytes using the given contract schema.
+ ///
+ ///
+ /// The raw bytes of one event entry (value) from the __events CLValue map.
+ ///
+ ///
+ /// The contract schema obtained from .
+ ///
+ ///
+ /// Thrown when the event name found in is not present in
+ /// .
+ ///
+ public static CESEvent ParseEvent(byte[] rawBytes, CESContractSchema schema)
+ {
+ // Depending on the origin of the rawBytes, the actual payload can be prepended with a u32 LE length value.
+ // Detect and skip that outer wrapper transparently.
+ var offset = 0;
+ if (rawBytes.Length >= 4)
+ {
+ var declaredLen = BitConverter.ToUInt32(rawBytes, 0);
+ if (!BitConverter.IsLittleEndian)
+ declaredLen = SwapU32(declaredLen);
+ if (declaredLen == rawBytes.Length - 4)
+ offset = 4;
+ }
+
+ using var ms = new MemoryStream(rawBytes, offset, rawBytes.Length - offset);
+ using var reader = new BinaryReader(ms);
+
+ var rawName = reader.ReadCLString();
+
+ // Strip the "event_" prefix that CES implementation prepend to event names.
+ const string prefix = "event_";
+ var eventName = rawName.StartsWith(prefix)
+ ? rawName.Substring(prefix.Length)
+ : rawName;
+
+ if (!schema.TryGetEventSchema(eventName, out var eventSchema))
+ throw new KeyNotFoundException(
+ $"Event '{rawName}' not found in the contract schema.");
+
+ var fields = new List(eventSchema.Fields.Count);
+ var clValueReader = new CLValueReader(reader);
+
+ foreach (var schemaField in eventSchema.Fields)
+ {
+ var clValue = clValueReader.Read(schemaField.CLTypeInfo);
+ fields.Add(new NamedArg(schemaField.Name, clValue));
+ }
+
+ return new CESEvent(eventName, fields)
+ {
+ ContractHash = schema.ContractHash,
+ ContractPackageHash = schema.ContractPackageHash,
+ };
+ }
+
+ private static uint SwapU32(uint value) =>
+ ((value & 0x000000FFu) << 24) |
+ ((value & 0x0000FF00u) << 8) |
+ ((value & 0x00FF0000u) >> 8) |
+ ((value & 0xFF000000u) >> 24);
+
+ // ─────────────────────────────────────────────────────────────────────
+ // JSON converter for IReadOnlyList
+ //
+ // Each NamedArg is serialised by NamedArg.NamedArgConverter as the
+ // two-element JSON array ["fieldName", {CLValue}], so the field list
+ // becomes an array of such pairs:
+ // [["amount", {...}], ["sender", {...}], ...]
+ // ─────────────────────────────────────────────────────────────────────
+
+ public class NamedArgListConverter : JsonConverter>
+ {
+ private static readonly NamedArg.NamedArgConverter _itemConverter =
+ new NamedArg.NamedArgConverter();
+
+ public override IReadOnlyList Read(
+ ref Utf8JsonReader reader,
+ Type typeToConvert,
+ JsonSerializerOptions options)
+ {
+ if (reader.TokenType != JsonTokenType.StartArray)
+ throw new JsonException("Expected start of array for CESEvent fields.");
+
+ reader.Read(); // move past the outer '[' to the first element (or ']')
+
+ var list = new List();
+ while (reader.TokenType != JsonTokenType.EndArray)
+ {
+ // Each element is itself a two-element array ["name", {CLValue}];
+ // NamedArgConverter.Read expects the reader to be at StartArray.
+ list.Add(_itemConverter.Read(ref reader, typeof(NamedArg), options));
+ reader.Read(); // advance past the EndArray of the just-read NamedArg
+ }
+
+ return list;
+ }
+
+ public override void Write(
+ Utf8JsonWriter writer,
+ IReadOnlyList value,
+ JsonSerializerOptions options)
+ {
+ writer.WriteStartArray();
+ foreach (var arg in value)
+ _itemConverter.Write(writer, arg, options);
+ writer.WriteEndArray();
+ }
+ }
+ }
+}
diff --git a/Casper.Network.SDK/CES/CESParser.cs b/Casper.Network.SDK/CES/CESParser.cs
new file mode 100644
index 0000000..8698e10
--- /dev/null
+++ b/Casper.Network.SDK/CES/CESParser.cs
@@ -0,0 +1,103 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Casper.Network.SDK.Types;
+
+namespace Casper.Network.SDK.CES
+{
+ ///
+ /// Parses CES (Casper Event Standard) schema and event bytes produced by
+ /// smart contracts following the make-software CES convention.
+ ///
+ /// Contracts store their event schema in the named key __events_schema
+ /// (a CLValue of type Any) and emit events into __events
+ /// (a CLValue of type Map(U32, Bytes)).
+ ///
+ ///
+ public static class CESParser
+ {
+ ///
+ /// Scans a list of execution-result transforms and returns every CES event emitted by
+ /// any of the watched contracts.
+ ///
+ ///
+ /// The Effect list from an (V1 or V2).
+ ///
+ ///
+ /// The list of instances to watch.
+ /// Each schema must have set; schemas
+ /// where it is null are silently skipped during matching.
+ ///
+ ///
+ /// Ordered list of instances found, in the same order as they
+ /// appear in . Never null; empty when no events are found.
+ ///
+ public static List GetEvents(
+ List transforms,
+ IReadOnlyList watchedContracts)
+ {
+ if (transforms == null)
+ throw new ArgumentNullException(nameof(transforms));
+ if (watchedContracts == null)
+ throw new ArgumentNullException(nameof(watchedContracts));
+
+ var results = new List();
+
+ for (int i = 0; i < transforms.Count; i++)
+ {
+ var transform = transforms[i];
+
+ // 1. Key must be a dictionary entry.
+ if (transform.Key is not DictionaryKey)
+ continue;
+
+ // 2. Kind must be a CLValue write.
+ if (transform.Kind is not WriteTransformKind writeKind)
+ continue;
+
+ var clValue = writeKind.Value?.CLValue;
+ if (clValue == null)
+ continue;
+
+ // 3. Parse the dictionary envelope.
+ CLValueDictionary dict;
+ try
+ {
+ dict = CLValueDictionary.Parse(clValue.Bytes);
+ }
+ catch
+ {
+ // Not a CES dictionary entry — skip.
+ continue;
+ }
+
+ // 4. Match the seed address against the watched contracts.
+ var seedHex = dict.Seed.ToHexString();
+ var schema = watchedContracts.FirstOrDefault(s =>
+ s.EventsURef != null && s.EventsURef.ToHexString() == seedHex);
+ if (schema == null)
+ continue;
+
+ // 5. Parse the event payload, stamping it with execution-result context.
+ try
+ {
+ var evt = CESEvent.ParseEvent(dict.Value, schema);
+ results.Add(new CESEvent(evt.Name, evt.Fields)
+ {
+ ContractHash = schema.ContractHash,
+ ContractPackageHash = schema.ContractPackageHash,
+ TransformKey = transform.Key,
+ TransformId = i,
+ EventId = dict.ItemKey,
+ });
+ }
+ catch (KeyNotFoundException)
+ {
+ // Event name not present in this version of the schema — skip.
+ }
+ }
+
+ return results;
+ }
+ }
+}
diff --git a/Casper.Network.SDK/CES/CESSchema.cs b/Casper.Network.SDK/CES/CESSchema.cs
new file mode 100644
index 0000000..a09a7ff
--- /dev/null
+++ b/Casper.Network.SDK/CES/CESSchema.cs
@@ -0,0 +1,371 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Text.Json.Serialization;
+using System.Threading.Tasks;
+using Casper.Network.SDK.Converters;
+using Casper.Network.SDK.Types;
+using Casper.Network.SDK.Utils;
+
+namespace Casper.Network.SDK.CES
+{
+ ///
+ /// Defines one field in a CES event schema: its name and Casper type.
+ ///
+ public class CESEventSchemaField
+ {
+ [JsonPropertyName("name")]
+ public string Name { get; }
+
+ [JsonPropertyName("cl_type")]
+ [JsonConverter(typeof(CLTypeInfoConverter))]
+ public CLTypeInfo CLTypeInfo { get; }
+
+ [JsonConstructor]
+ public CESEventSchemaField(string name, CLTypeInfo clTypeInfo)
+ {
+ Name = name;
+ CLTypeInfo = clTypeInfo;
+ }
+ }
+
+ ///
+ /// Schema for a single named CES event type, listing its fields in order.
+ ///
+ public class CESEventSchema
+ {
+ [JsonPropertyName("event_name")]
+ public string EventName { get; }
+
+ [JsonPropertyName("fields")]
+ public IReadOnlyList Fields { get; }
+
+ [JsonConstructor]
+ public CESEventSchema(string eventName, IReadOnlyList fields)
+ {
+ EventName = eventName;
+ Fields = fields;
+ }
+ }
+
+ ///
+ /// Full schema for a CES-compliant contract, parsed from the __events_schema named key.
+ /// Maps event names to their .
+ ///
+ public class CESContractSchema
+ {
+ [JsonPropertyName("events")]
+ public IReadOnlyDictionary Events { get; }
+
+ ///
+ /// The contract hash (e.g. "hash-abc…def" or "entity-contract-abc…def")
+ /// that owns this schema.
+ /// Set by the caller after , or automatically
+ /// populated when the schema is fetched via .
+ ///
+ [JsonPropertyName("contract_hash")]
+ public string ContractHash { get; init; }
+
+ ///
+ /// The contract-package hash supplied to (or set manually
+ /// after ).
+ ///
+ [JsonPropertyName("contract_package_hash")]
+ public string ContractPackageHash { get; init; }
+
+ ///
+ /// The URef stored under the __events_schema named key of the contract.
+ /// Automatically populated by ; null when the schema
+ /// was obtained via directly.
+ ///
+ [JsonPropertyName("schema_uref")]
+ [JsonConverter(typeof(GlobalStateKey.GlobalStateKeyConverter))]
+ public URef SchemaURef { get; init; }
+
+ ///
+ /// The URef stored under the __events named key of the contract.
+ /// This is the seed used to locate CES event entries in the global state transforms.
+ /// Automatically populated by ; null when the schema
+ /// was obtained via directly.
+ ///
+ [JsonPropertyName("events_uref")]
+ [JsonConverter(typeof(GlobalStateKey.GlobalStateKeyConverter))]
+ public URef EventsURef { get; init; }
+
+ [JsonConstructor]
+ public CESContractSchema(IReadOnlyDictionary events)
+ {
+ Events = events;
+ }
+
+ ///
+ /// Retrieves the schema for the given event name.
+ ///
+ ///
+ /// Thrown when is not present in this schema.
+ ///
+ public CESEventSchema GetEventSchema(string eventName)
+ {
+ if (!TryGetEventSchema(eventName, out var schema))
+ throw new KeyNotFoundException($"Event '{eventName}' not found in the contract schema.");
+ return schema;
+ }
+
+ ///
+ /// Tries to retrieve the schema for the given event name.
+ ///
+ public bool TryGetEventSchema(string eventName, out CESEventSchema schema)
+ {
+ return Events.TryGetValue(eventName, out schema);
+ }
+
+ // ─────────────────────────────────────────────────────────────────────
+ // Parse
+ // ─────────────────────────────────────────────────────────────────────
+
+ ///
+ /// Parses the raw bytes of the __events_schema CLValue (type Any)
+ /// into a .
+ ///
+ ///
+ /// The Bytes property of the CLValue retrieved from __events_schema.
+ ///
+ public static CESContractSchema ParseSchema(byte[] rawBytes)
+ {
+ using var ms = new MemoryStream(rawBytes);
+ using var reader = new BinaryReader(ms);
+
+ var numEvents = reader.ReadInt32();
+ var events = new Dictionary(numEvents);
+
+ for (int i = 0; i < numEvents; i++)
+ {
+ var eventName = reader.ReadCLString();
+
+ var numFields = reader.ReadInt32();
+ var fields = new List(numFields);
+
+ for (int j = 0; j < numFields; j++)
+ {
+ var fieldName = reader.ReadCLString();
+ var fieldType = reader.ReadCLTypeInfo();
+ fields.Add(new CESEventSchemaField(fieldName, fieldType));
+ }
+
+ events[eventName] = new CESEventSchema(eventName, fields);
+ }
+
+ return new CESContractSchema(events);
+ }
+
+ // ─────────────────────────────────────────────────────────────────────
+ // Network factory
+ // ─────────────────────────────────────────────────────────────────────
+
+ ///
+ /// Fetches and parses the CES event schema for a contract from the Casper network.
+ ///
+ ///
+ ///
+ /// The parameter accepts either a
+ /// contract hash or a contract-package hash:
+ ///
+ ///
+ /// -
+ /// If the value starts with "contract-" (e.g. "contract-dead…beef")
+ /// it is treated as a direct contract hash and the package-resolution step is skipped.
+ /// The parameter is ignored in this case.
+ ///
+ /// -
+ /// Otherwise it is treated as a contract-package hash
+ /// (e.g. "hash-abc…def" or "package-abc…def")
+ /// and the method enumerates the package to resolve the target contract version:
+ /// when is provided that exact version is selected;
+ /// when is null (the default) the highest-numbered
+ /// active (non-disabled) version is used.
+ ///
+ ///
+ ///
+ /// After resolving the contract the method fetches its named-key list to locate
+ /// the __events_schema and __events URefs, then queries the schema
+ /// CLValue directly from the __events_schema URef and parses it into a
+ /// .
+ /// The resulting schema has , ,
+ /// , and automatically populated.
+ ///
+ ///
+ /// Both the legacy (ContractPackage) and the new Casper-2.x
+ /// (Package / entity) contract models are supported.
+ ///
+ ///
+ /// An active instance.
+ ///
+ /// Either a contract hash (prefix "contract-") or a contract-package hash
+ /// (e.g. "hash-abc…def" or "package-abc…def").
+ ///
+ ///
+ /// The contract version number to load when a package hash is supplied.
+ /// Pass null (the default) to load the latest active version.
+ /// Ignored when a direct contract hash is provided.
+ ///
+ ///
+ /// A fully-populated with
+ /// and set.
+ /// When a direct contract hash is supplied, is null.
+ ///
+ ///
+ /// Thrown when or is null.
+ ///
+ ///
+ /// Thrown when a specific was requested but not found in the
+ /// package.
+ ///
+ ///
+ /// Thrown when the package has no active versions, the RPC result is in an unexpected
+ /// format, or the __events_schema named key is absent.
+ ///
+ public static async Task LoadAsync(
+ ICasperClient client,
+ string contractPackageHash,
+ uint? version = null)
+ {
+ if (client == null)
+ throw new ArgumentNullException(nameof(client));
+ if (string.IsNullOrWhiteSpace(contractPackageHash))
+ throw new ArgumentNullException(nameof(contractPackageHash));
+
+ string contractHash;
+ GlobalStateKey resolvedPackageHash;
+
+ if (contractPackageHash.StartsWith("contract-", StringComparison.OrdinalIgnoreCase))
+ {
+ // Input is already a contract hash — skip package resolution entirely.
+ contractHash = contractPackageHash;
+ resolvedPackageHash = null;
+ }
+ else
+ {
+ // ── 1. Fetch the package to enumerate versions ────────────────────
+ var pkgResult = (await client.QueryGlobalState(contractPackageHash)).Parse();
+ resolvedPackageHash = GlobalStateKey.FromString(contractPackageHash);
+
+ // ── 2. Resolve the target contract version ────────────────────────
+ if (pkgResult.StoredValue != null &&
+ pkgResult.StoredValue.ContractPackage != null)
+ {
+ // Legacy node: Versions contains only active (non-disabled) entries.
+ var pkg = pkgResult.StoredValue.ContractPackage;
+
+ ContractVersion chosen;
+ if (version.HasValue)
+ {
+ chosen = pkg.Versions.FirstOrDefault(v => v.Version == version.Value)
+ ?? throw new KeyNotFoundException(
+ $"Version {version.Value} not found in contract package '{contractPackageHash}'.");
+ }
+ else
+ {
+ chosen = pkg.Versions.OrderByDescending(v => v.Version).FirstOrDefault()
+ ?? throw new InvalidOperationException(
+ $"Contract package '{contractPackageHash}' has no active versions.");
+ }
+
+ contractHash = chosen.Hash; // e.g. "contract-hash-abc…def"
+ }
+ else if (pkgResult.StoredValue != null &&
+ pkgResult.StoredValue.Package != null)
+ {
+ // New node: Versions contains all versions; filter out disabled ones for latest.
+ var pkg = pkgResult.StoredValue.Package;
+ var disabledSet = new HashSet(
+ pkg.DisabledVersions?.Select(d => d.Version) ?? Enumerable.Empty());
+
+ EntityVersionAndHash chosen;
+ if (version.HasValue)
+ {
+ chosen = pkg.Versions.FirstOrDefault(v => v.EntityVersion.Version == version.Value)
+ ?? throw new KeyNotFoundException(
+ $"Version {version.Value} not found in contract package '{contractPackageHash}'.");
+ }
+ else
+ {
+ chosen = pkg.Versions
+ .Where(v => !disabledSet.Contains(v.EntityVersion.Version))
+ .OrderByDescending(v => v.EntityVersion.Version)
+ .FirstOrDefault()
+ ?? throw new InvalidOperationException(
+ $"Contract package '{contractPackageHash}' has no active versions.");
+ }
+
+ contractHash = chosen.AddressableEntity.ToString(); // e.g. "entity-contract-abc…def"
+ }
+ else
+ {
+ throw new InvalidOperationException(
+ $"GetPackage returned neither a ContractPackage nor a Package for '{contractPackageHash}'.");
+ }
+ }
+
+ var contractKey = GlobalStateKey.FromString(contractHash);
+
+ // ── 3. Fetch named keys to locate __events_schema and __events ────
+ List namedKeys;
+ if (contractKey is AddressableEntityKey)
+ {
+ // New node (Casper 2.x): named keys are returned by state_get_entity.
+ var entityResult = (await client.GetEntity(contractHash)).Parse();
+ namedKeys = entityResult.NamedKeys
+ ?? throw new InvalidOperationException(
+ $"No named keys returned for entity '{contractHash}'.");
+
+ resolvedPackageHash ??= entityResult.Entity.Package;
+ }
+ else
+ {
+ // Legacy node: QueryGlobalState without a path returns the Contract
+ // object, which includes its named-key list.
+ var contractResult = (await client.QueryGlobalState(contractKey)).Parse();
+ namedKeys = contractResult.StoredValue?.Contract?.NamedKeys
+ ?? throw new InvalidOperationException(
+ $"No named keys found for contract '{contractHash}'.");
+
+ resolvedPackageHash ??= GlobalStateKey.FromString(
+ contractResult.StoredValue.Contract.ContractPackageHash);
+ }
+
+ var schemaNamedKey = namedKeys.FirstOrDefault(k => k.Name == "__events_schema")
+ ?? throw new InvalidOperationException(
+ $"Named key '__events_schema' not found for contract '{contractHash}'.");
+
+ var eventsNamedKey = namedKeys.FirstOrDefault(k => k.Name == "__events")
+ ?? throw new InvalidOperationException(
+ $"Named key '__events' not found for contract '{contractHash}'.");
+
+ var schemaURef = schemaNamedKey.Key as URef
+ ?? throw new InvalidOperationException(
+ $"Expected a URef for '__events_schema' named key, got '{schemaNamedKey.Key?.GetType().Name}'.");
+
+ var eventsURef = eventsNamedKey.Key as URef
+ ?? throw new InvalidOperationException(
+ $"Expected a URef for '__events' named key, got '{eventsNamedKey.Key?.GetType().Name}'.");
+
+ // ── 4. Query the schema CLValue directly from its URef ────────────
+ var schemaResult = (await client.QueryGlobalState(schemaURef)).Parse();
+
+ var clValue = schemaResult.StoredValue?.CLValue
+ ?? throw new InvalidOperationException(
+ $"No CLValue at '__events_schema' URef for contract '{contractHash}'.");
+
+ // ── 5. Parse and annotate ─────────────────────────────────────────
+ var parsed = ParseSchema(clValue.Bytes);
+ return new CESContractSchema(parsed.Events)
+ {
+ ContractHash = contractKey.ToString(),
+ ContractPackageHash = resolvedPackageHash.ToString(),
+ SchemaURef = schemaURef,
+ EventsURef = eventsURef,
+ };
+ }
+ }
+}
diff --git a/Casper.Network.SDK/Types/CLValueReader.cs b/Casper.Network.SDK/Types/CLValueReader.cs
new file mode 100644
index 0000000..d3113b4
--- /dev/null
+++ b/Casper.Network.SDK/Types/CLValueReader.cs
@@ -0,0 +1,233 @@
+using System;
+using System.IO;
+
+namespace Casper.Network.SDK.Types
+{
+ ///
+ /// Reads a from a binary stream given a descriptor.
+ /// The caller is responsible for knowing the type layout of the stream; no type tag bytes are read.
+ ///
+ public class CLValueReader
+ {
+ private readonly BinaryReader _reader;
+
+ public CLValueReader(BinaryReader reader)
+ {
+ _reader = reader ?? throw new ArgumentNullException(nameof(reader));
+ }
+
+ ///
+ /// Reads from the underlying stream the bytes that represent a value of the given type
+ /// and returns a constructed from those bytes and the provided type info.
+ ///
+ public CLValue Read(CLTypeInfo typeInfo)
+ {
+ if (typeInfo == null)
+ throw new ArgumentNullException(nameof(typeInfo));
+
+ using var ms = new MemoryStream();
+ AppendValueBytes(ms, typeInfo);
+ return new CLValue(ms.ToArray(), typeInfo);
+ }
+
+ private void AppendValueBytes(MemoryStream ms, CLTypeInfo typeInfo)
+ {
+ switch (typeInfo.Type)
+ {
+ case CLType.Bool:
+ case CLType.U8:
+ {
+ ms.WriteByte(_reader.ReadByte());
+ break;
+ }
+
+ case CLType.I32:
+ case CLType.U32:
+ {
+ var bytes = _reader.ReadBytes(4);
+ ms.Write(bytes, 0, bytes.Length);
+ break;
+ }
+
+ case CLType.I64:
+ case CLType.U64:
+ {
+ var bytes = _reader.ReadBytes(8);
+ ms.Write(bytes, 0, bytes.Length);
+ break;
+ }
+
+ case CLType.U128:
+ case CLType.U256:
+ case CLType.U512:
+ {
+ // length-prefixed little-endian big integer: 1 byte length + N bytes
+ var len = _reader.ReadByte();
+ ms.WriteByte(len);
+ if (len > 0)
+ {
+ var bytes = _reader.ReadBytes(len);
+ ms.Write(bytes, 0, bytes.Length);
+ }
+ break;
+ }
+
+ case CLType.Unit:
+ break; // zero bytes on the wire
+
+ case CLType.String:
+ {
+ // 4-byte LE length prefix followed by UTF-8 bytes
+ var lenBytes = _reader.ReadBytes(4);
+ ms.Write(lenBytes, 0, lenBytes.Length);
+ var len = ToInt32LE(lenBytes);
+ if (len > 0)
+ {
+ var strBytes = _reader.ReadBytes(len);
+ ms.Write(strBytes, 0, strBytes.Length);
+ }
+ break;
+ }
+
+ case CLType.URef:
+ {
+ // 32-byte key + 1-byte access rights
+ var bytes = _reader.ReadBytes(33);
+ ms.Write(bytes, 0, bytes.Length);
+ break;
+ }
+
+ case CLType.PublicKey:
+ {
+ // 1-byte algo prefix (already included in key size) + key bytes
+ int algo = _reader.PeekChar();
+ var keyLen = algo == 0x01
+ ? KeyAlgo.ED25519.GetKeySizeInBytes()
+ : KeyAlgo.SECP256K1.GetKeySizeInBytes();
+ var bytes = _reader.ReadBytes(keyLen);
+ ms.Write(bytes, 0, bytes.Length);
+ break;
+ }
+
+ case CLType.Key:
+ {
+ // 1-byte tag + variant payload; mirrors GlobalStateKey.ReadCLGlobalStateKey
+ int keyId = _reader.PeekChar();
+ int keyLen;
+ if (keyId == (char)KeyIdentifier.EraInfo)
+ keyLen = 9; // 1 tag + 8 (u64)
+ else if (keyId == (char)KeyIdentifier.URef)
+ keyLen = 34; // 1 tag + 33 (URef)
+ else
+ keyLen = 33; // 1 tag + 32 (hash)
+ var bytes = _reader.ReadBytes(keyLen);
+ ms.Write(bytes, 0, bytes.Length);
+ break;
+ }
+
+ case CLType.Option:
+ {
+ if (typeInfo is not CLOptionTypeInfo optionTypeInfo)
+ throw new Exception("Expected CLOptionTypeInfo for Option CLType.");
+ var tag = _reader.ReadByte();
+ ms.WriteByte(tag);
+ if (tag != 0x00) // Some variant – read the inner value
+ AppendValueBytes(ms, optionTypeInfo.OptionType);
+ break;
+ }
+
+ case CLType.List:
+ {
+ if (typeInfo is not CLListTypeInfo listTypeInfo)
+ throw new Exception("Expected CLListTypeInfo for List CLType.");
+ var countBytes = _reader.ReadBytes(4);
+ ms.Write(countBytes, 0, countBytes.Length);
+ var count = ToInt32LE(countBytes);
+ for (var i = 0; i < count; i++)
+ AppendValueBytes(ms, listTypeInfo.ListType);
+ break;
+ }
+
+ case CLType.ByteArray:
+ {
+ if (typeInfo is not CLByteArrayTypeInfo baTypeInfo)
+ throw new Exception("Expected CLByteArrayTypeInfo for ByteArray CLType.");
+ var bytes = _reader.ReadBytes(baTypeInfo.Size);
+ ms.Write(bytes, 0, bytes.Length);
+ break;
+ }
+
+ case CLType.Result:
+ {
+ if (typeInfo is not CLResultTypeInfo resultTypeInfo)
+ throw new Exception("Expected CLResultTypeInfo for Result CLType.");
+ var tag = _reader.ReadByte();
+ ms.WriteByte(tag);
+ // 0x01 = Ok, 0x00 = Err
+ AppendValueBytes(ms, tag == 0x01 ? resultTypeInfo.Ok : resultTypeInfo.Err);
+ break;
+ }
+
+ case CLType.Map:
+ {
+ if (typeInfo is not CLMapTypeInfo mapTypeInfo)
+ throw new Exception("Expected CLMapTypeInfo for Map CLType.");
+ var countBytes = _reader.ReadBytes(4);
+ ms.Write(countBytes, 0, countBytes.Length);
+ var count = ToInt32LE(countBytes);
+ for (var i = 0; i < count; i++)
+ {
+ AppendValueBytes(ms, mapTypeInfo.KeyType);
+ AppendValueBytes(ms, mapTypeInfo.ValueType);
+ }
+ break;
+ }
+
+ case CLType.Tuple1:
+ {
+ if (typeInfo is not CLTuple1TypeInfo tuple1TypeInfo)
+ throw new Exception("Expected CLTuple1TypeInfo for Tuple1 CLType.");
+ AppendValueBytes(ms, tuple1TypeInfo.Type0);
+ break;
+ }
+
+ case CLType.Tuple2:
+ {
+ if (typeInfo is not CLTuple2TypeInfo tuple2TypeInfo)
+ throw new Exception("Expected CLTuple2TypeInfo for Tuple2 CLType.");
+ AppendValueBytes(ms, tuple2TypeInfo.Type0);
+ AppendValueBytes(ms, tuple2TypeInfo.Type1);
+ break;
+ }
+
+ case CLType.Tuple3:
+ {
+ if (typeInfo is not CLTuple3TypeInfo tuple3TypeInfo)
+ throw new Exception("Expected CLTuple3TypeInfo for Tuple3 CLType.");
+ AppendValueBytes(ms, tuple3TypeInfo.Type0);
+ AppendValueBytes(ms, tuple3TypeInfo.Type1);
+ AppendValueBytes(ms, tuple3TypeInfo.Type2);
+ break;
+ }
+
+ default:
+ throw new Exception($"Unknown/unsupported CLType '{typeInfo.Type}'");
+ }
+ }
+
+ ///
+ /// Interprets four bytes as a little-endian signed 32-bit integer, regardless of host endianness.
+ ///
+ private static int ToInt32LE(byte[] bytes)
+ {
+ if (!BitConverter.IsLittleEndian)
+ {
+ var copy = new byte[4];
+ Array.Copy(bytes, copy, 4);
+ Array.Reverse(copy);
+ return BitConverter.ToInt32(copy, 0);
+ }
+ return BitConverter.ToInt32(bytes, 0);
+ }
+ }
+}
diff --git a/Casper.Network.SDK/Types/GlobalStateKey/GlobalStateKey.cs b/Casper.Network.SDK/Types/GlobalStateKey/GlobalStateKey.cs
index 1655fed..424f610 100644
--- a/Casper.Network.SDK/Types/GlobalStateKey/GlobalStateKey.cs
+++ b/Casper.Network.SDK/Types/GlobalStateKey/GlobalStateKey.cs
@@ -145,6 +145,9 @@ protected GlobalStateKey(string key, string keyPrefix)
Key = keyPrefix + Hex.ToHexString(bytes);
}
+ ///
+ /// Returns a string with the hex string representation of the key, without the permissions byte.
+ ///
public string ToHexString()
{
return Hex.ToHexString(RawBytes);
diff --git a/Casper.Network.SDK/Utils/BinaryReaderExtensions.cs b/Casper.Network.SDK/Utils/BinaryReaderExtensions.cs
index 31e4033..235f318 100644
--- a/Casper.Network.SDK/Utils/BinaryReaderExtensions.cs
+++ b/Casper.Network.SDK/Utils/BinaryReaderExtensions.cs
@@ -385,5 +385,26 @@ public static TItem ReadCLItem(BinaryReader reader, CLTypeInfo typeInfo)
return (TItem) item;
}
+
+ ///
+ /// Reads a from the binary stream.
+ ///
+ public static CLTypeInfo ReadCLTypeInfo(this BinaryReader reader)
+ {
+ var tag = (CLType)reader.ReadByte();
+
+ return tag switch
+ {
+ CLType.Option => new CLOptionTypeInfo(ReadCLTypeInfo(reader)),
+ CLType.List => new CLListTypeInfo(ReadCLTypeInfo(reader)),
+ CLType.ByteArray => new CLByteArrayTypeInfo(reader.ReadInt32()),
+ CLType.Result => new CLResultTypeInfo(ReadCLTypeInfo(reader), ReadCLTypeInfo(reader)),
+ CLType.Map => new CLMapTypeInfo(ReadCLTypeInfo(reader), ReadCLTypeInfo(reader)),
+ CLType.Tuple1 => new CLTuple1TypeInfo(ReadCLTypeInfo(reader)),
+ CLType.Tuple2 => new CLTuple2TypeInfo(ReadCLTypeInfo(reader), ReadCLTypeInfo(reader)),
+ CLType.Tuple3 => new CLTuple3TypeInfo(ReadCLTypeInfo(reader), ReadCLTypeInfo(reader), ReadCLTypeInfo(reader)),
+ _ => new CLTypeInfo(tag)
+ };
+ }
}
}
\ No newline at end of file
diff --git a/Docs/Articles/CasperEventStandard.md b/Docs/Articles/CasperEventStandard.md
new file mode 100644
index 0000000..6d87478
--- /dev/null
+++ b/Docs/Articles/CasperEventStandard.md
@@ -0,0 +1,246 @@
+# Casper Event Standard (CES)
+
+The **Casper Event Standard (CES)** is a convention adopted by contract developers for emitting and consuming typed events from Casper smart contracts. Contracts that follow CES store a self-describing schema alongside their events, making it possible to decode any event without out-of-band knowledge of its structure.
+
+The Casper .NET SDK provides the `Casper.Network.SDK.CES` namespace with three classes: `CESContractSchema` (schema loading and parsing), `CESEvent` (event parsing and field access), and `CESParser` (scanning execution results).
+
+---
+
+## How CES Works
+
+A CES-compliant contract uses two special named keys for events:
+
+| Named key | Purpose |
+|---|---|---|
+| `__events_schema` | Binary-encoded schema listing every event type and its fields |
+| `__events` | Ordered map of event index → raw event bytes |
+
+### Schema (`__events_schema`)
+
+The schema describes every event the contract can emit. It is stored as a CLValue of type `Any`, whose raw bytes encode a sequence of event descriptors:
+
+```
+u32 LE — number of events
+for each event:
+ String — event name (4-byte LE length + UTF-8 bytes)
+ u32 LE — number of fields
+ for each field:
+ String — field name
+ CLType bytes — Casper binary type encoding (tag byte + optional inner types)
+```
+
+The `String` encoding used here is the standard Casper string encoding: a `u32` length followed by the UTF-8 content (no null terminator).
+
+### Events (`__events`)
+
+Each entry in the `__events` map is a `Bytes` (`Vec`) value. The raw bytes of one event entry are laid out as follows:
+
+```
+u32 LE — Vec outer length (= remaining bytes; auto-detected and skipped)
+String — event name, prefixed with "event_" (e.g. "event_Mint")
+for each field (in schema order):
+ raw field bytes — native Casper serialization, no CLValue wrapper
+```
+
+The `"event_"` prefix is stripped automatically when parsing the event raw bytes.
+
+---
+
+## Loading a Contract Schema from the Network
+
+The simplest way to load a contract's schema is `CESContractSchema.LoadAsync`. It accepts either a **contract-package hash** or a direct **contract hash**, reads the named keys to locate `__events_schema` and `__events`, and fetches and parses the schema — all in a single call.
+
+### Using a contract-package hash
+
+When you pass a contract-package hash, the method resolves the latest active contract version automatically. You can also request a specific version with the optional `version` parameter.
+
+```csharp
+using Casper.Network.SDK.CES;
+
+var client = new NetCasperClient("https://rpc.testnet.casperlabs.io/rpc");
+
+// Load the schema for the latest active version of the contract package
+CESContractSchema schema = await CESContractSchema.LoadAsync(
+ client,
+ "hash-17cb23ce7b6d663fc82bacbdd56a26dc722cabe7e69c84a3ca729bf3cb7fdc70");
+```
+
+### Using a contract hash directly
+
+If you already know the contract hash, you can pass it directly and the package-resolution step is skipped entirely. Use `"contract-"` prefix instead of `"hash-"` for a contract hash argument.The `version` parameter is ignored in this case.
+
+```csharp
+CESContractSchema schema = await CESContractSchema.LoadAsync(
+ client,
+ "contract-dead1234dead1234dead1234dead1234dead1234dead1234dead1234dead1234beef");
+```
+
+The returned `CESContractSchema` is fully annotated:
+
+| Property | Description |
+|---|------------------------------------------------------------------|
+| `Events` | Dictionary of event name → `CESEventSchema` |
+| `ContractHash` | Hash of the resolved (or supplied) contract version |
+| `ContractPackageHash` | Package hash passed in. May be `null` when a direct contract hash was supplied. |
+| `SchemaURef` | URef of the `__events_schema` named key |
+| `EventsURef` | URef of the `__events` named key (used when scanning transforms) |
+
+Both legacy (Casper 1.x) and Casper 2.x contract models are supported automatically.
+
+### Parsing a Schema from Raw Bytes
+
+If you have already fetched the `__events_schema` CLValue from the network, you can parse it directly:
+
+```csharp
+byte[] schemaBytes = /* raw bytes from __events_schema CLValue */;
+CESContractSchema schema = CESContractSchema.ParseSchema(schemaBytes);
+
+// Inspect the event types the contract supports
+foreach (var kvp in schema.Events)
+{
+ Console.WriteLine($"Event: {kvp.Key}");
+ foreach (var field in kvp.Value.Fields)
+ Console.WriteLine($" {field.Name}: {field.CLTypeInfo.Type}");
+}
+```
+
+Note that a schema created via `ParseSchema` directly has `SchemaURef` and `EventsURef` set to `null`. Use `LoadAsync` when you need these URefs.
+
+---
+
+## Parsing a Single Event
+
+To decode one event payload, pass the raw event bytes together with a schema to `CESEvent.ParseEvent`:
+
+```csharp
+byte[] eventBytes = /* raw bytes of one entry from __events map */;
+CESEvent evt = CESEvent.ParseEvent(eventBytes, schema);
+
+Console.WriteLine(evt.Name); // e.g. "event_Mint"
+
+// Access field values directly by name — returns CLValue, or null if not found
+CLValue recipient = evt["recipient"];
+Console.WriteLine(recipient.ToGlobalStateKey()); // hash-1262d06e...
+
+CLValue amount = evt["amount"];
+Console.WriteLine(amount.ToBigInteger()); // 1000000000000000000
+```
+
+The indexer `evt["fieldName"]` returns the `CLValue` of that field, or `null` if the field name is not found. The full list of fields is also available as `IReadOnlyList` through `evt.Fields`, which preserves both name and value.
+
+### Looking Up an Event Schema
+
+```csharp
+if (schema.TryGetEventSchema("Mint", out CESEventSchema mintSchema))
+{
+ Console.WriteLine($"Mint has {mintSchema.Fields.Count} fields.");
+}
+```
+
+---
+
+## Scanning Execution Results with `GetEvents`
+
+`CESParser.GetEvents` scans the transform list of an execution result and returns all CES events emitted by any of the watched contracts:
+
+```csharp
+var events = CESParser.GetEvents(
+ executionResult.Effect,
+ new List { schema1, schema2 });
+```
+
+For each transform the method:
+1. Skips transforms whose key is not a `DictionaryKey`.
+2. Skips transforms whose kind is not a `WriteTransformKind`.
+3. Tries to parse the CLValue as a `CLValueDictionary`; skips on failure.
+4. Matches the dictionary seed against each schema's `EventsURef` (access rights are ignored; only the 32-byte hash is compared).
+5. Parses the matching event payload using the schema.
+6. Silently skips events whose name is not present in the schema (graceful version mismatch handling).
+
+Each schema in the list must have `EventsURef` set (schemas loaded via `LoadAsync` always have it). Schemas with a `null` `EventsURef` are silently skipped.
+
+The returned `CESEvent` objects include execution-result context:
+
+| Property | Description |
+|---|---|
+| `Name` | Raw event name as stored in bytes (e.g. `"event_Transfer"`) |
+| `Fields` | Ordered list of `NamedArg` field values |
+| `ContractHash` | Propagated from the matched schema |
+| `ContractPackageHash` | Propagated from the matched schema |
+| `TransformKey` | `GlobalStateKey` of the transform that emitted the event |
+| `TransformId` | Zero-based index in the effect list |
+| `EventId` | Sequential event counter key from `__events` |
+
+---
+
+## Data Model
+
+```
+CESContractSchema
+ ├── ContractHash: string
+ ├── ContractPackageHash: string
+ ├── SchemaURef: URef
+ ├── EventsURef: URef
+ └── Events: Dictionary
+ └── CESEventSchema
+ ├── EventName: string
+ └── Fields: IReadOnlyList
+ ├── Name: string
+ └── CLTypeInfo: CLTypeInfo
+
+CESEvent
+ ├── Name: string (raw name, e.g. "event_Transfer")
+ ├── Fields: IReadOnlyList
+ ├── ContractHash: string
+ ├── ContractPackageHash: string
+ ├── TransformKey: GlobalStateKey
+ ├── TransformId: int
+ └── EventId: string
+```
+
+---
+
+## Real-World Example — Watching Multiple Contracts
+
+The following example loads schemas for two contracts, fetches a transaction, and prints the events emitted:
+
+```csharp
+using System.Collections.Generic;
+using System.Linq;
+using System.Text.Json;
+using Casper.Network.SDK;
+using Casper.Network.SDK.CES;
+
+var client = new NetCasperClient("https://node.testnet.casper.network/rpc");
+
+// Load the schema for each contract to watch
+var minterSchema = await CESContractSchema.LoadAsync(
+ client,
+ "hash-1262d06e53125ea098187fb4d1d5b10a7afed48e5e5eef182ed992fc5b100349");
+
+var cep18Schema = await CESContractSchema.LoadAsync(
+ client,
+ "hash-17cb23ce7b6d663fc82bacbdd56a26dc722cabe7e69c84a3ca729bf3cb7fdc70");
+
+// Fetch a transaction and extract its execution result transforms
+var getTxResult = await client.GetTransaction(
+ "8e46a16fc8fc15c38405e092959fb20acc44dcfca1d1caecb9bc59d018f50df6");
+var txResult = getTxResult.Parse();
+
+// Scan for CES events emitted by either watched contract
+var events = CESParser.GetEvents(
+ txResult.ExecutionInfo.ExecutionResult.Effect,
+ new List { minterSchema, cep18Schema });
+
+// Work with a specific event
+var buyEvent = events.FirstOrDefault(e => e.Name == "Buy");
+if (buyEvent != null)
+{
+ Console.WriteLine("Buy token: " + buyEvent["token"].ToGlobalStateKey());
+ Console.WriteLine("Buy amount: " + buyEvent["amount_token_out"].ToBigInteger());
+}
+
+// Serialize all events to JSON
+var json = JsonSerializer.Serialize(events);
+Console.WriteLine(json);
+```
diff --git a/Docs/Articles/toc.yml b/Docs/Articles/toc.yml
index 9f25e4b..e0ec0a6 100644
--- a/Docs/Articles/toc.yml
+++ b/Docs/Articles/toc.yml
@@ -9,4 +9,6 @@
href: KeyManagement.md
- name: Working with CLValue
href: WorkingWithCLValue.md
+ - name: Casper Event Standard (CES)
+ href: CasperEventStandard.md