@@ -54,7 +54,9 @@ public async Task<List<ChainNode>> GetActiveNodesAsync(int limit = 500, Cancella
5454 // RPC-first
5555 try
5656 {
57- return await _rpcClient . QueryNodesAsync ( 1 , limit , ct ) ;
57+ var rpcNodes = await _rpcClient . QueryNodesAsync ( 1 , limit , ct ) ;
58+ if ( rpcNodes . Count > 0 ) return rpcNodes ;
59+ _logger ? . Debug ( "RPC returned no active nodes; falling back to LCD to verify." ) ;
5860 }
5961 catch ( Exception ex ) { _logger ? . Debug ( $ "RPC GetActiveNodes failed, falling back to LCD: { ex . Message } ") ; }
6062
@@ -111,7 +113,9 @@ public async Task<List<Subscription>> GetSubscriptionsAsync(string address, Canc
111113 // RPC-first
112114 try
113115 {
114- return await _rpcClient . QuerySubscriptionsForAccountAsync ( address , ct : ct ) ;
116+ var rpcSubs = await _rpcClient . QuerySubscriptionsForAccountAsync ( address , ct : ct ) ;
117+ if ( rpcSubs . Count > 0 ) return rpcSubs ;
118+ _logger ? . Debug ( "RPC returned no subscriptions; falling back to LCD to verify." ) ;
115119 }
116120 catch ( Exception ex ) { _logger ? . Debug ( $ "RPC GetSubscriptions failed, falling back to LCD: { ex . Message } ") ; }
117121
@@ -135,10 +139,15 @@ public async Task<List<ChainSession>> GetSessionsAsync(string address, string st
135139 try
136140 {
137141 var sessions = await _rpcClient . QuerySessionsForAccountAsync ( address , ct : ct ) ;
138- // Filter by status if specified (RPC returns all statuses)
139- if ( status == "1" )
140- return sessions . Where ( s => s . Status == "active" || s . Status == "1" ) . ToList ( ) ;
141- return sessions ;
142+ if ( sessions . Count > 0 )
143+ {
144+ // Filter by status if specified (RPC returns all statuses)
145+ var filtered = status == "1"
146+ ? sessions . Where ( s => s . Status == "active" || s . Status == "1" ) . ToList ( )
147+ : sessions ;
148+ if ( filtered . Count > 0 ) return filtered ;
149+ }
150+ _logger ? . Debug ( "RPC returned no sessions; falling back to LCD to verify." ) ;
142151 }
143152 catch ( Exception ex ) { _logger ? . Debug ( $ "RPC GetSessions failed, falling back to LCD: { ex . Message } ") ; }
144153
@@ -159,7 +168,9 @@ public async Task<List<ChainNode>> GetPlanNodesAsync(int planId, CancellationTok
159168 // RPC-first
160169 try
161170 {
162- return await _rpcClient . QueryNodesForPlanAsync ( ( ulong ) planId , 1 , 5000 , ct ) ;
171+ var rpcNodes = await _rpcClient . QueryNodesForPlanAsync ( ( ulong ) planId , 1 , 5000 , ct ) ;
172+ if ( rpcNodes . Count > 0 ) return rpcNodes ;
173+ _logger ? . Debug ( "RPC returned no plan nodes; falling back to LCD to verify." ) ;
163174 }
164175 catch ( Exception ex ) { _logger ? . Debug ( $ "RPC GetPlanNodes failed, falling back to LCD: { ex . Message } ") ; }
165176
@@ -266,11 +277,23 @@ public async Task<List<DiscoveredPlan>> DiscoverPlansAsync(int maxId = 100, Canc
266277
267278 /// <summary>
268279 /// Get account number and sequence for transaction signing.
280+ /// RPC-first (cosmos.auth.v1beta1.Query/Account) with LCD fallback.
269281 /// </summary>
270282 /// <param name="address">Account address (sent1...).</param>
271283 /// <returns>Tuple of (accountNumber, sequence).</returns>
272284 internal async Task < ( ulong AccountNumber , ulong Sequence ) > GetAccountInfoAsync ( string address , CancellationToken ct = default )
273285 {
286+ // RPC-first: faster, no LCD rate-limit exposure.
287+ try
288+ {
289+ var rpcResult = await _rpcClient . QueryAccountAsync ( address , ct ) ;
290+ if ( rpcResult is not null )
291+ return rpcResult . Value ;
292+ _logger ? . Debug ( "RPC QueryAccount returned null; falling back to LCD." ) ;
293+ }
294+ catch ( Exception ex ) { _logger ? . Debug ( $ "RPC QueryAccount failed, falling back to LCD: { ex . Message } ") ; }
295+
296+ // LCD fallback
274297 var path = $ "/cosmos/auth/v1beta1/accounts/{ address } ";
275298 var json = await LcdGetAsync ( path , ct ) ;
276299
@@ -554,17 +577,23 @@ public async Task<IReadOnlyList<ActiveSession>> QueryActiveSessionsForAddressAsy
554577 {
555578 ArgumentNullException . ThrowIfNull ( walletAddress ) ;
556579
557- // RPC-first
580+ // RPC-first. Treat empty result as suspicious and fall through to LCD — a silent
581+ // empty return used to mask decoder bugs (see suggestions/2026-04-21-rpc-session-decoder-any-and-field-numbers.md).
558582 try
559583 {
560584 var rpcSessions = await _rpcClient . QuerySessionsForAccountAsync ( walletAddress , ct : ct ) ;
561- return rpcSessions
562- . Where ( s => s . Status == "active" || s . Status == "1" )
563- . Select ( s => new ActiveSession (
564- ulong . TryParse ( s . Id , out var sid ) ? sid : 0 ,
565- s . NodeAddress ,
566- SessionStatus . Active ) )
567- . ToList ( ) ;
585+ if ( rpcSessions . Count > 0 )
586+ {
587+ var active = rpcSessions
588+ . Where ( s => s . Status == "active" || s . Status == "1" )
589+ . Select ( s => new ActiveSession (
590+ ulong . TryParse ( s . Id , out var sid ) ? sid : 0 ,
591+ s . NodeAddress ,
592+ SessionStatus . Active ) )
593+ . ToList ( ) ;
594+ if ( active . Count > 0 ) return active ;
595+ }
596+ _logger ? . Debug ( "RPC QueryActiveSessions returned no active sessions; falling back to LCD to verify." ) ;
568597 }
569598 catch ( Exception ex ) { _logger ? . Debug ( $ "RPC QueryActiveSessions failed, falling back to LCD: { ex . Message } ") ; }
570599
@@ -605,7 +634,18 @@ public async Task<IReadOnlyList<ActiveSession>> QueryActiveSessionsForAddressAsy
605634
606635 // LCD fallback
607636 var path = $ "/sentinel/session/v3/sessions/{ sessionId } /allocations";
608- var json = await LcdGetAsync ( path , ct ) ;
637+ JsonElement json ;
638+ try
639+ {
640+ json = await LcdGetAsync ( path , ct ) ;
641+ }
642+ catch ( SentinelException ex ) when ( ex . Code == "CLIENT_HTTP_404" )
643+ {
644+ // Fresh session with no bandwidth reported yet (node hasn't called back to chain).
645+ // Treat as "allocation unknown" rather than failing session reuse — see
646+ // suggestions/2026-04-19-connect-widen-reuse-handshake-retry.md.
647+ return null ;
648+ }
609649
610650 if ( json . TryGetProperty ( "allocations" , out var arr ) && arr . ValueKind == JsonValueKind . Array )
611651 {
@@ -640,7 +680,9 @@ public async Task<List<SubscriptionAllocation>> QuerySubscriptionAllocationsAsyn
640680 // RPC-first (v2 — v3 returns 501)
641681 try
642682 {
643- return await _rpcClient . QuerySubscriptionAllocationsAsync ( subscriptionId , ct : ct ) ;
683+ var rpcAllocs = await _rpcClient . QuerySubscriptionAllocationsAsync ( subscriptionId , ct : ct ) ;
684+ if ( rpcAllocs . Count > 0 ) return rpcAllocs ;
685+ _logger ? . Debug ( "RPC returned no subscription allocations; falling back to LCD to verify." ) ;
644686 }
645687 catch ( Exception ex ) { _logger ? . Debug ( $ "RPC QuerySubAllocations failed, falling back to LCD: { ex . Message } ") ; }
646688
@@ -691,7 +733,9 @@ public async Task<List<ChainNode>> QueryPlanNodesAsync(int planId, CancellationT
691733 // RPC-first
692734 try
693735 {
694- return await _rpcClient . QueryNodesForPlanAsync ( ( ulong ) planId , 1 , 5000 , ct ) ;
736+ var rpcNodes = await _rpcClient . QueryNodesForPlanAsync ( ( ulong ) planId , 1 , 5000 , ct ) ;
737+ if ( rpcNodes . Count > 0 ) return rpcNodes ;
738+ _logger ? . Debug ( "RPC returned no plan nodes; falling back to LCD to verify." ) ;
695739 }
696740 catch ( Exception ex ) { _logger ? . Debug ( $ "RPC QueryPlanNodes failed, falling back to LCD: { ex . Message } ") ; }
697741
@@ -867,6 +911,7 @@ public async Task<IReadOnlyList<ChainNode>> GetAvailableNodesAsync(string wallet
867911
868912 /// <summary>
869913 /// Query all subscribers of a plan.
914+ /// RPC-first (verified wire format 2026-04-21); falls through to LCD on empty or exception.
870915 /// Optionally exclude an address (e.g. the plan owner) from the results.
871916 /// </summary>
872917 /// <param name="planId">Plan ID.</param>
@@ -878,14 +923,31 @@ public async Task<IReadOnlyList<PlanSubscriber>> QueryPlanSubscribersAsync(
878923 string ? excludeAddress = null ,
879924 CancellationToken ct = default )
880925 {
926+ // RPC-first (QuerySubscriptionsForPlan — direct PlanSubscription, not Any-wrapped).
927+ try
928+ {
929+ var rpcSubs = await _rpcClient . QuerySubscriptionsForPlanAsync ( ( ulong ) planId , ct : ct ) ;
930+ if ( rpcSubs . Count > 0 )
931+ {
932+ var filtered = excludeAddress is null
933+ ? rpcSubs
934+ : rpcSubs . Where ( s => ! string . Equals ( s . Address , excludeAddress , StringComparison . OrdinalIgnoreCase ) ) . ToList ( ) ;
935+ return filtered ;
936+ }
937+ _logger ? . Debug ( "RPC QueryPlanSubscribers returned no results; falling back to LCD to verify." ) ;
938+ }
939+ catch ( Exception ex ) { _logger ? . Debug ( $ "RPC QueryPlanSubscribers failed, falling back to LCD: { ex . Message } ") ; }
940+
941+ // LCD fallback
881942 var path = $ "/sentinel/subscription/v3/plans/{ planId } /subscriptions";
882943 var items = await LcdPaginatedAsync ( path , "subscriptions" , ct ) ;
883944
884945 var subscribers = new List < PlanSubscriber > ( ) ;
885946
886947 foreach ( var item in items )
887948 {
888- var address = item . TryGetProperty ( "address" , out var a ) ? a . GetString ( ) ?? "" : "" ;
949+ var address = item . TryGetProperty ( "acc_address" , out var a ) ? a . GetString ( ) ?? ""
950+ : item . TryGetProperty ( "address" , out var a2 ) ? a2 . GetString ( ) ?? "" : "" ;
889951 if ( string . IsNullOrEmpty ( address ) && item . TryGetProperty ( "subscriber" , out var sub ) )
890952 {
891953 address = sub . GetString ( ) ?? "" ;
@@ -1079,7 +1141,9 @@ public async Task<IReadOnlyList<AuthzGrant>> QueryAuthzGrantsAsync(
10791141 // RPC-first
10801142 try
10811143 {
1082- return await _rpcClient . QueryAuthzGrantsAsync ( granter , grantee , ct : ct ) ;
1144+ var rpcGrants = await _rpcClient . QueryAuthzGrantsAsync ( granter , grantee , ct : ct ) ;
1145+ if ( rpcGrants . Count > 0 ) return rpcGrants ;
1146+ _logger ? . Debug ( "RPC returned no authz grants; falling back to LCD to verify." ) ;
10831147 }
10841148 catch ( Exception ex ) { _logger ? . Debug ( $ "RPC QueryAuthzGrants failed, falling back to LCD: { ex . Message } ") ; }
10851149
0 commit comments