packages feed

utxorpc-client 0.0.2.0 → 0.0.4.1

raw patch · 6 files changed

+1667/−65 lines, 6 filesdep +base16-bytestringdep ~utxorpcnew-component:exe:examplenew-component:exe:quickstartPVP: major bump suggested

API removals or changes: PVP suggests a major version bump

Dependencies added: base16-bytestring

Dependency ranges changed: utxorpc

API changes (from Hackage documentation)

- Utxorpc.Client: [streamUtxos] :: QueryClient -> ServerStreamCall ReadUtxosRequest ReadUtxosResponse
+ Utxorpc.Client: [readEraSummary] :: QueryClient -> ReadEraSummaryRequest -> UnaryReply ReadEraSummaryResponse
+ Utxorpc.Client: [readGenesis] :: QueryClient -> ReadGenesisRequest -> UnaryReply ReadGenesisResponse
+ Utxorpc.Client: [readTip] :: SyncClient -> ReadTipRequest -> UnaryReply ReadTipResponse
- Utxorpc.Client: QueryClient :: (ReadParamsRequest -> UnaryReply ReadParamsResponse) -> (ReadUtxosRequest -> UnaryReply ReadUtxosResponse) -> (SearchUtxosRequest -> UnaryReply SearchUtxosResponse) -> ServerStreamCall ReadUtxosRequest ReadUtxosResponse -> QueryClient
+ Utxorpc.Client: QueryClient :: (ReadParamsRequest -> UnaryReply ReadParamsResponse) -> (ReadUtxosRequest -> UnaryReply ReadUtxosResponse) -> (SearchUtxosRequest -> UnaryReply SearchUtxosResponse) -> (ReadGenesisRequest -> UnaryReply ReadGenesisResponse) -> (ReadEraSummaryRequest -> UnaryReply ReadEraSummaryResponse) -> QueryClient
- Utxorpc.Client: SyncClient :: (FetchBlockRequest -> UnaryReply FetchBlockResponse) -> (DumpHistoryRequest -> UnaryReply DumpHistoryResponse) -> ServerStreamCall FollowTipRequest FollowTipResponse -> SyncClient
+ Utxorpc.Client: SyncClient :: (FetchBlockRequest -> UnaryReply FetchBlockResponse) -> (DumpHistoryRequest -> UnaryReply DumpHistoryResponse) -> ServerStreamCall FollowTipRequest FollowTipResponse -> (ReadTipRequest -> UnaryReply ReadTipResponse) -> SyncClient

Files

