@@ -502,6 +502,107 @@ private static byte[] BuildTxRaw(byte[] txBodyBytes, byte[] authInfoBytes, byte[
502502 return stream . ToArray ( ) ;
503503 }
504504
505+ // ─── High-Level Subscription Operations ───
506+
507+ /// <summary>
508+ /// Share a subscription's bandwidth with another address.
509+ /// Builds and broadcasts a MsgShareSubscriptionRequest.
510+ /// The bytes parameter specifies the bandwidth allowance (cosmossdk.io/math.Int).
511+ /// NOTE: Only bytes-based sharing is supported — the chain has no time/duration field.
512+ /// The operator must manage user expiry externally.
513+ /// </summary>
514+ /// <param name="subscriptionId">Subscription ID to share.</param>
515+ /// <param name="recipientAddress">Address to share with (sent1...).</param>
516+ /// <param name="bytes">Bytes of bandwidth to grant (e.g. 1073741824 for 1 GB).</param>
517+ /// <returns>Transaction result with hash and success status.</returns>
518+ public async Task < TxResult > ShareSubscriptionAsync (
519+ ulong subscriptionId , string recipientAddress , long bytes )
520+ {
521+ ArgumentException . ThrowIfNullOrWhiteSpace ( recipientAddress ) ;
522+ if ( subscriptionId == 0 )
523+ throw new ArgumentOutOfRangeException ( nameof ( subscriptionId ) , "Must be > 0" ) ;
524+ if ( bytes <= 0 )
525+ throw new ArgumentOutOfRangeException ( nameof ( bytes ) , "Must be > 0" ) ;
526+
527+ var msg = MessageBuilder . ShareSubscription (
528+ _wallet . Address , subscriptionId , recipientAddress , bytes ) ;
529+ return await BroadcastAsync ( msg ) ;
530+ }
531+
532+ /// <summary>
533+ /// Complete "add user to plan" flow: subscribe to plan, share bandwidth with user,
534+ /// and optionally grant a fee allowance so the user doesn't pay gas.
535+ /// This is the primary operation for plan operators onboarding new users.
536+ /// </summary>
537+ /// <param name="planId">Plan ID to subscribe to.</param>
538+ /// <param name="userAddress">User address to share with (sent1...).</param>
539+ /// <param name="bytes">Bytes of bandwidth to grant the user.</param>
540+ /// <param name="denom">Payment denomination (default: "udvpn").</param>
541+ /// <param name="grantFee">If true, also grants a fee allowance to the user.</param>
542+ /// <param name="feeSpendLimit">Max fee allowance in udvpn (default: 5,000,000 = 5 P2P, enough for ~125 sessions).</param>
543+ /// <param name="feeExpiration">Fee grant expiration (default: null = no expiry).</param>
544+ /// <returns>Onboard result with subscription ID and all TX hashes.</returns>
545+ public async Task < OnboardResult > OnboardPlanUserAsync (
546+ ulong planId ,
547+ string userAddress ,
548+ long bytes ,
549+ string denom = "udvpn" ,
550+ bool grantFee = false ,
551+ long feeSpendLimit = 5_000_000 ,
552+ DateTime ? feeExpiration = null )
553+ {
554+ ArgumentException . ThrowIfNullOrWhiteSpace ( userAddress ) ;
555+ if ( planId == 0 )
556+ throw new ArgumentOutOfRangeException ( nameof ( planId ) , "Must be > 0" ) ;
557+ if ( bytes <= 0 )
558+ throw new ArgumentOutOfRangeException ( nameof ( bytes ) , "Must be > 0" ) ;
559+
560+ // Step 1: Subscribe to plan
561+ var subMsg = MessageBuilder . StartSubscription ( _wallet . Address , planId , denom ) ;
562+ var subResult = await BroadcastAsync ( subMsg ) ;
563+
564+ if ( ! subResult . Success )
565+ throw new SentinelException ( "SUBSCRIBE_FAILED" ,
566+ $ "Subscribe to plan { planId } failed: { subResult . RawLog } ") ;
567+
568+ // Step 2: Query to find the subscription ID
569+ await Task . Delay ( 5000 ) ; // Wait for chain state to propagate
570+ var subs = await _client . GetSubscriptionsAsync ( _wallet . Address ) ;
571+ var match = subs
572+ . Where ( s => s . PlanId == planId . ToString ( ) &&
573+ s . Status . Contains ( "ACTIVE" , StringComparison . OrdinalIgnoreCase ) )
574+ . OrderByDescending ( s => s . Id )
575+ . FirstOrDefault ( ) ;
576+
577+ var subscriptionId = match ? . Id
578+ ?? throw new SentinelException ( "SUBSCRIPTION_NOT_FOUND" ,
579+ $ "Subscription for plan { planId } not found after successful TX") ;
580+
581+ // Step 3: Share subscription with user (bytes-based)
582+ if ( ! ulong . TryParse ( subscriptionId , out var subId ) )
583+ throw new SentinelException ( ErrorCodes . SubscriptionNotFound ,
584+ $ "Invalid subscription ID format: { subscriptionId } ") ;
585+ var shareMsg = MessageBuilder . ShareSubscription (
586+ _wallet . Address , subId , userAddress , bytes ) ;
587+ var shareResult = await BroadcastAsync ( shareMsg ) ;
588+
589+ if ( ! shareResult . Success )
590+ throw new SentinelException ( "SHARE_FAILED" ,
591+ $ "Share subscription { subscriptionId } failed: { shareResult . RawLog } ") ;
592+
593+ // Step 4: Optional fee grant
594+ string ? grantTxHash = null ;
595+ if ( grantFee )
596+ {
597+ var grantMsg = MessageBuilder . GrantFeeAllowance (
598+ _wallet . Address , userAddress , feeSpendLimit , feeExpiration ) ;
599+ var grantResult = await BroadcastAsync ( grantMsg ) ;
600+ grantTxHash = grantResult . TxHash ;
601+ }
602+
603+ return new OnboardResult ( subscriptionId , subResult . TxHash , shareResult . TxHash , grantTxHash ) ;
604+ }
605+
505606 // ─── Gas Estimation ───
506607
507608 /// <summary>
0 commit comments