Skip to content

Commit 9b8ea93

Browse files
feat: RPC queries, typed events, protobuf reader, type URLs (v2.0.0)
Blue C# SDK v2.0.0 — matching JS SDK learnings from TKD Alex comparison: 1. RpcClient.cs — Tendermint ABCI queries via RPC (faster than LCD) - QueryNodesAsync, QueryNodeAsync, QueryBalanceAsync, QueryNodesForPlanAsync - Endpoint failover across 3 RPC endpoints - Protobuf request encoding + response decoding 2. ProtobufReader.cs — Wire-format decoder (complements ProtobufWriter) - Decode varint, length-delimited, embedded messages - DecodeNode, DecodePrice helpers for chain response parsing 3. EventParser.cs — Typed event parsers (replaces string matching) - EventTypes constants for all Sentinel chain events - ParseSessionCreated, ParseSessionPay, ParseSubscriptionSession - ExtractSessionId with fallback to raw JSON search - Typed record types: SessionCreatedEvent, SessionPayEvent, etc. 4. TypeUrls.cs — All 23 Sentinel + Cosmos message type URL constants - Matches JS SDK TYPE_URLS export exactly - String constants for compile-time safety 5. Blue branding — package renamed to Blue.Sentinel.SDK.Core - Version bumped to 2.0.0 - Updated URLs to blue-csharp-sdk repo Build: 0 errors, 29 warnings (pre-existing xUnit analyzer warnings).
1 parent 27fb487 commit 9b8ea93

6 files changed

Lines changed: 646 additions & 7 deletions

File tree

src/Sentinel.SDK.Core/Constants.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ public static class Constants
4343
// ─── Version Info ───
4444

4545
/// <summary>SDK version string.</summary>
46-
public const string SdkVersion = "1.0.0";
46+
public const string SdkVersion = "2.0.0";
4747