example/Main.hs view
@@ -14,7 +14,7 @@ import KatipLogger (katipLogger) import Network.GRPC.Client (CIHeaderList) import Network.HTTP2.Frame (ErrorCode)-import Proto.Utxorpc.V1alpha.Sync.Sync_Fields+import Proto.Utxorpc.V1alpha.Sync.Sync_Fields (hash, slot, startToken, maxItems, intersect) import Safe (readMay) import SimpleLogger (simpleLogger) import System.Environment (getArgs)@@ -127,7 +127,7 @@         & intersect           .~ [ blockRef,                defMessage-                 & index .~ 41562539+                 & slot .~ 41562539                  & hash                    .~ TE.encodeUtf8                      "e4599d275375e54257e7fd922d2f486cf47dd90692d1a7e531804a4e90893346"@@ -135,7 +135,7 @@      blockRef =       defMessage-        & index .~ 41561535+        & slot .~ 41561535         & hash           .~ TE.encodeUtf8             "91ec40dfc09449d918ec7b5311d5ddba318e8a7c337eaf23c9916c6463c30fbe"
quick-start/Main.hs view
@@ -1,46 +1,1636 @@ {-# LANGUAGE LambdaCase #-} {-# LANGUAGE OverloadedStrings #-}--module Main (main) where--import Control.Exception (bracket, throwIO)-import Control.Lens.Operators ((&), (.~))-import Control.Monad (void)-import Data.ProtoLens (Message (..))-import qualified Data.Text.Encoding as T-import Proto.Utxorpc.V1alpha.Sync.Sync (FetchBlockRequest)-import Proto.Utxorpc.V1alpha.Sync.Sync_Fields (hash, index, ref)-import UnliftIO.Exception (throwString)-import Utxorpc.Client (close, fetchBlock, simpleUtxorpcClient, syncClient)--main :: IO ()-main = do-  -- Connect to a UTxO RPC service-  let mkClient = simpleUtxorpcClient "127.0.0.1" 3000 True-  -- Bracket making a request with closing the client connection-  bracket mkClient closeClient $ \case-    -- Panic if connection could not be established-    Left clientErr -> throwIO clientErr-    Right client -> do-      -- Make a unary request-      result <- fetchBlock (syncClient client) fetchBlockRequest-      case result of-        -- Handle success-        Right (Right (Right (_headers, _trailers, fetchBlockResponse))) ->-          print fetchBlockResponse-        err -> throwString $ show err-  where-    fetchBlockRequest :: FetchBlockRequest-    fetchBlockRequest =-      defMessage-        & ref-          .~ [ defMessage-                 & index .~ 40608434-                 & hash-                   .~ T.encodeUtf8-                     "3e4947072df1ed22a0518cb717c2904ebf0952b0c33292b402fae25d9562022e"-             ]--    -- Close the client connection if it was established-    closeClient (Left _) = return ()-    closeClient (Right client) = void (close client)+{-# LANGUAGE ScopedTypeVariables #-}+{-# OPTIONS_GHC -fconstraint-solver-iterations=10 #-}++module Main (main) where++import Control.Exception (bracket, throwIO, try, SomeException)+import Control.Lens ((^.), (.~), (&))+import Control.Monad (void, when, forM_)+import Control.Concurrent (forkIO, threadDelay)+import Control.Concurrent.MVar (newEmptyMVar, putMVar, takeMVar)+import Data.ProtoLens (Message (..))+import qualified Data.ByteString as BS+import qualified Data.ByteString.Base16 as Base16+import qualified Data.Text as T+import qualified Data.Text.Encoding as T+import Data.Int (Int64)+import Data.List (find)+-- import Data.Maybe (isJust, fromMaybe)+-- import Proto.Utxorpc.V1alpha.Query.Query+import qualified Proto.Utxorpc.V1alpha.Query.Query_Fields as QF+-- import Proto.Utxorpc.V1alpha.Cardano.Cardano+import qualified Proto.Utxorpc.V1alpha.Cardano.Cardano_Fields as CF+import Proto.Utxorpc.V1alpha.Sync.Sync (FollowTipResponse)+import qualified Proto.Utxorpc.V1alpha.Sync.Sync as S+import qualified Proto.Utxorpc.V1alpha.Sync.Sync_Fields as SF+import Proto.Utxorpc.V1alpha.Submit.Submit (WaitForTxResponse, WatchMempoolResponse, Stage(..), TxPredicate, AnyChainTxPattern)+import qualified Proto.Utxorpc.V1alpha.Submit.Submit as Sub+import qualified Proto.Utxorpc.V1alpha.Submit.Submit_Fields as SubF+import Proto.Utxorpc.V1alpha.Watch.Watch (WatchTxResponse, WatchTxResponse'Action(..))+import qualified Proto.Utxorpc.V1alpha.Watch.Watch as W+import qualified Proto.Utxorpc.V1alpha.Watch.Watch_Fields as WF+import Proto.Utxorpc.V1alpha.Cardano.Cardano (AddressPattern, AssetPattern, TxPattern)+import qualified Proto.Utxorpc.V1alpha.Cardano.Cardano as C+import UnliftIO.Exception (throwString)+import GHC.Stack (HasCallStack)+import Utxorpc.Client++-- Test helpers+assertEqual :: (Eq a, Show a) => String -> a -> a -> IO ()+assertEqual msg expected actual =+  when (expected /= actual) $+    throwString $ msg ++ ": expected " ++ show expected ++ ", got " ++ show actual++assertNotNull :: HasCallStack => String -> Maybe a -> IO ()+assertNotNull msg Nothing = throwString $ msg ++ " is null"+assertNotNull _ (Just _) = return ()++assertTrue :: HasCallStack => String -> Bool -> IO ()++-- BigInt helper - extracts Int64 from BigInt protobuf message+getBigInt :: C.BigInt -> Int64+getBigInt bigIntMsg = case bigIntMsg ^. CF.maybe'bigInt of+  Just (C.BigInt'Int val) -> val+  _ -> error "Expected BigInt'Int variant"+assertTrue msg False = throwString $ msg ++ " is false"+assertTrue _ True = return ()++assertSingle :: HasCallStack => String -> [a] -> IO a+assertSingle msg [] = throwString $ msg ++ " is empty"+assertSingle _ [x] = return x+assertSingle msg xs = throwString $ msg ++ " has " ++ show (length xs) ++ " items, expected 1"++main :: IO ()+main = do+  putStrLn "Testing Query and Submit endpoints with Dolos"++  -- Connect to Dolos UTxO RPC service+  let mkClient = simpleUtxorpcClient "127.0.0.1" 50051 False++  bracket mkClient closeClient $ \case+    Left clientErr -> throwIO clientErr+    Right client -> do+      -- Query tests+      testReadParams client+      testReadUtxosByOutputRef client+      testSearchUtxosByAddress client+      testSearchUtxosByPaymentPart client+      testSearchUtxosByDelegationPart client+      testSearchUtxosByPolicyID client+      testSearchUtxosByAsset client++      -- Sync tests+      testSyncFollowTip client+      testSyncReadTip client+      testSyncFetchBlock client+      testSyncDumpHistory client++      -- Submit tests+      testSubmitTx client+      testWaitForTx client++      -- WatchMempool tests+      -- Submit transaction once for all WatchMempool tests+      putStrLn "\n=== Submitting transaction for WatchMempool tests ==="+      let txCborHex = "84A300D9010282825820F3627A04A2C0020720ACBD47E8CC90A1D64E9DAC9FCA7F12EB78DB8174E1FBE601825820B8BE6DF6FE61C465ED957D1901BC095C03E31612D1BC70EBFCC05CAC984A0088000182A20058390053FBFFFAB7B001281917DE77F18A8087413BE03401DB4AA2A7DBF0AE1591D34D5B4B2728D04A80FDD041BB52EDB334DACBF25AA27877E738011A005B8D80A20058390053FBFFFAB7B001281917DE77F18A8087413BE03401DB4AA2A7DBF0AE1591D34D5B4B2728D04A80FDD041BB52EDB334DACBF25AA27877E738011B00000001D40D1E15021A00029E8DA100D9010281825820D504D361777F985265A6F0A727E601E70F837574652760456B41731FF4F952585840191 02F26AF35BF37D9B220F71D68CACF28C596F6F74C45C1732820B61F3CBAED991C2D20E61FA38179AC228A1673B670F32544015D91AF74D786AA24C07CE900F5F6"+          txCborBytes = case Base16.decode (T.encodeUtf8 $ T.pack $ filter (/= ' ') txCborHex) of+            Right bs -> bs+            Left err -> error $ "Invalid CBOR hex: " ++ err+          anyChainTx = defMessage & SubF.raw .~ txCborBytes+          submitRequest = defMessage & SubF.tx .~ anyChainTx++      submitResult <- submitTx (submitClient client) submitRequest+      case submitResult of+        Right (Right (Right (_headers, _trailers, Right response))) -> do+          let txHash = response ^. SubF.ref+          if not (BS.null txHash) then do+            putStrLn $ "  Transaction submitted! Hash: " ++ T.unpack (T.take 16 $ T.decodeUtf8 $ Base16.encode txHash) ++ "..."++            -- Now run all WatchMempool tests - they'll all detect this transaction+            testWatchMempoolByAddress client+            testWatchMempoolByPaymentPart client+            testWatchMempoolByDelegationPart client+            testWatchMempoolByPolicyId client+            testWatchMempoolByAsset client+          else+            throwString "Transaction submission returned no refs"+        err -> throwString $ "Failed to submit transaction: " ++ show err++      -- Watch tests - each test submits its own transaction+      putStrLn "\n=== Running Watch tests (each submits and watches for blockchain application) ==="+      testWatchTxForAddress client+      testWatchTxForPaymentPart client+      testWatchTxForDelegationPart client+      testWatchTxForPolicyId client+      testWatchTxForAsset client++      putStrLn "\n✅ All tests passed!"+  where+    closeClient (Left _) = return ()+    closeClient (Right client) = void (close client)++testReadParams :: UtxorpcClient -> IO ()+testReadParams client = do+  putStrLn "\n=== Test: ReadParams ==="++  result <- readParams (queryClient client) defMessage+  case result of+    Right (Right (Right (_headers, _trailers, Right response))) -> do+      putStrLn "✓ Response received from server"+      -- Get the parameters+      let params = response ^. QF.values . QF.cardano++      -- Show some actual values from the response+      putStrLn $ "  coinsPerUtxoByte: " ++ show (params ^. CF.coinsPerUtxoByte)+      putStrLn $ "  maxTxSize: " ++ show (params ^. CF.maxTxSize)+      putStrLn $ "  minFeeCoefficient: " ++ show (params ^. CF.minFeeCoefficient)+      putStrLn $ "  protocolVersion: " ++ show (params ^. CF.protocolVersion . CF.major)++      -- Assert all expected parameter values for Preview testnet+      -- BigInt fields need to be accessed via their bigInt oneof field using getBigInt helper+      assertEqual "coinsPerUtxoByte" 4310 (getBigInt $ params ^. CF.coinsPerUtxoByte)+      assertEqual "maxTxSize" 16384 (params ^. CF.maxTxSize)+      assertEqual "minFeeCoefficient" 44 (getBigInt $ params ^. CF.minFeeCoefficient)+      assertEqual "minFeeConstant" 155381 (getBigInt $ params ^. CF.minFeeConstant)+      assertEqual "maxBlockBodySize" 90112 (params ^. CF.maxBlockBodySize)+      assertEqual "maxBlockHeaderSize" 1100 (params ^. CF.maxBlockHeaderSize)+      assertEqual "stakeKeyDeposit" 2000000 (getBigInt $ params ^. CF.stakeKeyDeposit)+      assertEqual "poolDeposit" 500000000 (getBigInt $ params ^. CF.poolDeposit)+      assertEqual "desiredNumberOfPools" 500 (params ^. CF.desiredNumberOfPools)+      assertEqual "minPoolCost" 170000000 (getBigInt $ params ^. CF.minPoolCost)+      assertEqual "maxValueSize" 5000 (params ^. CF.maxValueSize)+      assertEqual "collateralPercentage" 150 (params ^. CF.collateralPercentage)+      assertEqual "maxCollateralInputs" 3 (params ^. CF.maxCollateralInputs)++      -- Protocol version+      assertEqual "protocolVersion.major" 9 (params ^. CF.protocolVersion . CF.major)+      +      -- Rational parameters+      assertEqual "poolInfluence.numerator" 5033165 (params ^. CF.poolInfluence . CF.numerator)+      assertEqual "poolInfluence.denominator" 16777216 (params ^. CF.poolInfluence . CF.denominator)+      +      assertEqual "monetaryExpansion.numerator" 6442451 (params ^. CF.monetaryExpansion . CF.numerator)+      assertEqual "monetaryExpansion.denominator" 2147483648 (params ^. CF.monetaryExpansion . CF.denominator)+      +      assertEqual "treasuryExpansion.numerator" 13421773 (params ^. CF.treasuryExpansion . CF.numerator)+      assertEqual "treasuryExpansion.denominator" 67108864 (params ^. CF.treasuryExpansion . CF.denominator)+      +      -- Prices+      assertEqual "prices.steps.numerator" 721 (params ^. CF.prices . CF.steps . CF.numerator)+      assertEqual "prices.steps.denominator" 10000000 (params ^. CF.prices . CF.steps . CF.denominator)+      +      assertEqual "prices.memory.numerator" 577 (params ^. CF.prices . CF.memory . CF.numerator)+      assertEqual "prices.memory.denominator" 10000 (params ^. CF.prices . CF.memory . CF.denominator)+      +      -- Execution units+      assertEqual "maxExecutionUnitsPerTransaction.steps" 10000000000 +        (params ^. CF.maxExecutionUnitsPerTransaction . CF.steps)+      assertEqual "maxExecutionUnitsPerTransaction.memory" 14000000 +        (params ^. CF.maxExecutionUnitsPerTransaction . CF.memory)+      +      assertEqual "maxExecutionUnitsPerBlock.steps" 20000000000 +        (params ^. CF.maxExecutionUnitsPerBlock . CF.steps)+      assertEqual "maxExecutionUnitsPerBlock.memory" 62000000 +        (params ^. CF.maxExecutionUnitsPerBlock . CF.memory)+      +      -- Governance parameters+      assertEqual "committeeTermLimit" 365 (params ^. CF.committeeTermLimit)+      assertEqual "governanceActionValidityPeriod" 30 (params ^. CF.governanceActionValidityPeriod)+      assertEqual "governanceActionDeposit" 100000000000 (getBigInt $ params ^. CF.governanceActionDeposit)+      assertEqual "drepDeposit" 500000000 (getBigInt $ params ^. CF.drepDeposit)+      assertEqual "drepInactivityPeriod" 20 (params ^. CF.drepInactivityPeriod)+      +      -- Voting thresholds+      let poolThresholds = params ^. CF.poolVotingThresholds . CF.thresholds+      assertEqual "poolVotingThresholds count" 5 (length poolThresholds)+      forM_ poolThresholds $ \threshold -> do+        assertEqual "poolThreshold.numerator" 51 (threshold ^. CF.numerator)+        assertEqual "poolThreshold.denominator" 100 (threshold ^. CF.denominator)+      +      let drepThresholds = params ^. CF.drepVotingThresholds . CF.thresholds+      assertEqual "drepVotingThresholds count" 10 (length drepThresholds)+      +      -- Cost models+      let plutusV1Values = params ^. CF.costModels . CF.plutusV1 . CF.values+      when (100788 `notElem` plutusV1Values) $ throwString "plutusV1 missing 100788"+      when (420 `notElem` plutusV1Values) $ throwString "plutusV1 missing 420"  +      when (1 `notElem` plutusV1Values) $ throwString "plutusV1 missing 1"+      when (1000 `notElem` plutusV1Values) $ throwString "plutusV1 missing 1000"+      +      putStrLn "✓ ReadParams test passed!"+      +    err -> throwString $ "ReadParams failed: " ++ show err++testReadUtxosByOutputRef :: UtxorpcClient -> IO ()+testReadUtxosByOutputRef client = do+  putStrLn "\n=== Test: ReadUtxosByOutputRef ==="++  -- Arrange+  let txHashHex = "9874bdf4ad47b2d30a2146fc4ba1f94859e58e772683e75001aca6e85de7690d"+      txHash = case Base16.decode txHashHex of+        Right bs -> bs+        Left err -> error $ "Invalid hex: " ++ err+      outputIndex = 0+      txoRef = defMessage+        & CF.hash .~ txHash+        & CF.index .~ outputIndex+      request = defMessage+        & QF.keys .~ [txoRef]++  -- Act+  result <- readUtxos (queryClient client) request+  case result of+    Right (Right (Right (_headers, _trailers, Right response))) -> do+      putStrLn "✓ Response received from server"+      -- Assert+      let items = response ^. QF.items+      utxo <- assertSingle "utxos" items+      assertNotNull "ledgerTip" (Just $ response ^. QF.ledgerTip)++      -- Show actual data from response+      let output = utxo ^. QF.cardano+      putStrLn $ "  UTXO coin value: " ++ show (output ^. CF.coin) ++ " lovelace"+      putStrLn $ "  TxHash: " ++ T.unpack (T.take 16 $ T.decodeUtf8 $ Base16.encode txHash) ++ "..."++      -- Verify the UTXO reference matches what we requested+      assertEqual "txHash" txHash (utxo ^. QF.txoRef . CF.hash)+      assertEqual "outputIndex" outputIndex (utxo ^. QF.txoRef . CF.index)+      +      -- Verify native bytes+      let expectedNativeBytes = "82583900729c67d0de8cde3c0afc768fb0fcb1596e8cfcbf781b553efcd228813b7bb577937983e016d4e8429ff48cf386d6818883f9e88b62a804e01a05f5e100"+      let actualNativeBytesHex = Base16.encode (utxo ^. QF.nativeBytes)+      assertEqual "nativeBytes" (T.toLower $ T.pack expectedNativeBytes) (T.decodeUtf8 actualNativeBytesHex)+      +      -- Verify parsed state+      let output = utxo ^. QF.cardano+      assertTrue "coin > 0" ((getBigInt $ output ^. CF.coin) > 0)+      putStrLn "✓ ReadUtxosByOutputRef test passed!"+        +    err -> throwString $ "ReadUtxosByOutputRef failed: " ++ show err++testSearchUtxosByAddress :: UtxorpcClient -> IO ()+testSearchUtxosByAddress client = do+  putStrLn "\n=== Test: SearchUtxosByAddress ==="++  -- Arrange+  let testAddressHex = "0053fbfffab7b001281917de77f18a8087413be03401db4aa2a7dbf0ae1591d34d5b4b2728d04a80fdd041bb52edb334dacbf25aa27877e738"+      addressBytes = case Base16.decode testAddressHex of+        Right bs -> bs+        Left err -> error $ "Invalid hex: " ++ err+      addressPattern = defMessage+        & CF.exactAddress .~ addressBytes+      cardanoPattern = defMessage+        & CF.address .~ addressPattern+      anyPattern = defMessage+        & QF.cardano .~ cardanoPattern+      predicate = defMessage+        & QF.match .~ anyPattern+      request = defMessage+        & QF.predicate .~ predicate+        & QF.maxItems .~ 10++  -- Act+  result <- searchUtxos (queryClient client) request+  case result of+    Right (Right (Right (_headers, _trailers, Right response))) -> do+      putStrLn "✓ Response received from server"+      -- Assert+      let items = response ^. QF.items+      assertNotNull "ledgerTip" (Just $ response ^. QF.ledgerTip)+      assertTrue "found UTXOs" (length items > 0)++      -- Show actual data from response+      putStrLn $ "  Found " ++ show (length items) ++ " UTXOs for address"+      when (length items > 0) $ do+        let firstUtxo = head items+        let firstOutput = firstUtxo ^. QF.cardano+        putStrLn $ "  First UTXO coin: " ++ show (firstOutput ^. CF.coin) ++ " lovelace"++      -- Verify all returned UTXOs belong to the searched address+      forM_ items $ \utxo -> do+        let output = utxo ^. QF.cardano+        assertEqual "address matches" addressBytes (output ^. CF.address)++      putStrLn $ "✓ SearchUtxosByAddress test passed! Found " ++ show (length items) ++ " UTXOs"+      +    err -> throwString $ "SearchUtxosByAddress failed: " ++ show err++testSearchUtxosByPaymentPart :: UtxorpcClient -> IO ()+testSearchUtxosByPaymentPart client = do+  putStrLn "\n=== Test: SearchUtxosByPaymentPart ==="++  -- Arrange+  let paymentCredHex = "53fbfffab7b001281917de77f18a8087413be03401db4aa2a7dbf0ae"+      paymentCredBytes = case Base16.decode paymentCredHex of+        Right bs -> bs+        Left err -> error $ "Invalid hex: " ++ err+      addressPattern = defMessage+        & CF.paymentPart .~ paymentCredBytes+      cardanoPattern = defMessage+        & CF.address .~ addressPattern+      anyPattern = defMessage+        & QF.cardano .~ cardanoPattern+      predicate = defMessage+        & QF.match .~ anyPattern+      request = defMessage+        & QF.predicate .~ predicate+        & QF.maxItems .~ 5++  -- Act+  result <- searchUtxos (queryClient client) request+  case result of+    Right (Right (Right (_headers, _trailers, Right response))) -> do+      putStrLn "✓ Response received from server"+      -- Assert+      let items = response ^. QF.items+      assertNotNull "ledgerTip" (Just $ response ^. QF.ledgerTip)+      assertTrue "found UTXOs" (length items > 0)++      -- Show actual data from response+      putStrLn $ "  Found " ++ show (length items) ++ " UTXOs matching payment credential"++      -- Verify all returned UTXOs have the correct payment credential+      forM_ items $ \utxo -> do+        let output = utxo ^. QF.cardano+        let utxoAddressBytes = output ^. CF.address+        -- Payment credential is bytes 1-28 (after network byte)+        let utxoPaymentCred = BS.take 28 $ BS.drop 1 utxoAddressBytes+        assertEqual "payment credential matches" paymentCredBytes utxoPaymentCred++      putStrLn $ "✓ SearchUtxosByPaymentPart test passed! Found " ++ show (length items) ++ " UTXOs"+      +    err -> throwString $ "SearchUtxosByPaymentPart failed: " ++ show err++testSearchUtxosByDelegationPart :: UtxorpcClient -> IO ()+testSearchUtxosByDelegationPart client = do+  putStrLn "\n=== Test: SearchUtxosByDelegationPart ==="++  -- Arrange+  let delegationCredHex = "1591d34d5b4b2728d04a80fdd041bb52edb334dacbf25aa27877e738"+      delegationCredBytes = case Base16.decode delegationCredHex of+        Right bs -> bs+        Left err -> error $ "Invalid hex: " ++ err+      addressPattern = defMessage+        & CF.delegationPart .~ delegationCredBytes+      cardanoPattern = defMessage+        & CF.address .~ addressPattern+      anyPattern = defMessage+        & QF.cardano .~ cardanoPattern+      predicate = defMessage+        & QF.match .~ anyPattern+      request = defMessage+        & QF.predicate .~ predicate+        & QF.maxItems .~ 5++  -- Act+  result <- searchUtxos (queryClient client) request+  case result of+    Right (Right (Right (_headers, _trailers, Right response))) -> do+      putStrLn "✓ Response received from server"+      -- Assert+      let items = response ^. QF.items+      assertNotNull "ledgerTip" (Just $ response ^. QF.ledgerTip)+      assertTrue "found UTXOs" (length items > 0)++      -- Show actual data from response+      putStrLn $ "  Found " ++ show (length items) ++ " UTXOs matching delegation credential"++      -- Verify all returned UTXOs have the correct delegation credential+      forM_ items $ \utxo -> do+        let output = utxo ^. QF.cardano+        let utxoAddressBytes = output ^. CF.address+        -- Base address should be 57 bytes: 1 (network) + 28 (payment) + 28 (delegation)+        assertEqual "address length" 57 (BS.length utxoAddressBytes)++        -- Delegation credential is bytes 29-56 (last 28 bytes)+        let utxoDelegationCred = BS.take 28 $ BS.drop 29 utxoAddressBytes+        assertEqual "delegation credential matches" delegationCredBytes utxoDelegationCred++      putStrLn $ "✓ SearchUtxosByDelegationPart test passed! Found " ++ show (length items) ++ " UTXOs"+      +    err -> throwString $ "SearchUtxosByDelegationPart failed: " ++ show err++testSearchUtxosByPolicyID :: UtxorpcClient -> IO ()+testSearchUtxosByPolicyID client = do+  putStrLn "\n=== Test: SearchUtxosByPolicyID ==="++  -- Arrange+  let policyIdHex = "047e0f912c4260fe66ae271e5ae494dcd5f79635bbbb1386be195f4e"+      policyIdBytes = case Base16.decode policyIdHex of+        Right bs -> bs+        Left err -> error $ "Invalid hex: " ++ err+      assetPattern = defMessage+        & CF.policyId .~ policyIdBytes+      cardanoPattern = defMessage+        & CF.asset .~ assetPattern+      anyPattern = defMessage+        & QF.cardano .~ cardanoPattern+      predicate = defMessage+        & QF.match .~ anyPattern+      request = defMessage+        & QF.predicate .~ predicate+        & QF.maxItems .~ 10++  -- Act+  result <- searchUtxos (queryClient client) request+  case result of+    Right (Right (Right (_headers, _trailers, Right response))) -> do+      putStrLn "✓ Response received from server"+      -- Assert+      let items = response ^. QF.items+      assertNotNull "ledgerTip" (Just $ response ^. QF.ledgerTip)+      assertTrue "found UTXOs" (length items > 0)++      -- Show actual data from response+      putStrLn $ "  Found " ++ show (length items) ++ " UTXOs containing policy ID"+      putStrLn $ "  Policy ID: " ++ T.unpack (T.decodeUtf8 $ Base16.encode policyIdBytes)++      -- Verify all returned UTXOs contain the searched policy ID+      forM_ items $ \utxo -> do+        let output = utxo ^. QF.cardano+        let assetGroups = output ^. CF.assets+        assertTrue "has assets" (length assetGroups > 0)++        -- Check that at least one asset group has the expected policy ID+        let hasExpectedPolicy = any (\ag -> (ag ^. CF.policyId) == policyIdBytes) assetGroups+        assertTrue "contains expected policy ID" hasExpectedPolicy++      putStrLn $ "✓ SearchUtxosByPolicyID test passed! Found " ++ show (length items) ++ " UTXOs"+      +    err -> throwString $ "SearchUtxosByPolicyID failed: " ++ show err++testSearchUtxosByAsset :: UtxorpcClient -> IO ()+testSearchUtxosByAsset client = do+  putStrLn "\n=== Test: SearchUtxosByAsset ==="++  -- Arrange+  let policyIdHex = "047e0f912c4260fe66ae271e5ae494dcd5f79635bbbb1386be195f4e"+      assetNameHex = "414c4c45594b41545a3030303630" -- "ALLEYKATZ00060"+      policyIdBytes = case Base16.decode policyIdHex of+        Right bs -> bs+        Left err -> error $ "Invalid hex: " ++ err+      assetNameOnlyBytes = case Base16.decode assetNameHex of+        Right bs -> bs+        Left err -> error $ "Invalid hex: " ++ err+      -- Concatenate policy ID and asset name for the assetName field+      fullAssetNameBytes = policyIdBytes <> assetNameOnlyBytes+      assetPattern = defMessage+        & CF.assetName .~ fullAssetNameBytes+      cardanoPattern = defMessage+        & CF.asset .~ assetPattern+      anyPattern = defMessage+        & QF.cardano .~ cardanoPattern+      predicate = defMessage+        & QF.match .~ anyPattern+      request = defMessage+        & QF.predicate .~ predicate+        & QF.maxItems .~ 10++  -- Act+  result <- searchUtxos (queryClient client) request+  case result of+    Right (Right (Right (_headers, _trailers, Right response))) -> do+      putStrLn "✓ Response received from server"+      -- Assert+      let items = response ^. QF.items+      assertNotNull "ledgerTip" (Just $ response ^. QF.ledgerTip)+      assertTrue "found UTXOs" (length items > 0)++      -- Show actual data from response+      putStrLn $ "  Found " ++ show (length items) ++ " UTXOs containing specific asset"+      putStrLn $ "  Asset: " ++ T.unpack (T.decodeUtf8 assetNameHex) ++ " (ALLEYKATZ00060)"++      -- Verify all returned UTXOs contain the exact asset+      forM_ items $ \utxo -> do+        let output = utxo ^. QF.cardano+        let assetGroups = output ^. CF.assets++        -- Find the asset group with our policy ID+        let maybeAssetGroup = find (\ag -> (ag ^. CF.policyId) == policyIdBytes) assetGroups+        case maybeAssetGroup of+          Just assetGroup -> do+            let assets = assetGroup ^. CF.assets++            -- Check that this group contains our asset name (just the name part, not policy+name)+            let hasExpectedAsset = any (\a -> (a ^. CF.name) == assetNameOnlyBytes) assets+            assertTrue "contains expected asset name" hasExpectedAsset+          Nothing -> throwString "Asset group with expected policy ID not found"++      putStrLn $ "✓ SearchUtxosByAsset test passed! Found " ++ show (length items) ++ " UTXOs"+      +    err -> throwString $ "SearchUtxosByAsset failed: " ++ show err++testSyncFollowTip :: UtxorpcClient -> IO ()+testSyncFollowTip client = do+  putStrLn "\n=== Test: SyncFollowTip ==="+  +  -- Arrange - Use the same test point as the Node SDK+  let testSlot = 85213090+      testHashHex = "e50842b1cc3ac813cb88d1533c3dea0f92e0ea945f53487c1d960c2210d0c3ba"+      testHashBytes = case Base16.decode testHashHex of+        Right bs -> bs+        Left err -> error $ "Invalid hex: " ++ err+      intersectPoint = defMessage+        & SF.slot .~ testSlot+        & SF.hash .~ testHashBytes+      request = defMessage+        & SF.intersect .~ [intersectPoint]+  +  -- Act - Follow the same pattern as the working example but catch the planned exception+  result <- try $ followTip (syncClient client) (0 :: Int) request streamHandler+  case result of+    Left (exc :: SomeException) -> +      if "FollowTip test completed successfully" `T.isInfixOf` T.pack (show exc) then+        putStrLn "✓ SyncFollowTip test passed! Validated block assertions and stopped"+      else+        putStrLn $ "SyncFollowTip failed: " ++ show exc+    Right streamResult -> case streamResult of+      Right (Right (messageCount, _headers, _trailers)) -> do+        putStrLn $ "✓ SyncFollowTip test passed! Received " ++ show messageCount ++ " messages"+      err -> putStrLn $ "SyncFollowTip failed: " ++ show err+  where+    streamHandler count _headers (response :: FollowTipResponse) = do+      putStrLn $ "Received stream message #" ++ show (count + 1)+      +      -- Get the block from the response+      let maybeBlock = case response ^. SF.maybe'action of+            Just action -> case action of+              S.FollowTipResponse'Apply applyAction -> Just applyAction+              S.FollowTipResponse'Undo undoAction -> Just undoAction+              _ -> Nothing+            Nothing -> Nothing+      +      case maybeBlock of+        Just block -> do+          -- Assert block has cardano data+          let cardanoBlock = block ^. SF.cardano+          let blockHeader = cardanoBlock ^. CF.header+          let blockSlot = blockHeader ^. SF.slot+          let blockHash = blockHeader ^. SF.hash+          +          -- Assert that slot is a positive number (like other tests)+          assertTrue ("followTip slot > 0 for message #" ++ show (count + 1)) (blockSlot > 0)+          +          -- Assert that hash is not empty (like other tests)+          assertTrue ("followTip hash not empty for message #" ++ show (count + 1)) (BS.length blockHash > 0)+          +          -- Assert that height is reasonable (like other tests)+          let blockHeight = blockHeader ^. SF.height+          assertTrue ("followTip height > 0 for message #" ++ show (count + 1)) (blockHeight > 0)+          +          putStrLn $ "✓ Message #" ++ show (count + 1) ++ " - Slot: " ++ show blockSlot ++ +                     ", Height: " ++ show blockHeight ++ +                     ", Hash: " ++ T.unpack (T.take 16 $ T.decodeUtf8 $ Base16.encode blockHash) ++ "..."+          +          -- Stop after validating a few successful messages (like Node SDK tests)+          if count >= 2 then+            throwString "FollowTip test completed successfully - stopping stream"+          else+            return (count + 1)+        Nothing -> do+          putStrLn $ "⚠ Message #" ++ show (count + 1) ++ " - No block data (might be reset action)"+          return (count + 1)++testSyncReadTip :: UtxorpcClient -> IO ()+testSyncReadTip client = do+  putStrLn "\n=== Test: SyncReadTip ==="++  -- Arrange+  let request = defMessage++  -- Act+  result <- readTip (syncClient client) request+  case result of+    Right (Right (Right (_headers, _trailers, Right response))) -> do+      putStrLn "✓ Response received from server"+      -- Assert - ReadTipResponse has a tip field, not slot/hash directly+      let tip = response ^. SF.tip+      let tipSlot = tip ^. SF.slot+      let tipHash = tip ^. SF.hash++      -- Show actual data from response+      putStrLn $ "  Current tip slot: " ++ show tipSlot+      putStrLn $ "  Current tip hash: " ++ T.unpack (T.take 16 $ T.decodeUtf8 $ Base16.encode tipHash) ++ "..."++      -- Assert that slot is a positive number+      assertTrue "slot > 0" (tipSlot > 0)++      -- Assert that hash is not empty+      assertTrue "hash not empty" (BS.length tipHash > 0)++      putStrLn $ "✓ SyncReadTip test passed!"+      +    err -> throwString $ "SyncReadTip failed: " ++ show err++testSyncFetchBlock :: UtxorpcClient -> IO ()+testSyncFetchBlock client = do+  putStrLn "\n=== Test: SyncFetchBlock ==="++  -- Arrange - Use the same test point as the Node SDK+  let testSlot = 85213090+      testHashHex = "e50842b1cc3ac813cb88d1533c3dea0f92e0ea945f53487c1d960c2210d0c3ba"+      testHashBytes = case Base16.decode testHashHex of+        Right bs -> bs+        Left err -> error $ "Invalid hex: " ++ err+      blockRef = defMessage+        & SF.slot .~ testSlot+        & SF.hash .~ testHashBytes+      request = defMessage+        & SF.ref .~ [blockRef]++  -- Act+  result <- fetchBlock (syncClient client) request+  case result of+    Right (Right (Right (_headers, _trailers, Right response))) -> do+      putStrLn "✓ Response received from server"+      -- Assert+      let blocks = response ^. SF.block+      block <- assertSingle "blocks" blocks++      -- Assert block has cardano data+      let cardanoBlock = block ^. SF.cardano+      let blockHeader = cardanoBlock ^. CF.header+      let blockBody = cardanoBlock ^. CF.body+      let txCount = length (blockBody ^. CF.tx)++      -- Show actual data from response+      putStrLn $ "  Block slot: " ++ show (blockHeader ^. SF.slot)+      putStrLn $ "  Block height: " ++ show (blockHeader ^. SF.height)+      putStrLn $ "  Block hash: " ++ T.unpack (T.take 16 $ T.decodeUtf8 $ Base16.encode testHashBytes) ++ "..."+      putStrLn $ "  Transaction count: " ++ show txCount++      assertEqual "block slot" testSlot (blockHeader ^. SF.slot)+      assertEqual "block height" 3399486 (blockHeader ^. SF.height)++      -- Assert block body exists (even if empty)+      assertNotNull "block body" (Just blockBody)++      putStrLn "✓ SyncFetchBlock test passed!"+      +    err -> throwString $ "SyncFetchBlock failed: " ++ show err++testSyncDumpHistory :: UtxorpcClient -> IO ()+testSyncDumpHistory client = do+  putStrLn "\n=== Test: SyncDumpHistory ==="++  -- Arrange - Use the same test point as the Node SDK+  let testSlot = 85213090+      testHashHex = "e50842b1cc3ac813cb88d1533c3dea0f92e0ea945f53487c1d960c2210d0c3ba"+      testHashBytes = case Base16.decode testHashHex of+        Right bs -> bs+        Left err -> error $ "Invalid hex: " ++ err+      startToken = defMessage+        & SF.slot .~ testSlot+        & SF.hash .~ testHashBytes+      request = defMessage+        & SF.startToken .~ startToken+        & SF.maxItems .~ 1++  -- Act+  result <- dumpHistory (syncClient client) request+  case result of+    Right (Right (Right (_headers, _trailers, Right response))) -> do+      putStrLn "✓ Response received from server"+      -- Assert+      let blocks = response ^. SF.block+      block <- assertSingle "blocks" blocks++      -- Assert block has cardano data+      let cardanoBlock = block ^. SF.cardano+      let blockHeader = cardanoBlock ^. CF.header+      let blockBody = cardanoBlock ^. CF.body+      let txCount = length (blockBody ^. CF.tx)++      -- Show actual data from response+      putStrLn $ "  Block slot: " ++ show (blockHeader ^. SF.slot)+      putStrLn $ "  Block height: " ++ show (blockHeader ^. SF.height)+      putStrLn $ "  Transaction count: " ++ show txCount++      assertEqual "block slot" testSlot (blockHeader ^. SF.slot)+      assertEqual "block height" 3399486 (blockHeader ^. SF.height)++      -- Assert block body exists (even if empty)+      assertNotNull "block body" (Just blockBody)++      putStrLn "✓ SyncDumpHistory test passed!"++    err -> throwString $ "SyncDumpHistory failed: " ++ show err++-- ============================================================================+-- Submit Tests+-- ============================================================================++testSubmitTx :: UtxorpcClient -> IO ()+testSubmitTx client = do+  putStrLn "\n=== Test: SubmitTx ==="++  -- Arrange - Use a pre-built signed transaction CBOR hex+  let txCborHex = "84A300D9010282825820AAD611BED4B7089272AEE943CF0975544C46626272929EB50BD6A7D319B43BD303825820970727113A6B9D6E54E2541E5FFFD86D6CF7B7CEA4F6793821DE2C0D58B8D6B5030182A20058390053FBFFFAB7B001281917DE77F18A8087413BE03401DB4AA2A7DBF0AE1591D34D5B4B2728D04A80FDD041BB52EDB334DACBF25AA27877E738011A005B8D80A20058390053FBFFFAB7B001281917DE77F18A8087413BE03401DB4AA2A7DBF0AE1591D34D5B4B2728D04A80FDD041BB52EDB334DACBF25AA27877E738011B00000001D35FFD4C021A00029E8DA100D9010281825820D504D361777F985265A6F0A727E601E70F837574652760456B41731FF4F95258584046563C4A3D87995D5A1EC03632BF1AF532A1468F7F2403C1055676776D03B8F986EC667A2D67329D97052C9216CF9752E38430E1430F0FEE0FFF958E25F6020AF5F6"+      txCborBytes = case Base16.decode (T.encodeUtf8 $ T.pack txCborHex) of+        Right bs -> bs+        Left err -> error $ "Invalid CBOR hex: " ++ err++      -- Create AnyChain with Cardano raw CBOR+      anyChainTx = defMessage+        & SubF.raw .~ txCborBytes++      -- Create request with the transaction+      request = defMessage+        & SubF.tx .~ anyChainTx++  -- Act+  result <- submitTx (submitClient client) request+  case result of+    Right (Right (Right (_headers, _trailers, Right response))) -> do+      putStrLn "✓ Response received from server"++      -- Assert.NotNull(response) - implicit (we have response)+      -- Assert.NotNull(response.Refs)+      let txHash = response ^. SubF.ref+      assertNotNull "response.Refs" (if BS.null txHash then Nothing else Just txHash)++      -- Assert.Equal(32, response.Refs.Length)+      assertEqual "Tx hash length" 32 (BS.length txHash)++      putStrLn $ "  Transaction submitted successfully!"+      putStrLn $ "  Tx hash: " ++ T.unpack (T.decodeUtf8 $ Base16.encode txHash)++      putStrLn "✓ SubmitTx test passed!"++    err -> throwString $ "SubmitTx failed: " ++ show err++testWaitForTx :: UtxorpcClient -> IO ()+testWaitForTx client = do+  putStrLn "\n=== Test: WaitForTx ==="++  -- Arrange - First submit a transaction to wait for+  let txCborHex = "84A300D90102828258207478ED8D8B53CDB520E257AADAFD4CA0EC8671ECA1AE0EEE6D045F9ABE357A78018258208841ABB27C76AF2841727ACF562149EBE47957A71C5C12C8A6A1FAE089CCAAD8020182A20058390053FBFFFAB7B001281917DE77F18A8087413BE03401DB4AA2A7DBF0AE1591D34D5B4B2728D04A80FDD041BB52EDB334DACBF25AA27877E738011A005B8D80A20058390053FBFFFAB7B001281917DE77F18A8087413BE03401DB4AA2A7DBF0AE1591D34D5B4B2728D04A80FDD041BB52EDB334DACBF25AA27877E738011B00000001D3D2B3A2021A00029E8DA100D9010281825820D504D361777F985265A6F0A727E601E70F837574652760456B41731FF4F952585840AC13B5036324F1273A576834F4A11474CC0FF7C79F31B1E1BD79301E671172CC94B7BCA8C816F80BE343A2389998E0E861C53F501614AD5186355B65F228EF0BF5F6"+      txCborBytes = case Base16.decode (T.encodeUtf8 $ T.pack txCborHex) of+        Right bs -> bs+        Left err -> error $ "Invalid CBOR hex: " ++ err++      anyChainTx = defMessage+        & SubF.raw .~ txCborBytes++      submitRequest = defMessage+        & SubF.tx .~ anyChainTx++  -- Submit the transaction first+  submitResult <- submitTx (submitClient client) submitRequest+  txHash <- case submitResult of+    Right (Right (Right (_headers, _trailers, Right response))) -> do+      let txHash = response ^. SubF.ref+      assertTrue "has tx ref" (not $ BS.null txHash)+      return txHash+    err -> throwString $ "Failed to submit tx for WaitForTx test: " ++ show err++  putStrLn $ "  Submitted tx hash: " ++ T.unpack (T.take 16 $ T.decodeUtf8 $ Base16.encode txHash) ++ "..."++  -- Create WaitForTxRequest with tx hash+  let waitRequest = defMessage+        & SubF.ref .~ [txHash]++  -- Act - Wait for the transaction (collect at least 2 stages)+  result <- try $ waitForTx (submitClient client) [] waitRequest streamHandler+  case result of+    Left (exc :: SomeException) ->+      if "WaitForTx test completed" `T.isInfixOf` T.pack (show exc) then+        putStrLn "✓ WaitForTx test passed! Validated stages and stopped"+      else+        throwString $ "WaitForTx failed: " ++ show exc+    Right streamResult -> case streamResult of+      Right (Right (stages, _headers, _trailers)) -> do+        -- Assert.True(stages.Count >= 1)+        assertTrue "stages.Count >= 1" (length stages >= 1)++        let firstStage = head stages+        -- Assert.NotNull(firstStage) - implicit+        -- Assert.NotNull(firstStage.Ref)+        let stageRef = firstStage ^. SubF.ref+        assertNotNull "firstStage.Ref" (if BS.null stageRef then Nothing else Just stageRef)++        -- Assert.Equal(txHash, firstStage.Ref)+        assertEqual "firstStage.Ref matches txHash" txHash stageRef++        -- Assert.True(firstStage.Stage >= Stage.Acknowledged)+        let stageNum = firstStage ^. SubF.stage+        assertTrue "firstStage.Stage >= Acknowledged" (stageNum >= STAGE_ACKNOWLEDGED)++        putStrLn $ "✓ WaitForTx test passed! Received " ++ show (length stages) ++ " stage updates"+      err -> throwString $ "WaitForTx failed: " ++ show err+  where+    streamHandler :: [WaitForTxResponse] -> a -> WaitForTxResponse -> IO [WaitForTxResponse]+    streamHandler stages _headers response = do+      let stageNum = response ^. SubF.stage+      putStrLn $ "  Stage update #" ++ show (length stages + 1) ++ ": " ++ show stageNum++      let newStages = stages ++ [response]+      -- Stop after 2 stages: if (stages.Count >= 2) break+      if length newStages >= 2 then+        throwString "WaitForTx test completed - stopping stream"+      else+        return newStages++testWatchMempoolByAddress :: UtxorpcClient -> IO ()+testWatchMempoolByAddress client = do+  putStrLn "\n=== Test: WatchMempoolByAddress ==="++  -- Arrange+  let receiverAddressHex = "0053fbfffab7b001281917de77f18a8087413be03401db4aa2a7dbf0ae1591d34d5b4b2728d04a80fdd041bb52edb334dacbf25aa27877e738"+      receiverAddressBytes = case Base16.decode receiverAddressHex of+        Right bs -> bs+        Left err -> error $ "Invalid address hex: " ++ err++      -- Create address predicate for exact address match+      addressPattern = (defMessage :: AddressPattern)+        & CF.exactAddress .~ receiverAddressBytes+      txPattern = (defMessage :: TxPattern)+        & CF.hasAddress .~ addressPattern+      anyChainPattern = (defMessage :: AnyChainTxPattern)+        & SubF.cardano .~ txPattern+      predicate = (defMessage :: TxPredicate)+        & SubF.match .~ anyChainPattern+      watchRequest = defMessage+        & SubF.predicate .~ predicate++  putStrLn $ "  Watching for address: " ++ T.unpack (T.take 20 $ T.decodeUtf8 $ Base16.encode receiverAddressBytes) ++ "..."++  -- Act - Watch mempool (transaction already submitted)+  result <- try $ watchMempool (submitClient client) [] watchRequest streamHandler+  case result of+    Left (exc :: SomeException) ->+      if "WatchMempoolByAddress completed" `T.isInfixOf` T.pack (show exc) then+        putStrLn "✓ WatchMempoolByAddress test passed! Found matching transaction"+      else+        throwString $ "WatchMempoolByAddress failed: " ++ show exc+    Right streamResult -> case streamResult of+      Right (Right (events, _headers, _trailers)) -> do+        -- Assert.True(events.Count > 0)+        assertTrue "events.Count > 0" (length events > 0)++        let firstEvent = head events+        -- Assert.NotNull(firstEvent.Tx)+        let maybeTx = firstEvent ^. SubF.maybe'tx+        assertNotNull "firstEvent.Tx" maybeTx++        case maybeTx of+          Just txInfo -> do+            -- Assert.NotNull(firstEvent.Tx?.Ref)+            let txRef = txInfo ^. SubF.ref+            assertNotNull "firstEvent.Tx.Ref" (if BS.null txRef then Nothing else Just txRef)++            -- Assert.True(firstEvent.Tx?.Stage >= Stage.Acknowledged)+            let stage = txInfo ^. SubF.stage+            assertTrue "firstEvent.Tx.Stage >= Acknowledged" (stage >= STAGE_ACKNOWLEDGED)++            putStrLn $ "✓ WatchMempoolByAddress test passed! Found " ++ show (length events) ++ " event(s)"+          Nothing -> throwString "firstEvent.Tx is null"++      err -> throwString $ "WatchMempoolByAddress failed: " ++ show err+  where+    streamHandler :: [WatchMempoolResponse] -> a -> WatchMempoolResponse -> IO [WatchMempoolResponse]+    streamHandler events _headers response = do+      let maybeTx = response ^. SubF.maybe'tx+      case maybeTx of+        Just txInfo -> do+          let txRef = txInfo ^. SubF.ref+          let stage = txInfo ^. SubF.stage+          putStrLn $ "  Found transaction in mempool!"+          putStrLn $ "    Tx ref: " ++ T.unpack (T.take 16 $ T.decodeUtf8 $ Base16.encode txRef) ++ "..."+          putStrLn $ "    Stage: " ++ show stage++          let newEvents = events ++ [response]+          -- Stop after 1 event: if (events.Count >= 1) break+          if length newEvents >= 1 then+            throwString "WatchMempoolByAddress completed - stopping stream"+          else+            return newEvents+        Nothing -> do+          putStrLn "  Waiting for matching transaction..."+          return events++testWatchMempoolByPaymentPart :: UtxorpcClient -> IO ()+testWatchMempoolByPaymentPart client = do+  putStrLn "\n=== Test: WatchMempoolByPaymentPart ==="++  -- Arrange - Extract payment credential from address+  let paymentCredHex = "53fbfffab7b001281917de77f18a8087413be03401db4aa2a7dbf0ae"+      paymentCredBytes = case Base16.decode paymentCredHex of+        Right bs -> bs+        Left err -> error $ "Invalid payment cred hex: " ++ err++      addressPattern = (defMessage :: AddressPattern)+        & CF.paymentPart .~ paymentCredBytes+      txPattern = (defMessage :: TxPattern)+        & CF.hasAddress .~ addressPattern+      anyChainPattern = (defMessage :: AnyChainTxPattern)+        & SubF.cardano .~ txPattern+      predicate = (defMessage :: TxPredicate)+        & SubF.match .~ anyChainPattern+      watchRequest = defMessage+        & SubF.predicate .~ predicate++  putStrLn $ "  Watching for payment credential: " ++ T.unpack (T.decodeUtf8 $ Base16.encode paymentCredBytes)++  -- Act - Watch mempool+  result <- try $ watchMempool (submitClient client) [] watchRequest streamHandler+  case result of+    Left (exc :: SomeException) ->+      if "WatchMempoolByPaymentPart completed" `T.isInfixOf` T.pack (show exc) then+        putStrLn "✓ WatchMempoolByPaymentPart test passed! Found matching transaction"+      else+        throwString $ "WatchMempoolByPaymentPart failed: " ++ show exc+    Right streamResult -> case streamResult of+      Right (Right (events, _headers, _trailers)) -> do+        assertTrue "events.Count > 0" (length events > 0)+        let firstEvent = head events+        let maybeTx = firstEvent ^. SubF.maybe'tx+        assertNotNull "firstEvent.Tx" maybeTx+        case maybeTx of+          Just txInfo -> do+            let txRef = txInfo ^. SubF.ref+            assertNotNull "firstEvent.Tx.Ref" (if BS.null txRef then Nothing else Just txRef)+            let stage = txInfo ^. SubF.stage+            assertTrue "firstEvent.Tx.Stage >= Acknowledged" (stage >= STAGE_ACKNOWLEDGED)+            putStrLn $ "✓ WatchMempoolByPaymentPart test passed!"+          Nothing -> throwString "firstEvent.Tx is null"+      err -> throwString $ "WatchMempoolByPaymentPart failed: " ++ show err+  where+    streamHandler :: [WatchMempoolResponse] -> a -> WatchMempoolResponse -> IO [WatchMempoolResponse]+    streamHandler events _headers response = do+      let maybeTx = response ^. SubF.maybe'tx+      case maybeTx of+        Just txInfo -> do+          let txRef = txInfo ^. SubF.ref+          let stage = txInfo ^. SubF.stage+          putStrLn $ "  Found transaction matching payment credential!"+          putStrLn $ "    Tx ref: " ++ T.unpack (T.take 16 $ T.decodeUtf8 $ Base16.encode txRef) ++ "..."+          putStrLn $ "    Stage: " ++ show stage+          let newEvents = events ++ [response]+          if length newEvents >= 1 then+            throwString "WatchMempoolByPaymentPart completed - stopping stream"+          else+            return newEvents+        Nothing -> return events++testWatchMempoolByDelegationPart :: UtxorpcClient -> IO ()+testWatchMempoolByDelegationPart client = do+  putStrLn "\n=== Test: WatchMempoolByDelegationPart ==="++  -- Arrange - Extract delegation credential from address+  let delegationCredHex = "1591d34d5b4b2728d04a80fdd041bb52edb334dacbf25aa27877e738"+      delegationCredBytes = case Base16.decode delegationCredHex of+        Right bs -> bs+        Left err -> error $ "Invalid delegation cred hex: " ++ err++      addressPattern = (defMessage :: AddressPattern)+        & CF.delegationPart .~ delegationCredBytes+      txPattern = (defMessage :: TxPattern)+        & CF.hasAddress .~ addressPattern+      anyChainPattern = (defMessage :: AnyChainTxPattern)+        & SubF.cardano .~ txPattern+      predicate = (defMessage :: TxPredicate)+        & SubF.match .~ anyChainPattern+      watchRequest = defMessage+        & SubF.predicate .~ predicate++  putStrLn $ "  Watching for delegation credential: " ++ T.unpack (T.decodeUtf8 $ Base16.encode delegationCredBytes)++  -- Act - Watch mempool+  result <- try $ watchMempool (submitClient client) [] watchRequest streamHandler+  case result of+    Left (exc :: SomeException) ->+      if "WatchMempoolByDelegationPart completed" `T.isInfixOf` T.pack (show exc) then+        putStrLn "✓ WatchMempoolByDelegationPart test passed! Found matching transaction"+      else+        throwString $ "WatchMempoolByDelegationPart failed: " ++ show exc+    Right streamResult -> case streamResult of+      Right (Right (events, _headers, _trailers)) -> do+        assertTrue "events.Count > 0" (length events > 0)+        let firstEvent = head events+        let maybeTx = firstEvent ^. SubF.maybe'tx+        assertNotNull "firstEvent.Tx" maybeTx+        case maybeTx of+          Just txInfo -> do+            let txRef = txInfo ^. SubF.ref+            assertNotNull "firstEvent.Tx.Ref" (if BS.null txRef then Nothing else Just txRef)+            let stage = txInfo ^. SubF.stage+            assertTrue "firstEvent.Tx.Stage >= Acknowledged" (stage >= STAGE_ACKNOWLEDGED)+            putStrLn $ "✓ WatchMempoolByDelegationPart test passed!"+          Nothing -> throwString "firstEvent.Tx is null"+      err -> throwString $ "WatchMempoolByDelegationPart failed: " ++ show err+  where+    streamHandler :: [WatchMempoolResponse] -> a -> WatchMempoolResponse -> IO [WatchMempoolResponse]+    streamHandler events _headers response = do+      let maybeTx = response ^. SubF.maybe'tx+      case maybeTx of+        Just txInfo -> do+          let txRef = txInfo ^. SubF.ref+          let stage = txInfo ^. SubF.stage+          putStrLn $ "  Found transaction matching delegation credential!"+          putStrLn $ "    Tx ref: " ++ T.unpack (T.take 16 $ T.decodeUtf8 $ Base16.encode txRef) ++ "..."+          putStrLn $ "    Stage: " ++ show stage+          let newEvents = events ++ [response]+          if length newEvents >= 1 then+            throwString "WatchMempoolByDelegationPart completed - stopping stream"+          else+            return newEvents+        Nothing -> return events++testWatchMempoolByPolicyId :: UtxorpcClient -> IO ()+testWatchMempoolByPolicyId client = do+  putStrLn "\n=== Test: WatchMempoolByPolicyId ==="++  -- Arrange+  let policyIdHex = "8b05e87a51c1d4a0fa888d2bb14dbc25e8c343ea379a171b63aa84a0"+      policyIdBytes = case Base16.decode policyIdHex of+        Right bs -> bs+        Left err -> error $ "Invalid policy ID hex: " ++ err++      assetPattern = (defMessage :: AssetPattern)+        & CF.policyId .~ policyIdBytes+      txPattern = (defMessage :: TxPattern)+        & CF.movesAsset .~ assetPattern+      anyChainPattern = (defMessage :: AnyChainTxPattern)+        & SubF.cardano .~ txPattern+      predicate = (defMessage :: TxPredicate)+        & SubF.match .~ anyChainPattern+      watchRequest = defMessage+        & SubF.predicate .~ predicate++  putStrLn $ "  Watching for policy ID: " ++ T.unpack (T.decodeUtf8 $ Base16.encode policyIdBytes)++  -- Act - Watch mempool+  result <- try $ watchMempool (submitClient client) [] watchRequest streamHandler+  case result of+    Left (exc :: SomeException) ->+      if "WatchMempoolByPolicyId completed" `T.isInfixOf` T.pack (show exc) then+        putStrLn "✓ WatchMempoolByPolicyId test passed! Found matching transaction"+      else+        throwString $ "WatchMempoolByPolicyId failed: " ++ show exc+    Right streamResult -> case streamResult of+      Right (Right (events, _headers, _trailers)) -> do+        assertTrue "events.Count > 0" (length events > 0)+        let firstEvent = head events+        let maybeTx = firstEvent ^. SubF.maybe'tx+        assertNotNull "firstEvent.Tx" maybeTx+        case maybeTx of+          Just txInfo -> do+            let txRef = txInfo ^. SubF.ref+            assertNotNull "firstEvent.Tx.Ref" (if BS.null txRef then Nothing else Just txRef)+            let stage = txInfo ^. SubF.stage+            assertTrue "firstEvent.Tx.Stage >= Acknowledged" (stage >= STAGE_ACKNOWLEDGED)+            putStrLn $ "✓ WatchMempoolByPolicyId test passed!"+          Nothing -> throwString "firstEvent.Tx is null"+      err -> throwString $ "WatchMempoolByPolicyId failed: " ++ show err+  where+    streamHandler :: [WatchMempoolResponse] -> a -> WatchMempoolResponse -> IO [WatchMempoolResponse]+    streamHandler events _headers response = do+      let maybeTx = response ^. SubF.maybe'tx+      case maybeTx of+        Just txInfo -> do+          let txRef = txInfo ^. SubF.ref+          let stage = txInfo ^. SubF.stage+          putStrLn $ "  Found transaction with policy ID!"+          putStrLn $ "    Tx ref: " ++ T.unpack (T.take 16 $ T.decodeUtf8 $ Base16.encode txRef) ++ "..."+          putStrLn $ "    Stage: " ++ show stage+          let newEvents = events ++ [response]+          if length newEvents >= 1 then+            throwString "WatchMempoolByPolicyId completed - stopping stream"+          else+            return newEvents+        Nothing -> return events++testWatchMempoolByAsset :: UtxorpcClient -> IO ()+testWatchMempoolByAsset client = do+  putStrLn "\n=== Test: WatchMempoolByAsset ==="++  -- Arrange+  let policyIdHex = "8b05e87a51c1d4a0fa888d2bb14dbc25e8c343ea379a171b63aa84a0"+      assetNameHex = "434e4354" -- "CNCT" in hex+      policyIdBytes = case Base16.decode policyIdHex of+        Right bs -> bs+        Left err -> error $ "Invalid policy ID hex: " ++ err+      assetNameOnlyBytes = case Base16.decode assetNameHex of+        Right bs -> bs+        Left err -> error $ "Invalid asset name hex: " ++ err+      -- Concatenate policy ID and asset name+      fullAssetNameBytes = policyIdBytes <> assetNameOnlyBytes++      assetPattern = (defMessage :: AssetPattern)+        & CF.policyId .~ policyIdBytes+        & CF.assetName .~ assetNameOnlyBytes+      txPattern = (defMessage :: TxPattern)+        & CF.movesAsset .~ assetPattern+      anyChainPattern = (defMessage :: AnyChainTxPattern)+        & SubF.cardano .~ txPattern+      predicate = (defMessage :: TxPredicate)+        & SubF.match .~ anyChainPattern+      watchRequest = defMessage+        & SubF.predicate .~ predicate++  putStrLn $ "  Watching for asset: " ++ T.unpack (T.decodeUtf8 assetNameHex) ++ " (CNCT)"++  -- Act - Watch mempool+  result <- try $ watchMempool (submitClient client) [] watchRequest streamHandler+  case result of+    Left (exc :: SomeException) ->+      if "WatchMempoolByAsset completed" `T.isInfixOf` T.pack (show exc) then+        putStrLn "✓ WatchMempoolByAsset test passed! Found matching transaction"+      else+        throwString $ "WatchMempoolByAsset failed: " ++ show exc+    Right streamResult -> case streamResult of+      Right (Right (events, _headers, _trailers)) -> do+        assertTrue "events.Count > 0" (length events > 0)+        let firstEvent = head events+        let maybeTx = firstEvent ^. SubF.maybe'tx+        assertNotNull "firstEvent.Tx" maybeTx+        case maybeTx of+          Just txInfo -> do+            let txRef = txInfo ^. SubF.ref+            assertNotNull "firstEvent.Tx.Ref" (if BS.null txRef then Nothing else Just txRef)+            let stage = txInfo ^. SubF.stage+            assertTrue "firstEvent.Tx.Stage >= Acknowledged" (stage >= STAGE_ACKNOWLEDGED)+            putStrLn $ "✓ WatchMempoolByAsset test passed!"+          Nothing -> throwString "firstEvent.Tx is null"+      err -> throwString $ "WatchMempoolByAsset failed: " ++ show err+  where+    streamHandler :: [WatchMempoolResponse] -> a -> WatchMempoolResponse -> IO [WatchMempoolResponse]+    streamHandler events _headers response = do+      let maybeTx = response ^. SubF.maybe'tx+      case maybeTx of+        Just txInfo -> do+          let txRef = txInfo ^. SubF.ref+          let stage = txInfo ^. SubF.stage+          putStrLn $ "  Found transaction with specific asset!"+          putStrLn $ "    Tx ref: " ++ T.unpack (T.take 16 $ T.decodeUtf8 $ Base16.encode txRef) ++ "..."+          putStrLn $ "    Stage: " ++ show stage+          let newEvents = events ++ [response]+          if length newEvents >= 1 then+            throwString "WatchMempoolByAsset completed - stopping stream"+          else+            return newEvents+        Nothing -> return events++-- ============================================================================+-- Watch Tests+-- ============================================================================++testWatchTxForAddress :: UtxorpcClient -> IO ()+testWatchTxForAddress client = do+  putStrLn "\n=== Test: WatchTxForAddress ==="++  -- Arrange+  let receiverAddressHex = "0053fbfffab7b001281917de77f18a8087413be03401db4aa2a7dbf0ae1591d34d5b4b2728d04a80fdd041bb52edb334dacbf25aa27877e738"+      receiverAddressBytes = case Base16.decode receiverAddressHex of+        Right bs -> bs+        Left err -> error $ "Invalid address hex: " ++ err++      -- Create address predicate for exact address match+      addressPattern = (defMessage :: AddressPattern)+        & CF.exactAddress .~ receiverAddressBytes+      txPattern = (defMessage :: TxPattern)+        & CF.hasAddress .~ addressPattern+      anyChainPattern = (defMessage :: W.AnyChainTxPattern)+        & WF.cardano .~ txPattern+      predicate = (defMessage :: W.TxPredicate)+        & WF.match .~ anyChainPattern+      watchRequest = defMessage+        & WF.predicate .~ predicate++  putStrLn $ "  Watching for address: " ++ T.unpack (T.take 20 $ T.decodeUtf8 $ Base16.encode receiverAddressBytes) ++ "..."++  -- Create MVar to store watcher result+  resultVar <- newEmptyMVar++  -- Start watching in background (before submitting transaction)+  _ <- forkIO $ do+    result <- try $ watchTx (watchClient client) [] watchRequest streamHandler+    putMVar resultVar result++  -- Give the watcher time to start+  putStrLn "  Started watching, waiting 1 second for watcher to initialize..."+  threadDelay 1000000  -- 1 second in microseconds++  -- Submit transaction+  putStrLn "  Submitting transaction..."+  let txCborHex = "84A300D90102828258209E0CC477DF20C37C684E47B9701F995C8733B07901DE5447FE4F61EDF14F6BCB01825820176A54A6E3911FF5C65134928079E40783C8A88F1143091687F3FE7775EEF55F000182A20058390053FBFFFAB7B001281917DE77F18A8087413BE03401DB4AA2A7DBF0AE1591D34D5B4B2728D04A80FDD041BB52EDB334DACBF25AA27877E738011A005B8D80A20058390053FBFFFAB7B001281917DE77F18A8087413BE03401DB4AA2A7DBF0AE1591D34D5B4B2728D04A80FDD041BB52EDB334DACBF25AA27877E738011B00000001D4478888021A00029E8DA100D9010281825820D504D361777F985265A6F0A727E601E70F837574652760456B41731FF4F952585840DBBF46C2B615D9EE1689F6339A537A3A16507C2566E067A717F0D379D9944F8FEF1045AA525B71EB6E29877BFAEA83F295F25707B0F7AF41222E3F33C9861C0CF5F6"+      txCborBytes = case Base16.decode (T.encodeUtf8 $ T.pack txCborHex) of+        Right bs -> bs+        Left err -> error $ "Invalid CBOR hex: " ++ err+      anyChainTx = defMessage & SubF.raw .~ txCborBytes+      submitRequest = defMessage & SubF.tx .~ anyChainTx++  submitResult <- submitTx (submitClient client) submitRequest+  case submitResult of+    Right (Right (Right (_headers, _trailers, Right response))) -> do+      let txHash = response ^. SubF.ref+      when (not (BS.null txHash)) $ do+        putStrLn $ "  Transaction submitted! Hash: " ++ T.unpack (T.take 16 $ T.decodeUtf8 $ Base16.encode txHash) ++ "..."+    _ -> putStrLn "  Warning: Transaction submission may have issues"++  putStrLn "  Waiting for Watch event (transaction to be applied to block)..."++  -- Wait for watcher result+  result <- takeMVar resultVar++  -- Assert+  case result of+    Left (exc :: SomeException) ->+      if "WatchTxForAddress completed" `T.isInfixOf` T.pack (show exc) then+        putStrLn "✓ WatchTxForAddress test passed! Found matching transaction in block"+      else+        throwString $ "WatchTxForAddress failed: " ++ show exc+    Right streamResult -> case streamResult of+      Right (Right (events, _headers, _trailers)) -> do+        -- Assert.True(events.Count > 0)+        assertTrue "events.Count > 0" (length events > 0)++        let firstEvent = head events+        -- Assert.NotNull(firstEvent)+        let maybeAction = firstEvent ^. WF.maybe'action+        assertNotNull "firstEvent.Action" maybeAction++        -- Assert.Equal(WatchTxAction.Apply, firstEvent.Action)+        case maybeAction of+          Just (WatchTxResponse'Apply _) ->+            putStrLn $ "✓ WatchTxForAddress test passed! Found " ++ show (length events) ++ " Apply event(s)"+          _ -> throwString "Expected Apply action"++      err -> throwString $ "WatchTxForAddress failed: " ++ show err+  where+    streamHandler :: [WatchTxResponse] -> a -> WatchTxResponse -> IO [WatchTxResponse]+    streamHandler events _headers response = do+      let maybeAction = response ^. WF.maybe'action+      case maybeAction of+        Just (WatchTxResponse'Apply _) -> do+          putStrLn $ "  Found Apply action for transaction!"+          let newEvents = events ++ [response]+          if length newEvents >= 1 then+            throwString "WatchTxForAddress completed - stopping stream"+          else+            return newEvents+        _ -> return events++testWatchTxForPaymentPart :: UtxorpcClient -> IO ()+testWatchTxForPaymentPart client = do+  putStrLn "\n=== Test: WatchTxForPaymentPart ==="++  -- Arrange - Extract payment credential from address+  let paymentCredHex = "53fbfffab7b001281917de77f18a8087413be03401db4aa2a7dbf0ae"+      paymentCredBytes = case Base16.decode paymentCredHex of+        Right bs -> bs+        Left err -> error $ "Invalid payment cred hex: " ++ err++      addressPattern = (defMessage :: AddressPattern)+        & CF.paymentPart .~ paymentCredBytes+      txPattern = (defMessage :: TxPattern)+        & CF.hasAddress .~ addressPattern+      anyChainPattern = (defMessage :: W.AnyChainTxPattern)+        & WF.cardano .~ txPattern+      predicate = (defMessage :: W.TxPredicate)+        & WF.match .~ anyChainPattern+      watchRequest = defMessage+        & WF.predicate .~ predicate++  putStrLn $ "  Watching for payment credential: " ++ T.unpack (T.decodeUtf8 $ Base16.encode paymentCredBytes)++  -- Create MVar to store watcher result+  resultVar <- newEmptyMVar++  -- Start watching in background+  _ <- forkIO $ do+    result <- try $ watchTx (watchClient client) [] watchRequest streamHandler+    putMVar resultVar result++  -- Give the watcher time to start+  putStrLn "  Started watching, waiting 1 second..."+  threadDelay 1000000++  -- Submit transaction+  putStrLn "  Submitting transaction..."+  let txCborHex = "84A300D9010282825820896E271D8CC28CB59E9E9D0464C06D3C2A48A0766438AD9FFBABC9597C4E072101825820CE5996492EA64AE09B9D06A062FD8C98D2231A325FF48071C811729E8B7D420E000182A20058390053FBFFFAB7B001281917DE77F18A8087413BE03401DB4AA2A7DBF0AE1591D34D5B4B2728D04A80FDD041BB52EDB334DACBF25AA27877E738011A005B8D80A20058390053FBFFFAB7B001281917DE77F18A8087413BE03401DB4AA2A7DBF0AE1591D34D5B4B2728D04A80FDD041BB52EDB334DACBF25AA27877E738011B00000001D481F2FB021A00029E8DA100D9010281825820D504D361777F985265A6F0A727E601E70F837574652760456B41731FF4F952585840A92445E02162D6F7212713A41124B8ED260DB632EC37A9F77BEB1EE813E108813F216A06CEBCB42708D1C87CD20F861F395B59871FE9A0CB9CC6A294C6FE4104F5F6"+      txCborBytes = case Base16.decode (T.encodeUtf8 $ T.pack txCborHex) of+        Right bs -> bs+        Left err -> error $ "Invalid CBOR hex: " ++ err+      anyChainTx = defMessage & SubF.raw .~ txCborBytes+      submitRequest = defMessage & SubF.tx .~ anyChainTx++  submitResult <- submitTx (submitClient client) submitRequest+  case submitResult of+    Right (Right (Right (_headers, _trailers, Right response))) -> do+      let txHash = response ^. SubF.ref+      when (not (BS.null txHash)) $ do+        putStrLn $ "  Transaction submitted! Hash: " ++ T.unpack (T.take 16 $ T.decodeUtf8 $ Base16.encode txHash) ++ "..."+    _ -> putStrLn "  Warning: Transaction submission may have issues"++  putStrLn "  Waiting for Watch event..."++  -- Wait for watcher result+  result <- takeMVar resultVar++  -- Assert+  case result of+    Left (exc :: SomeException) ->+      if "WatchTxForPaymentPart completed" `T.isInfixOf` T.pack (show exc) then+        putStrLn "✓ WatchTxForPaymentPart test passed! Found matching transaction"+      else+        throwString $ "WatchTxForPaymentPart failed: " ++ show exc+    Right streamResult -> case streamResult of+      Right (Right (events, _headers, _trailers)) -> do+        assertTrue "events.Count > 0" (length events > 0)+        let firstEvent = head events+        let maybeAction = firstEvent ^. WF.maybe'action+        assertNotNull "firstEvent.Action" maybeAction+        case maybeAction of+          Just (WatchTxResponse'Apply _) ->+            putStrLn $ "✓ WatchTxForPaymentPart test passed!"+          _ -> throwString "Expected Apply action"+      err -> throwString $ "WatchTxForPaymentPart failed: " ++ show err+  where+    streamHandler :: [WatchTxResponse] -> a -> WatchTxResponse -> IO [WatchTxResponse]+    streamHandler events _headers response = do+      let maybeAction = response ^. WF.maybe'action+      case maybeAction of+        Just (WatchTxResponse'Apply _) -> do+          putStrLn $ "  Found Apply action for transaction matching payment credential!"+          let newEvents = events ++ [response]+          if length newEvents >= 1 then+            throwString "WatchTxForPaymentPart completed - stopping stream"+          else+            return newEvents+        _ -> return events++testWatchTxForDelegationPart :: UtxorpcClient -> IO ()+testWatchTxForDelegationPart client = do+  putStrLn "\n=== Test: WatchTxForDelegationPart ==="++  -- Arrange - Extract delegation credential from address+  let delegationCredHex = "1591d34d5b4b2728d04a80fdd041bb52edb334dacbf25aa27877e738"+      delegationCredBytes = case Base16.decode delegationCredHex of+        Right bs -> bs+        Left err -> error $ "Invalid delegation cred hex: " ++ err++      addressPattern = (defMessage :: AddressPattern)+        & CF.delegationPart .~ delegationCredBytes+      txPattern = (defMessage :: TxPattern)+        & CF.hasAddress .~ addressPattern+      anyChainPattern = (defMessage :: W.AnyChainTxPattern)+        & WF.cardano .~ txPattern+      predicate = (defMessage :: W.TxPredicate)+        & WF.match .~ anyChainPattern+      watchRequest = defMessage+        & WF.predicate .~ predicate++  putStrLn $ "  Watching for delegation credential: " ++ T.unpack (T.decodeUtf8 $ Base16.encode delegationCredBytes)++  -- Create MVar to store watcher result+  resultVar <- newEmptyMVar++  -- Start watching in background+  _ <- forkIO $ do+    result <- try $ watchTx (watchClient client) [] watchRequest streamHandler+    putMVar resultVar result++  -- Give the watcher time to start+  putStrLn "  Started watching, waiting 1 second..."+  threadDelay 1000000++  -- Submit transaction+  putStrLn "  Submitting transaction..."+  let txCborHex = "84A300D90102828258201F0716BFC556282969C9C3CC41E8F598A4D668AAE0120554BBFBE1D6DA2AD1CC01825820488B24DAE71016C45A8BEE133185D0FA22DC18F19C24C464898810B1944D5A28000182A20058390053FBFFFAB7B001281917DE77F18A8087413BE03401DB4AA2A7DBF0AE1591D34D5B4B2728D04A80FDD041BB52EDB334DACBF25AA27877E738011A005B8D80A20058390053FBFFFAB7B001281917DE77F18A8087413BE03401DB4AA2A7DBF0AE1591D34D5B4B2728D04A80FDD041BB52EDB334DACBF25AA27877E738011B00000001D4BC5D6E021A00029E8DA100D9010281825820D504D361777F985265A6F0A727E601E70F837574652760456B41731FF4F95258584054224628C469C35313471E6C274C93218D0B40093307E9B70A0A246F697D0870CB81E9333FAB6FE7B297ECC0ADA9E9145F0B46AA3E2395A3DECF42970D1B510DF5F6"+      txCborBytes = case Base16.decode (T.encodeUtf8 $ T.pack txCborHex) of+        Right bs -> bs+        Left err -> error $ "Invalid CBOR hex: " ++ err+      anyChainTx = defMessage & SubF.raw .~ txCborBytes+      submitRequest = defMessage & SubF.tx .~ anyChainTx++  submitResult <- submitTx (submitClient client) submitRequest+  case submitResult of+    Right (Right (Right (_headers, _trailers, Right response))) -> do+      let txHash = response ^. SubF.ref+      when (not (BS.null txHash)) $ do+        putStrLn $ "  Transaction submitted! Hash: " ++ T.unpack (T.take 16 $ T.decodeUtf8 $ Base16.encode txHash) ++ "..."+    _ -> putStrLn "  Warning: Transaction submission may have issues"++  putStrLn "  Waiting for Watch event..."++  -- Wait for watcher result+  result <- takeMVar resultVar++  -- Assert+  case result of+    Left (exc :: SomeException) ->+      if "WatchTxForDelegationPart completed" `T.isInfixOf` T.pack (show exc) then+        putStrLn "✓ WatchTxForDelegationPart test passed! Found matching transaction"+      else+        throwString $ "WatchTxForDelegationPart failed: " ++ show exc+    Right streamResult -> case streamResult of+      Right (Right (events, _headers, _trailers)) -> do+        assertTrue "events.Count > 0" (length events > 0)+        let firstEvent = head events+        let maybeAction = firstEvent ^. WF.maybe'action+        assertNotNull "firstEvent.Action" maybeAction+        case maybeAction of+          Just (WatchTxResponse'Apply _) ->+            putStrLn $ "✓ WatchTxForDelegationPart test passed!"+          _ -> throwString "Expected Apply action"+      err -> throwString $ "WatchTxForDelegationPart failed: " ++ show err+  where+    streamHandler :: [WatchTxResponse] -> a -> WatchTxResponse -> IO [WatchTxResponse]+    streamHandler events _headers response = do+      let maybeAction = response ^. WF.maybe'action+      case maybeAction of+        Just (WatchTxResponse'Apply _) -> do+          putStrLn $ "  Found Apply action for transaction matching delegation credential!"+          let newEvents = events ++ [response]+          if length newEvents >= 1 then+            throwString "WatchTxForDelegationPart completed - stopping stream"+          else+            return newEvents+        _ -> return events++testWatchTxForPolicyId :: UtxorpcClient -> IO ()+testWatchTxForPolicyId client = do+  putStrLn "\n=== Test: WatchTxForPolicyId ==="++  -- Arrange+  let policyIdHex = "8b05e87a51c1d4a0fa888d2bb14dbc25e8c343ea379a171b63aa84a0"+      policyIdBytes = case Base16.decode policyIdHex of+        Right bs -> bs+        Left err -> error $ "Invalid policy ID hex: " ++ err++      assetPattern = (defMessage :: AssetPattern)+        & CF.policyId .~ policyIdBytes+      txPattern = (defMessage :: TxPattern)+        & CF.movesAsset .~ assetPattern+      anyChainPattern = (defMessage :: W.AnyChainTxPattern)+        & WF.cardano .~ txPattern+      predicate = (defMessage :: W.TxPredicate)+        & WF.match .~ anyChainPattern+      watchRequest = defMessage+        & WF.predicate .~ predicate++  putStrLn $ "  Watching for policy ID: " ++ T.unpack (T.decodeUtf8 $ Base16.encode policyIdBytes)++  -- Create MVar to store watcher result+  resultVar <- newEmptyMVar++  -- Start watching in background+  _ <- forkIO $ do+    result <- try $ watchTx (watchClient client) [] watchRequest streamHandler+    putMVar resultVar result++  -- Give the watcher time to start+  putStrLn "  Started watching, waiting 1 second..."+  threadDelay 1000000++  -- Submit transaction+  putStrLn "  Submitting transaction..."+  let txCborHex = "84A300D90102828258209E0CC477DF20C37C684E47B9701F995C8733B07901DE5447FE4F61EDF14F6BCB01825820176A54A6E3911FF5C65134928079E40783C8A88F1143091687F3FE7775EEF55F000182A20058390053FBFFFAB7B001281917DE77F18A8087413BE03401DB4AA2A7DBF0AE1591D34D5B4B2728D04A80FDD041BB52EDB334DACBF25AA27877E738011A005B8D80A20058390053FBFFFAB7B001281917DE77F18A8087413BE03401DB4AA2A7DBF0AE1591D34D5B4B2728D04A80FDD041BB52EDB334DACBF25AA27877E738011B00000001D4478888021A00029E8DA100D9010281825820D504D361777F985265A6F0A727E601E70F837574652760456B41731FF4F952585840DBBF46C2B615D9EE1689F6339A537A3A16507C2566E067A717F0D379D9944F8FEF1045AA525B71EB6E29877BFAEA83F295F25707B0F7AF41222E3F33C9861C0CF5F6"+      txCborBytes = case Base16.decode (T.encodeUtf8 $ T.pack txCborHex) of+        Right bs -> bs+        Left err -> error $ "Invalid CBOR hex: " ++ err+      anyChainTx = defMessage & SubF.raw .~ txCborBytes+      submitRequest = defMessage & SubF.tx .~ anyChainTx++  submitResult <- submitTx (submitClient client) submitRequest+  case submitResult of+    Right (Right (Right (_headers, _trailers, Right response))) -> do+      let txHash = response ^. SubF.ref+      when (not (BS.null txHash)) $ do+        putStrLn $ "  Transaction submitted! Hash: " ++ T.unpack (T.take 16 $ T.decodeUtf8 $ Base16.encode txHash) ++ "..."+    _ -> putStrLn "  Warning: Transaction submission may have issues"++  putStrLn "  Waiting for Watch event..."++  -- Wait for watcher result+  result <- takeMVar resultVar++  -- Assert+  case result of+    Left (exc :: SomeException) ->+      if "WatchTxForPolicyId completed" `T.isInfixOf` T.pack (show exc) then+        putStrLn "✓ WatchTxForPolicyId test passed! Found matching transaction"+      else+        throwString $ "WatchTxForPolicyId failed: " ++ show exc+    Right streamResult -> case streamResult of+      Right (Right (events, _headers, _trailers)) -> do+        assertTrue "events.Count > 0" (length events > 0)+        let firstEvent = head events+        let maybeAction = firstEvent ^. WF.maybe'action+        assertNotNull "firstEvent.Action" maybeAction+        case maybeAction of+          Just (WatchTxResponse'Apply _) ->+            putStrLn $ "✓ WatchTxForPolicyId test passed!"+          _ -> throwString "Expected Apply action"+      err -> throwString $ "WatchTxForPolicyId failed: " ++ show err+  where+    streamHandler :: [WatchTxResponse] -> a -> WatchTxResponse -> IO [WatchTxResponse]+    streamHandler events _headers response = do+      let maybeAction = response ^. WF.maybe'action+      case maybeAction of+        Just (WatchTxResponse'Apply _) -> do+          putStrLn $ "  Found Apply action for transaction with policy ID!"+          let newEvents = events ++ [response]+          if length newEvents >= 1 then+            throwString "WatchTxForPolicyId completed - stopping stream"+          else+            return newEvents+        _ -> return events++testWatchTxForAsset :: UtxorpcClient -> IO ()+testWatchTxForAsset client = do+  putStrLn "\n=== Test: WatchTxForAsset ==="++  -- Arrange+  let policyIdHex = "8b05e87a51c1d4a0fa888d2bb14dbc25e8c343ea379a171b63aa84a0"+      assetNameHex = "434e4354" -- "CNCT" in hex+      policyIdBytes = case Base16.decode policyIdHex of+        Right bs -> bs+        Left err -> error $ "Invalid policy ID hex: " ++ err+      assetNameOnlyBytes = case Base16.decode assetNameHex of+        Right bs -> bs+        Left err -> error $ "Invalid asset name hex: " ++ err++      assetPattern = (defMessage :: AssetPattern)+        & CF.policyId .~ policyIdBytes+        & CF.assetName .~ assetNameOnlyBytes+      txPattern = (defMessage :: TxPattern)+        & CF.movesAsset .~ assetPattern+      anyChainPattern = (defMessage :: W.AnyChainTxPattern)+        & WF.cardano .~ txPattern+      predicate = (defMessage :: W.TxPredicate)+        & WF.match .~ anyChainPattern+      watchRequest = defMessage+        & WF.predicate .~ predicate++  putStrLn $ "  Watching for asset: " ++ T.unpack (T.decodeUtf8 assetNameHex) ++ " (CNCT)"++  -- Create MVar to store watcher result+  resultVar <- newEmptyMVar++  -- Start watching in background+  _ <- forkIO $ do+    result <- try $ watchTx (watchClient client) [] watchRequest streamHandler+    putMVar resultVar result++  -- Give the watcher time to start+  putStrLn "  Started watching, waiting 1 second..."+  threadDelay 1000000++  -- Submit transaction+  putStrLn "  Submitting transaction..."+  let txCborHex = "84A300D90102828258209E0CC477DF20C37C684E47B9701F995C8733B07901DE5447FE4F61EDF14F6BCB01825820176A54A6E3911FF5C65134928079E40783C8A88F1143091687F3FE7775EEF55F000182A20058390053FBFFFAB7B001281917DE77F18A8087413BE03401DB4AA2A7DBF0AE1591D34D5B4B2728D04A80FDD041BB52EDB334DACBF25AA27877E738011A005B8D80A20058390053FBFFFAB7B001281917DE77F18A8087413BE03401DB4AA2A7DBF0AE1591D34D5B4B2728D04A80FDD041BB52EDB334DACBF25AA27877E738011B00000001D4478888021A00029E8DA100D9010281825820D504D361777F985265A6F0A727E601E70F837574652760456B41731FF4F952585840DBBF46C2B615D9EE1689F6339A537A3A16507C2566E067A717F0D379D9944F8FEF1045AA525B71EB6E29877BFAEA83F295F25707B0F7AF41222E3F33C9861C0CF5F6"+      txCborBytes = case Base16.decode (T.encodeUtf8 $ T.pack txCborHex) of+        Right bs -> bs+        Left err -> error $ "Invalid CBOR hex: " ++ err+      anyChainTx = defMessage & SubF.raw .~ txCborBytes+      submitRequest = defMessage & SubF.tx .~ anyChainTx++  submitResult <- submitTx (submitClient client) submitRequest+  case submitResult of+    Right (Right (Right (_headers, _trailers, Right response))) -> do+      let txHash = response ^. SubF.ref+      when (not (BS.null txHash)) $ do+        putStrLn $ "  Transaction submitted! Hash: " ++ T.unpack (T.take 16 $ T.decodeUtf8 $ Base16.encode txHash) ++ "..."+    _ -> putStrLn "  Warning: Transaction submission may have issues"++  putStrLn "  Waiting for Watch event..."++  -- Wait for watcher result+  result <- takeMVar resultVar++  -- Assert+  case result of+    Left (exc :: SomeException) ->+      if "WatchTxForAsset completed" `T.isInfixOf` T.pack (show exc) then+        putStrLn "✓ WatchTxForAsset test passed! Found matching transaction"+      else+        throwString $ "WatchTxForAsset failed: " ++ show exc+    Right streamResult -> case streamResult of+      Right (Right (events, _headers, _trailers)) -> do+        assertTrue "events.Count > 0" (length events > 0)+        let firstEvent = head events+        let maybeAction = firstEvent ^. WF.maybe'action+        assertNotNull "firstEvent.Action" maybeAction+        case maybeAction of+          Just (WatchTxResponse'Apply _) ->+            putStrLn $ "✓ WatchTxForAsset test passed!"+          _ -> throwString "Expected Apply action"+      err -> throwString $ "WatchTxForAsset failed: " ++ show err+  where+    streamHandler :: [WatchTxResponse] -> a -> WatchTxResponse -> IO [WatchTxResponse]+    streamHandler events _headers response = do+      let maybeAction = response ^. WF.maybe'action+      case maybeAction of+        Just (WatchTxResponse'Apply _) -> do+          putStrLn $ "  Found Apply action for transaction with specific asset!"+          let newEvents = events ++ [response]+          if length newEvents >= 1 then+            throwString "WatchTxForAsset completed - stopping stream"+          else+            return newEvents+        _ -> return events
src/Utxorpc/Client.hs view
@@ -161,7 +161,8 @@     (loggedUnary logger readParamsRPC client)     (loggedUnary logger readUtxosRPC client)     (loggedUnary logger searchUtxosRPC client)-    (loggedSStream logger streamUtxosRPC client)+    (loggedUnary logger readGenesisRPC client)+    (loggedUnary logger readEraSummaryRPC client)  readParamsRPC :: RPC QueryService "readParams" readParamsRPC = RPC@@ -172,9 +173,12 @@ searchUtxosRPC :: RPC QueryService "searchUtxos" searchUtxosRPC = RPC -streamUtxosRPC :: RPC QueryService "streamUtxos"-streamUtxosRPC = RPC+readGenesisRPC :: RPC QueryService "readGenesis"+readGenesisRPC = RPC +readEraSummaryRPC :: RPC QueryService "readEraSummary"+readEraSummaryRPC = RPC+ {--------------------------------------   SUBMIT --------------------------------------}@@ -209,15 +213,19 @@     (loggedUnary logger fetchBlockRPC client)     (loggedUnary logger dumpHistoryRPC client)     (loggedSStream logger followTipRPC client)+    (loggedUnary logger readTipRPC client) -fetchBlockRPC :: RPC ChainSyncService "fetchBlock"+fetchBlockRPC :: RPC SyncService "fetchBlock" fetchBlockRPC = RPC -dumpHistoryRPC :: RPC ChainSyncService "dumpHistory"+dumpHistoryRPC :: RPC SyncService "dumpHistory" dumpHistoryRPC = RPC -followTipRPC :: RPC ChainSyncService "followTip"+followTipRPC :: RPC SyncService "followTip" followTipRPC = RPC++readTipRPC :: RPC SyncService "readTip"+readTipRPC = RPC  {--------------------------------------   WATCH
src/Utxorpc/Types.hs view
@@ -76,7 +76,8 @@   { readParams :: ReadParamsRequest -> UnaryReply ReadParamsResponse,     readUtxos :: ReadUtxosRequest -> UnaryReply ReadUtxosResponse,     searchUtxos :: SearchUtxosRequest -> UnaryReply SearchUtxosResponse,-    streamUtxos :: ServerStreamCall ReadUtxosRequest ReadUtxosResponse+    readGenesis :: ReadGenesisRequest -> UnaryReply ReadGenesisResponse,+    readEraSummary :: ReadEraSummaryRequest -> UnaryReply ReadEraSummaryResponse   }  {---------------------------------------@@ -99,7 +100,8 @@ data SyncClient = SyncClient   { fetchBlock :: FetchBlockRequest -> UnaryReply FetchBlockResponse,     dumpHistory :: DumpHistoryRequest -> UnaryReply DumpHistoryResponse,-    followTip :: ServerStreamCall FollowTipRequest FollowTipResponse+    followTip :: ServerStreamCall FollowTipRequest FollowTipResponse,+    readTip :: ReadTipRequest -> UnaryReply ReadTipResponse   }  {---------------------------------------
test/logged_test.hs view
@@ -22,7 +22,7 @@  type UnaryReply = FetchBlockResponse -type UnaryRpc = RPC ChainSyncService "fetchBlock"+type UnaryRpc = RPC SyncService "fetchBlock"  unaryRpc :: UnaryRpc unaryRpc = RPC@@ -37,7 +37,7 @@ streamReplies :: Int -> [StreamReply] streamReplies n = replicate n defMessage -type StreamRpc = RPC ChainSyncService "followTip"+type StreamRpc = RPC SyncService "followTip"  streamRpc :: StreamRpc streamRpc = RPC
utxorpc-client.cabal view
@@ -1,7 +1,7 @@ cabal-version: 3.0  name:           utxorpc-client-version:        0.0.2.0+version:        0.0.4.1 synopsis:       An SDK for clients of the UTxO RPC specification. description:    An SDK to reduce boilerplate, improve ease-of-use, and support logging for `utxorpc`.                 To get started, see the documentation for `Utxorpc.Client` below.@@ -52,11 +52,11 @@     , http2-grpc-proto-lens < 0.2     , http2-grpc-types < 0.6     , proto-lens < 0.8-    , utxorpc >= 0.0.5 && < 0.0.6+    , utxorpc >= 0.0.18 && < 0.0.19     , uuid < 1.4   default-language: Haskell2010 -executable client-example+executable example   main-is:     Main.hs   other-modules:@@ -92,12 +92,12 @@     , time < 1.13     , transformers < 0.7     , unliftio < 0.3-    , utxorpc >= 0.0.5 && < 0.0.6+    , utxorpc >= 0.0.18 && < 0.0.19     , utxorpc-client     , uuid < 1.4   default-language: Haskell2010 -executable client-quick-start+executable quickstart   main-is:     Main.hs   hs-source-dirs:@@ -115,15 +115,17 @@     -Wunused-packages   build-depends:     , base >=4.7 && <5+    , base16-bytestring < 1.1+    , bytestring < 0.13     , lens < 5.3     , proto-lens < 0.8     , text < 2.2     , unliftio < 0.3-    , utxorpc >= 0.0.5 && < 0.0.6+    , utxorpc >= 0.0.18 && < 0.0.19     , utxorpc-client   default-language: Haskell2010 -test-suite logged_test_client+test-suite unit   type:     exitcode-stdio-1.0   main-is:@@ -154,6 +156,6 @@     , http2-grpc-types >= 0.5.0.0 && < 0.6     , hspec >= 2.11.7 && < 2.12     , proto-lens < 0.8-    , utxorpc >= 0.0.5 && < 0.0.6+    , utxorpc >= 0.0.18 && < 0.0.19     , uuid < 1.4   default-language: Haskell2010