4848
/// <summary>Compatible Sentinel chain version.</summary>
4949
public const string ChainVersion = "v12.0.0";
Lines changed: 269 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,269 @@
1+
using System.Text.Json;
2+
3+
namespace Sentinel.SDK.Core;
4+
5+
/// <summary>
6+
/// Typed event parsers for Sentinel chain transaction events.
7+
/// Replaces string-matching with structured parsers that guarantee field access.
8+
/// Matches the JS SDK's protocol/events.js pattern.
9+
/// </summary>
10+
public static class EventParser
11+
{
12+
// ─── Event Type Constants ──────────────────────────────────────
13+
14+
/// <summary>Sentinel chain event type URLs.</summary>
15+
public static class EventTypes
16+
{
17+
public const string NodeCreateSession = "sentinel.node.v3.EventCreateSession";
18+
public const string NodePay = "sentinel.node.v3.EventPay";
19+
public const string NodeRefund = "sentinel.node.v3.EventRefund";
20+
public const string NodeUpdateStatus = "sentinel.node.v3.EventUpdateStatus";
21+
public const string SessionEnd = "sentinel.session.v3.EventEnd";
22+
public const string SessionUpdateDetails = "sentinel.session.v3.EventUpdateDetails";
23+
public const string SubscriptionCreate = "sentinel.subscription.v3.EventCreate";
24+
public const string SubscriptionCreateSession = "sentinel.subscription.v3.EventCreateSession";
25+
public const string SubscriptionPay = "sentinel.subscription.v3.EventPay";
26+
public const string SubscriptionEnd = "sentinel.subscription.v3.EventEnd";
27+
public const string LeaseCreate = "sentinel.lease.v1.EventCreate";
28+
public const string LeaseEnd = "sentinel.lease.v1.EventEnd";
29+
}
30+
31+
// ─── Parsed Event Records ──────────────────────────────────────
32+
33+
/// <summary>Parsed sentinel.node.v3.EventCreateSession.</summary>
34+
public record SessionCreatedEvent(
35+
long SessionId,
36+
string AccAddress,
37+
string NodeAddress,
38+
string MaxBytes,
39+
string MaxDuration
40+
);
41+
42+
/// <summary>Parsed sentinel.node.v3.EventPay.</summary>
43+
public record SessionPayEvent(
44+
long SessionId,
45+
string AccAddress,
46+
string NodeAddress,
47+
string Payment,
48+
string StakingReward
49+
);
50+
51+
/// <summary>Parsed sentinel.session.v3.EventEnd.</summary>
52+
public record SessionEndEvent(
53+
long SessionId,
54+
string AccAddress,
55+
string NodeAddress
56+
);
57+
58+
/// <summary>Parsed sentinel.subscription.v3.EventCreateSession.</summary>
59+
public record SubscriptionSessionEvent(
60+
long SessionId,
61+
long SubscriptionId,
62+
string AccAddress,
63+
string NodeAddress
64+
);
65+
66+
/// <summary>Parsed sentinel.subscription.v3.EventCreate.</summary>
67+
public record SubscriptionCreatedEvent(
68+
long SubscriptionId,
69+
long PlanId,
70+
string AccAddress
71+
);
72+
73+
/// <summary>Parsed sentinel.lease.v1.EventCreate.</summary>
74+
public record LeaseCreatedEvent(
75+
long LeaseId,
76+
string NodeAddress,
77+
string ProvAddress,
78+
int MaxHours
79+
);
80+
81+
// ─── Parsing Methods ───────────────────────────────────────────
82+
83+
/// <summary>
84+
/// Extract session ID from a transaction result's event log.
85+
/// Searches both node.v3.EventCreateSession and subscription.v3.EventCreateSession.
86+
/// </summary>
87+
/// <param name="txEventsJson">The raw events JSON from a broadcast TX result.</param>
88+
/// <returns>Session ID, or null if not found.</returns>
89+
public static long? ExtractSessionId(string? txEventsJson)
90+
{
91+
if (string.IsNullOrEmpty(txEventsJson)) return null;
92+
93+
try
94+
{
95+
using var doc = JsonDocument.Parse(txEventsJson);
96+
var root = doc.RootElement;
97+
98+
// Handle both array of events and nested log formats
99+
var events = root.ValueKind == JsonValueKind.Array ? root : default;
100+
101+
if (events.ValueKind == JsonValueKind.Array)
102+
{
103+
foreach (var ev in events.EnumerateArray())
104+
{
105+
var sessionId = TryExtractSessionIdFromEvent(ev);
106+
if (sessionId.HasValue) return sessionId;
107+
}
108+
}
109+
}
110+
catch
111+
{
112+
// Fallback: try to find session_id pattern in raw string
113+
return ExtractSessionIdFromRaw(txEventsJson);
114+
}
115+
116+
return ExtractSessionIdFromRaw(txEventsJson);
117+
}
118+
119+
/// <summary>
120+
/// Parse a specific event type from TX result events JSON.
121+
/// </summary>
122+
/// <param name="txEventsJson">Raw events JSON.</param>
123+
/// <param name="eventType">Event type URL to search for (use EventTypes constants).</param>
124+
/// <returns>Dictionary of attribute key→value pairs, or null if event not found.</returns>
125+
public static Dictionary<string, string>? FindEvent(string? txEventsJson, string eventType)
126+
{
127+
if (string.IsNullOrEmpty(txEventsJson)) return null;
128+
129+
try
130+
{
131+
using var doc = JsonDocument.Parse(txEventsJson);
132+
if (doc.RootElement.ValueKind != JsonValueKind.Array) return null;
133+
134+
foreach (var ev in doc.RootElement.EnumerateArray())
135+
{
136+
if (!ev.TryGetProperty("type", out var typeProp)) continue;
137+
if (typeProp.GetString() != eventType) continue;
138+
139+
var attrs = new Dictionary<string, string>();
140+
if (ev.TryGetProperty("attributes", out var attrArray) && attrArray.ValueKind == JsonValueKind.Array)
141+
{
142+
foreach (var attr in attrArray.EnumerateArray())
143+
{
144+
var key = DecodeAttr(attr, "key");
145+
var value = DecodeAttr(attr, "value");
146+
if (key != null) attrs[key] = value ?? "";
147+
}
148+
}
149+
return attrs;
150+
}
151+
}
152+
catch { }
153+
154+
return null;
155+
}
156+
157+
/// <summary>Parse a SessionCreatedEvent from TX result events.</summary>
158+
public static SessionCreatedEvent? ParseSessionCreated(string? txEventsJson)
159+
{
160+
var attrs = FindEvent(txEventsJson, EventTypes.NodeCreateSession);
161+
if (attrs is null) return null;
162+
163+
return new SessionCreatedEvent(
164+
SessionId: ParseLong(attrs.GetValueOrDefault("session_id") ?? attrs.GetValueOrDefault("id")),
165+
AccAddress: attrs.GetValueOrDefault("acc_address") ?? attrs.GetValueOrDefault("address") ?? "",
166+
NodeAddress: attrs.GetValueOrDefault("node_address") ?? "",
167+
MaxBytes: attrs.GetValueOrDefault("max_bytes") ?? "0",
168+
MaxDuration: attrs.GetValueOrDefault("max_duration") ?? "0"
169+
);
170+
}
171+
172+
/// <summary>Parse a SessionPayEvent from TX result events.</summary>
173+
public static SessionPayEvent? ParseSessionPay(string? txEventsJson)
174+
{
175+
var attrs = FindEvent(txEventsJson, EventTypes.NodePay);
176+
if (attrs is null) return null;
177+
178+
return new SessionPayEvent(
179+
SessionId: ParseLong(attrs.GetValueOrDefault("session_id")),
180+
AccAddress: attrs.GetValueOrDefault("acc_address") ?? "",
181+
NodeAddress: attrs.GetValueOrDefault("node_address") ?? "",
182+
Payment: attrs.GetValueOrDefault("payment") ?? "0",
183+
StakingReward: attrs.GetValueOrDefault("staking_reward") ?? "0"
184+
);
185+
}
186+
187+
/// <summary>Parse a SubscriptionSessionEvent from TX result events.</summary>
188+
public static SubscriptionSessionEvent? ParseSubscriptionSession(string? txEventsJson)
189+
{
190+
var attrs = FindEvent(txEventsJson, EventTypes.SubscriptionCreateSession);
191+
if (attrs is null) return null;
192+
193+
return new SubscriptionSessionEvent(
194+
SessionId: ParseLong(attrs.GetValueOrDefault("session_id") ?? attrs.GetValueOrDefault("id")),
195+
SubscriptionId: ParseLong(attrs.GetValueOrDefault("subscription_id")),
196+
AccAddress: attrs.GetValueOrDefault("acc_address") ?? "",
197+
NodeAddress: attrs.GetValueOrDefault("node_address") ?? ""
198+
);
199+
}
200+
201+
// ─── Helpers ───────────────────────────────────────────────────
202+
203+
private static long? TryExtractSessionIdFromEvent(JsonElement ev)
204+
{
205+
if (!ev.TryGetProperty("type", out var typeProp)) return null;
206+
var type = typeProp.GetString();
207+
if (type is null) return null;
208+
209+
if (!type.Contains("Session", StringComparison.OrdinalIgnoreCase)) return null;
210+
211+
if (!ev.TryGetProperty("attributes", out var attrs) || attrs.ValueKind != JsonValueKind.Array) return null;
212+
213+
foreach (var attr in attrs.EnumerateArray())
214+
{
215+
var key = DecodeAttr(attr, "key");
216+
if (key is "session_id" or "SessionID" or "id")
217+
{
218+
var val = DecodeAttr(attr, "value");
219+
if (val != null && long.TryParse(val.Trim('"'), out var id) && id > 0) return id;
220+
}
221+
}
222+
223+
return null;
224+
}
225+
226+
private static long? ExtractSessionIdFromRaw(string raw)
227+
{
228+
// Fallback: regex-like search for session_id in raw JSON string
229+
var patterns = new[] { "\"session_id\":\"", "\"session_id\": \"", "\"SessionID\":\"" };
230+
foreach (var pattern in patterns)
231+
{
232+
var idx = raw.IndexOf(pattern, StringComparison.Ordinal);
233+
if (idx < 0) continue;
234+
var start = idx + pattern.Length;
235+
var end = raw.IndexOf('"', start);
236+
if (end <= start) continue;
237+
var val = raw[start..end];
238+
if (long.TryParse(val, out var id) && id > 0) return id;
239+
}
240+
241+
return null;
242+
}
243+
244+
private static string? DecodeAttr(JsonElement attr, string prop)
245+
{
246+
if (!attr.TryGetProperty(prop, out var value)) return null;
247+
var str = value.GetString();
248+
if (str is null) return null;
249+
250+
// Try base64 decode (older CosmJS format)
251+
str = str.Trim('"');
252+
try
253+
{
254+
var bytes = Convert.FromBase64String(str);
255+
var decoded = System.Text.Encoding.UTF8.GetString(bytes);
256+
// If decoded looks like a readable string, use it
257+
if (decoded.All(c => c >= 32 && c < 127)) return decoded.Trim('"');
258+
}
259+
catch { }
260+
261+
return str;
262+
}
263+
264+
private static long ParseLong(string? val)
265+
{
266+
if (string.IsNullOrEmpty(val)) return 0;
267+
return long.TryParse(val.Trim('"'), out var v) ? v : 0;
268+
}
269+
}

0 commit comments

Comments
 (0)