packages feed

solana-haskell-sdk 1.2.0.0 → 1.3.0.0

raw patch · 48 files changed

+3595/−1350 lines, 48 filesnew-component:exe:example-priority-feenew-component:exe:example-spl-transferPVP ok

version bump matches the API change (PVP)

API changes (from Hackage documentation)

- Network.Solana.Core.Account: [accDataObj] :: AccountData -> String
- Network.Solana.Core.Crypto: instance GHC.Generics.Generic Network.Solana.Core.Crypto.SolanaPrivateKey
- Network.Solana.Core.Crypto: instance GHC.Generics.Generic Network.Solana.Core.Crypto.SolanaPublicKey
- Network.Solana.Core.Crypto: instance GHC.Generics.Generic Network.Solana.Core.Crypto.SolanaSignature
+ Network.Solana.Core.Account: [accDataParsed] :: AccountData -> Value
+ Network.Solana.Core.Account: [accDataProgram] :: AccountData -> String
+ Network.Solana.Core.Account: [accDataSpace] :: AccountData -> Word64
+ Network.Solana.Core.Crypto: mkPrivateKeyFromBytes :: ByteString -> Either String SolanaPrivateKey
+ Network.Solana.Core.Message: mkNewMessageWithPayer :: SolanaPublicKey -> BlockHash -> [Instruction] -> Message
+ Network.Solana.Core.Message: orderSigningKeys :: Message -> [SolanaPrivateKey] -> Either CompileException [SolanaPrivateKey]
+ Network.Solana.Core.VersionedMessage: compileV0MessageWithPayer :: SolanaPublicKey -> BlockHash -> [Instruction] -> [AddressLookupTableAccount] -> Either CompileException ByteString
+ Network.Solana.Core.VersionedMessage: newV0TransactionIntentWithPayer :: SolanaPublicKey -> [SolanaPrivateKey] -> [Instruction] -> [AddressLookupTableAccount] -> BlockHash -> Either CompileException String
+ Network.Solana.SolanaWeb3: cfgNonceSendConfirmed :: ConfigurationObject
+ Network.Solana.SolanaWeb3: confirmFinalized :: SolanaSignature -> Web3 ()
- Network.Solana.Core.Account: AccountDataJSON :: String -> AccountData
+ Network.Solana.Core.Account: AccountDataJSON :: String -> Value -> Word64 -> AccountData
- Network.Solana.RPC.HTTP.Chain: PerformanceSample :: Slot -> Word64 -> Word64 -> Word16 -> Word64 -> PerformanceSample
+ Network.Solana.RPC.HTTP.Chain: PerformanceSample :: Slot -> Word64 -> Word64 -> Word16 -> Maybe Word64 -> PerformanceSample
- Network.Solana.RPC.HTTP.Chain: SolanaVersion :: String -> Word32 -> SolanaVersion
+ Network.Solana.RPC.HTTP.Chain: SolanaVersion :: String -> Maybe Word32 -> SolanaVersion
- Network.Solana.RPC.HTTP.Chain: [feature_set] :: SolanaVersion -> Word32
+ Network.Solana.RPC.HTTP.Chain: [feature_set] :: SolanaVersion -> Maybe Word32
- Network.Solana.RPC.HTTP.Chain: [numNonVoteTransactions] :: PerformanceSample -> Word64
+ Network.Solana.RPC.HTTP.Chain: [numNonVoteTransactions] :: PerformanceSample -> Maybe Word64
- Network.Solana.RPC.HTTP.Types: Context :: Text -> Slot -> Context
+ Network.Solana.RPC.HTTP.Types: Context :: Maybe Text -> Slot -> Context
- Network.Solana.RPC.HTTP.Types: [apiVersion] :: Context -> Text
+ Network.Solana.RPC.HTTP.Types: [apiVersion] :: Context -> Maybe Text

Files

CHANGELOG.md view
@@ -1,11 +1,131 @@ # Revision history for solana-haskell-sdk +## 1.3.0.0 -- 2026-09-13++* Fixed (security): `readSigningKeyFromFile` accepted key files of any length+  and silently wrapped out-of-range values, producing a malformed+  `SolanaPrivateKey` whose use in `dsign`/`sign` read past the end of the key+  buffer in the ed25519 C code. The loader now decodes the file as a JSON+  array of exactly 64 bytes and verifies the public-key half against the seed+  (as `solana-sdk`'s `Keypair::from_bytes` does), throwing an `IOError`+  otherwise.+* Added `mkPrivateKeyFromBytes`, a total constructor performing those checks+  on raw bytes.+* Changed: `unsafeSolanaPublicKeyRaw` and `unsafeSolanaPrivateKeyRaw` now call+  `error` unless given exactly 32 / 64 bytes, matching the other `unsafe*`+  constructors; `mkPrivateKeyFromString` (and so `unsafeSolanaPrivateKey`)+  applies the same public-key-half check as `mkPrivateKeyFromBytes`.+* Changed (breaking): `SolanaPublicKey`, `SolanaPrivateKey` and+  `SolanaSignature` no longer derive `Generic`, which let generic code build+  unchecked values behind the abstract constructors.+* Fixed: `newTransaction` now names its first signing key as the fee payer+  (via `newTransactionIntentWithPayer`), so transactions whose only signer is+  a read-only authority -- a memo-only transaction, or a standalone SPL+  `mintTo`/`transferChecked` -- are no longer rejected by the node with+  "Transaction failed to sanitize accounts offsets correctly". The first key+  is forced writable and pinned to account 0 even if no instruction+  references it (sponsored fees), the remaining keys may be passed in any+  order, an empty key list throws `userError`, and an unused key is reported+  as a `CompileException` before the send. Output is byte-identical for every+  transaction that was already valid.+* Added `compileV0MessageWithPayer` and `newV0TransactionIntentWithPayer`,+  the explicit-fee-payer counterparts of the v0 compiler and signer+  (mirroring the Rust SDK's `v0::Message::try_compile(payer, ..)`), plus+  `mkNewMessageWithPayer` and `orderSigningKeys` as exported building blocks.+* Fixed: `newNonceTransaction` now submits with `preflightCommitment =+  "confirmed"` (new `cfgNonceSendConfirmed`), matching the commitment it reads+  the nonce account at. Previously the node's default `finalized` preflight+  rejected the send with `BlockhashNotFound` whenever the nonce account had+  been created or advanced within the last ~32 slots -- i.e. on every+  back-to-back use -- unless the caller first waited for finalization.+* Fixed: `AccountData`'s `FromJSON` decoded a bare `data` string as (lenient)+  base64. Per the RPC account-data format a bare string is the node's legacy+  `binary` encoding, i.e. base58, so `getAccountInfo'`, `getProgramAccounts'`+  and `getTokenAccountsByOwner'`/`ByDelegate'` called with a configuration+  that omits `encoding` (or sets `"binary"`), and `getMultipleAccounts'` with+  `"binary"`, returned silently corrupted bytes for accounts of up to 128+  bytes. Bare strings now decode strictly as+  base58 (invalid input is a parse error) and `AccountData` round-trips+  through its own `ToJSON`; the `[data, "base64"]` form and the+  base64-requesting wrappers are unchanged.+* Fixed: `AccountData` now parses the `{program, parsed, space}` object a+  node returns for `encoding: "jsonParsed"` when it has a parser for the+  owner program (nonce, SPL Token, stake, vote, sysvars, ...). Previously+  every such response failed with `parsing AccountDataArray failed, expected+  Array, but encountered Object` through `getAccountInfo'`,+  `getMultipleAccounts'`, `getProgramAccounts'`, `getTokenAccountsByOwner'`+  and `getTokenAccountsByDelegate'`. The base64 pair the node falls back to+  when no parser exists still decodes to `AccountDataBinary`.+* Changed (breaking): `AccountDataJSON` now carries `accDataProgram ::+  String`, `accDataParsed :: Data.Aeson.Value` and `accDataSpace :: Word64`+  instead of `accDataObj :: String`, and its `ToJSON` instance emits the+  node's object. The `["...","json"]` / `["...","jsonParsed"]` pair forms,+  which no node emits, are no longer accepted. `base64+zstd` account data is+  still rejected as unsupported.+* Fixed: `createMasterEditionV3` now marks the metadata account (index 5)+  writable, matching the official `mpl-token-metadata` client. Previously the+  on-chain program silently left `token_standard` un-upgraded+  (`FungibleAsset` instead of `NonFungible`) when the instruction was sent on+  its own. The builder's account metas are now golden-tested against the+  crate (`master-edition-some-accounts`).+* Fixed: `compileInstruction`, and every message compiler built on it+  (`newMessage`, `newTransactionIntent`, `newTransactionIntentWithPayer`, the+  durable-nonce variants, `compileV0Message`, `newV0TransactionIntent`, and+  the `SolanaWeb3` senders), silently wrapped account indices at or past 256+  into the wrong byte, producing a message the node rejects; such messages+  now fail with a `CompileException` (`MissingIndex` carrying an "account+  index overflow" message), mirroring the Rust SDK's+  `CompileError::AccountIndexOverflow`. Messages with up to 256 account keys+  (static plus table-loaded for v0) are unaffected.+* Changed (breaking): `Context.apiVersion` is now `Maybe Text`,+  `PerformanceSample.numNonVoteTransactions` is now `Maybe Word64` and+  `SolanaVersion.feature_set` is now `Maybe Word32`, matching the upstream+  schema where older nodes and some RPC proxies omit `apiVersion` and older+  performance samples report `numNonVoteTransactions` as `null`; previously+  such responses failed to parse.+* WebSocket: a `signatureNotification` whose value lacks the `err` key is now+  rejected by `parseWsMessage` (and reported as `Left` by `awaitSignature`)+  instead of being treated as a successful confirmation.+* Docs: `getBlock`/`getBlock'`/`getBlockTime` now document that skipped,+  pruned and not-yet-available slots are JSON-RPC errors (-32007/-32001/+  -32004) raised as a `JsonRpcException` that `runWeb3'` does not catch,+  rather than `Nothing`; module headers added for `Core.Instruction`,+  `NativePrograms.ComputeBudget`, `NativePrograms.SystemProgram` and+  `SplPrograms.Memo`; the `SolanaWeb3` header reflects the integration suite;+  CONTRIBUTING links defined.+* Added `confirmFinalized` to `SolanaWeb3`: polls `getSignatureStatuses` up+  to 60 s until a signature is `finalized`, throwing on an on-chain error or+  timeout (previously an integration-suite-only helper). Use it before+  `printBalances`/`getBalance`, which read at the node's default `finalized`+  commitment; `confirmTransaction` returns at `confirmed`, so balances read+  right after it can still show pre-transaction state.+* README usage examples are now complete flows that run as-is against+  `solana-test-validator`: payers are funded and finalized, the SPL example+  creates its own mint and token account, and every flow finalizes before+  reading state. The SPL and priority-fee examples are built as the+  `example-spl-transfer` and `example-priority-fee` executables, and a unit+  test keeps the README text identical to them. The README now states that+  the examples also need the `web3-provider` package (`Network.Web3.Provider`)+  in `build-depends`.+* Corrected the 1.2.0.0 note below, which overstated RPC golden-test coverage.+  `tools/rpc-record` now records every RPC method the SDK binds except+  `requestAirdrop`, `sendTransaction` and `simulateTransaction`, aborts+  instead of silently skipping a method the node rejects, and requires+  `solana-test-validator --slots-per-epoch 32` so `getInflationReward` can be+  captured; `Test.RPC.Parsers` now asserts every RPC-layer `FromJSON`+  instance, including default-encoding (base58) and `jsonParsed` account data.+* The integration suite gains cases for the fixes above: memo-only sends+  through `newTransaction` (legacy and v0), the README SPL and priority-fee+  flows, back-to-back durable-nonce use right after a confirmed create or+  advance, default-encoding and `jsonParsed` account reads, and `getBlock` on+  a slot holding no block.+ ## 1.2.0.0 -- 2026-08-03  * Added on-chain account-state decoders (SPL token accounts and mints, address lookup tables, stake and nonce accounts, Metaplex metadata) with typed SolanaWeb3 fetch helpers; decoded lookup tables feed compileV0Message directly. * Added developer-experience helpers: explicit fee-payer transactions with automatic signature ordering (sponsored fees supported), a durable-nonce transaction flow, and priority-fee estimation. * Added an opt-in local-validator integration test suite (`integration-tests`, behind the `integration` cabal flag) covering SOL and sponsored transfers, the SPL token lifecycle, durable nonces, address lookup tables with v0 transactions, priority fees, and WebSocket confirmation; the suite skips itself when no validator is running.-* Added golden tests for the JSON-RPC response parsers, replaying 37 responses recorded from a live validator by `tools/rpc-record`, so every `FromJSON` instance is asserted against JSON a node actually sends.+* Added golden tests for the JSON-RPC response parsers, replaying 37 responses recorded from a live validator by `tools/rpc-record` (36 of the SDK's 52 RPC methods; every RPC-layer `FromJSON` instance except `HighestSnapshotSlot`, `PrioritizationFee` and `InflationReward`; account data in `base64` encoding only -- see the 1.3.0.0 entry). * Added Solana PubSub (WebSocket) support in `Network.Solana.RPC.WebSocket`: signature and account subscribe/unsubscribe requests, notification parsing, and `awaitSignature` — a push-based confirmation that replaces polling. The module is transport-agnostic (`WsTransport`), so the library gains no new dependency and works with both `ws://` and `wss://` endpoints.  ## 1.1.0.0 -- 2026-08-01
README.md view
@@ -86,7 +86,7 @@  ### Release status -The current stable release is **v1.2.0.0**. Serialization is verified byte-for-byte against the official Rust SDK by golden-vector tests (see test/fixtures/ and [tools/README.md](https://github.com/mariusgeorgescu/solana-haskell-sdk/blob/main/tools/README.md)).+The current stable release is **v1.3.0.0**. Serialization is verified byte-for-byte against the official Rust SDK by golden-vector tests (see test/fixtures/ and [tools/README.md](https://github.com/mariusgeorgescu/solana-haskell-sdk/blob/main/tools/README.md)).  ## Usage Examples @@ -100,24 +100,25 @@  2. We connect to a local Solana validator using an HTTP provider. -3. The newly generated keypair receives an airdrop of 10 SOL to ensure it has sufficient funds.+3. The newly generated keypair receives an airdrop of 10 SOL, and we wait for it to be finalized (later sends are simulated against the finalized bank).  4. We define a recipient's public address from a base58 encoded string.  5. We **construct**, **sign** and **submit** the transaction by defining the signers and the transaction's list of instructions with their parameters. -Before and after performing the transfer of 1 SOL to the recipient, we check and print the account balances to verify the transaction's success.+Before and after performing the transfer of 1 SOL to the recipient, we check and print the account balances to verify the transaction's success. The second read happens after waiting for the transfer to finalize: `getBalance` reads at the node's default commitment, `finalized`, so a balance read right after a merely `confirmed` transaction would still show the old value.  This straightforward example highlights the convenience and expressiveness of Haskell when building decentralized applications on Solana.  > The examples use `GHC2021` (this package's `default-language`). If you compile them under `Haskell2010`, additionally enable `NumericUnderscores` and `ImportQualifiedPost`. +> The programs need `solana-haskell-sdk` and `web3-provider` (the package providing `Network.Web3.Provider`, i.e. `runWeb3'` and `HttpProvider`) in `build-depends`. RPC failures such as a rejected preflight simulation are raised as a `JsonRpcException` from `runWeb3'`, not returned as `Left`. Each example is also built as an executable of this package (`solana-haskell-sdk`, `example-spl-transfer`, `example-priority-fee`) and runs as-is against `solana-test-validator`.+ ```haskell {-# LANGUAGE OverloadedStrings #-}  module Main where -import Control.Monad (void) import Control.Monad.IO.Class (liftIO) import Network.Solana.Core.Crypto import Network.Solana.NativePrograms.SystemProgram qualified as SystemProgram@@ -132,9 +133,9 @@    -- Create Connection, local validator in this example   result <- runWeb3' (HttpProvider "http://127.0.0.1:8899") $ do-    -- Fund fee payer-    void $ requestAirdrop myPublicKey 10_000_000_000-    wait 15 -- Wait 15 seconds be sure the tx was confirmed+    -- Fund the fee payer and wait until the airdrop is finalized: sends are+    -- preflighted against the finalized bank, and balances are read there too+    requestAirdrop myPublicKey 10_000_000_000 >>= confirmFinalized      -- Define recipient's address from a base58-encoded string     let recipient = "A988FuUtUVk8jMUuVc1ccaoTA3VS9CB4dkEf9XUAUqV4"@@ -145,7 +146,7 @@     -- Create a new transaction.     txId <-       newTransaction-        [myPrivateKey] -- Signing keys (with all required signers)+        [myPrivateKey] -- Signing keys (the first key pays the fee)         -- List of instructions         [ SystemProgram.transfer             myPublicKey -- sender address@@ -154,8 +155,9 @@         ]     liftIO $ putStrLn ("Transaction sent: " <> show txId) -    void $ confirmTransaction txId-    -- Check balance+    -- Wait for finalization (throws if the transaction failed on-chain or+    -- timed out), so the balances below reflect the transfer+    confirmFinalized txId     printBalances [myPublicKey, recipient]    either (\e -> putStrLn ("RPC error: " <> show e)) pure result@@ -163,16 +165,19 @@  ### SPL token transfer (Associated Token Accounts) -Tokens live in *associated token accounts* (ATAs) — program-derived addresses computed from the wallet and the mint. This example derives both ATAs with `getAssociatedTokenAddress`, creates the recipient's ATA if missing (idempotent, safe to include unconditionally), and moves tokens with the decimals-checked `transferChecked`.+Tokens live in *associated token accounts* (ATAs) — program-derived addresses computed from the wallet and the mint. This example first creates a mint and funds the sender's token account (so it runs as-is against a local validator), then derives both ATAs with `getAssociatedTokenAddress`, creates the recipient's ATA if missing (idempotent, safe to include unconditionally), and moves tokens with the decimals-checked `transferChecked`.  ```haskell {-# LANGUAGE OverloadedStrings #-}  module Main where -import Control.Monad (void)+import Control.Monad.IO.Class (liftIO) import Data.Maybe (fromJust) import Network.Solana.Core.Crypto+import Network.Solana.NativePrograms.SystemProgram qualified as SystemProgram+import Network.Solana.RPC.HTTP.Tokenomics (getMinimumBalanceForRentExemption)+import Network.Solana.RPC.HTTP.Transaction (requestAirdrop) import Network.Solana.SolanaWeb3 import Network.Solana.SplPrograms.AssociatedTokenAccount qualified as Ata import Network.Solana.SplPrograms.Token qualified as Token@@ -181,24 +186,41 @@ main :: IO () main = do   (myPublicKey, myPrivateKey) <- createSolanaKeyPair+  (mint, mintPrivateKey) <- createSolanaKeyPair -- the new token's mint account -  void $ runWeb3' (HttpProvider "http://127.0.0.1:8899") $ do-    let mint = "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU" -- the token's mint address-        recipient = "A988FuUtUVk8jMUuVc1ccaoTA3VS9CB4dkEf9XUAUqV4" -- recipient wallet+  result <- runWeb3' (HttpProvider "http://127.0.0.1:8899") $ do+    requestAirdrop myPublicKey 10_000_000_000 >>= confirmFinalized +    let recipient = "A988FuUtUVk8jMUuVc1ccaoTA3VS9CB4dkEf9XUAUqV4" -- recipient wallet+         -- ATAs are PDAs of (wallet, token program, mint): derived, not generated.         sourceAta = fromJust (Ata.getAssociatedTokenAddress myPublicKey mint)         destinationAta = fromJust (Ata.getAssociatedTokenAddress recipient mint) -    void $+    -- Setup: create a mint with 6 decimals (we are its mint authority), our own+    -- token account, and mint 10 tokens into it. The mint keypair co-signs+    -- because createAccount requires the new account's signature.+    mintRent <- getMinimumBalanceForRentExemption 82 -- a mint account is 82 bytes+    setupTx <-       newTransaction+        [myPrivateKey, mintPrivateKey]+        [ SystemProgram.createAccount myPublicKey mint mintRent 82 Token.tokenProgramId,+          Token.initializeMint2 mint 6 myPublicKey Nothing,+          Ata.createAssociatedTokenAccount myPublicKey myPublicKey mint,+          Token.mintTo mint sourceAta myPublicKey [] 10_000_000+        ]+    confirmFinalized setupTx++    -- Transfer 1 token to the recipient+    transferTx <-+      newTransaction         [myPrivateKey]         [ -- Create the recipient's token account if it does not exist yet (no-op otherwise).           Ata.createAssociatedTokenAccountIdempotent             myPublicKey -- funder (pays rent)             recipient -- wallet that will own the ATA             mint,-          -- Transfer 1 token (here: 6 decimals); mint and decimals are verified on-chain.+          -- Transfer 1 token (6 decimals); mint and decimals are verified on-chain.           Token.transferChecked             sourceAta -- source token account             mint -- token mint@@ -208,21 +230,28 @@             1_000_000 -- amount in base units             6 -- decimals of the mint         ]+    confirmFinalized transferTx++    -- Read back the recipient's token account (decoded on-chain state)+    account <- getTokenAccount destinationAta+    liftIO $ putStrLn ("Recipient token balance: " <> show (Token.taAmount <$> account))++  either (\e -> putStrLn ("RPC error: " <> show e)) pure result ```  ### Priority fees and memo -Compute Budget instructions raise a transaction's scheduling priority by paying a fee per compute unit; a memo attaches a signed, human-readable note recorded on-chain. Both are ordinary `Instruction`s added to the same instruction list.+Compute Budget instructions raise a transaction's scheduling priority by paying a fee per compute unit; a memo attaches a signed, human-readable note recorded on-chain. Both are ordinary `Instruction`s added to the same instruction list. The payer is funded first.  ```haskell {-# LANGUAGE OverloadedStrings #-}  module Main where -import Control.Monad (void) import Network.Solana.Core.Crypto import Network.Solana.NativePrograms.ComputeBudget qualified as ComputeBudget import Network.Solana.NativePrograms.SystemProgram qualified as SystemProgram+import Network.Solana.RPC.HTTP.Transaction (requestAirdrop) import Network.Solana.SolanaWeb3 import Network.Solana.SplPrograms.Memo qualified as Memo import Network.Web3.Provider@@ -231,10 +260,12 @@ main = do   (myPublicKey, myPrivateKey) <- createSolanaKeyPair -  void $ runWeb3' (HttpProvider "http://127.0.0.1:8899") $ do+  result <- runWeb3' (HttpProvider "http://127.0.0.1:8899") $ do+    requestAirdrop myPublicKey 10_000_000_000 >>= confirmFinalized+     let recipient = "A988FuUtUVk8jMUuVc1ccaoTA3VS9CB4dkEf9XUAUqV4" -    void $+    txId <-       newTransaction         [myPrivateKey]         [ ComputeBudget.setComputeUnitLimit 200_000, -- cap the compute units this tx may use@@ -242,6 +273,10 @@           SystemProgram.transfer myPublicKey recipient 1_000_000_000, -- 1 SOL           Memo.buildMemo "thanks for the coffee" [myPublicKey] -- signed on-chain note         ]+    confirmFinalized txId+    printBalances [recipient]++  either (\e -> putStrLn ("RPC error: " <> show e)) pure result ```  ## Contributing
app/Main.hs view
@@ -2,7 +2,6 @@  module Main where -import Control.Monad (void) import Control.Monad.IO.Class (liftIO) import Network.Solana.Core.Crypto import Network.Solana.NativePrograms.SystemProgram qualified as SystemProgram@@ -17,9 +16,9 @@    -- Create Connection, local validator in this example   result <- runWeb3' (HttpProvider "http://127.0.0.1:8899") $ do-    -- Fund fee payer-    void $ requestAirdrop myPublicKey 10_000_000_000-    wait 15 -- Wait 15 seconds be sure the tx was confirmed+    -- Fund the fee payer and wait until the airdrop is finalized: sends are+    -- preflighted against the finalized bank, and balances are read there too+    requestAirdrop myPublicKey 10_000_000_000 >>= confirmFinalized      -- Define recipient's address from a base58-encoded string     let recipient = "A988FuUtUVk8jMUuVc1ccaoTA3VS9CB4dkEf9XUAUqV4"@@ -30,7 +29,7 @@     -- Create a new transaction.     txId <-       newTransaction-        [myPrivateKey] -- Signing keys (with all required signers)+        [myPrivateKey] -- Signing keys (the first key pays the fee)         -- List of instructions         [ SystemProgram.transfer             myPublicKey -- sender address@@ -39,8 +38,9 @@         ]     liftIO $ putStrLn ("Transaction sent: " <> show txId) -    void $ confirmTransaction txId-    -- Check balance+    -- Wait for finalization (throws if the transaction failed on-chain or+    -- timed out), so the balances below reflect the transfer+    confirmFinalized txId     printBalances [myPublicKey, recipient]    either (\e -> putStrLn ("RPC error: " <> show e)) pure result
+ app/PriorityFee.hs view
@@ -0,0 +1,33 @@+{-# LANGUAGE OverloadedStrings #-}++module Main where++import Network.Solana.Core.Crypto+import Network.Solana.NativePrograms.ComputeBudget qualified as ComputeBudget+import Network.Solana.NativePrograms.SystemProgram qualified as SystemProgram+import Network.Solana.RPC.HTTP.Transaction (requestAirdrop)+import Network.Solana.SolanaWeb3+import Network.Solana.SplPrograms.Memo qualified as Memo+import Network.Web3.Provider++main :: IO ()+main = do+  (myPublicKey, myPrivateKey) <- createSolanaKeyPair++  result <- runWeb3' (HttpProvider "http://127.0.0.1:8899") $ do+    requestAirdrop myPublicKey 10_000_000_000 >>= confirmFinalized++    let recipient = "A988FuUtUVk8jMUuVc1ccaoTA3VS9CB4dkEf9XUAUqV4"++    txId <-+      newTransaction+        [myPrivateKey]+        [ ComputeBudget.setComputeUnitLimit 200_000, -- cap the compute units this tx may use+          ComputeBudget.setComputeUnitPrice 10_000, -- priority fee: micro-lamports per compute unit+          SystemProgram.transfer myPublicKey recipient 1_000_000_000, -- 1 SOL+          Memo.buildMemo "thanks for the coffee" [myPublicKey] -- signed on-chain note+        ]+    confirmFinalized txId+    printBalances [recipient]++  either (\e -> putStrLn ("RPC error: " <> show e)) pure result
+ app/SplTransfer.hs view
@@ -0,0 +1,69 @@+{-# LANGUAGE OverloadedStrings #-}++module Main where++import Control.Monad.IO.Class (liftIO)+import Data.Maybe (fromJust)+import Network.Solana.Core.Crypto+import Network.Solana.NativePrograms.SystemProgram qualified as SystemProgram+import Network.Solana.RPC.HTTP.Tokenomics (getMinimumBalanceForRentExemption)+import Network.Solana.RPC.HTTP.Transaction (requestAirdrop)+import Network.Solana.SolanaWeb3+import Network.Solana.SplPrograms.AssociatedTokenAccount qualified as Ata+import Network.Solana.SplPrograms.Token qualified as Token+import Network.Web3.Provider++main :: IO ()+main = do+  (myPublicKey, myPrivateKey) <- createSolanaKeyPair+  (mint, mintPrivateKey) <- createSolanaKeyPair -- the new token's mint account++  result <- runWeb3' (HttpProvider "http://127.0.0.1:8899") $ do+    requestAirdrop myPublicKey 10_000_000_000 >>= confirmFinalized++    let recipient = "A988FuUtUVk8jMUuVc1ccaoTA3VS9CB4dkEf9XUAUqV4" -- recipient wallet++        -- ATAs are PDAs of (wallet, token program, mint): derived, not generated.+        sourceAta = fromJust (Ata.getAssociatedTokenAddress myPublicKey mint)+        destinationAta = fromJust (Ata.getAssociatedTokenAddress recipient mint)++    -- Setup: create a mint with 6 decimals (we are its mint authority), our own+    -- token account, and mint 10 tokens into it. The mint keypair co-signs+    -- because createAccount requires the new account's signature.+    mintRent <- getMinimumBalanceForRentExemption 82 -- a mint account is 82 bytes+    setupTx <-+      newTransaction+        [myPrivateKey, mintPrivateKey]+        [ SystemProgram.createAccount myPublicKey mint mintRent 82 Token.tokenProgramId,+          Token.initializeMint2 mint 6 myPublicKey Nothing,+          Ata.createAssociatedTokenAccount myPublicKey myPublicKey mint,+          Token.mintTo mint sourceAta myPublicKey [] 10_000_000+        ]+    confirmFinalized setupTx++    -- Transfer 1 token to the recipient+    transferTx <-+      newTransaction+        [myPrivateKey]+        [ -- Create the recipient's token account if it does not exist yet (no-op otherwise).+          Ata.createAssociatedTokenAccountIdempotent+            myPublicKey -- funder (pays rent)+            recipient -- wallet that will own the ATA+            mint,+          -- Transfer 1 token (6 decimals); mint and decimals are verified on-chain.+          Token.transferChecked+            sourceAta -- source token account+            mint -- token mint+            destinationAta -- destination token account+            myPublicKey -- owner of the source account+            [] -- extra multisig signers (none)+            1_000_000 -- amount in base units+            6 -- decimals of the mint+        ]+    confirmFinalized transferTx++    -- Read back the recipient's token account (decoded on-chain state)+    account <- getTokenAccount destinationAta+    liftIO $ putStrLn ("Recipient token balance: " <> show (Token.taAmount <$> account))++  either (\e -> putStrLn ("RPC error: " <> show e)) pure result
solana-haskell-sdk.cabal view
@@ -1,6 +1,6 @@ cabal-version:   3.0 name:            solana-haskell-sdk-version:         1.2.0.0+version:         1.3.0.0 synopsis:        Solana SDK: transaction building, signing, program clients, and JSON-RPC. homepage:        https://github.com/mariusgeorgescu/solana-haskell-sdk bug-reports:     https://github.com/mariusgeorgescu/solana-haskell-sdk/issues@@ -141,6 +141,31 @@   -- Base language which the package is written in.   default-language: GHC2021 +-- The README's SPL-token example, compiled so the documented flow cannot+-- silently break (Test.Readme keeps the README text identical to app/).+executable example-spl-transfer+  import:           warnings+  main-is:          SplTransfer.hs+  build-depends:+    , base                ^>=4.18.0.0+    , solana-haskell-sdk+    , web3-provider       ^>=1.1++  hs-source-dirs:   app+  default-language: GHC2021++-- The README's priority-fee example, compiled for the same reason.+executable example-priority-fee+  import:           warnings+  main-is:          PriorityFee.hs+  build-depends:+    , base                ^>=4.18.0.0+    , solana-haskell-sdk+    , web3-provider       ^>=1.1++  hs-source-dirs:   app+  default-language: GHC2021+ test-suite solana-haskell-sdk-test   import:           warnings   default-language: GHC2021@@ -168,7 +193,10 @@     Test.NativePrograms.Vote     Test.RPC.Chain     Test.RPC.Parsers+    Test.RPC.Types     Test.RPC.WebSocket+    Test.Readme+    Test.SolanaWeb3     Test.SplPrograms.AssociatedTokenAccount     Test.SplPrograms.Memo     Test.SplPrograms.Token@@ -193,6 +221,7 @@   main-is:          Main.hs   other-modules:     Test.Integration.Alt+    Test.Integration.Block     Test.Integration.Nonce     Test.Integration.PriorityFee     Test.Integration.Setup
src/Network/Solana/Core/Account.hs view
@@ -12,7 +12,6 @@ import Data.Text qualified as T import Data.Vector qualified as V import Data.Word (Word64)-import GHC.Base (Alternative (..)) import GHC.Generics (Generic) import Network.Solana.Constants import Network.Solana.Core.Crypto@@ -99,22 +98,39 @@  ------------------------------------------------------------------------------------------------ --- | A byte array that stores arbitrary data for an account.+-- | An account's @data@ field as the node returns it: the raw bytes for the+-- @base58@ / @base64@ encodings, or the node's own program-parsed view for+-- @jsonParsed@.+--+-- On the wire, binary @data@ is either a bare Base58 string (the node's+-- default @binary@ encoding, only offered for accounts of at most 128 bytes)+-- or a @[data, encoding]@ pair; 'toJSON' emits the bare Base58 form. With+-- @jsonParsed@ the node only parses accounts whose owner program it has a+-- parser for and falls back to the @base64@ pair otherwise, so a+-- @jsonParsed@ request can still yield 'AccountDataBinary'. data AccountData   = AccountDataBinary       { accData :: S.ByteString       }-  | AccountDataJSON {accDataObj :: String}+  | AccountDataJSON+      { -- | The node parser that produced 'accDataParsed' (e.g. @"nonce"@, @"spl-token"@, @"sysvar"@).+        accDataProgram :: String,+        -- | The program-specific parsed state, as the node returned it.+        accDataParsed :: Value,+        -- | The account's data length in bytes.+        accDataSpace :: Word64+      }   deriving (Eq, Generic)  instance Show AccountData where   show :: AccountData -> String   show (AccountDataBinary bs) = toBase58String bs-  show (AccountDataJSON o) = show o+  show (AccountDataJSON prog parsed _) = prog <> ": " <> show parsed  instance ToJSON AccountData where   toJSON :: AccountData -> Value-  toJSON ac = toJSON (show ac)+  toJSON (AccountDataBinary bs) = toJSON (toBase58String bs)+  toJSON (AccountDataJSON prog parsed space) = object ["program" .= prog, "parsed" .= parsed, "space" .= space]  instance FromJSON AccountData where   parseJSON :: Value -> Parser AccountData@@ -141,9 +157,16 @@                   else case arr V.! 1 of                     "base58" -> base58StringParser $ arr V.! 0                     "base64" -> base64StringParser $ arr V.! 0-                    "json" -> withText "AccountDataJSON" (return . AccountDataJSON . T.unpack) $ arr V.! 0-                    "jsonParsed" -> withText "AccountDataJSON" (return . AccountDataJSON . T.unpack) $ arr V.! 0                     other -> fail ("AccountData: unsupported encoding: " <> show other)             )             v-     in base64StringParser v <|> encodedParser++        parsedParser =+          withObject+            "AccountDataJSON"+            (\o -> AccountDataJSON <$> o .: "program" <*> o .: "parsed" <*> o .: "space")+     in case v of+          String _ -> base58StringParser v+          Array _ -> encodedParser+          Object _ -> parsedParser v+          _ -> typeMismatch "AccountData" v
src/Network/Solana/Core/Crypto.hs view
@@ -33,6 +33,7 @@     readSigningKeyFromFile,     mkPublicKeyFromString,     mkPrivateKeyFromString,+    mkPrivateKeyFromBytes,   ) where @@ -46,12 +47,9 @@ import Data.ByteString qualified as BS import Data.ByteString.Base58 import Data.ByteString.Base64 (decodeBase64Lenient, encodeBase64')-import Data.ByteString.Char8 qualified as BS8 import Data.Either.Extra (maybeToEither) import Data.String (IsString, fromString) import Data.Text qualified as Text-import GHC.Generics (Generic)-import Text.Read (readMaybe)  -- | Encode a byte string as Base58 text (Bitcoin alphabet), the encoding -- Solana uses for addresses, signatures and hashes.@@ -87,7 +85,7 @@ -- | A 64-byte detached Ed25519 signature. 'Show' and the JSON instances use -- the Base58 rendering. newtype SolanaSignature = SolanaSignature Ed25519.Signature-  deriving (Eq, Ord, Generic)+  deriving (Eq, Ord)  instance Show SolanaSignature where   show :: SolanaSignature -> String@@ -118,7 +116,7 @@ -- 'IsString' instance is partial (see 'unsafeSolanaPublicKey'). newtype SolanaPublicKey   = SolanaPublicKey Ed25519.PublicKey-  deriving (Eq, Ord, Generic)+  deriving (Eq, Ord)  instance Show SolanaPublicKey where   show :: SolanaPublicKey -> String@@ -159,7 +157,7 @@ -- 'Show' renders it in Base58 — avoid logging values of this type. newtype SolanaPrivateKey   = SolanaPrivateKey Ed25519.SecretKey-  deriving (Eq, Ord, Generic)+  deriving (Eq, Ord)  instance Show SolanaPrivateKey where   show :: SolanaPrivateKey -> String@@ -196,35 +194,51 @@ mkPublicKeyFromString = mkKeyFromString 32 (SolanaPublicKey . Ed25519.PublicKey)  -- | Parse Base58 text into a 'SolanaPrivateKey'. 'Left' if the input is not--- Base58 or has the wrong length.+-- Base58 or fails the checks of 'mkPrivateKeyFromBytes' (64 bytes whose+-- public-key half is derived from the seed). mkPrivateKeyFromString :: String -> Either String SolanaPrivateKey-mkPrivateKeyFromString = mkKeyFromString 64 (SolanaPrivateKey . Ed25519.SecretKey)+mkPrivateKeyFromString str = maybeToEither "Not base58" (fromBase58String str) >>= mkPrivateKeyFromBytes +-- | Build a 'SolanaPrivateKey' from the raw 64 bytes of a NaCl secret key+-- (32-byte seed followed by the 32-byte public key). 'Left' unless the input+-- is exactly 64 bytes and its public-key half is the one derived from the+-- seed — the checks @solana-sdk@'s @Keypair::from_bytes@ performs.+mkPrivateKeyFromBytes :: BS.ByteString -> Either String SolanaPrivateKey+mkPrivateKeyFromBytes bs+  | BS.length bs /= 64 = Left ("Invalid private key length: expected 64 bytes, got " <> show (BS.length bs))+  | otherwise = case createSolanaKeypairFromSeed (BS.take 32 bs) of+      Just (_, sk) | getSolanaPrivateKeyRaw sk == bs -> Right sk+      _ -> Left "Private key public-key half does not match the key derived from its seed"+ unsafeKeyFromString :: forall f. Int -> (BS.ByteString -> f) -> String -> f unsafeKeyFromString n cstr str = either error id $ mkKeyFromString n cstr str -unsafeKeyFromWords :: forall f. (BS.ByteString -> f) -> [Word8] -> f-unsafeKeyFromWords cstr ws = cstr (BS.pack ws)+unsafeKeyFromWords :: forall f. Int -> (BS.ByteString -> f) -> [Word8] -> f+unsafeKeyFromWords n cstr ws+  | length ws == n = cstr (BS.pack ws)+  | otherwise = error ("Invalid key length: expected " <> show n <> " bytes, got " <> show (length ws))  -- | Partial version of 'mkPublicKeyFromString': calls 'error' on invalid -- input. Prefer the total variant outside of literals and tests. unsafeSolanaPublicKey :: String -> SolanaPublicKey unsafeSolanaPublicKey = unsafeKeyFromString 32 (SolanaPublicKey . Ed25519.PublicKey) --- | Build a 'SolanaPublicKey' directly from raw bytes. No length check is--- performed — the caller must supply exactly 32 bytes.+-- | Build a 'SolanaPublicKey' directly from raw bytes. Calls 'error' unless+-- exactly 32 bytes are supplied. unsafeSolanaPublicKeyRaw :: [Word8] -> SolanaPublicKey-unsafeSolanaPublicKeyRaw = unsafeKeyFromWords (SolanaPublicKey . Ed25519.PublicKey)+unsafeSolanaPublicKeyRaw = unsafeKeyFromWords 32 (SolanaPublicKey . Ed25519.PublicKey)  -- | Partial version of 'mkPrivateKeyFromString': calls 'error' on invalid -- input. unsafeSolanaPrivateKey :: String -> SolanaPrivateKey-unsafeSolanaPrivateKey = unsafeKeyFromString 64 (SolanaPrivateKey . Ed25519.SecretKey)+unsafeSolanaPrivateKey = either error id . mkPrivateKeyFromString --- | Build a 'SolanaPrivateKey' directly from raw bytes (e.g. the 64 numbers--- in a @solana-keygen@ keypair file). No length check is performed.+-- | Build a 'SolanaPrivateKey' directly from the raw 64 bytes of a NaCl+-- secret key. Calls 'error' on any other length; the public-key half is not+-- checked — prefer 'mkPrivateKeyFromBytes' (or 'readSigningKeyFromFile' for+-- @solana-keygen@ files). unsafeSolanaPrivateKeyRaw :: [Word8] -> SolanaPrivateKey-unsafeSolanaPrivateKeyRaw = unsafeKeyFromWords (SolanaPrivateKey . Ed25519.SecretKey)+unsafeSolanaPrivateKeyRaw = unsafeKeyFromWords 64 (SolanaPrivateKey . Ed25519.SecretKey)  -- | The raw 32 bytes of a public key. getSolanaPublicKeyRaw :: SolanaPublicKey -> BS.ByteString@@ -274,12 +288,17 @@ ---- ---- --- | Read a private key from a @solana-keygen@-style keypair file containing--- a list of byte values (e.g. @[1,2,...]@). Throws an 'IOError' if the file--- cannot be read, or if its contents cannot be parsed as a @[Word8]@ list.+-- | Read a private key from a @solana-keygen@-style keypair file: a JSON+-- array of the 64 byte values of the secret key (e.g. @[1,2,...]@). Throws+-- an 'IOError' if the file cannot be read, is not an array of exactly 64+-- integers in @0..255@, or fails the checks of 'mkPrivateKeyFromBytes'. readSigningKeyFromFile :: FilePath -> IO SolanaPrivateKey readSigningKeyFromFile path = do-  contents <- BS8.readFile path-  case readMaybe (BS8.unpack contents) of-    Just word8List -> return $ unsafeSolanaPrivateKeyRaw (word8List :: [Word8])-    Nothing -> fail ("readSigningKeyFromFile: could not parse " <> path <> " as a list of bytes (e.g. \"[1,2,...]\")")+  contents <- BS.readFile path+  -- The JSON parser's own error would quote the unparsed remainder of the+  -- file, i.e. secret bytes, so decode failures get a fixed message.+  case eitherDecodeStrict contents of+    Left _ -> failWith "not a JSON array of 64 integers in 0..255"+    Right ws -> either failWith return (mkPrivateKeyFromBytes (BS.pack ws))+  where+    failWith err = fail ("readSigningKeyFromFile: " <> path <> ": " <> err)
src/Network/Solana/Core/Instruction.hs view
@@ -1,6 +1,12 @@ {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE RecordWildCards #-} +-- | Instructions -- a program id, the 'AccountMeta's it touches and its input+-- bytes -- and their compilation into the message wire format:+-- 'compileInstruction' resolves each account (and the program id) to an+-- index into a message's ordered account list, failing with+-- 'CompileException' when a key is absent or its index does not fit in a+-- byte. module Network.Solana.Core.Instruction   ( Instruction,     mkInstruction,@@ -148,28 +154,45 @@  -- | Replace each pubkey referenced by an instruction with its index into -- the given account-key table. 'Left' when a key is not present in the--- table.+-- table, or sits at position 256 or beyond -- a compiled index is a single+-- byte, so such a key is rejected rather than silently wrapped into the+-- wrong index (mirroring the Rust SDK's @CompileError::AccountIndexOverflow@). compileInstruction :: [SolanaPublicKey] -> Instruction -> Either CompileException CompiledInstruction compileInstruction keys instruction = do   programIdIndex <- keyToIndex (iProgramId instruction) keys   accIndices <- mapM ((`keyToIndex` keys) . accountPubKey) (iAccounts instruction)   return $     CompiledInstruction-      { ciProgramIdIndex = fromIntegral programIdIndex,-        ciAccounts = mkCompact (fromIntegral <$> accIndices),+      { ciProgramIdIndex = programIdIndex,+        ciAccounts = mkCompact accIndices,         ciData = mkCompact . S.unpack $ instrData (iData instruction)       } -keyToIndex :: SolanaPublicKey -> [SolanaPublicKey] -> Either CompileException Int-keyToIndex k keys = maybeToRight (MissingIndex $ show k) $ k `elemIndex` keys+-- | Position of a key in the account table, narrowed to the byte the wire+-- format requires; 'Left' if the key is absent or its position does not+-- fit in a byte.+keyToIndex :: SolanaPublicKey -> [SolanaPublicKey] -> Either CompileException Word8+keyToIndex k keys = do+  i <- maybeToRight (MissingIndex $ show k) $ k `elemIndex` keys+  if i <= fromIntegral (maxBound :: Word8)+    then Right (fromIntegral i)+    else+      Left+        ( MissingIndex+            ( "account index overflow: " <> show k <> " sits at position " <> show i+                <> " of the account table, past the 256 addressable by a byte"+            )+        )  ------------------------------------------------------------------------------------------------  -- *** CompileException  --------------------------------------------------------------------------------------------------- | Compilation failure: an instruction references an account key that is--- missing from the message's account table.+-- | Compilation failure, carrying a message: an instruction references an+-- account key that is missing from the message's account table, an account+-- or lookup-table index does not fit in a byte, or a signing key does not+-- match the message's required signers. newtype CompileException = MissingIndex String   deriving (Show) 
src/Network/Solana/Core/Message.hs view
@@ -18,6 +18,8 @@     newMessage,     newMessageToBase64String,     mkNewMessage,+    mkNewMessageWithPayer,+    orderSigningKeys,   ) where @@ -56,6 +58,11 @@ -- account keys. If you would rather name the fee payer explicitly and not -- worry about signer order, consider 'newTransactionIntentWithPayer', -- which orders signatures automatically.+--+-- If no instruction marks a signer writable (a memo-only or SPL+-- authority-only instruction list), the message compiled here has no valid+-- fee payer and the node rejects it at sanitize time; use+-- 'newTransactionIntentWithPayer' for such transactions. newTransactionIntent :: [SolanaPrivateKey] -> [Instruction] -> SignedTransactionIntent newTransactionIntent signers instructions blockhash = do   msg <- newMessage blockhash instructions -- make the binary message@@ -76,21 +83,30 @@ newTransactionIntentWithPayer payer signingKeys instructions blockhash = do   let msg = mkNewMessageWithPayer payer blockhash instructions   msgBytes <- compileMessageToBinary msg-  let requiredSigners = take (fromIntegral (numRequiredSignatures (mHeader msg))) (mAccountKeys msg)-      keyedByPubkey = [(toSolanaPublicKey k, k) | k <- signingKeys]-  orderedKeys <- mapM (findSigningKey keyedByPubkey) requiredSigners-  mapM_ (checkKeyIsRequired requiredSigners . fst) keyedByPubkey+  orderedKeys <- orderSigningKeys msg signingKeys   let signatures = S.toStrict . Data.Binary.encode $ mkCompact $ flip dsign msgBytes <$> orderedKeys   return $ toBase64String $ S.append signatures msgBytes++-- | Orders the given private keys to match the required signers of the given+-- message (its first @numRequiredSignatures@ account keys, fee payer first).+-- 'Left' if a required signer has no corresponding key, or if a given key+-- corresponds to no required signer. Duplicate keys for the same signer are+-- tolerated: the first match signs, mirroring the Rust SDK's @try_sign@.+orderSigningKeys :: Message -> [SolanaPrivateKey] -> Either CompileException [SolanaPrivateKey]+orderSigningKeys msg signingKeys = do+  orderedKeys <- mapM findSigningKey requiredSigners+  mapM_ (checkKeyIsRequired . fst) keyedByPubkey+  pure orderedKeys   where-    findSigningKey keyedByPubkey pk =+    requiredSigners = take (fromIntegral (numRequiredSignatures (mHeader msg))) (mAccountKeys msg)+    keyedByPubkey = [(toSolanaPublicKey k, k) | k <- signingKeys]+    findSigningKey pk =       case lookup pk keyedByPubkey of         Just k -> Right k         Nothing -> Left (MissingIndex ("missing signer for " <> show pk))-    checkKeyIsRequired requiredSigners pk =-      if pk `elem` requiredSigners-        then Right ()-        else Left (MissingIndex ("unused signing key " <> show pk))+    checkKeyIsRequired pk+      | pk `elem` requiredSigners = Right ()+      | otherwise = Left (MissingIndex ("unused signing key " <> show pk))  -- | Builds and signs a durable-nonce transaction: prepends -- @SystemProgram.advanceNonceAccount nonceAccount nonceAuthority@ ahead of@@ -168,7 +184,7 @@ -- | Like 'mkNewMessage', but with an explicitly named fee payer. The payer -- is seeded as a writable signer before the instructions' account metas are -- folded in, so it ends up in the message (and pinned first by--- 'canonicalizeAccountOrder') even if no instruction references it —+-- @canonicalizeAccountOrder@) even if no instruction references it — -- enabling sponsored-fee transactions. mkNewMessageWithPayer :: SolanaPublicKey -> BlockHash -> [Instruction] -> Message mkNewMessageWithPayer payer bh =
src/Network/Solana/Core/VersionedMessage.hs view
@@ -7,7 +7,9 @@   ( AddressLookupTableAccount (..),     MessageAddressTableLookup (..),     compileV0Message,+    compileV0MessageWithPayer,     newV0TransactionIntent,+    newV0TransactionIntentWithPayer,   ) where @@ -94,6 +96,9 @@ -- cannot be represented in the byte-sized lookup index the wire format -- requires, so it is rejected rather than silently truncated (wrapped) into -- the wrong index, mirroring Rust's @AddressLookupTableIndexOverflow@.+-- Compilation also fails when the static and table-loaded keys together+-- exceed 256, since instruction account indices are single bytes (the+-- on-chain v0 sanitize rule; see 'compileInstruction'). -- -- __A trap this module can't protect you from:__ an address only just added -- to a lookup table by an @ExtendLookupTable@ instruction that landed in@@ -114,12 +119,27 @@ -- 'Network.Solana.SolanaWeb3.getLookupTable' drops that field and returns -- only 'AddressLookupTableAccount', so it can't be used for this check. compileV0Message :: BlockHash -> [Instruction] -> [AddressLookupTableAccount] -> Either CompileException BS.ByteString-compileV0Message bh instructions tables = do-  let legacy = mkNewMessage bh instructions-      header = mHeader legacy+compileV0Message bh instructions = compileV0FromLegacy (mkNewMessage bh instructions)++-- | Like 'compileV0Message', but with an explicitly named fee payer, seeded+-- as a writable signer and pinned to account 0 via+-- 'Network.Solana.Core.Message.mkNewMessageWithPayer' -- which is what the+-- Rust SDK's @v0::Message::try_compile(payer, ..)@ always does. Required+-- when no instruction marks a signer writable (a memo-only or SPL+-- authority-only instruction list) and for sponsored fees.+compileV0MessageWithPayer ::+  SolanaPublicKey -> BlockHash -> [Instruction] -> [AddressLookupTableAccount] -> Either CompileException BS.ByteString+compileV0MessageWithPayer payer bh instructions = compileV0FromLegacy (mkNewMessageWithPayer payer bh instructions)++-- | Move eligible keys of an assembled legacy message into the given lookup+-- tables and serialize the v0 message (shared body of 'compileV0Message'+-- and 'compileV0MessageWithPayer').+compileV0FromLegacy :: Message -> [AddressLookupTableAccount] -> Either CompileException BS.ByteString+compileV0FromLegacy legacy tables = do+  let header = mHeader legacy       keys = mAccountKeys legacy       (rws, ros, rwus, rous) = splitAccountsByPurpose header keys-      programIds = iProgramId <$> instructions+      programIds = iProgramId <$> mInstructions legacy       (rwusRemaining, rousRemaining, tableLoads) = drainTables programIds rwus rous tables       statics = rws <> ros <> rwusRemaining <> rousRemaining       newHeader = header {numReadonlyUnsignedAccounts = fromIntegral (length rousRemaining)}@@ -147,7 +167,8 @@ -- followed by the message bytes). Mirrors 'Network.Solana.Core.Message.newTransactionIntent'. -- The private keys passed here must be ordered to match the resulting account -- order (fee payer first), otherwise the produced signatures will not correspond--- to the right account keys.+-- to the right account keys. If you would rather name the fee payer explicitly+-- and not worry about signer order, use 'newV0TransactionIntentWithPayer'. -- -- If any given table was extended very recently, see the note on -- 'compileV0Message' about the resulting transaction's activation window:@@ -158,6 +179,21 @@ newV0TransactionIntent signers instructions tables bh = do   msg <- compileV0Message bh instructions tables   let signatures = BL.toStrict . encode $ mkCompact $ flip dsign msg <$> signers+  return $ toBase64String $ BS.append signatures msg++-- | Like 'newV0TransactionIntent', but with an explicitly named fee payer+-- (see 'compileV0MessageWithPayer'); the given private keys may be listed+-- in any order -- they are matched against the message's required signers+-- and placed in message order automatically, with the same 'Left' cases as+-- 'Network.Solana.Core.Message.newTransactionIntentWithPayer'.+newV0TransactionIntentWithPayer ::+  SolanaPublicKey -> [SolanaPrivateKey] -> [Instruction] -> [AddressLookupTableAccount] -> BlockHash ->+  Either CompileException String+newV0TransactionIntentWithPayer payer signingKeys instructions tables bh = do+  let legacy = mkNewMessageWithPayer payer bh instructions+  msg <- compileV0FromLegacy legacy tables+  orderedKeys <- orderSigningKeys legacy signingKeys+  let signatures = BL.toStrict . encode $ mkCompact $ flip dsign msg <$> orderedKeys   return $ toBase64String $ BS.append signatures msg  ------------------------------------------------------------------------------------------------
src/Network/Solana/Metaplex/TokenMetadata.hs view
@@ -353,7 +353,8 @@ -- 2. `[SIGNER]` Update authority -- 3. `[SIGNER]` Mint authority -- 4. `[WRITE, SIGNER]` Payer--- 5. `[]` Metadata account (derived)+-- 5. `[WRITE]` Metadata account (derived; the program upgrades its @token_standard@ to+--    @NonFungible@ only if it is writable) -- 6. `[]` Token program -- 7. `[]` System program -- 8. `[]` Rent sysvar@@ -366,7 +367,7 @@       AccountMeta {accountPubKey = updateAuthority, isSigner = True, isWritable = False},       AccountMeta {accountPubKey = mintAuthority, isSigner = True, isWritable = False},       AccountMeta {accountPubKey = payer, isSigner = True, isWritable = True},-      AccountMeta {accountPubKey = metadata, isSigner = False, isWritable = False},+      AccountMeta {accountPubKey = metadata, isSigner = False, isWritable = True},       AccountMeta {accountPubKey = Token.tokenProgramId, isSigner = False, isWritable = False},       AccountMeta {accountPubKey = SystemProgram.systemProgramId, isSigner = False, isWritable = False},       AccountMeta {accountPubKey = Sysvar.rent, isSigner = False, isWritable = False}
src/Network/Solana/NativePrograms/ComputeBudget.hs view
@@ -1,5 +1,9 @@ {-# LANGUAGE OverloadedStrings #-} +-- | Client for the Compute Budget program: instructions setting a+-- transaction's compute-unit limit, compute-unit price (priority fee), heap+-- size and loaded-accounts data size limit. Instruction data is+-- byte-verified against the Rust SDK. module Network.Solana.NativePrograms.ComputeBudget where  import Data.Binary
src/Network/Solana/NativePrograms/SystemProgram.hs view
@@ -1,5 +1,9 @@ {-# LANGUAGE OverloadedStrings #-} +-- | Client for the System program: account creation and allocation (plain+-- and seed-derived), program assignment, lamport transfers and durable-nonce+-- account management, plus 'decodeNonceAccount' for nonce account state.+-- Instruction data is byte-verified against the Rust SDK. module Network.Solana.NativePrograms.SystemProgram where  import Data.Binary
src/Network/Solana/RPC/HTTP/Block.hs view
@@ -32,7 +32,14 @@  -- | Returns identity and transaction information about a confirmed block in -- the ledger, using the given configuration.--- Returns 'Nothing' if the slot has been skipped or pruned from the ledger.+--+-- A slot that holds no block is answered by the node with a JSON-RPC error,+-- not with @null@: @-32007@ (slot skipped), @-32001@ (pruned from the ledger)+-- or @-32004@ (not yet produced or rooted). The transport raises such errors+-- as a @JsonRpcException@ (@CallException@) that @runWeb3'@ does not catch,+-- so wrap the call in 'Control.Exception.try' to handle them. 'Nothing' is+-- returned only when the node itself answers @null@, which Agave does solely+-- for a rooted slot missing from its blockstore without being flagged skipped. getBlock' :: (JsonRpc m) => Slot -> ConfigurationObject -> m (Maybe BlockInfo) getBlock' = do   remote "getBlock"@@ -45,7 +52,8 @@ -- transactions are returned without their address-lookup-table data, so -- instruction account indices may reference addresses not present in the -- parsed account keys.--- Returns 'Nothing' if the slot has been skipped or pruned from the ledger.+-- A slot holding no block is a JSON-RPC error rather than 'Nothing'; see+-- 'getBlock''. getBlock :: (JsonRpc m) => Slot -> m (Maybe BlockInfo) getBlock slot = getBlock' slot (defaultConfigObject {encoding = Just "json", maxSupportedTransactionVersion = Just 0}) {-# INLINE getBlock #-}@@ -209,7 +217,8 @@  -- | Returns the estimated production time of a block (as Unix timestamp). -- This is based on stake-weighted votes and validator-reported timestamps.--- Returns 'Nothing' if the timestamp is not available for the given slot.+-- Returns 'Nothing' if the block exists but carries no timestamp; a slot+-- holding no block is a JSON-RPC error, as for 'getBlock''. getBlockTime :: (JsonRpc m) => Slot -> m (Maybe Int) getBlockTime = do   remote "getBlockTime"
src/Network/Solana/RPC/HTTP/Chain.hs view
@@ -142,8 +142,9 @@     numSlots :: Word64,     -- | Duration of the sampling window, in seconds.     samplePeriodSecs :: Word16,-    -- | Number of non-vote transactions during the sample.-    numNonVoteTransactions :: Word64+    -- | Number of non-vote transactions during the sample; 'Nothing' (@null@+    -- on the wire) for older samples that do not record it.+    numNonVoteTransactions :: Maybe Word64   }   deriving (Generic, Show, FromJSON) @@ -208,8 +209,9 @@ data SolanaVersion = SolanaVersion   { -- | Software version of 'solana-core'.     solana_core :: String,-    -- | Unique identifier of the software's feature set.-    feature_set :: Word32+    -- | Unique identifier of the software's feature set; 'Nothing' if the+    -- node does not report one.+    feature_set :: Maybe Word32   }   deriving (Generic, Show, Eq) @@ -218,4 +220,4 @@   parseJSON = withObject "SolanaVersion" $ \v ->     SolanaVersion       <$> v .: "solana-core"-      <*> v .: "feature-set"+      <*> v .:? "feature-set"
src/Network/Solana/RPC/HTTP/Types.hs view
@@ -30,8 +30,9 @@ -- -- Includes API version and the slot at which the response is valid. data Context = Context-  { -- | API version string returned by the cluster (e.g. @"1.17.0"@).-    apiVersion :: Text,+  { -- | API version string reported by the node (e.g. @"2.1.16"@); 'Nothing'+    -- when the node omits it, as older nodes and some RPC proxies do.+    apiVersion :: Maybe Text,     -- | The slot at which the data in the response is valid.     contextSlot :: Slot   }@@ -41,7 +42,7 @@   parseJSON :: Value -> Parser Context   parseJSON = withObject "Context" $ \v ->     Context-      <$> v .: "apiVersion"+      <$> v .:? "apiVersion"       <*> v .: "slot"  -- | Generic wrapper for Solana RPC responses that include a context and a value.@@ -77,7 +78,9 @@ data ConfigurationObject = ConfigurationObject   { -- | Optional commitment level (e.g. @"finalized"@, @"confirmed"@).     commitment :: Maybe String,-    -- | Desired encoding for returned data (e.g. @"base58"@, @"base64"@).+    -- | Desired encoding for returned data (e.g. @"base58"@, @"base64"@,+    -- @"jsonParsed"@). Account data returned as @"base64+zstd"@ is not+    -- decoded by 'Network.Solana.Core.Account.AccountData' and is rejected.     encoding :: Maybe String,     -- | Optional partial data slice configuration.     dataSlice :: Maybe Object,
src/Network/Solana/RPC/WebSocket.hs view
@@ -196,9 +196,9 @@ -- | Parses one PubSub frame. -- -- Note that PubSub notifications carry a @context@ object holding only a--- @slot@ -- unlike the HTTP RPC responses modelled by--- 'Network.Solana.RPC.HTTP.Types.Context', they omit @apiVersion@ -- so they--- are parsed here rather than through @RPCResponse@.+-- @slot@ (no @apiVersion@, unlike the HTTP RPC responses modelled by+-- 'Network.Solana.RPC.HTTP.Types.Context'), so they are parsed here rather+-- than through @RPCResponse@. parseWsMessage :: BS.ByteString -> Either String WsMessage parseWsMessage raw = eitherDecodeStrict' raw >>= parseEither wsMessageParser @@ -244,7 +244,9 @@   case value of     String "receivedSignature" -> pure (SignatureNotification slot Nothing True)     _ -> flip (withObject "signature notification value") value $ \v -> do-      err <- v .:? "err"+      -- The node always sends @err@ (null on success); a value without it+      -- is malformed, not a success.+      err <- v .: "err"       pure (SignatureNotification slot err False)  accountNotificationParser :: Value -> Parser AccountNotification
src/Network/Solana/SolanaWeb3.hs view
@@ -13,9 +13,12 @@ -- 'getLookupTable', 'getStakeAccount', 'getNonceAccount', -- 'getMetadataAccount') that fetch an account over RPC and decode its state -- with the corresponding on-chain-state decoder, so callers don't have to--- extract and decode 'Network.Solana.Core.Account.AccountData' by hand; like--- every other helper in this module, they require a reachable RPC node and--- are compile-verified only.+-- extract and decode 'Network.Solana.Core.Account.AccountData' by hand. Like+-- every other helper in this module, they require a reachable RPC node, so+-- the unit suite only compiles them; live coverage comes from the opt-in+-- local-validator integration suite (@cabal test integration-tests+-- --flags=integration@, see the README), which exercises most but not all of+-- them. module Network.Solana.SolanaWeb3 where  import Control.Concurrent (threadDelay)@@ -27,7 +30,7 @@ import Network.Solana.Core.Account (AccountData (..), AccountInfo (dataField)) import Network.Solana.Core.Crypto import Network.Solana.Core.Instruction-import Network.Solana.Core.Message (newDurableNonceTransactionIntentWithPayer, newTransactionIntent)+import Network.Solana.Core.Message (newDurableNonceTransactionIntentWithPayer, newTransactionIntentWithPayer) import Network.Solana.Core.VersionedMessage qualified as VM import Network.Solana.Metaplex.TokenMetadata qualified as TM import Network.Solana.NativePrograms.AddressLookupTable qualified as ALT@@ -37,7 +40,13 @@ import Network.Solana.RPC.HTTP.Block (getTheLatestBlockhash) import Network.Solana.RPC.HTTP.Chain (PrioritizationFee (prioritizationFee), getRecentPrioritizationFees, percentilePriorityFee) import Network.Solana.RPC.HTTP.Transaction-import Network.Solana.RPC.HTTP.Types (ConfigurationObject, cfgJustEncodingBase64, commitment, value)+import Network.Solana.RPC.HTTP.Types+  ( ConfigurationObject,+    cfgJustEncodingBase64,+    commitment,+    preflightCommitment,+    value,+  ) import Network.Solana.SplPrograms.Token qualified as Tok import Network.Web3 hiding (AccountData, value) @@ -45,16 +54,27 @@ -- instructions, returning its signature. -- -- Fetches the latest blockhash from the cluster, compiles the instructions--- into a message, signs it with the given keys (the first key is the fee--- payer), and broadcasts the result with 'sendTransaction'.+-- with 'newTransactionIntentWithPayer' naming the first key as the fee+-- payer (forced into account 0 as a writable signer, even if no instruction+-- marks it writable -- e.g. a memo-only or SPL authority-only transaction+-- -- or references it at all), signs with the given keys (any order;+-- signatures are placed in message order automatically), and broadcasts+-- the result with 'sendTransaction'. ----- Throws a 'CompileException' if an instruction references an account key--- that cannot be resolved in the compiled message.+-- Throws @'userError' "newTransaction: no signers given"@ if the key list+-- is empty, and a 'CompileException' if an instruction references an+-- account key that cannot be resolved in the compiled message, if a+-- required signer has no corresponding key, or if a given key signs+-- nothing. newTransaction :: [SolanaPrivateKey] -> [Instruction] -> Web3 SolanaSignature-newTransaction signers instructions = do-  let newTxInt = newTransactionIntent signers instructions+newTransaction [] _ = liftIO . throwIO . userError $ "newTransaction: no signers given"+newTransaction signers@(payer : _) instructions = do   bh <- getTheLatestBlockhash-  signedTx <- either (liftIO . throwIO) pure (newTxInt bh)+  signedTx <-+    either+      (liftIO . throwIO)+      pure+      (newTransactionIntentWithPayer (toSolanaPublicKey payer) signers instructions bh)   sendTransaction signedTx  -- | Configuration used to fetch the nonce account in 'newNonceTransaction':@@ -65,6 +85,15 @@ cfgNonceAccountConfirmed :: ConfigurationObject cfgNonceAccountConfirmed = cfgJustEncodingBase64 {commitment = Just "confirmed"} +-- | Configuration used to submit the transaction in 'newNonceTransaction':+-- 'defaultRpcSendTransactionConfig' with the preflight simulation pinned at+-- the commitment the nonce account is read at ('cfgNonceAccountConfirmed').+-- The node's default @finalized@ preflight bank may not yet hold the nonce+-- value the transaction was built against (or the account at all) right+-- after creation or an advance, and rejects the send with @BlockhashNotFound@.+cfgNonceSendConfirmed :: ConfigurationObject+cfgNonceSendConfirmed = defaultRpcSendTransactionConfig {preflightCommitment = commitment cfgNonceAccountConfirmed}+ -- | Builds, signs, and submits a durable-nonce transaction with an -- explicitly named fee payer that executes the given instructions, -- returning its signature.@@ -76,7 +105,11 @@ -- 'newDurableNonceTransactionIntentWithPayer', which prepends the -- nonce-advance instruction ahead of the given instructions and uses the -- nonce account's durable nonce in place of a recent block hash (so the--- transaction never expires, unlike 'newTransaction'). The fee payer is+-- transaction never expires, unlike 'newTransaction'), and submits it with+-- 'sendTransaction'' under 'cfgNonceSendConfirmed', so the node's preflight+-- simulation runs at that same @confirmed@ commitment instead of its default+-- @finalized@ bank (which may not yet contain the nonce value used, or the+-- account at all, immediately after creation or an advance). The fee payer is -- always account 0 (sponsored fees are supported), and the given signers -- may be listed in any order — they must include private keys -- corresponding to both the payer and the nonce account's authority@@ -105,7 +138,7 @@         SP.NonceInitialized authority durableNonce _ -> do           let newTxInt = newDurableNonceTransactionIntentWithPayer payer signers nonceAccount authority instructions           signedTx <- either (liftIO . throwIO) pure (newTxInt durableNonce)-          sendTransaction signedTx+          sendTransaction' signedTx cfgNonceSendConfirmed  -- | Estimates a priority fee (in micro-lamports per compute unit) for -- landing a transaction that writes to the given accounts, as the @p@-th@@ -146,7 +179,7 @@ -- node reports no on-chain error for it (its @err@ field is 'Nothing'); a -- transaction that landed but failed on-chain (e.g. a program error) yields -- 'False', not 'True'. Also returns 'False' if it does not confirm within--- the poll budget.+-- the poll budget. See 'confirmFinalized' to wait for finalization instead. confirmTransaction :: SolanaSignature -> Web3 Bool confirmTransaction sig = go (30 :: Int)   where@@ -161,6 +194,31 @@         _ -> liftIO . throwIO . userError $           "confirmTransaction: expected exactly one status for one signature, got " <> show (length statuses) +-- | Polls 'Network.Solana.RPC.HTTP.Account.getSignatureStatuses' once a+-- second, up to 60 times, until the given signature reaches @finalized@+-- status.+--+-- Throws a 'userError' if the transaction finalizes with an on-chain error,+-- or if it does not finalize within the poll budget (naming the signature in+-- both cases). Use it before reading state through helpers that use the+-- node's default commitment ('printBalances',+-- 'Network.Solana.RPC.HTTP.Account.getBalance', 'getTokenAccount', ...): a+-- transaction that 'confirmTransaction' reports at @confirmed@ is not yet+-- visible there.+confirmFinalized :: SolanaSignature -> Web3 ()+confirmFinalized sig = go (60 :: Int)+  where+    go 0 = liftIO . throwIO . userError $ "confirmFinalized: timed out waiting for finalization of " <> show sig+    go n = do+      statuses <- getSignatureStatuses [sig]+      case statuses of+        [Just s] | confirmationStatusTxStatus s == Just "finalized" ->+          case errTxStatus s of+            Nothing -> pure ()+            Just err ->+              liftIO . throwIO . userError $ "confirmFinalized: " <> show sig <> " failed on-chain: " <> show err+        _ -> liftIO (threadDelay 1000000) >> go (n - 1)+ ------------------------------------------------------------------------------------------------  -- * Account-state fetch helpers@@ -175,7 +233,7 @@ -- failure. accountDataBytes :: AccountData -> Either String BS.ByteString accountDataBytes (AccountDataBinary bs) = Right bs-accountDataBytes (AccountDataJSON _) = Left "account data was not base64-encoded"+accountDataBytes AccountDataJSON {} = Left "account data was returned jsonParsed, not as bytes"  -- | Fetches the given account with 'Network.Solana.RPC.HTTP.Account.getAccountInfo' -- and decodes its data with the given decoder, prefixing any failure message
src/Network/Solana/SplPrograms/Memo.hs view
@@ -1,5 +1,8 @@ {-# LANGUAGE OverloadedStrings #-} +-- | Client for the SPL Memo program (v2): a single instruction whose data is+-- the memo's UTF-8 bytes; any accounts passed along must sign the+-- transaction, which the program verifies. module Network.Solana.SplPrograms.Memo where  import Data.Binary
test-integration/Main.hs view
@@ -16,6 +16,7 @@ import System.IO (BufferMode (LineBuffering), hSetBuffering, stdout) import System.Timeout (timeout) import Test.Integration.Alt qualified as Alt+import Test.Integration.Block qualified as Block import Test.Integration.Nonce qualified as Nonce import Test.Integration.PriorityFee qualified as PriorityFee import Test.Integration.Setup (rpcUrl)@@ -47,7 +48,7 @@ -- | The suite has no @-threaded@/@-N@ RTS options, so 'testGroup' already -- runs its children one at a time today; 'sequentialTestGroup' makes that a -- load-bearing property of the test tree itself rather than an accident of--- the cabal stanza, so the five groups below (each hitting the same local+-- the cabal stanza, so the groups below (each hitting the same local -- validator with fresh airdrops) can't start running concurrently if -- @-threaded@ is ever added. 'AllFinish' keeps every group's results visible -- even if an earlier one fails, matching today's behaviour.@@ -59,4 +60,8 @@ -- @-p@ no longer isolates just the matched test the way it would under a -- plain 'testGroup'. integrationTests :: TestTree-integrationTests = sequentialTestGroup "integration" AllFinish [Transfer.tests, Token.tests, Nonce.tests, PriorityFee.tests, Alt.tests, WebSocket.tests]+integrationTests =+  sequentialTestGroup+    "integration"+    AllFinish+    [Transfer.tests, Token.tests, Nonce.tests, PriorityFee.tests, Alt.tests, WebSocket.tests, Block.tests]
test-integration/Test/Integration/Alt.hs view
@@ -7,13 +7,15 @@ import Control.Exception (throwIO) import Control.Monad.IO.Class (liftIO) import Network.Solana.Core.Crypto (createSolanaKeyPair)-import Network.Solana.Core.VersionedMessage (altAddresses, newV0TransactionIntent)+import Network.Solana.Core.VersionedMessage (altAddresses, newV0TransactionIntent, newV0TransactionIntentWithPayer) import Network.Solana.NativePrograms.AddressLookupTable (createLookupTable, extendLookupTable) import Network.Solana.NativePrograms.SystemProgram qualified as SystemProgram import Network.Solana.RPC.HTTP.Account (getBalance) import Network.Solana.RPC.HTTP.Block (getTheLatestBlockhash) import Network.Solana.RPC.HTTP.Ledger (getSlot)+import Network.Solana.RPC.HTTP.Transaction (sendTransaction) import Network.Solana.SolanaWeb3 (getLookupTable)+import Network.Solana.SplPrograms.Memo qualified as Memo import Network.Web3.Provider (Web3) import Test.Integration.Setup import Test.Tasty@@ -23,7 +25,9 @@ tests =   testGroup     "Alt"-    [testCase "on-chain ALT: create, extend, decode, v0 transfer through the table" (run altV0Flow)]+    [ testCase "on-chain ALT: create, extend, decode, v0 transfer through the table" (run altV0Flow),+      testCase "v0 memo-only transaction via explicit fee payer lands without a lookup table" (run v0MemoOnly)+    ]  -- | Creates a lookup table at the current finalized slot (guaranteed present -- in the SlotHashes sysvar for the on-chain create check), extends it with a@@ -61,3 +65,17 @@   confirmFinalized sig   bal <- getBalance recipient   liftIO (bal @?= 200_000_000)++-- | The v0 counterpart of the legacy memo-only case: the memo's only signer+-- is read-only, so only an explicitly named fee payer yields a valid header.+v0MemoOnly :: Web3 ()+v0MemoOnly = do+  (payerPk, payerSk) <- fundedKeypair 1_000_000_000+  bh <- getTheLatestBlockhash+  tx <-+    either+      (liftIO . throwIO)+      pure+      (newV0TransactionIntentWithPayer payerPk [payerSk] [Memo.buildMemo "v0" [payerPk]] [] bh)+  sig <- sendTransaction tx+  confirmFinalized sig
+ test-integration/Test/Integration/Block.hs view
@@ -0,0 +1,29 @@+-- | Live coverage of the block-retrieval contract: a slot holding no block+-- is answered by the node with a JSON-RPC error (raised as an exception the+-- transport does not turn into 'Left'), never with 'Nothing'.+module Test.Integration.Block (tests) where++import Control.Exception (SomeException, try)+import Data.List (isInfixOf)+import Data.Maybe (isJust)+import Network.Solana.RPC.HTTP.Block (getBlock)+import Network.Solana.RPC.HTTP.Ledger (getSlot)+import Test.Integration.Setup+import Test.Tasty+import Test.Tasty.HUnit++tests :: TestTree+tests =+  testGroup+    "Block"+    [ testCase "getBlock on a slot holding no block raises the node's -32004 error, not Nothing" $ do+        result <- try @SomeException (run (getSlot >>= \s -> getBlock (s + 100_000)))+        case result of+          Left e -> assertBool (show e) ("-32004" `isInfixOf` show e)+          Right b -> assertFailure ("expected a JSON-RPC error, got a result (Just: " <> show (isJust b) <> ")"),+      -- The current finalized slot: produced by the sole validator, and not yet+      -- pruned (old slots are cleaned up under the default ledger size limit).+      testCase "getBlock on a produced slot returns the block" $ do+        b <- run (getSlot >>= getBlock)+        assertBool "expected Just a block" (isJust b)+    ]
test-integration/Test/Integration/Nonce.hs view
@@ -1,15 +1,20 @@ -- | Live durable-nonce coverage: create and initialize a nonce account, use -- it via 'newNonceTransaction' to send a transfer, and check the durable--- nonce advances and the transfer lands.+-- nonce advances and the transfer lands -- both after finalization and, as+-- the helper promises, immediately after a merely-confirmed create or+-- advance. module Test.Integration.Nonce (tests) where  import Control.Exception (throwIO) import Control.Monad.IO.Class (liftIO)-import Network.Solana.Core.Crypto (createSolanaKeyPair)+import Network.Solana.Core.Account (AccountData (..), AccountInfo (dataField))+import Network.Solana.Core.Block (BlockHash)+import Network.Solana.Core.Crypto (SolanaPublicKey, createSolanaKeyPair) import Network.Solana.NativePrograms.SystemProgram qualified as SystemProgram-import Network.Solana.RPC.HTTP.Account (getBalance)+import Network.Solana.RPC.HTTP.Account (getAccountInfo, getAccountInfo', getBalance) import Network.Solana.RPC.HTTP.Tokenomics (getMinimumBalanceForRentExemption)-import Network.Solana.SolanaWeb3 (getNonceAccount, newNonceTransaction)+import Network.Solana.RPC.HTTP.Types (defaultConfigObject, encoding, value)+import Network.Solana.SolanaWeb3 (accountDataBytes, cfgNonceAccountConfirmed, getNonceAccount, newNonceTransaction) import Network.Web3.Provider (Web3) import Test.Integration.Setup import Test.Tasty@@ -19,16 +24,21 @@ tests =   testGroup     "Nonce"-    [testCase "durable-nonce flow: create, use via newNonceTransaction, nonce advances" (run nonceFlow)]+    [ testCase "durable-nonce flow: create, use via newNonceTransaction, nonce advances" (run nonceFlow),+      testCase+        "newNonceTransaction works immediately after a confirmed create and a confirmed advance"+        (run nonceImmediateUse)+    ]  -- | Creates and initializes a nonce account (payer as authority), checks its -- decoded state before use, sends a transfer through 'newNonceTransaction', -- and checks the durable nonce advanced and the transfer landed with the -- exact amount. ----- 'newNonceTransaction' reads the nonce account at @confirmed@ commitment--- internally, but the node preflights its send against the @finalized@--- bank, so the create transaction is finalized before it is called.+-- 'getNonceAccount' reads at the node's default @finalized@ commitment, so+-- the create transaction is finalized before its state is read;+-- 'newNonceTransaction' itself needs only @confirmed@ state (see+-- 'nonceImmediateUse'). nonceFlow :: Web3 () nonceFlow = do   (payerPk, payerSk) <- fundedKeypair 2_000_000_000@@ -40,7 +50,21 @@       [ SystemProgram.createAccount payerPk noncePk rent 80 SystemProgram.systemProgramId,         SystemProgram.initializeNonceAccount noncePk payerPk       ]-  confirmFinalized createSig -- newNonceTransaction preflights against the finalized bank+  confirmFinalized createSig -- getNonceAccount reads the finalized bank+  -- The node's default encoding (a bare base58 string) and the explicit+  -- base64 pair must decode to the same 80 bytes.+  raw <- value <$> getAccountInfo' noncePk defaultConfigObject+  b64 <- getAccountInfo noncePk+  liftIO (fmap dataField raw @?= fmap dataField b64)+  -- Under encoding "jsonParsed" the node returns its parsed view for accounts+  -- it has a parser for (the nonce account) and the base64 pair otherwise+  -- (the payer, a plain system account with no data).+  parsed <- value <$> getAccountInfo' noncePk (defaultConfigObject {encoding = Just "jsonParsed"})+  case dataField <$> parsed of+    Just (AccountDataJSON prog _ space) -> liftIO ((prog @?= "nonce") >> (space @?= 80))+    other -> liftIO (assertFailure ("expected parsed nonce data, got " <> show other))+  fallback <- value <$> getAccountInfo' payerPk (defaultConfigObject {encoding = Just "jsonParsed"})+  liftIO (fmap dataField fallback @?= Just (AccountDataBinary mempty))   before <- requireJust "nonce account (before)" =<< getNonceAccount noncePk   nonceBefore <- case before of     SystemProgram.NonceInitialized auth dn _ -> liftIO (auth @?= payerPk) >> pure dn@@ -54,3 +78,50 @@     _ -> liftIO (throwIO (userError "nonce account no longer initialized"))   bal <- getBalance recipient   liftIO (bal @?= 100_000_000)++-- | The helper's contract: usable as soon as the nonce account's creation or+-- last advance is merely @confirmed@, with no finalization wait. Sends two+-- transfers back to back, the second within the finalization window of the+-- first one's nonce advance.+nonceImmediateUse :: Web3 ()+nonceImmediateUse = do+  (payerPk, payerSk) <- fundedKeypair 2_000_000_000+  (noncePk, nonceSk) <- liftIO createSolanaKeyPair+  rent <- getMinimumBalanceForRentExemption 80+  _ <-+    sendAndConfirm+      [payerSk, nonceSk]+      [ SystemProgram.createAccount payerPk noncePk rent 80 SystemProgram.systemProgramId,+        SystemProgram.initializeNonceAccount noncePk payerPk+      ]+  nonce0 <- durableNonceAtConfirmed noncePk+  recipient <- fst <$> liftIO createSolanaKeyPair+  sig1 <- newNonceTransaction payerPk [payerSk] noncePk [SystemProgram.transfer payerPk recipient 100_000_000]+  confirmOrFail "first nonce transaction" sig1+  nonce1 <- durableNonceAtConfirmed noncePk+  liftIO (assertBool "durable nonce advanced after the first use" (nonce1 /= nonce0))+  sig2 <- newNonceTransaction payerPk [payerSk] noncePk [SystemProgram.transfer payerPk recipient 100_000_000]+  confirmFinalized sig2+  afterAcc <- requireJust "nonce account (after)" =<< getNonceAccount noncePk+  case afterAcc of+    SystemProgram.NonceInitialized _ dn _ ->+      liftIO (assertBool "durable nonce advanced after the second use" (dn /= nonce1))+    _ -> liftIO (throwIO (userError "nonce account no longer initialized"))+  bal <- getBalance recipient+  liftIO (bal @?= 200_000_000)++-- | The nonce account's current durable nonce read at @confirmed@+-- commitment -- the same read 'newNonceTransaction' performs.+-- 'getNonceAccount' reads at the node's default @finalized@ commitment and+-- would still report the pre-advance value here.+durableNonceAtConfirmed :: SolanaPublicKey -> Web3 BlockHash+durableNonceAtConfirmed noncePk = do+  acc <- requireJust "nonce account (confirmed)" . value =<< getAccountInfo' noncePk cfgNonceAccountConfirmed+  state <-+    either+      (liftIO . throwIO . userError)+      pure+      (accountDataBytes (dataField acc) >>= SystemProgram.decodeNonceAccount)+  case state of+    SystemProgram.NonceInitialized _ dn _ -> pure dn+    _ -> liftIO (throwIO (userError "nonce account not initialized"))
test-integration/Test/Integration/PriorityFee.hs view
@@ -8,7 +8,8 @@ import Network.Solana.NativePrograms.ComputeBudget qualified as ComputeBudget import Network.Solana.NativePrograms.SystemProgram qualified as SystemProgram import Network.Solana.RPC.HTTP.Account (getBalance)-import Network.Solana.SolanaWeb3 (estimatePriorityFee)+import Network.Solana.SolanaWeb3 (estimatePriorityFee, newTransaction)+import Network.Solana.SplPrograms.Memo qualified as Memo import Network.Web3.Provider (Web3) import Test.Integration.Setup import Test.Tasty@@ -19,7 +20,8 @@   testGroup     "PriorityFee"     [ testCase "prioritized transfer lands with the exact amount" (run priorityFeeTransfer),-      testCase "estimatePriorityFee smoke: returns without throwing" (run estimateSmoke)+      testCase "estimatePriorityFee smoke: returns without throwing" (run estimateSmoke),+      testCase "README priority-fee flow via newTransaction lands with its memo" (run readmePriorityFlow)     ]  -- | Sends a transfer prioritized with a compute unit limit and price, and@@ -50,3 +52,22 @@   pk <- fst <$> liftIO createSolanaKeyPair   fee <- estimatePriorityFee [pk] 0.5   liftIO (fee `seq` pure ())++-- | The README's priority-fee example through the public 'newTransaction':+-- compute-budget instructions, a transfer and a memo whose only signer is+-- the (writable) fee payer.+readmePriorityFlow :: Web3 ()+readmePriorityFlow = do+  (payerPk, payerSk) <- fundedKeypair 2_000_000_000+  recipient <- fst <$> liftIO createSolanaKeyPair+  sig <-+    newTransaction+      [payerSk]+      [ ComputeBudget.setComputeUnitLimit 200_000,+        ComputeBudget.setComputeUnitPrice 10_000,+        SystemProgram.transfer payerPk recipient 1_000_000_000,+        Memo.buildMemo "thanks for the coffee" [payerPk]+      ]+  confirmFinalized sig+  bal <- getBalance recipient+  liftIO (bal @?= 1_000_000_000)
test-integration/Test/Integration/Setup.hs view
@@ -10,9 +10,10 @@ -- before returning (later sends preflight against the finalized bank), and -- 'sendAndConfirm' (via 'sendConfirmedPreflight') pins preflight at -- @confirmed@ so multi-step flows can build on merely-confirmed intermediate--- state. Callers are expected to 'confirmFinalized' the last transaction of--- a flow before asserting on account state or balances (finalization is--- monotone by slot, so one wait covers every prior transaction).+-- state. Callers are expected to 'confirmFinalized' (re-exported from+-- "Network.Solana.SolanaWeb3") the last transaction of a flow before+-- asserting on account state or balances (finalization is monotone by slot,+-- so one wait covers every prior transaction). module Test.Integration.Setup   ( rpcUrl,     wsEndpoint,@@ -27,7 +28,6 @@   ) where -import Control.Concurrent (threadDelay) import Control.Exception (throwIO) import Control.Monad.IO.Class (liftIO) import Data.List (stripPrefix)@@ -36,11 +36,11 @@ import Network.Solana.Core.Crypto (SolanaPrivateKey, SolanaPublicKey, SolanaSignature, createSolanaKeyPair, toSolanaPublicKey) import Network.Solana.Core.Instruction (Instruction) import Network.Solana.Core.Message (newTransactionIntentWithPayer)-import Network.Solana.RPC.HTTP.Account (confirmationStatusTxStatus, errTxStatus, getSignatureStatuses)+import Network.Solana.RPC.HTTP.Account (errTxStatus, getSignatureStatuses) import Network.Solana.RPC.HTTP.Block (getTheLatestBlockhash) import Network.Solana.RPC.HTTP.Transaction (requestAirdrop, sendTransaction') import Network.Solana.RPC.HTTP.Types (defaultConfigObject, encoding, preflightCommitment)-import Network.Solana.SolanaWeb3 (confirmTransaction)+import Network.Solana.SolanaWeb3 (confirmFinalized, confirmTransaction) import Network.Web3.Provider (Provider (HttpProvider), Web3, runWeb3') import System.Environment (lookupEnv) @@ -117,25 +117,6 @@         [Just s] | Just err <- errTxStatus s ->           liftIO . throwIO . userError $ what <> " failed on-chain: " <> show sig <> ": " <> show err         _ -> liftIO . throwIO . userError $ what <> " failed to confirm: " <> show sig---- | Polls 'getSignatureStatuses' once a second, up to 60 times, until the--- given signature reaches @finalized@ status.------ Throws a 'userError' if the transaction finalizes with an on-chain error,--- or if it does not finalize within the poll budget (naming the signature in--- both cases).-confirmFinalized :: SolanaSignature -> Web3 ()-confirmFinalized sig = go (60 :: Int)-  where-    go 0 = liftIO . throwIO . userError $ "confirmFinalized: timed out waiting for finalization of " <> show sig-    go n = do-      statuses <- getSignatureStatuses [sig]-      case statuses of-        [Just s] | confirmationStatusTxStatus s == Just "finalized" ->-          case errTxStatus s of-            Nothing -> pure ()-            Just err -> liftIO . throwIO . userError $ "confirmFinalized: " <> show sig <> " failed on-chain: " <> show err-        _ -> liftIO (threadDelay 1000000) >> go (n - 1)  -- | Sends an already-encoded transaction with preflight pinned at -- @confirmed@ -- the suite's one commitment override, single-sourced here so
test-integration/Test/Integration/Token.hs view
@@ -7,7 +7,7 @@ import Network.Solana.Core.Crypto (createSolanaKeyPair) import Network.Solana.NativePrograms.SystemProgram qualified as SystemProgram import Network.Solana.RPC.HTTP.Tokenomics (getMinimumBalanceForRentExemption)-import Network.Solana.SolanaWeb3 (getMint, getTokenAccount)+import Network.Solana.SolanaWeb3 (getMint, getTokenAccount, newTransaction) import Network.Solana.SplPrograms.AssociatedTokenAccount qualified as ATA import Network.Solana.SplPrograms.Token qualified as Token import Network.Web3.Provider (Web3)@@ -19,7 +19,9 @@ tests =   testGroup     "Token"-    [testCase "mint -> ATAs -> mintTo -> transferChecked, decoded state matches" (run tokenLifecycle)]+    [ testCase "mint -> ATAs -> mintTo -> transferChecked, decoded state matches" (run tokenLifecycle),+      testCase "README SPL flow via newTransaction: setup tx, then idempotent ATA + transferChecked" (run readmeSplFlow)+    ]  -- | Creates a mint (payer as mint authority, no freeze authority), derives -- the associated token accounts for the payer and a second wallet, mints@@ -51,3 +53,37 @@   liftIO ((Token.taAmount accA @?= 750_000) >> (Token.taMint accA @?= mintPk) >> (Token.taOwner accA @?= payerPk))   accB <- requireJust "token account B" =<< getTokenAccount ataB   liftIO ((Token.taAmount accB @?= 250_000) >> (Token.taOwner accB @?= walletB))++-- | The README's SPL example, structurally verbatim: one setup transaction+-- (createAccount + initializeMint2 + createAssociatedTokenAccount + mintTo,+-- signed by the payer and the mint) and one transfer transaction+-- (createAssociatedTokenAccountIdempotent + transferChecked), both through+-- the public 'newTransaction' rather than the suite's 'sendAndConfirm'.+readmeSplFlow :: Web3 ()+readmeSplFlow = do+  (payerPk, payerSk) <- fundedKeypair 3_000_000_000+  (mintPk, mintSk) <- liftIO createSolanaKeyPair+  recipient <- fst <$> liftIO createSolanaKeyPair+  sourceAta <- requireJust "source ATA derivation" (ATA.getAssociatedTokenAddress payerPk mintPk)+  destinationAta <- requireJust "destination ATA derivation" (ATA.getAssociatedTokenAddress recipient mintPk)+  mintRent <- getMinimumBalanceForRentExemption 82+  setupTx <-+    newTransaction+      [payerSk, mintSk]+      [ SystemProgram.createAccount payerPk mintPk mintRent 82 Token.tokenProgramId,+        Token.initializeMint2 mintPk 6 payerPk Nothing,+        ATA.createAssociatedTokenAccount payerPk payerPk mintPk,+        Token.mintTo mintPk sourceAta payerPk [] 10_000_000+      ]+  confirmFinalized setupTx+  transferTx <-+    newTransaction+      [payerSk]+      [ ATA.createAssociatedTokenAccountIdempotent payerPk recipient mintPk,+        Token.transferChecked sourceAta mintPk destinationAta payerPk [] 1_000_000 6+      ]+  confirmFinalized transferTx+  source <- requireJust "source token account" =<< getTokenAccount sourceAta+  liftIO (Token.taAmount source @?= 9_000_000)+  destination <- requireJust "destination token account" =<< getTokenAccount destinationAta+  liftIO (Token.taAmount destination @?= 1_000_000)
test-integration/Test/Integration/Transfer.hs view
@@ -1,11 +1,14 @@ -- | Live SOL transfer coverage: a plain transfer from a freshly airdropped--- payer, and a sponsored transfer where the fee payer and the transfer's--- signer are different accounts.+-- payer, a sponsored transfer where the fee payer and the transfer's signer+-- are different accounts, and the fee-payer contract of the public+-- 'newTransaction' helper (first key pays, read-only-signer-only+-- instructions land, empty signer list rejected). module Test.Integration.Transfer (tests) where -import Control.Exception (throwIO)+import Control.Exception (SomeException, throwIO, try) import Control.Monad (void) import Control.Monad.IO.Class (liftIO)+import Data.List (isInfixOf) import Network.Solana.Core.Crypto (createSolanaKeyPair) import Network.Solana.Core.Message (newTransactionIntentWithPayer) import Network.Solana.NativePrograms.SystemProgram qualified as SystemProgram@@ -14,6 +17,7 @@ import Network.Solana.RPC.HTTP.Chain (getVersion) import Network.Solana.RPC.HTTP.Transaction (sendTransaction) import Network.Solana.SolanaWeb3 (newTransaction)+import Network.Solana.SplPrograms.Memo qualified as Memo import Network.Web3.Provider (Web3) import Test.Integration.Setup import Test.Tasty@@ -25,11 +29,14 @@     "Transfer"     [ testCase "getVersion answers" (run (void getVersion)),       testCase "airdropped SOL transfer lands with the exact amount" (run basicTransfer),-      testCase "sponsored transfer: sponsor pays the fee, signer order is free" (run sponsoredTransfer)+      testCase "sponsored transfer: sponsor pays the fee, signer order is free" (run sponsoredTransfer),+      testCase "newTransaction: memo-only instruction (read-only authority signer) lands" (run memoOnly),+      testCase "newTransaction: empty signer list is rejected before any RPC call" noSigners,+      testCase "newTransaction: first key pays even when unreferenced, signer order is free" (run firstKeyPays)     ]  -- | Airdrops a payer, sends it through 'newTransaction' (which builds its--- own blockhash and derives the fee payer from signer order), and checks the+-- own blockhash and names its first key as the fee payer), and checks the -- recipient receives exactly the transferred amount. Live-covers -- 'newTransaction' with every dependency (airdrop, blockhash, confirmation) -- finalized.@@ -41,6 +48,10 @@   confirmFinalized sig   bal <- getBalance recipient   liftIO (bal @?= 1_000_000_000)+  -- The README's sequence: balances read right after confirmFinalized reflect+  -- the transfer on both sides.+  senderBal <- getBalance payerPk+  liftIO (assertBool "sender paid the transfer and the fee" (senderBal < 1_000_000_000))  -- | A sponsor pays the fee for a transfer sent by another funded account, with -- the private keys deliberately passed in the wrong order to@@ -63,6 +74,47 @@       pure       (newTransactionIntentWithPayer sponsorPk [senderSk, sponsorSk] [SystemProgram.transfer senderPk recipient 500_000_000] bh)   sig <- sendTransaction tx+  confirmFinalized sig+  recipientBal <- getBalance recipient+  liftIO (recipientBal @?= 500_000_000)+  senderBal <- getBalance senderPk+  liftIO (senderBal @?= 500_000_000) -- sender paid NO fee: exactly the transfer left+  sponsorBal <- getBalance sponsorPk+  liftIO (assertBool "sponsor paid the fee" (sponsorBal < 2_000_000_000))++-- | A memo instruction's only account is its signer as a /read-only/ signer,+-- so a fee payer derived from the instructions alone would be read-only and+-- the node would reject the transaction at sanitize time. 'newTransaction'+-- must name its first key as the (writable) fee payer instead.+memoOnly :: Web3 ()+memoOnly = do+  (payerPk, payerSk) <- fundedKeypair 1_000_000_000+  sig <- newTransaction [payerSk] [Memo.buildMemo "solana-haskell-sdk" [payerPk]]+  confirmFinalized sig++-- | With no signing key there is no fee payer; 'newTransaction' must say so+-- before touching the network rather than submit a signature-less+-- transaction for the node to reject.+noSigners :: Assertion+noSigners = do+  pk <- fst <$> createSolanaKeyPair+  recipient <- fst <$> createSolanaKeyPair+  r <- try @SomeException (run (newTransaction [] [SystemProgram.transfer pk recipient 1]))+  case r of+    Left e -> assertBool ("expected a \"no signers\" error, got: " <> show e) ("no signers" `isInfixOf` show e)+    Right sig -> assertFailure ("sent a transaction with no signers: " <> show sig)++-- | The first key given to 'newTransaction' pays the fee even if no+-- instruction references it (sponsored fees), and the remaining keys may be+-- passed in any order.+firstKeyPays :: Web3 ()+firstKeyPays = do+  pairs <- fundedKeypairs [2_000_000_000, 1_000_000_000]+  ((sponsorPk, sponsorSk), (senderPk, senderSk)) <- case pairs of+    [a, b] -> pure (a, b)+    _ -> liftIO (throwIO (userError "expected two keypairs"))+  recipient <- fst <$> liftIO createSolanaKeyPair+  sig <- newTransaction [sponsorSk, senderSk] [SystemProgram.transfer senderPk recipient 500_000_000]   confirmFinalized sig   recipientBal <- getBalance recipient   liftIO (recipientBal @?= 500_000_000)
test/Main.hs view
@@ -19,7 +19,10 @@ import Test.NativePrograms.Vote qualified import Test.RPC.Chain qualified import Test.RPC.Parsers qualified+import Test.RPC.Types qualified import Test.RPC.WebSocket qualified+import Test.Readme qualified+import Test.SolanaWeb3 qualified import Test.SplPrograms.AssociatedTokenAccount qualified import Test.SplPrograms.Memo qualified import Test.SplPrograms.Token qualified@@ -49,7 +52,10 @@           Test.NativePrograms.Vote.tests,           Test.RPC.Chain.tests,           Test.RPC.Parsers.tests,+          Test.RPC.Types.tests,           Test.RPC.WebSocket.tests,+          Test.Readme.tests,+          Test.SolanaWeb3.tests,           Test.SplPrograms.AssociatedTokenAccount.tests,           Test.SplPrograms.Memo.tests,           Test.SplPrograms.Token.tests
test/Test/Core/Account.hs view
@@ -2,11 +2,36 @@  module Test.Core.Account (tests) where -import Data.Aeson (eitherDecode)+import Data.Aeson (eitherDecode, encode)+import Data.Aeson.Types (parseMaybe, withObject, (.:))+import Data.ByteString qualified as BS+import Data.ByteString.Lazy qualified as BL+import Data.Either (isLeft)+import Data.List (isInfixOf)+import Data.Word (Word8) import Network.Solana.Core.Account import Test.Tasty import Test.Tasty.HUnit+import Test.Tasty.QuickCheck +-- | The Rent sysvar's 17 data bytes (the cluster-independent defaults), as+-- Agave returns them: bare base58 under the default encoding, a tagged pair+-- under @base64@.+rentBytes :: BS.ByteString+rentBytes = BS.pack [152, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 64, 50]++-- | A nonce account's @data@ under @encoding: "jsonParsed"@, as Agave 2.1.16+-- returns it.+nonceParsedJson :: BL.ByteString+nonceParsedJson =+  "{\"parsed\":{\"info\":{\"authority\":\"AkagUZWfCDrEDJTtsuxvAGv9AkhNQAgywJLxyzHPnFQ9\",\"blockhash\":\"EKp95XWYEyvEKaNoZMqaBBBiHPuEyodG9ZC4iHiKHMjj\",\"feeCalculator\":{\"lamportsPerSignature\":\"5000\"}},\"type\":\"initialized\"},\"program\":\"nonce\",\"space\":80}"++-- | The Clock sysvar as a whole @getAccountInfo@ value under+-- @encoding: "jsonParsed"@ (the shape that failed at @$.value.data@).+clockAccountJson :: BL.ByteString+clockAccountJson =+  "{\"data\":{\"parsed\":{\"info\":{\"epoch\":0,\"epochStartTimestamp\":1789286435,\"leaderScheduleEpoch\":1,\"slot\":1367,\"unixTimestamp\":1789287077},\"type\":\"clock\"},\"program\":\"sysvar\",\"space\":40},\"executable\":false,\"lamports\":1169280,\"owner\":\"Sysvar1111111111111111111111111111111111111\",\"rentEpoch\":0,\"space\":40}"+ tests :: TestTree tests =   testGroup@@ -18,5 +43,51 @@       testCase "AccountData FromJSON rejects invalid base58 in a base58-tagged pair" $         case eitherDecode "[\"0OIl\",\"base58\"]" :: Either String AccountData of           Left _ -> pure ()-          Right ad -> assertFailure ("expected parse failure, got " <> show ad)+          Right ad -> assertFailure ("expected parse failure, got " <> show ad),+      testCase "AccountData FromJSON decodes a bare string as base58 (the node's default binary encoding)" $+        eitherDecode "\"2RsdFKVyfoKRJwMEPcvasdsF\"" @?= Right (AccountDataBinary rentBytes),+      testCase "bare string and base58-tagged pair decode identically" $ do+        eitherDecode "\"1111\"" @?= Right (AccountDataBinary (BS.replicate 4 0))+        eitherDecode "[\"1111\",\"base58\"]" @?= Right (AccountDataBinary (BS.replicate 4 0)),+      testCase "base64-tagged pair still decodes as base64" $+        eitherDecode "[\"mA0AAAAAAAAAAAAAAAAAQDI=\",\"base64\"]" @?= Right (AccountDataBinary rentBytes),+      testCase "bare empty string decodes to empty account data" $+        eitherDecode "\"\"" @?= Right (AccountDataBinary BS.empty),+      testCase "AccountData FromJSON rejects a bare string that is not base58" $+        case eitherDecode "\"0OIl\"" :: Either String AccountData of+          Left err -> assertBool ("expected \"invalid base58\" in: " <> err) ("invalid base58" `isInfixOf` err)+          Right ad -> assertFailure ("expected parse failure, got " <> show ad),+      testProperty "AccountData JSON round-trip (ToJSON emits the bare base58 form)" $ \(ws :: [Word8]) ->+        let bs = BS.pack ws+         in eitherDecode (encode (AccountDataBinary bs)) === Right (AccountDataBinary bs),+      testCase "AccountData FromJSON decodes a jsonParsed object into AccountDataJSON" $+        case eitherDecode nonceParsedJson of+          Left err -> assertFailure err+          Right (AccountDataJSON prog parsed space) -> do+            prog @?= "nonce"+            space @?= 80+            parseMaybe (withObject "parsed" (.: "type")) parsed @?= Just ("initialized" :: String)+          Right other -> assertFailure ("expected AccountDataJSON, got " <> show other),+      testCase "AccountData FromJSON decodes the jsonParsed base64 fallback pair as binary" $+        eitherDecode "[\"c3lzdGVtX3Byb2dyYW0=\",\"base64\"]" @?= Right (AccountDataBinary "system_program"),+      testCase "AccountData FromJSON rejects a jsonParsed object missing space" $+        assertBool+          "accepted an object without space"+          (isLeft (eitherDecode "{\"parsed\":{},\"program\":\"nonce\"}" :: Either String AccountData)),+      testCase "AccountData FromJSON rejects the never-emitted [text, \"jsonParsed\"] pair" $+        assertBool+          "accepted a jsonParsed pair"+          (isLeft (eitherDecode "[\"{}\",\"jsonParsed\"]" :: Either String AccountData)),+      testCase "AccountData ToJSON/FromJSON round-trips parsed data" $+        case eitherDecode nonceParsedJson :: Either String AccountData of+          Left err -> assertFailure err+          Right ad -> eitherDecode (encode ad) @?= Right ad,+      testCase "AccountInfo FromJSON accepts a jsonParsed account value" $+        case eitherDecode clockAccountJson :: Either String AccountInfo of+          Left err -> assertFailure err+          Right info -> do+            lamports info @?= Lamport 1169280+            case dataField info of+              AccountDataJSON prog _ space -> (prog @?= "sysvar") >> (space @?= 40)+              other -> assertFailure ("expected AccountDataJSON, got " <> show other)     ]
test/Test/Core/Crypto.hs view
@@ -2,9 +2,12 @@  module Test.Core.Crypto (tests) where +import Control.Exception (ErrorCall, IOException, evaluate, try) import Data.Aeson (eitherDecode) import Data.Binary (decode, encode) import Data.ByteString qualified as BS+import Data.Either (isLeft)+import Data.List (isInfixOf) import Network.Solana.Core.Crypto import Test.Tasty import Test.Tasty.HUnit@@ -16,6 +19,13 @@ instance Arbitrary Bytes32 where   arbitrary = Bytes32 . BS.pack <$> vectorOf 32 arbitrary +-- | The keypair derived from a seed of 32 one-bytes; its 64 secret-key bytes+-- are what @test/fixtures/keypair.json@ holds in @solana-keygen@ format.+fixtureKeypair :: (SolanaPublicKey, SolanaPrivateKey)+fixtureKeypair = case createSolanaKeypairFromSeed (BS.replicate 32 1) of+  Just kp -> kp+  Nothing -> error "fixtureKeypair: 32-byte seed rejected"+ tests :: TestTree tests =   testGroup@@ -45,5 +55,84 @@       testCase "SolanaSignature FromJSON rejects invalid base58" $         case eitherDecode "\"0OIl\"" :: Either String SolanaSignature of           Left _ -> pure ()-          Right sig -> assertFailure ("expected parse failure, got " <> show sig)+          Right sig -> assertFailure ("expected parse failure, got " <> show sig),+      testGroup+        "mkPrivateKeyFromBytes"+        [ testCase "accepts a genuine 64-byte secret key" $ do+            let (pk, sk) = fixtureKeypair+            mkPrivateKeyFromBytes (getSolanaPrivateKeyRaw sk) @?= Right sk+            fmap toSolanaPublicKey (mkPrivateKeyFromBytes (getSolanaPrivateKeyRaw sk)) @?= Right pk,+          testCase "rejects empty, truncated, seed-only and over-long input" $+            mapM_+              (\bs -> assertBool ("accepted " <> show (BS.length bs) <> " bytes") (isLeft (mkPrivateKeyFromBytes bs)))+              [BS.empty, BS.pack [1], BS.replicate 32 1, BS.replicate 63 1, BS.replicate 65 1],+          testCase "rejects a mismatched public-key half" $ do+            let raw = getSolanaPrivateKeyRaw (snd fixtureKeypair)+                corrupted = BS.init raw <> BS.singleton (BS.last raw + 1)+            assertBool+              "accepted a key whose public half is not derived from its seed"+              (isLeft (mkPrivateKeyFromBytes corrupted)),+          testProperty "rejects every length other than 64" $+            forAll (choose (0, 130) `suchThat` (/= 64)) $ \n ->+              forAll (vectorOf n arbitrary) $ \ws -> isLeft (mkPrivateKeyFromBytes (BS.pack ws)),+          testProperty "rejects random 64-byte input" $+            forAll (vectorOf 64 arbitrary) $ \ws -> isLeft (mkPrivateKeyFromBytes (BS.pack ws)),+          testCase "mkPrivateKeyFromString applies the same checks to Base58 input" $ do+            let raw = getSolanaPrivateKeyRaw (snd fixtureKeypair)+                corrupted = BS.take 32 raw <> BS.replicate 32 9+            mkPrivateKeyFromString (toBase58String raw) @?= Right (snd fixtureKeypair)+            assertBool+              "accepted a Base58 key whose public half is not derived from its seed"+              (isLeft (mkPrivateKeyFromString (toBase58String corrupted)))+        ],+      testGroup+        "readSigningKeyFromFile"+        [ testCase "loads a solana-keygen keypair file" $ do+            sk <- readSigningKeyFromFile "test/fixtures/keypair.json"+            sk @?= snd fixtureKeypair+            show (toSolanaPublicKey sk) @?= "AKnL4NNf3DGWZJS6cPknBuEGnVsV4A4m5tgebLHaRSZ9"+            dverify (toSolanaPublicKey sk) "solana-haskell-sdk" (dsign sk "solana-haskell-sdk") @?= True,+          testCase "rejects a truncated key file" $ do+            r <- try (readSigningKeyFromFile "test/fixtures/keypair_truncated.json")+            case r of+              Left (_ :: IOException) -> pure ()+              Right _ -> assertFailure "accepted a 1-byte key file",+          testCase "rejects out-of-range byte values" $ do+            r <- try (readSigningKeyFromFile "test/fixtures/keypair_out_of_range.json")+            case r of+              Left (_ :: IOException) -> pure ()+              Right _ -> assertFailure "accepted a key file with an entry of 256",+          -- A JSON parser's syntax error quotes the unparsed remainder of its+          -- input; for a key file that would put secret bytes into an exception+          -- applications routinely log.+          testCase "does not echo the file's contents in the error for a malformed file" $ do+            r <- try (readSigningKeyFromFile "test/fixtures/keypair_syntax_error.json")+            case r of+              Left (e :: IOException) -> do+                assertBool ("unexpected message: " <> show e) ("not a JSON array of 64 integers" `isInfixOf` show e)+                assertBool+                  ("key bytes leaked into: " <> show e)+                  (not ("1,1,1" `isInfixOf` show e) && not ("138" `isInfixOf` show e))+              Right _ -> assertFailure "accepted a malformed key file"+        ],+      testGroup+        "raw constructors"+        [ testCase "unsafeSolanaPrivateKeyRaw calls error on a wrong-length input" $ do+            r <- try (evaluate (unsafeSolanaPrivateKeyRaw [1]))+            case r of+              Left (_ :: ErrorCall) -> pure ()+              Right _ -> assertFailure "built a 1-byte private key",+          testCase "unsafeSolanaPublicKeyRaw calls error on a wrong-length input" $ do+            r <- try (evaluate (unsafeSolanaPublicKeyRaw (replicate 33 0)))+            case r of+              Left (_ :: ErrorCall) -> pure ()+              Right _ -> assertFailure "built a 33-byte public key",+          testCase "unsafeSolanaPrivateKey calls error on a mismatched public-key half" $ do+            let raw = getSolanaPrivateKeyRaw (snd fixtureKeypair)+                corrupted = toBase58String (BS.take 32 raw <> BS.replicate 32 9)+            r <- try (evaluate (unsafeSolanaPrivateKey corrupted))+            case r of+              Left (_ :: ErrorCall) -> pure ()+              Right _ -> assertFailure "built a private key whose public half is not derived from its seed"+        ]     ]
test/Test/Core/Instruction.hs view
@@ -3,14 +3,20 @@ module Test.Core.Instruction (tests) where  import Data.Aeson (eitherDecode)+import Data.Binary (encode)+import Data.ByteString.Lazy qualified as BL+import Data.Either (isRight)+import Data.List (isInfixOf) import Network.Solana.Core.Crypto (SolanaPublicKey, unsafeSolanaPublicKeyRaw) import Network.Solana.Core.Instruction import Test.Tasty import Test.Tasty.HUnit+import Test.Tasty.QuickCheck -progPk, accPk :: SolanaPublicKey+progPk, accPk, fillerKey :: SolanaPublicKey progPk = unsafeSolanaPublicKeyRaw (replicate 32 11) accPk = unsafeSolanaPublicKeyRaw (replicate 32 12)+fillerKey = unsafeSolanaPublicKeyRaw (replicate 32 50)  tests :: TestTree tests =@@ -24,5 +30,28 @@       testCase "CompiledInstruction FromJSON rejects invalid base58 data" $         case eitherDecode "{\"programIdIndex\":0,\"accounts\":[0,1],\"data\":\"0OIl\"}" :: Either String CompiledInstruction of           Left _ -> pure ()-          Right _ -> assertFailure "expected parse failure on invalid base58"+          Right _ -> assertFailure "expected parse failure on invalid base58",+      testCase "compileInstruction rejects an account at index 256 instead of wrapping it" $ do+        let keys = replicate 256 fillerKey <> [accPk]+            ix = mkInstruction fillerKey [AccountMeta accPk False False] ()+        case compileInstruction keys ix of+          Left (MissingIndex msg) -> assertBool ("expected \"overflow\" in: " <> msg) ("overflow" `isInfixOf` msg)+          Right _ -> assertFailure "compiled an account index of 256",+      testCase "compileInstruction rejects a program id at index 256" $ do+        let keys = replicate 256 fillerKey <> [progPk]+            ix = mkInstruction progPk [AccountMeta fillerKey False False] ()+        case compileInstruction keys ix of+          Left (MissingIndex msg) -> assertBool ("expected \"overflow\" in: " <> msg) ("overflow" `isInfixOf` msg)+          Right _ -> assertFailure "compiled a program id index of 256",+      testCase "compileInstruction keeps index 255 (boundary)" $ do+        let keys = replicate 255 fillerKey <> [accPk]+            ix = mkInstruction fillerKey [AccountMeta accPk False False] ()+        case compileInstruction keys ix of+          Left err -> assertFailure (show err)+          Right ci -> BL.unpack (encode ci) @?= [0, 1, 255, 0],+      testProperty "compileInstruction succeeds exactly when the key position fits in a byte" $+        forAll (chooseInt (0, 600)) $ \i ->+          let keys = replicate i fillerKey <> [accPk]+              ix = mkInstruction accPk [AccountMeta accPk False False] ()+           in isRight (compileInstruction keys ix) === (i <= 255)     ]
test/Test/Core/Message.hs view
@@ -103,6 +103,24 @@             counterexample "writable key in readonly section" (property (not (any writableOf (sr <> ur))))           ] +-- | 'mkNewMessageWithPayer' must pin the named payer to account 0 as a+-- writable signer whatever the instructions declare (or fail to declare)+-- about it.+prop_payerInvariants :: Property+prop_payerInvariants =+  forAll (resize 5 (listOf1 genInstruction)) $ \instrs ->+    forAll (elements keyPool) $ \payer ->+      let msg = mkNewMessageWithPayer payer fixedBlockhash instrs+          hdr = mHeader msg+          keys = mAccountKeys msg+       in conjoin+            [ counterexample "payer is not account 0" (take 1 keys === [payer]),+              counterexample+                "no writable signer"+                (property (numReadonlySignedAccounts hdr < numRequiredSignatures hdr)),+              counterexample "duplicate keys" (keys === nub keys)+            ]+ tests :: TestTree tests =   testGroup@@ -233,5 +251,49 @@         case newDurableNonceTransactionIntentWithPayer sponsorPk [sponsorPriv, authorityPriv] noncePk authorityPk [] durableNonceBh of           Left err -> assertFailure (show err)           Right b64 -> BS.head (fromBase64String b64) @?= 2,-      testProperty "message header/key-ordering invariants" prop_messageInvariants+      testProperty "message header/key-ordering invariants" prop_messageInvariants,+      testCase "memo-only transaction via explicit fee payer matches Rust" $ do+        fs <- loadFixtures "test/fixtures/transactions.json"+        let ix = Memo.buildMemo "hello-memo" [fst payerKeys]+        case newTransactionIntentWithPayer (fst payerKeys) [snd payerKeys] [ix] fixedBlockhash of+          Left err -> assertFailure (show err)+          Right b64 -> b64 @?= toBase64String (requireFixture "memo-only-transaction" fs),+      testCase "memo-only message: explicit fee payer seeds a writable signer, instruction-derived payer does not" $ do+        let ix = Memo.buildMemo "hello-memo" [fst payerKeys]+        mHeader (mkNewMessageWithPayer (fst payerKeys) fixedBlockhash [ix]) @?= MessageHeader 1 0 1+        mHeader (mkNewMessage fixedBlockhash [ix]) @?= MessageHeader 1 1 1,+      testProperty "explicit fee payer is account 0 and a writable signer" prop_payerInvariants,+      testCase "orderSigningKeys: permuted keys are ordered to the message's required signers" $ do+        let ix = SP.createAccount (fst payerKeys) (fst newAccountKeys) 1000000 165 ownerPk+            msg = mkNewMessageWithPayer (fst newAccountKeys) fixedBlockhash [ix]+        case orderSigningKeys msg [snd payerKeys, snd newAccountKeys] of+          Left err -> assertFailure (show err)+          Right ordered -> map toSolanaPublicKey ordered @?= take 2 (mAccountKeys msg),+      testCase "orderSigningKeys: missing and unused keys are reported" $ do+        let ix = SP.createAccount (fst payerKeys) (fst newAccountKeys) 1000000 165 ownerPk+            msg = mkNewMessageWithPayer (fst newAccountKeys) fixedBlockhash [ix]+        case orderSigningKeys msg [snd payerKeys] of+          Right _ -> assertFailure "expected Left for missing signer"+          Left (MissingIndex msg') ->+            assertBool ("expected \"missing signer\" in: " <> msg') ("missing signer" `isInfixOf` msg')+        case orderSigningKeys msg [snd payerKeys, snd newAccountKeys, snd extraKeys] of+          Right _ -> assertFailure "expected Left for unused signing key"+          Left (MissingIndex msg') ->+            assertBool ("expected \"unused signing key\" in: " <> msg') ("unused signing key" `isInfixOf` msg'),+      testCase "a message with more than 256 account keys fails to compile instead of wrapping indices" $ do+        let metas = [AccountMeta (unsafeSolanaPublicKeyRaw (replicate 31 7 <> [b])) False False | b <- [0 .. 254]]+            ix = mkInstruction Memo.memoProgramId (AccountMeta (fst payerKeys) True True : metas) ()+        case newMessage fixedBlockhash [ix] of+          Left (MissingIndex msg) -> assertBool ("expected \"overflow\" in: " <> msg) ("overflow" `isInfixOf` msg)+          Right _ -> assertFailure "compiled a message with 257 account keys"+        case newTransactionIntentWithPayer (fst payerKeys) [snd payerKeys] [ix] fixedBlockhash of+          Left (MissingIndex msg) -> assertBool ("expected \"overflow\" in: " <> msg) ("overflow" `isInfixOf` msg)+          Right _ -> assertFailure "signed a message with 257 account keys",+      testCase "a message with exactly 256 account keys compiles and its header does not wrap" $ do+        let metas = [AccountMeta (unsafeSolanaPublicKeyRaw (replicate 31 7 <> [b])) False False | b <- [0 .. 253]]+            ix = mkInstruction Memo.memoProgramId (AccountMeta (fst payerKeys) True True : metas) ()+        numReadonlyUnsignedAccounts (mHeader (mkNewMessage fixedBlockhash [ix])) @?= 255+        case newMessage fixedBlockhash [ix] of+          Left err -> assertFailure (show err)+          Right _ -> pure ()     ]
test/Test/Core/VersionedMessage.hs view
@@ -10,6 +10,7 @@ import Network.Solana.Core.VersionedMessage import Network.Solana.NativePrograms.AddressLookupTable qualified as ALT import Network.Solana.NativePrograms.SystemProgram qualified as SP+import Network.Solana.SplPrograms.Memo qualified as Memo import Test.Fixtures import Test.Tasty import Test.Tasty.HUnit@@ -23,9 +24,24 @@     Just kp -> kp     Nothing -> error "failed to derive payer keypair from seed" +newAccountKeys :: (SolanaPublicKey, SolanaPrivateKey)+newAccountKeys =+  case createSolanaKeypairFromSeed (BS.replicate 32 10) of+    Just kp -> kp+    Nothing -> error "failed to derive new-account keypair from seed"++extraKeys :: (SolanaPublicKey, SolanaPrivateKey)+extraKeys =+  case createSolanaKeypairFromSeed (BS.replicate 32 40) of+    Just kp -> kp+    Nothing -> error "failed to derive extra keypair from seed"+ recipientPk :: SolanaPublicKey recipientPk = unsafeSolanaPublicKeyRaw (replicate 32 2) +ownerPk :: SolanaPublicKey+ownerPk = unsafeSolanaPublicKeyRaw (replicate 32 5)+ addr29 :: SolanaPublicKey addr29 = unsafeSolanaPublicKeyRaw (replicate 32 29) @@ -137,8 +153,71 @@                   ]             case compileV0Message fixedBlockhash ixs [table] of               Left err -> assertFailure (show err)-              Right bytes -> bytes @?= requireFixture "v0-message" txFs+              Right bytes -> bytes @?= requireFixture "v0-message" txFs,+      testCase "v0 signed transaction via explicit fee payer matches Rust" $ do+        fs <- loadFixtures "test/fixtures/transactions.json"+        let ixs =+              [ SP.transfer (fst payerKeys) recipientPk 1000000000,+                SP.transfer (fst payerKeys) addr29 500000+              ]+        case newV0TransactionIntentWithPayer (fst payerKeys) [snd payerKeys] ixs [lookupTable] fixedBlockhash of+          Left err -> assertFailure (show err)+          Right b64 -> b64 @?= toBase64String (requireFixture "v0-transfer-transaction" fs),+      testCase "v0 memo-only transaction via explicit fee payer matches Rust" $ do+        fs <- loadFixtures "test/fixtures/transactions.json"+        let ix = Memo.buildMemo "hello-memo" [fst payerKeys]+        case newV0TransactionIntentWithPayer (fst payerKeys) [snd payerKeys] [ix] [] fixedBlockhash of+          Left err -> assertFailure (show err)+          Right b64 -> b64 @?= toBase64String (requireFixture "v0-memo-only-transaction" fs),+      testCase "v0 memo-only message: explicit fee payer seeds a writable signer, compileV0Message does not" $ do+        let ix = Memo.buildMemo "hello-memo" [fst payerKeys]+        case compileV0MessageWithPayer (fst payerKeys) fixedBlockhash [ix] [] of+          Left err -> assertFailure (show err)+          Right bytes -> BS.take 4 bytes @?= BS.pack [0x80, 1, 0, 1]+        case compileV0Message fixedBlockhash [ix] [] of+          Left err -> assertFailure (show err)+          Right bytes -> BS.take 4 bytes @?= BS.pack [0x80, 1, 1, 1],+      testCase "v0 explicit fee payer: signers may be given in any order" $ do+        let ix = SP.createAccount (fst payerKeys) (fst newAccountKeys) 1000000 165 ownerPk+            sign keys = newV0TransactionIntentWithPayer (fst payerKeys) keys [ix] [] fixedBlockhash+        case (sign [snd newAccountKeys, snd payerKeys], sign [snd payerKeys, snd newAccountKeys]) of+          (Right p, Right o) -> p @?= o+          (Left err, _) -> assertFailure (show err)+          (_, Left err) -> assertFailure (show err),+      testCase "v0 explicit fee payer: missing and unused signing keys are reported" $ do+        let ix = SP.createAccount (fst payerKeys) (fst newAccountKeys) 1000000 165 ownerPk+        case newV0TransactionIntentWithPayer (fst payerKeys) [snd payerKeys] [ix] [] fixedBlockhash of+          Right _ -> assertFailure "expected Left for missing signer"+          Left (MissingIndex msg) ->+            assertBool ("expected \"missing signer\" in: " <> msg) ("missing signer" `isInfixOf` msg)+        let tooManyKeys = [snd payerKeys, snd newAccountKeys, snd extraKeys]+        case newV0TransactionIntentWithPayer (fst payerKeys) tooManyKeys [ix] [] fixedBlockhash of+          Right _ -> assertFailure "expected Left for unused signing key"+          Left (MissingIndex msg) ->+            assertBool ("expected \"unused signing key\" in: " <> msg) ("unused signing key" `isInfixOf` msg),+      testCase "static and table-loaded keys totalling more than 256 fail to compile" $ do+        -- Each table stays under 256 addresses (the per-table check passes);+        -- 2 static keys + 300 loaded keys exceed what a byte index can address.+        let tableA = AddressLookupTableAccount tableKey [numberedKey b | b <- [0 .. 199]]+            tableB = AddressLookupTableAccount otherTableKey [numberedKey b | b <- [200 .. 299]]+            metas = [AccountMeta (numberedKey b) False True | b <- [0 .. 299]]+            ix = mkInstruction dummyProgramId (AccountMeta (fst payerKeys) True True : metas) ()+        case compileV0Message fixedBlockhash [ix] [tableA, tableB] of+          Left (MissingIndex msg) ->+            assertBool ("expected \"account index overflow\" in: " <> msg) ("account index overflow" `isInfixOf` msg)+          Right _ -> assertFailure "compiled a v0 message resolving more than 256 keys",+      testCase "static plus table-loaded keys totalling exactly 256 compile" $ do+        let table = AddressLookupTableAccount tableKey [numberedKey b | b <- [0 .. 253]]+            metas = [AccountMeta (numberedKey b) False True | b <- [0 .. 253]]+            ix = mkInstruction dummyProgramId (AccountMeta (fst payerKeys) True True : metas) ()+        case compileV0Message fixedBlockhash [ix] [table] of+          Left err -> assertFailure (show err)+          Right _ -> pure ()     ]++-- | A distinct 32-byte key per number (up to 65535), for large key sets.+numberedKey :: Int -> SolanaPublicKey+numberedKey b = unsafeSolanaPublicKeyRaw (2 : fromIntegral (b `div` 256) : fromIntegral (b `mod` 256) : replicate 29 0)  -- | Compare two compile results by their message bytes ('CompileException' -- has no 'Eq' instance to compare), failing loudly if either side failed.
test/Test/Metaplex/TokenMetadata.hs view
@@ -16,9 +16,6 @@ import Network.Solana.Core.Crypto (SolanaPublicKey, createSolanaKeypairFromSeed, getSolanaPublicKeyRaw, unsafeSolanaPublicKeyRaw) import Network.Solana.Core.Instruction (AccountMeta (..), iAccounts) import Network.Solana.Metaplex.TokenMetadata qualified as TM-import Network.Solana.NativePrograms.SystemProgram qualified as SystemProgram-import Network.Solana.SplPrograms.Token qualified as Token-import Network.Solana.Sysvar qualified as Sysvar import Test.Fixtures import Test.Tasty import Test.Tasty.HUnit@@ -66,8 +63,9 @@     }  -- metadata_instruction_data.json mixes two shapes in one array: six--- {name, hex} entries (the golden vectors) and one {name, accounts} entry--- (create-v3-full-accounts). Test.Fixtures.loadFixtures decodes the whole+-- {name, hex} entries (the golden vectors) and two {name, accounts} entries+-- (create-v3-full-accounts, master-edition-some-accounts).+-- Test.Fixtures.loadFixtures decodes the whole -- file as [Fixture], which requires every element to carry a "hex" field, so -- it can't be used directly here. Load the file as [Value] instead and pick -- out each shape locally, without extending the shared loader.@@ -79,8 +77,8 @@   vals <- either fail pure =<< eitherDecodeFileStrict path   pure (mapMaybe (parseMaybe parseJSON) (vals :: [Value])) --- Local decode of the `create-v3-full-accounts` entry (ground truth for the--- createMetadataAccountV3 meta test).+-- Local decode of the {name, accounts} entries (ground truth for the+-- builder meta tests).  data FixtureAccountMeta = FixtureAccountMeta T.Text Bool Bool @@ -97,12 +95,20 @@   parseJSON = withObject "AccountsFixture" $ \v ->     AccountsFixture <$> v .: "name" <*> v .: "accounts" -loadCreateV3FullAccounts :: FilePath -> IO [AccountMeta]-loadCreateV3FullAccounts path = do+-- | The {name, accounts} entries of the given fixture file; elements of the+-- other shape are skipped.+loadAccountsFixtures :: FilePath -> IO [AccountsFixture]+loadAccountsFixtures path = do   vals <- either fail pure =<< eitherDecodeFileStrict path-  case find ((== "create-v3-full-accounts") . afName) (mapMaybe (parseMaybe parseJSON) (vals :: [Value])) of-    Just af -> pure (map toAccountMeta (afAccounts af))-    Nothing -> fail "create-v3-full-accounts entry not found"+  pure (mapMaybe (parseMaybe parseJSON) (vals :: [Value]))++-- | The account metas of the named {name, accounts} entry; calls 'error' if+-- it is missing (mirroring 'requireFixture').+requireAccounts :: String -> [AccountsFixture] -> [AccountMeta]+requireAccounts name afs =+  case find ((== name) . afName) afs of+    Just af -> map toAccountMeta (afAccounts af)+    Nothing -> error ("accounts fixture not found: " <> name)   where     toAccountMeta (FixtureAccountMeta hexPubkey signer writable) =       AccountMeta@@ -110,7 +116,8 @@           isSigner = signer,           isWritable = writable         }-    decodeHex hexPubkey = either (error . ("bad hex in create-v3-full-accounts: " <>)) id (B16.decode (TE.encodeUtf8 hexPubkey))+    decodeHex hexPubkey =+      either (error . (("bad hex in " <> name <> ": ") <>)) id (B16.decode (TE.encodeUtf8 hexPubkey))  -- Small QuickCheck generators covering every support type and instruction -- variant, for the round-trip property below.@@ -165,11 +172,14 @@ enc :: TM.TokenMetadataInstruction -> BS.ByteString enc = BL.toStrict . encode +accountsFixturePath :: FilePath+accountsFixturePath = "test/fixtures/metadata_instruction_data.json"+ tests :: TestTree tests =   withResource (loadDataFixtures "test/fixtures/metadata_instruction_data.json") (const (pure ())) $ \getFixtures ->     withResource (loadFixtures "test/fixtures/pda.json") (const (pure ())) $ \getPdaFixtures ->-      withResource (loadCreateV3FullAccounts "test/fixtures/metadata_instruction_data.json") (const (pure ())) $ \getCreateV3Accounts ->+      withResource (loadAccountsFixtures accountsFixturePath) (const (pure ())) $ \getAccountsFixtures ->         withResource (loadFixtures "test/fixtures/state_fixtures.json") (const (pure ())) $ \getStateFixtures ->           testGroup             "Metaplex Token Metadata"@@ -266,7 +276,7 @@               testGroup                 "builder metas"                 [ testCase "createMetadataAccountV3 matches create-v3-full-accounts fixture" $ do-                    expected <- getCreateV3Accounts+                    expected <- requireAccounts "create-v3-full-accounts" <$> getAccountsFixtures                     iAccounts (TM.createMetadataAccountV3 mintPk payerPk payerPk payerPk True dataV2Full True (Just (TM.CollectionDetailsV1 0)))                       @?= expected,                   testCase "updateMetadataAccountV2 metas" $@@ -277,21 +287,9 @@                           @?= [ AccountMeta {accountPubKey = metadataAddr, isSigner = False, isWritable = True},                                 AccountMeta {accountPubKey = payerPk, isSigner = True, isWritable = False}                               ],-                  testCase "createMasterEditionV3 metas" $-                    case (TM.deriveMasterEditionAddress mintPk, TM.deriveMetadataAddress mintPk) of-                      (Just editionAddr, Just metadataAddr) ->-                        iAccounts (TM.createMasterEditionV3 mintPk payerPk payerPk payerPk (Just 100))-                          @?= [ AccountMeta {accountPubKey = editionAddr, isSigner = False, isWritable = True},-                                AccountMeta {accountPubKey = mintPk, isSigner = False, isWritable = True},-                                AccountMeta {accountPubKey = payerPk, isSigner = True, isWritable = False},-                                AccountMeta {accountPubKey = payerPk, isSigner = True, isWritable = False},-                                AccountMeta {accountPubKey = payerPk, isSigner = True, isWritable = True},-                                AccountMeta {accountPubKey = metadataAddr, isSigner = False, isWritable = False},-                                AccountMeta {accountPubKey = Token.tokenProgramId, isSigner = False, isWritable = False},-                                AccountMeta {accountPubKey = SystemProgram.systemProgramId, isSigner = False, isWritable = False},-                                AccountMeta {accountPubKey = Sysvar.rent, isSigner = False, isWritable = False}-                              ]-                      _ -> assertFailure "PDA derivation failed"+                  testCase "createMasterEditionV3 matches master-edition-some-accounts fixture" $ do+                    expected <- requireAccounts "master-edition-some-accounts" <$> getAccountsFixtures+                    iAccounts (TM.createMasterEditionV3 mintPk payerPk payerPk payerPk (Just 100)) @?= expected                 ]             ]   where
test/Test/RPC/Chain.hs view
@@ -1,11 +1,17 @@+{-# LANGUAGE OverloadedStrings #-}+ module Test.RPC.Chain (tests) where -import Network.Solana.RPC.HTTP.Chain (percentilePriorityFee)+import Data.Aeson (eitherDecode)+import Network.Solana.RPC.HTTP.Chain (PerformanceSample (..), SolanaVersion (..), percentilePriorityFee) import Test.Tasty import Test.Tasty.HUnit  tests :: TestTree-tests =+tests = testGroup "Network.Solana.RPC.HTTP.Chain" [percentileTests, sampleTests, versionTests]++percentileTests :: TestTree+percentileTests =   testGroup     "percentilePriorityFee"     [ testCase "empty samples -> 0" $ percentilePriorityFee 0.5 [] @?= 0,@@ -17,4 +23,40 @@       testCase "negative p clamps to 0" $ percentilePriorityFee (-1) [9] @?= 9,       testCase "p above 1 clamps to 1" $ percentilePriorityFee 2 [1, 9] @?= 9,       testCase "zeros filtered before ranking" $ percentilePriorityFee 0.5 [0, 5, 0, 1] @?= 1+    ]++-- | Upstream @RpcPerfSample.num_non_vote_transactions@ is an @Option@: a+-- current node reports @null@ for samples recorded before it started+-- tracking the field.+sampleTests :: TestTree+sampleTests =+  testGroup+    "PerformanceSample"+    [ testCase "numNonVoteTransactions omitted parses as Nothing" $+        case eitherDecode (sample "") of+          Left err -> assertFailure err+          Right s -> numNonVoteTransactions s @?= Nothing,+      testCase "numNonVoteTransactions null parses as Nothing" $+        case eitherDecode (sample ",\"numNonVoteTransactions\":null") of+          Left err -> assertFailure err+          Right s -> numNonVoteTransactions s @?= Nothing,+      testCase "numNonVoteTransactions present is kept" $+        case eitherDecode (sample ",\"numNonVoteTransactions\":4") of+          Left err -> assertFailure err+          Right s -> numNonVoteTransactions s @?= Just 4+    ]+  where+    -- A sample's mandatory fields, plus whatever tail the case under test needs.+    sample tail' = "{\"slot\":1,\"numTransactions\":2,\"numSlots\":3,\"samplePeriodSecs\":60" <> tail' <> "}"++-- | Upstream @RpcVersionInfo.feature_set@ is an @Option@ as well.+versionTests :: TestTree+versionTests =+  testGroup+    "SolanaVersion"+    [ testCase "feature-set omitted parses as Nothing" $+        eitherDecode "{\"solana-core\":\"2.1.16\"}" @?= Right (SolanaVersion "2.1.16" Nothing),+      testCase "feature-set present is kept" $+        eitherDecode "{\"solana-core\":\"2.1.16\",\"feature-set\":3271415109}"+          @?= Right (SolanaVersion "2.1.16" (Just 3271415109))     ]
test/Test/RPC/Parsers.hs view
@@ -19,8 +19,17 @@  import Data.Aeson (FromJSON, Value) import Data.Aeson.Types (parseEither, parseJSON)+import Data.Map.Strict qualified as M import Data.Maybe (isJust)-import Network.Solana.Core.Account (Account, AccountInfo, Lamport (..), executable, lamports)+import Network.Solana.Core.Account+  ( Account,+    AccountData (..),+    AccountInfo,+    Lamport (..),+    dataField,+    executable,+    lamports,+  ) import Network.Solana.Core.Block (BlockHash, BlockHeight) import Network.Solana.Core.Crypto (SolanaPublicKey) import Network.Solana.RPC.HTTP.Account hiding (lamports)@@ -32,6 +41,7 @@ import Network.Solana.RPC.HTTP.Tokenomics import Network.Solana.RPC.HTTP.Transaction import Network.Solana.RPC.HTTP.Types+import Network.Solana.SplPrograms.Token (decodeMint, mDecimals, mSupply) import Test.Fixtures import Test.Tasty import Test.Tasty.HUnit@@ -79,7 +89,14 @@     testCase "getHealth" $ withResult @String "getHealth" $ \health ->       health @?= "ok",     testCase "getRecentPerformanceSamples" $ withResult @[PerformanceSample] "getRecentPerformanceSamples" $ \samples ->-      assertBool "samples cover a non-zero period" (all ((> 0) . samplePeriodSecs) samples)+      assertBool "samples cover a non-zero period" (all ((> 0) . samplePeriodSecs) samples),+    testCase "getHighestSnapshotSlot" $ withResult @HighestSnapshotSlot "getHighestSnapshotSlot" $ \s ->+      assertBool "a full snapshot exists" (full s > 0),+    testCase "getRecentPrioritizationFees" $ withResult @[PrioritizationFee] "getRecentPrioritizationFees" $ \fees ->+      assertBool "recent slots are sampled" (not (null fees) && length fees <= 150),+    testCase "getRecentPrioritizationFees (address filter)" $+      withResult @[PrioritizationFee] "getRecentPrioritizationFees-filtered" $ \fees ->+        assertBool "recent slots are sampled" (not (null fees))   ]  ledgerTests :: [TestTree]@@ -99,7 +116,24 @@     testCase "getFirstAvailableBlock" $ withResult @Slot "getFirstAvailableBlock" $ \_ ->       pure (),     testCase "getStakeMinimumDelegation" $ withResult @(RPCResponse Lamport) "getStakeMinimumDelegation" $ \r ->-      assertBool "context slot is populated" (contextSlot (context r) > 0)+      assertBool "context slot is populated" (contextSlot (context r) > 0),+    -- A single-validator cluster: the schedule names exactly the node itself.+    testCase "getLeaderSchedule" $ withResult @NodeIdentity "getIdentity" $ \node ->+      withResult @(Maybe LeaderSchedule) "getLeaderSchedule" $ \case+        Nothing -> assertFailure "expected a schedule for the current epoch"+        Just schedule -> do+          M.keys schedule @?= [identity node]+          assertBool "every leader has slots" (all (not . null) (M.elems schedule)),+    testCase "getSlotLeader" $ withResult @SolanaPublicKey "getSlotLeader" $ \_ ->+      pure (),+    testCase "getSlotLeaders" $ withResult @[SolanaPublicKey] "getSlotLeaders" $ \leaders ->+      length leaders @?= 3, -- the recorder asks for three+    testCase "getMaxRetransmitSlot" $ withResult @Slot "getMaxRetransmitSlot" $ \_ ->+      pure (),+    testCase "getMaxShredInsertSlot" $ withResult @Slot "getMaxShredInsertSlot" $ \_ ->+      pure (),+    testCase "minimumLedgerSlot" $ withResult @Slot "minimumLedgerSlot" $ \_ ->+      pure ()   ]  blockTests :: [TestTree]@@ -119,7 +153,11 @@       case value r of         BlockProduction identities _ -> assertBool "at least one leader produced blocks" (not (null identities)),     testCase "getBlocks" $ withResult @[Slot] "getBlocks" $ \slots ->-      assertBool "the queried range is non-empty" (not (null slots))+      assertBool "the queried range is non-empty" (not (null slots)),+    testCase "getBlocksWithLimit" $ withResult @[Slot] "getBlocksWithLimit" $ \slots ->+      assertBool "at most the requested limit of three" (not (null slots) && length slots <= 3),+    testCase "getBlockTime" $ withResult @(Maybe Int) "getBlockTime" $ \t ->+      assertBool "the block carries a timestamp" (maybe False (> 0) t)   ]  tokenomicsTests :: [TestTree]@@ -133,7 +171,13 @@     testCase "getVoteAccounts" $ withResult @VoteAccounts "getVoteAccounts" $ \accounts ->       assertBool "the test validator votes" (not (null (current accounts))),     testCase "getMinimumBalanceForRentExemption" $ withResult @Lamport "getMinimumBalanceForRentExemption" $ \rent ->-      assertBool "rent exemption for 165 bytes is non-trivial" (rent > Lamport 1000000)+      assertBool "rent exemption for 165 bytes is non-trivial" (rent > Lamport 1000000),+    -- The bootstrap stake account's reward for the previous (32-slot) epoch.+    testCase "getInflationReward" $ withResult @[Maybe InflationReward] "getInflationReward" $ \case+      [Just r] -> do+        assertBool "the bootstrap stake earned a reward" (amountReward r > Lamport 0)+        assertBool "post balance includes the reward" (postBalance r > amountReward r)+      other -> assertFailure ("expected exactly one reward, got " <> show (length other))   ]  accountTests :: [TestTree]@@ -155,7 +199,28 @@     testCase "getLargestAccounts" $ withResult @(RPCResponse [AddressAndLamports]) "getLargestAccounts" $ \r ->       assertBool "accounts are listed with balances" (not (null (value r)) && all ((> Lamport 0) . RpcAccount.lamports) (value r)),     testCase "getProgramAccounts" $ withResult @[Account] "getProgramAccounts" $ \accounts ->-      assertBool "the token program owns the mint and token account" (length accounts >= 2)+      assertBool "the token program owns the mint and token account" (length accounts >= 2),+    -- Recorded with no configuration: the node's default encoding is a bare+    -- base58 string (the recorder's 82-byte mint).+    testCase "getAccountInfo (node default encoding: bare base58)" $+      withResult @(RPCResponse (Maybe AccountInfo)) "getAccountInfo-base58" $ \r ->+        case dataField <$> value r of+          Just (AccountDataBinary bs) -> case decodeMint bs of+            Left e -> assertFailure e+            Right m -> do+              mDecimals m @?= 6+              mSupply m @?= 42000000+          other -> assertFailure ("expected binary mint data, got " <> show other),+    testCase "getAccountInfo (jsonParsed)" $+      withResult @(RPCResponse (Maybe AccountInfo)) "getAccountInfo-jsonParsed" $ \r ->+        case dataField <$> value r of+          Just (AccountDataJSON prog _ space) -> do+            prog @?= "spl-token"+            space @?= 165+          other -> assertFailure ("expected parsed token account data, got " <> show other),+    testCase "getAccountInfo (jsonParsed falls back to base64 without a parser)" $+      withResult @(RPCResponse (Maybe AccountInfo)) "getAccountInfo-jsonParsed-fallback" $ \r ->+        fmap dataField (value r) @?= Just (AccountDataBinary "")   ]  transactionTests :: [TestTree]@@ -172,7 +237,10 @@           confirmationStatusTxStatus s @?= Just "finalized"         other -> assertFailure ("expected exactly one status, got " <> show (length other)),     testCase "getTransaction" $ withResult @(Maybe TransactionResult) "getTransaction" $ \result ->-      assertBool "the recorded transfer is retrievable" (isJust result)+      assertBool "the recorded transfer is retrievable" (isJust result),+    -- One signature at the test validator's default fee.+    testCase "getFeeForMessage" $ withResult @(RPCResponse (Maybe Int)) "getFeeForMessage" $ \r ->+      value r @?= Just 5000   ]  tokenTests :: [TestTree]@@ -190,5 +258,8 @@         [] -> assertFailure "expected the minted account to be listed"         (a : _) -> amount' a @?= "42000000",     testCase "getTokenAccountsByOwner" $ withResult @(RPCResponse [Account]) "getTokenAccountsByOwner" $ \r ->+      length (value r) @?= 1,+    -- The recorder approves the recipient as delegate of the one token account.+    testCase "getTokenAccountsByDelegate" $ withResult @(RPCResponse [Account]) "getTokenAccountsByDelegate" $ \r ->       length (value r) @?= 1   ]
+ test/Test/RPC/Types.hs view
@@ -0,0 +1,35 @@+{-# LANGUAGE OverloadedStrings #-}++-- | Hand-written response shapes for "Network.Solana.RPC.HTTP.Types" that a+-- local validator never produces (older nodes and RPC proxies do), so they+-- cannot live in the recorded-fixture suite "Test.RPC.Parsers".+module Test.RPC.Types (tests) where++import Data.Aeson (eitherDecode)+import Network.Solana.RPC.HTTP.Types+import Test.Tasty+import Test.Tasty.HUnit++tests :: TestTree+tests =+  testGroup+    "RPC types"+    [ testCase "Context without apiVersion parses" $+        case eitherDecode "{\"slot\":41}" :: Either String Context of+          Left err -> assertFailure err+          Right ctx -> do+            apiVersion ctx @?= Nothing+            contextSlot ctx @?= 41,+      testCase "Context with apiVersion null parses as Nothing" $+        case eitherDecode "{\"apiVersion\":null,\"slot\":41}" :: Either String Context of+          Left err -> assertFailure err+          Right ctx -> apiVersion ctx @?= Nothing,+      testCase "Context keeps apiVersion when present" $+        case eitherDecode "{\"apiVersion\":\"2.1.16\",\"slot\":41}" :: Either String Context of+          Left err -> assertFailure err+          Right ctx -> apiVersion ctx @?= Just "2.1.16",+      testCase "RPCResponse without context.apiVersion still yields its value" $+        case eitherDecode "{\"context\":{\"slot\":41},\"value\":7}" :: Either String (RPCResponse Int) of+          Left err -> assertFailure err+          Right r -> value r @?= 7+    ]
test/Test/RPC/WebSocket.hs view
@@ -119,7 +119,13 @@     testCase "an unknown notification method is rejected rather than misread" $       assertBool         "expected a Left"-        (isLeft (parseWsMessage "{\"jsonrpc\":\"2.0\",\"method\":\"slotNotification\",\"params\":{\"result\":{\"slot\":1},\"subscription\":1}}"))+        (isLeft (parseWsMessage "{\"jsonrpc\":\"2.0\",\"method\":\"slotNotification\",\"params\":{\"result\":{\"slot\":1},\"subscription\":1}}")),+    -- The node always sends err (null on success); a value without it is+    -- malformed and must not read as a success.+    testCase "signature notification value without an err key is rejected" $+      assertBool+        "expected a Left"+        (isLeft (parseWsMessage "{\"jsonrpc\":\"2.0\",\"method\":\"signatureNotification\",\"params\":{\"result\":{\"context\":{\"slot\":5},\"value\":{}},\"subscription\":1}}"))   ]  -- | A 'WsTransport' that replays a fixed script of incoming frames and@@ -182,7 +188,17 @@     testCase "times out when the node never answers" $ do       (transport, _) <- scriptedTransport []       result <- awaitSignature transport (RequestId 1) Nothing 1 (unsafeSigFromString docSignature)-      assertBool ("expected a timeout, got " <> show result) (either ("timed out" `isInfixOf`) (const False) result)+      assertBool ("expected a timeout, got " <> show result) (either ("timed out" `isInfixOf`) (const False) result),+    testCase "does not report success for a notification lacking err" $ do+      (transport, _) <-+        scriptedTransport+          [ ack,+            "{\"jsonrpc\":\"2.0\",\"method\":\"signatureNotification\",\"params\":{\"result\":{\"context\":{\"slot\":5},\"value\":{}},\"subscription\":24006}}"+          ]+      result <- awaitSignature transport (RequestId 1) Nothing 5 (unsafeSigFromString docSignature)+      assertBool+        ("expected a parse failure naming the missing key, got " <> show result)+        (either (\e -> "err" `isInfixOf` e && not ("timed out" `isInfixOf` e)) (const False) result)   ]  isLeft :: Either a b -> Bool
+ test/Test/Readme.hs view
@@ -0,0 +1,44 @@+{-# LANGUAGE OverloadedStrings #-}++-- | Keeps the README's usage examples and the example executables under+-- @app/@ identical, so the text users copy is the text CI compiles.+module Test.Readme (tests) where++import Data.ByteString qualified as BS+import Data.Text (Text)+import Data.Text qualified as T+import Data.Text.Encoding qualified as TE+import Test.Tasty+import Test.Tasty.HUnit++readUtf8 :: FilePath -> IO Text+readUtf8 path = TE.decodeUtf8 <$> BS.readFile path++-- | The fenced Haskell blocks of the README that are complete programs (they+-- open with a LANGUAGE pragma), in order of appearance.+programBlocks :: Text -> [Text]+programBlocks = go . T.lines+  where+    go [] = []+    go (l : ls)+      | l == "```haskell" =+          let (body, rest) = break (== "```") ls+              others = go (drop 1 rest)+           in if isProgram body then T.unlines body : others else others+      | otherwise = go ls+    isProgram (first : _) = "{-# LANGUAGE" `T.isPrefixOf` first+    isProgram [] = False++-- | The example executables, in the order their sources appear in the README.+examplePrograms :: [FilePath]+examplePrograms = ["app/Main.hs", "app/SplTransfer.hs", "app/PriorityFee.hs"]++tests :: TestTree+tests =+  testGroup+    "README"+    [ testCase "usage examples are byte-identical to the compiled example programs" $ do+        blocks <- programBlocks <$> readUtf8 "README.md"+        programs <- mapM readUtf8 examplePrograms+        blocks @?= programs+    ]
+ test/Test/SolanaWeb3.hs view
@@ -0,0 +1,24 @@+{-# LANGUAGE OverloadedStrings #-}++-- | Pure checks on the "Network.Solana.SolanaWeb3" helpers' configuration+-- values (the helpers themselves need a node; see the integration suite).+module Test.SolanaWeb3 (tests) where++import Data.Aeson (Value (String), object, toJSON, (.=))+import Network.Solana.RPC.HTTP.Types (commitment, preflightCommitment)+import Network.Solana.SolanaWeb3 (cfgNonceAccountConfirmed, cfgNonceSendConfirmed)+import Test.Tasty+import Test.Tasty.HUnit++tests :: TestTree+tests =+  testGroup+    "SolanaWeb3"+    [ testCase "newNonceTransaction preflights at the commitment it reads the nonce at" $+        preflightCommitment cfgNonceSendConfirmed @?= commitment cfgNonceAccountConfirmed,+      testCase "nonce commitment is confirmed (finalized/finalized would silently drop advanced-nonce sends)" $+        commitment cfgNonceAccountConfirmed @?= Just "confirmed",+      testCase "nonce send config wire form keeps preflight on and pins it" $+        toJSON cfgNonceSendConfirmed+          @?= object ["encoding" .= String "base64", "preflightCommitment" .= String "confirmed"]+    ]
+ test/fixtures/keypair.json view
@@ -0,0 +1,1 @@+[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,138,136,227,221,116,9,241,149,253,82,219,45,60,186,93,114,202,103,9,191,29,148,18,27,243,116,136,1,180,15,111,92]
+ test/fixtures/keypair_out_of_range.json view
@@ -0,0 +1,1 @@+[256,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,138,136,227,221,116,9,241,149,253,82,219,45,60,186,93,114,202,103,9,191,29,148,18,27,243,116,136,1,180,15,111,92]
+ test/fixtures/keypair_syntax_error.json view
@@ -0,0 +1,1 @@+[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1;138,136,227,221,116,9,241,149,253,82,219,45,60,186,93,114,202,103,9,191,29,148,18,27,243,116,136,1,180,15,111,92]
+ test/fixtures/keypair_truncated.json view
@@ -0,0 +1,1 @@+[1]
test/fixtures/metadata_instruction_data.json view
@@ -57,5 +57,55 @@       }     ],     "name": "create-v3-full-accounts"+  },+  {+    "accounts": [+      {+        "pubkey": "a8b0140612ffc013e3300b768c9e666c6e1712d945bf6ca2cead99fbaa29c1e7",+        "signer": false,+        "writable": true+      },+      {+        "pubkey": "0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c",+        "signer": false,+        "writable": true+      },+      {+        "pubkey": "8a88e3dd7409f195fd52db2d3cba5d72ca6709bf1d94121bf3748801b40f6f5c",+        "signer": true,+        "writable": false+      },+      {+        "pubkey": "8a88e3dd7409f195fd52db2d3cba5d72ca6709bf1d94121bf3748801b40f6f5c",+        "signer": true,+        "writable": false+      },+      {+        "pubkey": "8a88e3dd7409f195fd52db2d3cba5d72ca6709bf1d94121bf3748801b40f6f5c",+        "signer": true,+        "writable": true+      },+      {+        "pubkey": "56a9c8b183e68c0582ef458b28a9f28894971e5aa21bd380aa210f6824f6ea89",+        "signer": false,+        "writable": true+      },+      {+        "pubkey": "06ddf6e1d765a193d9cbe146ceeb79ac1cb485ed5f5b37913a8cf5857eff00a9",+        "signer": false,+        "writable": false+      },+      {+        "pubkey": "0000000000000000000000000000000000000000000000000000000000000000",+        "signer": false,+        "writable": false+      },+      {+        "pubkey": "06a7d517192c5c51218cc94c3d4af17f58daee089ba1fd44e3dbd98a00000000",+        "signer": false,+        "writable": false+      }+    ],+    "name": "master-edition-some-accounts"   } ]
test/fixtures/rpc_responses.json view
@@ -17,1143 +17,2058 @@     "response": {       "jsonrpc": "2.0",       "result": {-        "identity": "EWEDURnEjTRSa5Uz99Kf3hdThUD8X2Rv7RvMFLSQ1DCm"-      },-      "id": 1-    }-  },-  {-    "name": "getClusterNodes",-    "method": "getClusterNodes",-    "response": {-      "jsonrpc": "2.0",-      "result": [-        {-          "featureSet": 3271415109,-          "gossip": "127.0.0.1:1024",-          "pubkey": "EWEDURnEjTRSa5Uz99Kf3hdThUD8X2Rv7RvMFLSQ1DCm",-          "pubsub": "127.0.0.1:8900",-          "rpc": "127.0.0.1:8899",-          "serveRepair": "127.0.0.1:1035",-          "shredVersion": 24374,-          "tpu": "127.0.0.1:1027",-          "tpuForwards": "127.0.0.1:1028",-          "tpuForwardsQuic": "127.0.0.1:1034",-          "tpuQuic": "127.0.0.1:1033",-          "tpuVote": "127.0.0.1:1029",-          "tvu": "127.0.0.1:1025",-          "version": "2.1.16"-        }-      ],-      "id": 1-    }-  },-  {-    "name": "getHealth",-    "method": "getHealth",-    "response": {-      "jsonrpc": "2.0",-      "result": "ok",-      "id": 1-    }-  },-  {-    "name": "getEpochInfo",-    "method": "getEpochInfo",-    "response": {-      "jsonrpc": "2.0",-      "result": {-        "absoluteSlot": 41,-        "blockHeight": 41,-        "epoch": 0,-        "slotIndex": 41,-        "slotsInEpoch": 432000,-        "transactionCount": 45-      },-      "id": 1-    }-  },-  {-    "name": "getEpochSchedule",-    "method": "getEpochSchedule",-    "response": {-      "jsonrpc": "2.0",-      "result": {-        "firstNormalEpoch": 0,-        "firstNormalSlot": 0,-        "leaderScheduleSlotOffset": 432000,-        "slotsPerEpoch": 432000,-        "warmup": false-      },-      "id": 1-    }-  },-  {-    "name": "getGenesisHash",-    "method": "getGenesisHash",-    "response": {-      "jsonrpc": "2.0",-      "result": "SDdtDuPR4GB5dZDmAwuQbYTwP4x3EZcsDN4QvwJQHe4",-      "id": 1-    }-  },-  {-    "name": "getSlot",-    "method": "getSlot",-    "response": {-      "jsonrpc": "2.0",-      "result": 41,-      "id": 1-    }-  },-  {-    "name": "getBlockHeight",-    "method": "getBlockHeight",-    "response": {-      "jsonrpc": "2.0",-      "result": 41,-      "id": 1-    }-  },-  {-    "name": "getTransactionCount",-    "method": "getTransactionCount",-    "response": {-      "jsonrpc": "2.0",-      "result": 45,-      "id": 1-    }-  },-  {-    "name": "getFirstAvailableBlock",-    "method": "getFirstAvailableBlock",-    "response": {-      "jsonrpc": "2.0",-      "result": 0,-      "id": 1-    }-  },-  {-    "name": "getRecentPerformanceSamples",-    "method": "getRecentPerformanceSamples",-    "response": {-      "jsonrpc": "2.0",-      "result": [],-      "id": 1-    },-    "params": [-      2-    ]-  },-  {-    "name": "getStakeMinimumDelegation",-    "method": "getStakeMinimumDelegation",-    "response": {-      "jsonrpc": "2.0",-      "result": {-        "context": {-          "apiVersion": "2.1.16",-          "slot": 41-        },-        "value": 1000000000-      },-      "id": 1-    }-  },-  {-    "name": "getLatestBlockhash",-    "method": "getLatestBlockhash",-    "response": {-      "jsonrpc": "2.0",-      "result": {-        "context": {-          "apiVersion": "2.1.16",-          "slot": 41-        },-        "value": {-          "blockhash": "FZjZju6aDpmcT8AhLTyvt7igyL4cwTRCUsyJNoinpQgS",-          "lastValidBlockHeight": 191-        }-      },-      "id": 1-    }-  },-  {-    "name": "isBlockhashValid",-    "method": "isBlockhashValid",-    "response": {-      "jsonrpc": "2.0",-      "result": {-        "context": {-          "apiVersion": "2.1.16",-          "slot": 72-        },-        "value": true-      },-      "id": 1-    },-    "params": [-      "FZjZju6aDpmcT8AhLTyvt7igyL4cwTRCUsyJNoinpQgS",-      {-        "commitment": "processed"-      }-    ]-  },-  {-    "name": "getBlock",-    "method": "getBlock",-    "response": {-      "jsonrpc": "2.0",-      "result": {-        "blockHeight": 5,-        "blockTime": 1785728550,-        "blockhash": "EXVev5y6y5FHypiG2c1QmQ7WR7jUeU1rwesbfTS8XML3",-        "parentSlot": 4,-        "previousBlockhash": "4phmBxWpU7uba3moNmwyDKbH1iADu4zsUUKHbXyVbqfx",-        "transactions": [-          {-            "meta": {-              "computeUnitsConsumed": 2100,-              "err": null,-              "fee": 10000,-              "innerInstructions": [],-              "loadedAddresses": {-                "readonly": [],-                "writable": []-              },-              "logMessages": [-                "Program Vote111111111111111111111111111111111111111 invoke [1]",-                "Program Vote111111111111111111111111111111111111111 success"-              ],-              "postBalances": [-                499999977500,-                1000000000000000,-                1-              ],-              "postTokenBalances": [],-              "preBalances": [-                499999987500,-                1000000000000000,-                1-              ],-              "preTokenBalances": [],-              "rewards": null,-              "status": {-                "Ok": null-              }-            },-            "transaction": {-              "message": {-                "accountKeys": [-                  "EWEDURnEjTRSa5Uz99Kf3hdThUD8X2Rv7RvMFLSQ1DCm",-                  "3nsvie2PSWMWitUgkKNzKqzaFEUhGahKW51HhR8ThqTc",-                  "Vote111111111111111111111111111111111111111"-                ],-                "header": {-                  "numReadonlySignedAccounts": 0,-                  "numReadonlyUnsignedAccounts": 1,-                  "numRequiredSignatures": 2-                },-                "instructions": [-                  {-                    "accounts": [-                      1,-                      1-                    ],-                    "data": "FMkqDfKmbTuLt2xkJs2QxzsZaVMEoSdQwbEzyF2LEjFHRAGq6Q1zFhNzyUNcBvC5DeMjm87vWA6u5RBkg5JD3XA8S9Hwq8fPEo4HS4KUfYx9viFQHaQa7xHLwfbvtP9H",-                    "programIdIndex": 2,-                    "stackHeight": null-                  }-                ],-                "recentBlockhash": "4phmBxWpU7uba3moNmwyDKbH1iADu4zsUUKHbXyVbqfx"-              },-              "signatures": [-                "4h86FydrrWzWxWnFpSBxFhbh8zbuyUcKSXUZa58Pv9Xx97oosuCdrZk5KB67QVhpDtHbwu8JoG3bx7iVW2XQagBm",-                "9QCQT8kz1t256TtJbG7iLMKwtagGVgxbY1BubfKjdxWD5RRzqpkozRKJ9zmLNMXyAFRd7nTtMqb1yhMpkRJmxme"-              ]-            },-            "version": "legacy"-          },-          {-            "meta": {-              "computeUnitsConsumed": 150,-              "err": null,-              "fee": 5000,-              "innerInstructions": [],-              "loadedAddresses": {-                "readonly": [],-                "writable": []-              },-              "logMessages": [-                "Program 11111111111111111111111111111111 invoke [1]",-                "Program 11111111111111111111111111111111 success"-              ],-              "postBalances": [-                8999995000,-                1000000000,-                1-              ],-              "postTokenBalances": [],-              "preBalances": [-                10000000000,-                0,-                1-              ],-              "preTokenBalances": [],-              "rewards": null,-              "status": {-                "Ok": null-              }-            },-            "transaction": {-              "message": {-                "accountKeys": [-                  "6o185vKXeS8D27dPfAKxJ2dNVD8iNjmRpFwdYYk3uSrT",-                  "HR7KwDBsCkkoe9VPCuZhtwgcJMUMLMgFJb6n9WuLqyop",-                  "11111111111111111111111111111111"-                ],-                "header": {-                  "numReadonlySignedAccounts": 0,-                  "numReadonlyUnsignedAccounts": 1,-                  "numRequiredSignatures": 1-                },-                "instructions": [-                  {-                    "accounts": [-                      0,-                      1-                    ],-                    "data": "3Bxs3zzLZLuLQEYX",-                    "programIdIndex": 2,-                    "stackHeight": null-                  }-                ],-                "recentBlockhash": "4phmBxWpU7uba3moNmwyDKbH1iADu4zsUUKHbXyVbqfx"-              },-              "signatures": [-                "4xzhXw7xxNCGfC74NKbZNEcr92zWdGviq4GJbksRxdXz2NGYFh8Jzyox5X7trorYyYvKEnPHkfNsAfCQA5KAhFRC"-              ]-            },-            "version": "legacy"-          }-        ]-      },-      "id": 1-    },-    "params": [-      5,-      {-        "encoding": "json",-        "maxSupportedTransactionVersion": 0,-        "transactionDetails": "full",-        "rewards": false-      }-    ]-  },-  {-    "name": "getBlockCommitment",-    "method": "getBlockCommitment",-    "response": {-      "jsonrpc": "2.0",-      "result": {-        "commitment": [-          0,-          0,-          0,-          0,-          0,-          0,-          0,-          0,-          0,-          0,-          0,-          0,-          0,-          0,-          0,-          0,-          0,-          0,-          0,-          0,-          0,-          0,-          0,-          0,-          0,-          0,-          0,-          0,-          0,-          0,-          0,-          999999997717120-        ],-        "totalStake": 999999997717120-      },-      "id": 1-    },-    "params": [-      5-    ]-  },-  {-    "name": "getBlockProduction",-    "method": "getBlockProduction",-    "response": {-      "jsonrpc": "2.0",-      "result": {-        "context": {-          "apiVersion": "2.1.16",-          "slot": 41-        },-        "value": {-          "byIdentity": {-            "EWEDURnEjTRSa5Uz99Kf3hdThUD8X2Rv7RvMFLSQ1DCm": [-              42,-              42-            ]-          },-          "range": {-            "firstSlot": 0,-            "lastSlot": 41-          }-        }-      },-      "id": 1-    }-  },-  {-    "name": "getBlocks",-    "method": "getBlocks",-    "response": {-      "jsonrpc": "2.0",-      "result": [-        3,-        4,-        5-      ],-      "id": 1-    },-    "params": [-      3,-      5-    ]-  },-  {-    "name": "getInflationGovernor",-    "method": "getInflationGovernor",-    "response": {-      "jsonrpc": "2.0",-      "result": {-        "foundation": 0.05,-        "foundationTerm": 7.0,-        "initial": 0.08,-        "taper": 0.15,-        "terminal": 0.015-      },-      "id": 1-    }-  },-  {-    "name": "getInflationRate",-    "method": "getInflationRate",-    "response": {-      "jsonrpc": "2.0",-      "result": {-        "epoch": 0,-        "foundation": 0.004,-        "total": 0.08,-        "validator": 0.076-      },-      "id": 1-    }-  },-  {-    "name": "getSupply",-    "method": "getSupply",-    "response": {-      "jsonrpc": "2.0",-      "result": {-        "context": {-          "apiVersion": "2.1.16",-          "slot": 41-        },-        "value": {-          "circulating": 503000508500862375,-          "nonCirculating": 0,-          "nonCirculatingAccounts": [-            "8W58E8JVJjH1jCy5CeHJQgvwFXTyAVyesuXRZGbcSUGG",-            "DrKzW5koKSZp4mg4BdHLwr72MMXscd2kTiWgckCvvPXz",-            "GmyW1nqYcrw7P7JqrcyP9ivU9hYNbrgZ1r5SYJJH41Fs",-            "HbZ5FfmKWNHC7uwk6TF1hVi6TCs7dtYfdjEcuPGgzFAg",-            "nGME7HgBT6tAJN1f6YuCCngpqT5cvSTndZUVLjQ4jwA",-            "2WWb1gRzuXDd5viZLQF7pNRR6Y7UiyeaPpaL35X6j3ve",-            "3ZrsTmNM6AkMcqFfv3ryfhQ2jMfqP64RQbqVyAaxqhrQ",-            "CzAHrrrHKx9Lxf6wdCMrsZkLvk74c7J2vGv8VYPUmY6v",-            "6o5v1HC7WhBnLfRHp8mQTtCP2khdXXjhuyGyYEoy2Suy",-            "5khMKAcvmsFaAhoKkdg3u5abvKsmjUQNmhTNP624WB1F",-            "4vuWt1oHRqLMhf8Nv1zyEXZsYaeK7dipwrfKLoYU9Riq",-            "EMhn1U3TMimW3bvWYbPUvN2eZnCfsuBN4LGWhzzYhiWR",-            "Ep5Y58PaSyALPrdFxDVAdfKtVdP55vApvsWjb3jSmXsG",-            "HQJtLqvEGGxgNYfRXUurfxV8E1swvCnsbC3456ik27HY",-            "5D5NxsNVTgXHyVziwV7mDFwVDS6voaBsyyGxUbhQrhNW",-            "GdnSyH3YtwcxFvQrVVJMm1JhTS4QVX7MFsX56uJLUfiZ",-            "EMAY24PrS6rWfvpqffFCsTsFJypeeYYmtUc26wdh3Wup",-            "P8aKfWQPeRnsZtpBrwWTYzyAoRk74KMz56xc6NEpC4J",-            "GpxpMVhrBBBEYbEJxdR62w3daWz444V7m6dxYDZKH77D",-            "8rT45mqpuDBR1vcnDc9kwP9DrZAXDR4ZeuKWw3u1gTGa",-            "AsrYX4FeLXnZcrjcZmrASY2Eq1jvEeQfwxtNTxS5zojA",-            "14FUT96s9swbmH7ZjpDvfEDywnAYy9zaNhv4xvezySGu",-            "8pNBEppa1VcFAsx4Hzq9CpdXUXZjUXbvQwLX2K7QsCwb",-            "CsUqV42gVQLJwQsKyjWHqGkfHarxn9hcY4YeSjgaaeTd",-            "9S2M3UYPpnPZTBtbcUvehYmiWFK3kBhwfzV2iWuwvaVy",-            "Ab1UcdsFXZVnkSt1Z3vcYU65GQk5MvCbs54SviaiaqHb",-            "CY7X5o3Wi2eQhTocLmUS6JSWyx1NinBfW7AXRrkRCpi8",-            "GK8R4uUmrawcREZ5xJy5dAzVV5V7aFvYg77id37pVTK",-            "9xbcBZoGYFnfJZe81EDuDYKUm8xGkjzW8z4EgnVhNvsv",-            "HuqDWJodFhAEWh6aWdsDVUqsjRket5DYXMYyDYtD8hdN",-            "3itU5ME8L6FDqtMiRoUiT1F7PwbkTtHBbW51YWD5jtjm",-            "5PLJZLJiRR9vf7d1JCCg7UuWjtyN9nkab9uok6TqSyuP",-            "CUageMFi49kzoDqtdU8NvQ4Bq3sbtJygjKDAXJ45nmAi",-            "4pV47TiPzZ7SSBPHmgUvSLmH9mMSe8tjyPhQZGbi1zPC",-            "CVgyXrbEd1ctEuvq11QdpnCQVnPit8NLdhyqXQHLprM2",-            "5XdtyEDREHJXXW1CTtCsVjJRjBapAwK78ZquzvnNVRrV",-            "BsKsunvENxAraBrL77UfAn1Gi7unVEmQAdCbhsjUN6tU",-            "7Np41oeYqPefeNQEHSv1UDhYrehxin3NStELsSKCT4K2",-            "5q54XjQ7vDx4y6KphPeE97LUNiYGtP55spjvXAWPGBuf",-            "3fV2GaDKa3pZxyDcpMh5Vrh2FVAMUiWUKbYmnBFv8As3",-            "Dc2oHxFXQaC2QfLStuU7txtD3U5HZ82MrCSGDooWjbsv",-            "3iPvAS4xdhYr6SkhVDHCLr7tJjMAFK4wvvHWJxFQVg15",-            "Br3aeVGapRb2xTq17RU2pYZCoJpWA7bq6TKBCcYtMSmt",-            "6yKHERk8rsbmJxvMpPuwPs1ct3hRiP7xaJF2tvnGU6nK",-            "7Y8smnoUrYKGGuDq2uaFKVxJYhojgg7DVixHyAtGTYEV",-            "GvpCiTgq9dmEeojCDBivoLoZqc4AkbUDACpqPMwYLWKh",-            "CakcnaRDHka2gXyfbEd2d3xsvkJkqsLw2akB3zsN1D2S",-            "8vqrX3H2BYLaXVintse3gorPEM4TgTwTFZNN1Fm9TdYs",-            "3bTGcGB9F98XxnrBNftmmm48JGfPgi5sYxDEKiCjQYk3",-            "BUnRE27mYXN9p8H1Ay24GXhJC88q2CuwLoNU2v2CrW4W",-            "GEWSkfWgHkpiLbeKaAnwvqnECGdRNf49at5nFccVey7c",-            "8ndGYFjav6NDXvzYcxs449Aub3AxYv4vYpk89zRDwgj7",-            "CuatS6njAcfkFHnvai7zXCs7syA9bykXWsDCJEWfhjHG",-            "AG3m2bAibcY8raMt4oXEGqRHwX4FWKPPJVjZxn1LySDX",-            "GumSE5HsMV5HCwBTv2D2D81yy9x17aDkvobkqAfTRgmo",-            "FwfaykN7ACnsEUDHANzGHqTGQZMcGnUSsahAHUqbdPrz",-            "F9MWFw8cnYVwsRq8Am1PGfFL3cQUZV37mbGoxZftzLjN",-            "BUjkdqUuH5Lz9XzcMcR4DdEMnFG6r8QzUMBm16Rfau96",-            "C7C8odR8oashR5Feyrq2tJKaXL18id1dSj2zbkDGL2C2",-            "CHmdL15akDcJgBkY6BP3hzs98Dqr6wbdDC5p8odvtSbq",-            "FR84wZQy3Y3j2gWz6pgETUiUoJtreMEuWfbg6573UCj9",-            "8CUUMKYNGxdgYio5CLHRHyzMEhhVRMcqefgE6dLqnVRK",-            "AzHQ8Bia1grVVbcGyci7wzueSWkgvu7YZVZ4B9rkL5P6",-            "63DtkW7zuARcd185EmHAkfF44bDcC2SiTSEj2spLP3iA",-            "xQadXQiUTCCFhfHjvQx1hyJK6KVWr1w2fD6DT3cdwj7",-            "HUAkU5psJXZuw54Lrg1ksbXzHv2fzczQ9sNbmisVMeJU",-            "AVYpwVou2BhdLivAwLxKPALZQsY7aZNkNmGbP2fZw7RU",-            "CWeRmXme7LmbaUWTZWFLt6FMnpzLCHaQLuR2TdgFn4Lq",-            "Fg12tB1tz8w6zJSQ4ZAGotWoCztdMJF9hqK8R11pakog",-            "6nN69B4uZuESZYxr9nrLDjmKRtjDZQXrehwkfQTKw62U",-            "FiWYY85b58zEEcPtxe3PuqzWPjqBJXqdwgZeqSBmT9Cn",-            "HCV5dGFJXRrJ3jhDYA4DCeb9TEDTwGGYXtT3wHksu2Zr",-            "GNiz4Mq886bTNDT3pijGsu2gbw6it7sqrwncro45USeB",-            "GK2zqSsXLA2rwVZk347RYhh6jJpRsCA69FjLW93ZGi3B",-            "4sxwau4mdqZ8zEJsfryXq4QFYnMJSCp3HWuZQod8WU5k",-            "H3Ni7vG1CsmJZdTvxF7RkAf9UM5qk4RsohJsmPvtZNnu",-            "7cvkjYAkUYs4W8XcXsca7cBrEGFeSUjeZmKoNBvEwyri",-            "Hz9nydgN1k15wnwffKX7CSmZp4VFTnTwLXAEdomFGNXy",-            "H1rt8KvXkNhQExTRfkY8r9wjZbZ8yCih6J4wQ5Fz9HGP",-            "Es13uD2p64UVPFpEWfDtd6SERdoNR2XVgqBQBZcZSLqW",-            "E8jcgWvrvV7rwYHJThwfiBeQ8VAH4FgNEEMG9aAuCMAq",-            "AzVV9ZZDxTgW4wWfJmsG6ytaHpQGSe1yz76Nyy84VbQF",-            "Mc5XB47H3DKJHym5RLa9mPzWv5snERsF3KNv5AauXK8",-            "9huDUZfxoJ7wGMTffUE7vh1xePqef7gyrLJu9NApncqA",-            "CND6ZjRTzaCFVdX7pSSWgjTfHZuhxqFDoUBqWBJguNoA",-            "JCwT5Ygmq3VeBEbDjL8s8E82Ra2rP9bq45QfZE7Xyaq7",-            "7xJ9CLtEAcEShw9kW2gSoZkRWL566Dg12cvgzANJwbTr",-            "DE1bawNcRJB9rVm3buyMVfr8mBEoyyu73NBovf2oXJsJ",-            "EziVYi3Sv5kJWxmU77PnbrT8jmkVuqwdiFLLzZpLVEn7",-            "Eyr9P5XsjK2NUKNCnfu39eqpGoiLFgVAv1LSQgMZCwiQ",-            "CQDYc4ET2mbFhVpgj41gXahL6Exn5ZoPcGAzSHuYxwmE",-            "DQQGPtj7pphPHCLzzBuEyDDQByUcKGrsJdsH7SP3hAug",-            "3o6xgkJ9sTmDeQWyfj3sxwon18fXJB9PV5LDc8sfgR4a",-            "Hm9JW7of5i9dnrboS8pCUCSeoQUPh7JsP1rkbJnW7An4",-            "BhvLngiqqKeZ8rpxch2uGjeCiC88zzewoWPRuoxpp1aS",-            "BuCEvc9ze8UoAQwwsQLy8d447C8sA4zeVtVpc6m5wQeS",-            "GhsotwFMH6XUrRLJCxcx62h7748N2Uq8mf87hUGkmPhg",-            "8UVjvYyoqP6sqcctTso3xpCdCfgTMiv3VRh7vraC2eJk",-            "8DE8fqPfv1fp9DHyGyDFFaMjpopMgDeXspzoi9jpBJjC",-            "HKJgYGTTYYR2ZkfJKHbn58w676fKueQXmvbtpyvrSM3N",-            "3jnknRabs7G2V9dKhxd2KP85pNWXKXiedYnYxtySnQMs",-            "Fgyh8EeYGZtbW8sS33YmNQnzx54WXPrJ5KWNPkCfWPot",-            "5smrYwb1Hr2T8XMnvsqccTgXxuqQs14iuE8RbHFYf2Cf",-            "6zw7em7uQdmMpuS9fGz8Nq9TLHa5YQhEKKwPjo5PwDK4",-            "9hknftBZAQL4f48tWfk3bUEV5YSLcYYtDRqNmpNnhCWG",-            "DUS1KxwUhUyDKB4A81E8vdnTe3hSahd92Abtn9CXsEcj",-            "GLUmCeJpXB8veNcchPwibkRYwCwvQbKodex5mEjrgToi",-            "EAJJD6nDqtXcZ4DnQb19F9XEz8y8bRDHxbWbahatZNbL",-            "GpYnVDgB7dzvwSgsjQFeHznjG6Kt1DLBFYrKxjGU1LuD",-            "CTvhdUVf8KNyMbyEdnvRrBCHJjBKtQwkbj6zwoqcEssG"-          ],-          "total": 503000508500862375-        }-      },-      "id": 1-    }-  },-  {-    "name": "getVoteAccounts",-    "method": "getVoteAccounts",-    "response": {-      "jsonrpc": "2.0",-      "result": {-        "current": [-          {-            "activatedStake": 999999997717120,-            "commission": 0,-            "epochCredits": [-              [-                0,-                144,-                0-              ]-            ],-            "epochVoteAccount": true,-            "lastVote": 40,-            "nodePubkey": "EWEDURnEjTRSa5Uz99Kf3hdThUD8X2Rv7RvMFLSQ1DCm",-            "rootSlot": 9,-            "votePubkey": "3nsvie2PSWMWitUgkKNzKqzaFEUhGahKW51HhR8ThqTc"-          }-        ],-        "delinquent": []-      },-      "id": 1-    }-  },-  {-    "name": "getMinimumBalanceForRentExemption",-    "method": "getMinimumBalanceForRentExemption",-    "response": {-      "jsonrpc": "2.0",-      "result": 2039280,-      "id": 1-    },-    "params": [-      165-    ]-  },-  {-    "name": "getBalance",-    "method": "getBalance",-    "response": {-      "jsonrpc": "2.0",-      "result": {-        "context": {-          "apiVersion": "2.1.16",-          "slot": 41-        },-        "value": 8996474120-      },-      "id": 1-    },-    "params": [-      "6o185vKXeS8D27dPfAKxJ2dNVD8iNjmRpFwdYYk3uSrT"-    ]-  },-  {-    "name": "getAccountInfo",-    "method": "getAccountInfo",-    "response": {-      "jsonrpc": "2.0",-      "result": {-        "context": {-          "apiVersion": "2.1.16",-          "slot": 41-        },-        "value": {-          "data": [-            "",-            "base64"-          ],-          "executable": false,-          "lamports": 8996474120,-          "owner": "11111111111111111111111111111111",-          "rentEpoch": 18446744073709551615,-          "space": 0-        }-      },-      "id": 1-    },-    "params": [-      "6o185vKXeS8D27dPfAKxJ2dNVD8iNjmRpFwdYYk3uSrT",-      {-        "encoding": "base64"-      }-    ]-  },-  {-    "name": "getAccountInfo-missing",-    "method": "getAccountInfo",-    "response": {-      "jsonrpc": "2.0",-      "result": {-        "context": {-          "apiVersion": "2.1.16",-          "slot": 41-        },-        "value": null-      },-      "id": 1-    },-    "params": [-      "11111111111111111111111111111112",-      {-        "encoding": "base64"-      }-    ]-  },-  {-    "name": "getMultipleAccounts",-    "method": "getMultipleAccounts",-    "response": {-      "jsonrpc": "2.0",-      "result": {-        "context": {-          "apiVersion": "2.1.16",-          "slot": 41-        },-        "value": [-          {-            "data": [-              "",-              "base64"-            ],-            "executable": false,-            "lamports": 8996474120,-            "owner": "11111111111111111111111111111111",-            "rentEpoch": 18446744073709551615,-            "space": 0-          },-          {-            "data": [-              "",-              "base64"-            ],-            "executable": false,-            "lamports": 1000000000,-            "owner": "11111111111111111111111111111111",-            "rentEpoch": 18446744073709551615,-            "space": 0-          }-        ]-      },-      "id": 1-    },-    "params": [-      [-        "6o185vKXeS8D27dPfAKxJ2dNVD8iNjmRpFwdYYk3uSrT",-        "HR7KwDBsCkkoe9VPCuZhtwgcJMUMLMgFJb6n9WuLqyop"-      ],-      {-        "encoding": "base64"-      }-    ]-  },-  {-    "name": "getLargestAccounts",-    "method": "getLargestAccounts",-    "response": {-      "jsonrpc": "2.0",-      "result": {-        "context": {-          "apiVersion": "2.1.16",-          "slot": 41-        },-        "value": [-          {-            "address": "594C9C199Zp8fK2zvmXrSveE359gijrM6tsoZLYk9obv",-            "lamports": 500000000000000000-          },-          {-            "address": "BxqNA9vLoWDNiEBzZ4na79hBvUUke2jfTXiki4sfr5J4",-            "lamports": 1000000000000000-          },-          {-            "address": "3nsvie2PSWMWitUgkKNzKqzaFEUhGahKW51HhR8ThqTc",-            "lamports": 1000000000000000-          },-          {-            "address": "D758ERd8UUz83zLzxG2ykpTGSWog3HFjST7Bu1nd3bPm",-            "lamports": 999989999995000-          },-          {-            "address": "EWEDURnEjTRSa5Uz99Kf3hdThUD8X2Rv7RvMFLSQ1DCm",-            "lamports": 499999815000-          },-          {-            "address": "6o185vKXeS8D27dPfAKxJ2dNVD8iNjmRpFwdYYk3uSrT",-            "lamports": 8996474120-          },-          {-            "address": "DoU57AYuPFu2QU514RktNPG22QhApEjnKxnBcu4BHDTY",-            "lamports": 3773078640-          },-          {-            "address": "HR7KwDBsCkkoe9VPCuZhtwgcJMUMLMgFJb6n9WuLqyop",-            "lamports": 1000000000-          },-          {-            "address": "So11111111111111111111111111111111111111112",-            "lamports": 1000000000-          },-          {-            "address": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",-            "lamports": 929020800-          },-          {-            "address": "SysvarS1otHistory11111111111111111111111111",-            "lamports": 913326000-          },-          {-            "address": "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL",-            "lamports": 731913600-          },-          {-            "address": "MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr",-            "lamports": 521498880-          },-          {-            "address": "SysvarS1otHashes111111111111111111111111111",-            "lamports": 143487360-          },-          {-            "address": "Memo1UhkJRfHyvLMcVucJwxXeuD728EqVDDwQDxFMNo",-            "lamports": 119712000-          },-          {-            "address": "SysvarStakeHistory1111111111111111111111111",-            "lamports": 114979200-          },-          {-            "address": "SysvarRecentB1ockHashes11111111111111111111",-            "lamports": 42706560-          },-          {-            "address": "B2kGD5aSJLL8yHUUC8v7VWWyF6DRN6m2kpjhiPin53oe",-            "lamports": 2039280-          },-          {-            "address": "nsAeRzrkWAUJkGnTnnx1nhr3DokGui17CLUNuEwnoaw",-            "lamports": 1461600-          },-          {-            "address": "SysvarC1ock11111111111111111111111111111111",-            "lamports": 1169280-          }-        ]-      },-      "id": 1-    }-  },-  {-    "name": "getProgramAccounts",-    "method": "getProgramAccounts",-    "response": {-      "jsonrpc": "2.0",-      "result": [-        {-          "account": {-            "data": [-              "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAJAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==",-              "base64"-            ],-            "executable": false,-            "lamports": 1000000000,-            "owner": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",-            "rentEpoch": 1,-            "space": 82-          },-          "pubkey": "So11111111111111111111111111111111111111112"-        },-        {-          "account": {-            "data": [-              "AQAAAFYTSjtFgV37DhAKCpVZ7UaTRdOtkDEDLd/1PcZokXcYgN6AAgAAAAAGAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==",-              "base64"-            ],-            "executable": false,-            "lamports": 1461600,-            "owner": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",-            "rentEpoch": 18446744073709551615,-            "space": 82-          },-          "pubkey": "nsAeRzrkWAUJkGnTnnx1nhr3DokGui17CLUNuEwnoaw"-        },-        {-          "account": {-            "data": [-              "C7/cEE+JL6u+L/IR/d1bk+Y8tafJlr26pB6MKVNffjBWE0o7RYFd+w4QCgqVWe1Gk0XTrZAxAy3f9T3GaJF3GIDegAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",-              "base64"-            ],-            "executable": false,-            "lamports": 2039280,-            "owner": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",-            "rentEpoch": 18446744073709551615,-            "space": 165-          },-          "pubkey": "B2kGD5aSJLL8yHUUC8v7VWWyF6DRN6m2kpjhiPin53oe"-        }-      ],-      "id": 1-    },-    "params": [-      "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",-      {-        "encoding": "base64"-      }-    ]-  },-  {-    "name": "getSignaturesForAddress",-    "method": "getSignaturesForAddress",-    "response": {-      "jsonrpc": "2.0",-      "result": [-        {-          "blockTime": 1785728550,-          "confirmationStatus": "finalized",-          "err": null,-          "memo": null,-          "signature": "4xzhXw7xxNCGfC74NKbZNEcr92zWdGviq4GJbksRxdXz2NGYFh8Jzyox5X7trorYyYvKEnPHkfNsAfCQA5KAhFRC",-          "slot": 5-        }-      ],-      "id": 1-    },-    "params": [-      "HR7KwDBsCkkoe9VPCuZhtwgcJMUMLMgFJb6n9WuLqyop",-      {-        "limit": 3-      }-    ]-  },-  {-    "name": "getSignatureStatuses",-    "method": "getSignatureStatuses",-    "response": {-      "jsonrpc": "2.0",-      "result": {-        "context": {-          "apiVersion": "2.1.16",-          "slot": 72-        },-        "value": [-          {-            "confirmationStatus": "finalized",-            "confirmations": null,-            "err": null,-            "slot": 5,-            "status": {-              "Ok": null-            }-          }-        ]-      },-      "id": 1-    },-    "params": [-      [-        "4xzhXw7xxNCGfC74NKbZNEcr92zWdGviq4GJbksRxdXz2NGYFh8Jzyox5X7trorYyYvKEnPHkfNsAfCQA5KAhFRC"-      ],-      {-        "searchTransactionHistory": true-      }-    ]-  },-  {-    "name": "getTransaction",-    "method": "getTransaction",-    "response": {-      "jsonrpc": "2.0",-      "result": {-        "blockTime": 1785728550,-        "meta": {-          "computeUnitsConsumed": 150,-          "err": null,-          "fee": 5000,-          "innerInstructions": [],-          "loadedAddresses": {-            "readonly": [],-            "writable": []-          },-          "logMessages": [-            "Program 11111111111111111111111111111111 invoke [1]",-            "Program 11111111111111111111111111111111 success"-          ],-          "postBalances": [-            8999995000,-            1000000000,-            1-          ],-          "postTokenBalances": [],-          "preBalances": [-            10000000000,-            0,-            1-          ],-          "preTokenBalances": [],-          "rewards": [],-          "status": {-            "Ok": null-          }-        },-        "slot": 5,-        "transaction": {-          "message": {-            "accountKeys": [-              "6o185vKXeS8D27dPfAKxJ2dNVD8iNjmRpFwdYYk3uSrT",-              "HR7KwDBsCkkoe9VPCuZhtwgcJMUMLMgFJb6n9WuLqyop",-              "11111111111111111111111111111111"-            ],-            "header": {-              "numReadonlySignedAccounts": 0,-              "numReadonlyUnsignedAccounts": 1,-              "numRequiredSignatures": 1-            },-            "instructions": [-              {-                "accounts": [-                  0,-                  1-                ],-                "data": "3Bxs3zzLZLuLQEYX",-                "programIdIndex": 2,-                "stackHeight": null-              }-            ],-            "recentBlockhash": "4phmBxWpU7uba3moNmwyDKbH1iADu4zsUUKHbXyVbqfx"-          },-          "signatures": [-            "4xzhXw7xxNCGfC74NKbZNEcr92zWdGviq4GJbksRxdXz2NGYFh8Jzyox5X7trorYyYvKEnPHkfNsAfCQA5KAhFRC"-          ]-        },-        "version": "legacy"-      },-      "id": 1-    },-    "params": [-      "4xzhXw7xxNCGfC74NKbZNEcr92zWdGviq4GJbksRxdXz2NGYFh8Jzyox5X7trorYyYvKEnPHkfNsAfCQA5KAhFRC",-      {-        "encoding": "json",-        "maxSupportedTransactionVersion": 0-      }-    ]-  },-  {-    "name": "getTokenSupply",-    "method": "getTokenSupply",-    "response": {-      "jsonrpc": "2.0",-      "result": {-        "context": {-          "apiVersion": "2.1.16",-          "slot": 41-        },-        "value": {-          "amount": "42000000",-          "decimals": 6,-          "uiAmount": 42.0,-          "uiAmountString": "42"-        }-      },-      "id": 1-    },-    "params": [-      "nsAeRzrkWAUJkGnTnnx1nhr3DokGui17CLUNuEwnoaw"-    ]-  },-  {-    "name": "getTokenAccountBalance",-    "method": "getTokenAccountBalance",-    "response": {-      "jsonrpc": "2.0",-      "result": {-        "context": {-          "apiVersion": "2.1.16",-          "slot": 41-        },-        "value": {-          "amount": "42000000",-          "decimals": 6,-          "uiAmount": 42.0,-          "uiAmountString": "42"-        }-      },-      "id": 1-    },-    "params": [-      "B2kGD5aSJLL8yHUUC8v7VWWyF6DRN6m2kpjhiPin53oe"-    ]-  },-  {-    "name": "getTokenLargestAccounts",-    "method": "getTokenLargestAccounts",-    "response": {-      "jsonrpc": "2.0",-      "result": {-        "context": {-          "apiVersion": "2.1.16",-          "slot": 41-        },-        "value": [-          {-            "address": "B2kGD5aSJLL8yHUUC8v7VWWyF6DRN6m2kpjhiPin53oe",-            "amount": "42000000",-            "decimals": 6,-            "uiAmount": 42.0,-            "uiAmountString": "42"-          }-        ]-      },-      "id": 1-    },-    "params": [-      "nsAeRzrkWAUJkGnTnnx1nhr3DokGui17CLUNuEwnoaw"-    ]-  },-  {-    "name": "getTokenAccountsByOwner",-    "method": "getTokenAccountsByOwner",-    "response": {-      "jsonrpc": "2.0",-      "result": {-        "context": {-          "apiVersion": "2.1.16",-          "slot": 41-        },-        "value": [-          {-            "account": {-              "data": [-                "C7/cEE+JL6u+L/IR/d1bk+Y8tafJlr26pB6MKVNffjBWE0o7RYFd+w4QCgqVWe1Gk0XTrZAxAy3f9T3GaJF3GIDegAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",-                "base64"-              ],-              "executable": false,-              "lamports": 2039280,-              "owner": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",-              "rentEpoch": 18446744073709551615,-              "space": 165-            },-            "pubkey": "B2kGD5aSJLL8yHUUC8v7VWWyF6DRN6m2kpjhiPin53oe"-          }-        ]-      },-      "id": 1-    },-    "params": [-      "6o185vKXeS8D27dPfAKxJ2dNVD8iNjmRpFwdYYk3uSrT",-      {-        "mint": "nsAeRzrkWAUJkGnTnnx1nhr3DokGui17CLUNuEwnoaw"+        "identity": "8XmVUUfAbS6kPE9EYK3Pt9JbitW1LYcez4GaEwJhWy13"+      },+      "id": 1+    }+  },+  {+    "name": "getClusterNodes",+    "method": "getClusterNodes",+    "response": {+      "jsonrpc": "2.0",+      "result": [+        {+          "featureSet": 3271415109,+          "gossip": "127.0.0.1:1024",+          "pubkey": "8XmVUUfAbS6kPE9EYK3Pt9JbitW1LYcez4GaEwJhWy13",+          "pubsub": "127.0.0.1:8900",+          "rpc": "127.0.0.1:8899",+          "serveRepair": "127.0.0.1:1035",+          "shredVersion": 21032,+          "tpu": "127.0.0.1:1027",+          "tpuForwards": "127.0.0.1:1028",+          "tpuForwardsQuic": "127.0.0.1:1034",+          "tpuQuic": "127.0.0.1:1033",+          "tpuVote": "127.0.0.1:1029",+          "tvu": "127.0.0.1:1025",+          "version": "2.1.16"+        }+      ],+      "id": 1+    }+  },+  {+    "name": "getHealth",+    "method": "getHealth",+    "response": {+      "jsonrpc": "2.0",+      "result": "ok",+      "id": 1+    }+  },+  {+    "name": "getHighestSnapshotSlot",+    "method": "getHighestSnapshotSlot",+    "response": {+      "jsonrpc": "2.0",+      "result": {+        "full": 1100,+        "incremental": null+      },+      "id": 1+    }+  },+  {+    "name": "getRecentPrioritizationFees",+    "method": "getRecentPrioritizationFees",+    "response": {+      "jsonrpc": "2.0",+      "result": [+        {+          "prioritizationFee": 0,+          "slot": 82+        },+        {+          "prioritizationFee": 0,+          "slot": 94+        },+        {+          "prioritizationFee": 0,+          "slot": 116+        },+        {+          "prioritizationFee": 0,+          "slot": 126+        },+        {+          "prioritizationFee": 0,+          "slot": 148+        },+        {+          "prioritizationFee": 0,+          "slot": 161+        },+        {+          "prioritizationFee": 0,+          "slot": 180+        },+        {+          "prioritizationFee": 10000,+          "slot": 193+        },+        {+          "prioritizationFee": 0,+          "slot": 212+        },+        {+          "prioritizationFee": 0,+          "slot": 228+        },+        {+          "prioritizationFee": 0,+          "slot": 244+        },+        {+          "prioritizationFee": 0,+          "slot": 260+        },+        {+          "prioritizationFee": 0,+          "slot": 277+        },+        {+          "prioritizationFee": 0,+          "slot": 293+        },+        {+          "prioritizationFee": 0,+          "slot": 309+        },+        {+          "prioritizationFee": 0,+          "slot": 341+        },+        {+          "prioritizationFee": 0,+          "slot": 373+        },+        {+          "prioritizationFee": 0,+          "slot": 375+        },+        {+          "prioritizationFee": 0,+          "slot": 378+        },+        {+          "prioritizationFee": 0,+          "slot": 380+        },+        {+          "prioritizationFee": 0,+          "slot": 382+        },+        {+          "prioritizationFee": 0,+          "slot": 414+        },+        {+          "prioritizationFee": 0,+          "slot": 446+        },+        {+          "prioritizationFee": 0,+          "slot": 478+        },+        {+          "prioritizationFee": 0,+          "slot": 511+        },+        {+          "prioritizationFee": 0,+          "slot": 545+        },+        {+          "prioritizationFee": 0,+          "slot": 577+        },+        {+          "prioritizationFee": 0,+          "slot": 610+        },+        {+          "prioritizationFee": 0,+          "slot": 644+        },+        {+          "prioritizationFee": 0,+          "slot": 646+        },+        {+          "prioritizationFee": 0,+          "slot": 648+        },+        {+          "prioritizationFee": 0,+          "slot": 680+        },+        {+          "prioritizationFee": 1000,+          "slot": 713+        },+        {+          "prioritizationFee": 0,+          "slot": 745+        },+        {+          "prioritizationFee": 10000,+          "slot": 777+        },+        {+          "prioritizationFee": 0,+          "slot": 809+        },+        {+          "prioritizationFee": 0,+          "slot": 842+        },+        {+          "prioritizationFee": 0,+          "slot": 844+        },+        {+          "prioritizationFee": 0,+          "slot": 881+        },+        {+          "prioritizationFee": 0,+          "slot": 914+        },+        {+          "prioritizationFee": 0,+          "slot": 946+        },+        {+          "prioritizationFee": 0,+          "slot": 978+        },+        {+          "prioritizationFee": 0,+          "slot": 1010+        },+        {+          "prioritizationFee": 0,+          "slot": 1138+        },+        {+          "prioritizationFee": 0,+          "slot": 1139+        },+        {+          "prioritizationFee": 0,+          "slot": 1141+        },+        {+          "prioritizationFee": 0,+          "slot": 1143+        },+        {+          "prioritizationFee": 0,+          "slot": 1176+        },+        {+          "prioritizationFee": 0,+          "slot": 1177+        }+      ],+      "id": 1+    }+  },+  {+    "name": "getRecentPrioritizationFees-filtered",+    "method": "getRecentPrioritizationFees",+    "response": {+      "jsonrpc": "2.0",+      "result": [+        {+          "prioritizationFee": 0,+          "slot": 82+        },+        {+          "prioritizationFee": 0,+          "slot": 94+        },+        {+          "prioritizationFee": 0,+          "slot": 116+        },+        {+          "prioritizationFee": 0,+          "slot": 126+        },+        {+          "prioritizationFee": 0,+          "slot": 148+        },+        {+          "prioritizationFee": 0,+          "slot": 161+        },+        {+          "prioritizationFee": 0,+          "slot": 180+        },+        {+          "prioritizationFee": 10000,+          "slot": 193+        },+        {+          "prioritizationFee": 0,+          "slot": 212+        },+        {+          "prioritizationFee": 0,+          "slot": 228+        },+        {+          "prioritizationFee": 0,+          "slot": 244+        },+        {+          "prioritizationFee": 0,+          "slot": 260+        },+        {+          "prioritizationFee": 0,+          "slot": 277+        },+        {+          "prioritizationFee": 0,+          "slot": 293+        },+        {+          "prioritizationFee": 0,+          "slot": 309+        },+        {+          "prioritizationFee": 0,+          "slot": 341+        },+        {+          "prioritizationFee": 0,+          "slot": 373+        },+        {+          "prioritizationFee": 0,+          "slot": 375+        },+        {+          "prioritizationFee": 0,+          "slot": 378+        },+        {+          "prioritizationFee": 0,+          "slot": 380+        },+        {+          "prioritizationFee": 0,+          "slot": 382+        },+        {+          "prioritizationFee": 0,+          "slot": 414+        },+        {+          "prioritizationFee": 0,+          "slot": 446+        },+        {+          "prioritizationFee": 0,+          "slot": 478+        },+        {+          "prioritizationFee": 0,+          "slot": 511+        },+        {+          "prioritizationFee": 0,+          "slot": 545+        },+        {+          "prioritizationFee": 0,+          "slot": 577+        },+        {+          "prioritizationFee": 0,+          "slot": 610+        },+        {+          "prioritizationFee": 0,+          "slot": 644+        },+        {+          "prioritizationFee": 0,+          "slot": 646+        },+        {+          "prioritizationFee": 0,+          "slot": 648+        },+        {+          "prioritizationFee": 0,+          "slot": 680+        },+        {+          "prioritizationFee": 1000,+          "slot": 713+        },+        {+          "prioritizationFee": 0,+          "slot": 745+        },+        {+          "prioritizationFee": 10000,+          "slot": 777+        },+        {+          "prioritizationFee": 0,+          "slot": 809+        },+        {+          "prioritizationFee": 0,+          "slot": 842+        },+        {+          "prioritizationFee": 0,+          "slot": 844+        },+        {+          "prioritizationFee": 0,+          "slot": 881+        },+        {+          "prioritizationFee": 0,+          "slot": 914+        },+        {+          "prioritizationFee": 0,+          "slot": 946+        },+        {+          "prioritizationFee": 0,+          "slot": 978+        },+        {+          "prioritizationFee": 0,+          "slot": 1010+        },+        {+          "prioritizationFee": 0,+          "slot": 1138+        },+        {+          "prioritizationFee": 0,+          "slot": 1139+        },+        {+          "prioritizationFee": 0,+          "slot": 1141+        },+        {+          "prioritizationFee": 0,+          "slot": 1143+        },+        {+          "prioritizationFee": 0,+          "slot": 1176+        },+        {+          "prioritizationFee": 0,+          "slot": 1177+        }+      ],+      "id": 1+    },+    "params": [+      [+        "9cjs2tCQQft8cR1bDXrRYerzkw1kS2Z2VvrMDhmzUnpU"+      ]+    ]+  },+  {+    "name": "getEpochInfo",+    "method": "getEpochInfo",+    "response": {+      "jsonrpc": "2.0",+      "result": {+        "absoluteSlot": 1179,+        "blockHeight": 1179,+        "epoch": 36,+        "slotIndex": 27,+        "slotsInEpoch": 32,+        "transactionCount": 1229+      },+      "id": 1+    }+  },+  {+    "name": "getEpochSchedule",+    "method": "getEpochSchedule",+    "response": {+      "jsonrpc": "2.0",+      "result": {+        "firstNormalEpoch": 0,+        "firstNormalSlot": 0,+        "leaderScheduleSlotOffset": 32,+        "slotsPerEpoch": 32,+        "warmup": false+      },+      "id": 1+    }+  },+  {+    "name": "getGenesisHash",+    "method": "getGenesisHash",+    "response": {+      "jsonrpc": "2.0",+      "result": "AShwgvZDRwHrzE3fEfQaDVzuADQxmX2w2pWZzViPiLkr",+      "id": 1+    }+  },+  {+    "name": "getSlot",+    "method": "getSlot",+    "response": {+      "jsonrpc": "2.0",+      "result": 1179,+      "id": 1+    }+  },+  {+    "name": "getBlockHeight",+    "method": "getBlockHeight",+    "response": {+      "jsonrpc": "2.0",+      "result": 1179,+      "id": 1+    }+  },+  {+    "name": "getTransactionCount",+    "method": "getTransactionCount",+    "response": {+      "jsonrpc": "2.0",+      "result": 1229,+      "id": 1+    }+  },+  {+    "name": "getFirstAvailableBlock",+    "method": "getFirstAvailableBlock",+    "response": {+      "jsonrpc": "2.0",+      "result": 0,+      "id": 1+    }+  },+  {+    "name": "getRecentPerformanceSamples",+    "method": "getRecentPerformanceSamples",+    "response": {+      "jsonrpc": "2.0",+      "result": [+        {+          "numNonVoteTransactions": 1,+          "numSlots": 129,+          "numTransactions": 130,+          "samplePeriodSecs": 60,+          "slot": 1160+        },+        {+          "numNonVoteTransactions": 4,+          "numSlots": 128,+          "numTransactions": 132,+          "samplePeriodSecs": 60,+          "slot": 1031+        }+      ],+      "id": 1+    },+    "params": [+      2+    ]+  },+  {+    "name": "getStakeMinimumDelegation",+    "method": "getStakeMinimumDelegation",+    "response": {+      "jsonrpc": "2.0",+      "result": {+        "context": {+          "apiVersion": "2.1.16",+          "slot": 1179+        },+        "value": 1000000000+      },+      "id": 1+    }+  },+  {+    "name": "getLeaderSchedule",+    "method": "getLeaderSchedule",+    "response": {+      "jsonrpc": "2.0",+      "result": {+        "8XmVUUfAbS6kPE9EYK3Pt9JbitW1LYcez4GaEwJhWy13": [+          0,+          1,+          2,+          3,+          4,+          5,+          6,+          7,+          8,+          9,+          10,+          11,+          12,+          13,+          14,+          15,+          16,+          17,+          18,+          19,+          20,+          21,+          22,+          23,+          24,+          25,+          26,+          27,+          28,+          29,+          30,+          31+        ]+      },+      "id": 1+    }+  },+  {+    "name": "getSlotLeader",+    "method": "getSlotLeader",+    "response": {+      "jsonrpc": "2.0",+      "result": "8XmVUUfAbS6kPE9EYK3Pt9JbitW1LYcez4GaEwJhWy13",+      "id": 1+    }+  },+  {+    "name": "getSlotLeaders",+    "method": "getSlotLeaders",+    "response": {+      "jsonrpc": "2.0",+      "result": [+        "8XmVUUfAbS6kPE9EYK3Pt9JbitW1LYcez4GaEwJhWy13",+        "8XmVUUfAbS6kPE9EYK3Pt9JbitW1LYcez4GaEwJhWy13",+        "8XmVUUfAbS6kPE9EYK3Pt9JbitW1LYcez4GaEwJhWy13"+      ],+      "id": 1+    },+    "params": [+      1139,+      3+    ]+  },+  {+    "name": "getMaxRetransmitSlot",+    "method": "getMaxRetransmitSlot",+    "response": {+      "jsonrpc": "2.0",+      "result": 0,+      "id": 1+    }+  },+  {+    "name": "getMaxShredInsertSlot",+    "method": "getMaxShredInsertSlot",+    "response": {+      "jsonrpc": "2.0",+      "result": 0,+      "id": 1+    }+  },+  {+    "name": "minimumLedgerSlot",+    "method": "minimumLedgerSlot",+    "response": {+      "jsonrpc": "2.0",+      "result": 0,+      "id": 1+    }+  },+  {+    "name": "getLatestBlockhash",+    "method": "getLatestBlockhash",+    "response": {+      "jsonrpc": "2.0",+      "result": {+        "context": {+          "apiVersion": "2.1.16",+          "slot": 1179+        },+        "value": {+          "blockhash": "9T2jk2eWMKXFxfzXDikbSBZVTyoDT3jX8wanugptwD2C",+          "lastValidBlockHeight": 1329+        }+      },+      "id": 1+    }+  },+  {+    "name": "isBlockhashValid",+    "method": "isBlockhashValid",+    "response": {+      "jsonrpc": "2.0",+      "result": {+        "context": {+          "apiVersion": "2.1.16",+          "slot": 1210+        },+        "value": true+      },+      "id": 1+    },+    "params": [+      "Dsw1cH6sgkBV86MjpDU7JiBzwxpu6aJuKBYZ2esXgV28",+      {+        "commitment": "processed"+      }+    ]+  },+  {+    "name": "getBlock",+    "method": "getBlock",+    "response": {+      "jsonrpc": "2.0",+      "result": {+        "blockHeight": 1139,+        "blockTime": 1789304691,+        "blockhash": "9ABjGUtGC2FDYzUDYyQvCZ9UyS7oTuXt66ur19zSFYeb",+        "parentSlot": 1138,+        "previousBlockhash": "GVHCpiV2HaJBYYxpxTvaA7sHiD9BKfmAc8zPQWUvz37C",+        "transactions": [+          {+            "meta": {+              "computeUnitsConsumed": 2100,+              "err": null,+              "fee": 10000,+              "innerInstructions": [],+              "loadedAddresses": {+                "readonly": [],+                "writable": []+              },+              "logMessages": [+                "Program Vote111111111111111111111111111111111111111 invoke [1]",+                "Program Vote111111111111111111111111111111111111111 success"+              ],+              "postBalances": [+                499994441520,+                1000000000000000,+                1+              ],+              "postTokenBalances": [],+              "preBalances": [+                499994451520,+                1000000000000000,+                1+              ],+              "preTokenBalances": [],+              "rewards": null,+              "status": {+                "Ok": null+              }+            },+            "transaction": {+              "message": {+                "accountKeys": [+                  "8XmVUUfAbS6kPE9EYK3Pt9JbitW1LYcez4GaEwJhWy13",+                  "6PE42VjbVgozfFAEu8qMXQgvST4spCnw37FBfeYCrL16",+                  "Vote111111111111111111111111111111111111111"+                ],+                "header": {+                  "numReadonlySignedAccounts": 0,+                  "numReadonlyUnsignedAccounts": 1,+                  "numRequiredSignatures": 2+                },+                "instructions": [+                  {+                    "accounts": [+                      1,+                      1+                    ],+                    "data": "67MGn4nXadYkNgbgzrF38aJ9SAiTjzMaSDcDx5WSFsQSgrMCY3FtcxBw8sSJ6qUmiaAqsAUFVreXshpdo75uwtHcM3rtmfKGSRanHwDCrFjsVCXSVM9WqDcd2S7MxDj48mXXsid3DiBmSNxLjFSxeCLAMm84mNFRzzSxCgLTaGPp2o5XxZSX4TLc5svDhhJ5Wfy3YqBb8s",+                    "programIdIndex": 2,+                    "stackHeight": null+                  }+                ],+                "recentBlockhash": "GVHCpiV2HaJBYYxpxTvaA7sHiD9BKfmAc8zPQWUvz37C"+              },+              "signatures": [+                "256ZKFLjMhx4fK1NLCzZ7dUDgsbD3xhnLJeysfi9WKxjdNn473XT3piVWAeMxK1KWsFtYBY8NcvTYuCd5YeVio3K",+                "3MVSryX7NdqzF2HHNqdWu9MVTfAWfjNyoDe2AhySGiTU5KXMEiUPZxZNh7bH4fhGQjmQsLn4Ym8qoV8fv7Y8yFec"+              ]+            },+            "version": "legacy"+          },+          {+            "meta": {+              "computeUnitsConsumed": 150,+              "err": null,+              "fee": 5000,+              "innerInstructions": [],+              "loadedAddresses": {+                "readonly": [],+                "writable": []+              },+              "logMessages": [+                "Program 11111111111111111111111111111111 invoke [1]",+                "Program 11111111111111111111111111111111 success"+              ],+              "postBalances": [+                8999995000,+                1000000000,+                1+              ],+              "postTokenBalances": [],+              "preBalances": [+                10000000000,+                0,+                1+              ],+              "preTokenBalances": [],+              "rewards": null,+              "status": {+                "Ok": null+              }+            },+            "transaction": {+              "message": {+                "accountKeys": [+                  "9cjs2tCQQft8cR1bDXrRYerzkw1kS2Z2VvrMDhmzUnpU",+                  "7CJWFMwehUcFsPqFpxbCrcZM5QwzkvFyhqTyMvbLd57t",+                  "11111111111111111111111111111111"+                ],+                "header": {+                  "numReadonlySignedAccounts": 0,+                  "numReadonlyUnsignedAccounts": 1,+                  "numRequiredSignatures": 1+                },+                "instructions": [+                  {+                    "accounts": [+                      0,+                      1+                    ],+                    "data": "3Bxs3zzLZLuLQEYX",+                    "programIdIndex": 2,+                    "stackHeight": null+                  }+                ],+                "recentBlockhash": "GVHCpiV2HaJBYYxpxTvaA7sHiD9BKfmAc8zPQWUvz37C"+              },+              "signatures": [+                "7LWBsxy64WXiaaPRWic6Bz71xfN9mCrE9Ai54pHCRHjA1a6f454FFc1FaHafQV7VYvAtd35WpGB7b3CSLPkiimt"+              ]+            },+            "version": "legacy"+          }+        ]+      },+      "id": 1+    },+    "params": [+      1139,+      {+        "encoding": "json",+        "maxSupportedTransactionVersion": 0,+        "transactionDetails": "full",+        "rewards": false+      }+    ]+  },+  {+    "name": "getBlockCommitment",+    "method": "getBlockCommitment",+    "response": {+      "jsonrpc": "2.0",+      "result": {+        "commitment": [+          0,+          0,+          0,+          0,+          0,+          0,+          0,+          0,+          0,+          0,+          0,+          0,+          0,+          0,+          0,+          0,+          0,+          0,+          0,+          0,+          0,+          0,+          0,+          0,+          0,+          0,+          0,+          0,+          0,+          0,+          0,+          1000558210106701+        ],+        "totalStake": 1000558210106701+      },+      "id": 1+    },+    "params": [+      1139+    ]+  },+  {+    "name": "getBlockProduction",+    "method": "getBlockProduction",+    "response": {+      "jsonrpc": "2.0",+      "result": {+        "context": {+          "apiVersion": "2.1.16",+          "slot": 1179+        },+        "value": {+          "byIdentity": {+            "8XmVUUfAbS6kPE9EYK3Pt9JbitW1LYcez4GaEwJhWy13": [+              28,+              28+            ]+          },+          "range": {+            "firstSlot": 1152,+            "lastSlot": 1179+          }+        }+      },+      "id": 1+    }+  },+  {+    "name": "getBlocks",+    "method": "getBlocks",+    "response": {+      "jsonrpc": "2.0",+      "result": [+        1137,+        1138,+        1139+      ],+      "id": 1+    },+    "params": [+      1137,+      1139+    ]+  },+  {+    "name": "getBlocksWithLimit",+    "method": "getBlocksWithLimit",+    "response": {+      "jsonrpc": "2.0",+      "result": [+        1137,+        1138,+        1139+      ],+      "id": 1+    },+    "params": [+      1137,+      3+    ]+  },+  {+    "name": "getBlockTime",+    "method": "getBlockTime",+    "response": {+      "jsonrpc": "2.0",+      "result": 1789304691,+      "id": 1+    },+    "params": [+      1139+    ]+  },+  {+    "name": "getInflationGovernor",+    "method": "getInflationGovernor",+    "response": {+      "jsonrpc": "2.0",+      "result": {+        "foundation": 0.05,+        "foundationTerm": 7.0,+        "initial": 0.08,+        "taper": 0.15,+        "terminal": 0.015+      },+      "id": 1+    }+  },+  {+    "name": "getInflationRate",+    "method": "getInflationRate",+    "response": {+      "jsonrpc": "2.0",+      "result": {+        "epoch": 36,+        "foundation": 0.0039999905074868366,+        "total": 0.07999981014973673,+        "validator": 0.07599981964224989+      },+      "id": 1+    }+  },+  {+    "name": "getInflationReward",+    "method": "getInflationReward",+    "response": {+      "jsonrpc": "2.0",+      "result": [+        {+          "amount": 15505890732,+          "commission": 0,+          "effectiveSlot": 1153,+          "epoch": 35,+          "postBalance": 1000542706499393+        }+      ],+      "id": 1+    },+    "params": [+      [+        "ADdiFqTcHJQS8ay9LJqXRcBArpetNszyJcUyoqWL43ge"+      ]+    ]+  },+  {+    "name": "getSupply",+    "method": "getSupply",+    "response": {+      "jsonrpc": "2.0",+      "result": {+        "context": {+          "apiVersion": "2.1.16",+          "slot": 1179+        },+        "value": {+          "circulating": 503001051202993908,+          "nonCirculating": 0,+          "nonCirculatingAccounts": [+            "HuqDWJodFhAEWh6aWdsDVUqsjRket5DYXMYyDYtD8hdN",+            "GEWSkfWgHkpiLbeKaAnwvqnECGdRNf49at5nFccVey7c",+            "Dc2oHxFXQaC2QfLStuU7txtD3U5HZ82MrCSGDooWjbsv",+            "HQJtLqvEGGxgNYfRXUurfxV8E1swvCnsbC3456ik27HY",+            "DE1bawNcRJB9rVm3buyMVfr8mBEoyyu73NBovf2oXJsJ",+            "FiWYY85b58zEEcPtxe3PuqzWPjqBJXqdwgZeqSBmT9Cn",+            "Fg12tB1tz8w6zJSQ4ZAGotWoCztdMJF9hqK8R11pakog",+            "BUnRE27mYXN9p8H1Ay24GXhJC88q2CuwLoNU2v2CrW4W",+            "4sxwau4mdqZ8zEJsfryXq4QFYnMJSCp3HWuZQod8WU5k",+            "9huDUZfxoJ7wGMTffUE7vh1xePqef7gyrLJu9NApncqA",+            "Eyr9P5XsjK2NUKNCnfu39eqpGoiLFgVAv1LSQgMZCwiQ",+            "8ndGYFjav6NDXvzYcxs449Aub3AxYv4vYpk89zRDwgj7",+            "3jnknRabs7G2V9dKhxd2KP85pNWXKXiedYnYxtySnQMs",+            "AG3m2bAibcY8raMt4oXEGqRHwX4FWKPPJVjZxn1LySDX",+            "5XdtyEDREHJXXW1CTtCsVjJRjBapAwK78ZquzvnNVRrV",+            "7Y8smnoUrYKGGuDq2uaFKVxJYhojgg7DVixHyAtGTYEV",+            "7cvkjYAkUYs4W8XcXsca7cBrEGFeSUjeZmKoNBvEwyri",+            "CzAHrrrHKx9Lxf6wdCMrsZkLvk74c7J2vGv8VYPUmY6v",+            "8CUUMKYNGxdgYio5CLHRHyzMEhhVRMcqefgE6dLqnVRK",+            "CHmdL15akDcJgBkY6BP3hzs98Dqr6wbdDC5p8odvtSbq",+            "6yKHERk8rsbmJxvMpPuwPs1ct3hRiP7xaJF2tvnGU6nK",+            "CUageMFi49kzoDqtdU8NvQ4Bq3sbtJygjKDAXJ45nmAi",+            "HbZ5FfmKWNHC7uwk6TF1hVi6TCs7dtYfdjEcuPGgzFAg",+            "5smrYwb1Hr2T8XMnvsqccTgXxuqQs14iuE8RbHFYf2Cf",+            "7xJ9CLtEAcEShw9kW2gSoZkRWL566Dg12cvgzANJwbTr",+            "2WWb1gRzuXDd5viZLQF7pNRR6Y7UiyeaPpaL35X6j3ve",+            "DQQGPtj7pphPHCLzzBuEyDDQByUcKGrsJdsH7SP3hAug",+            "6o5v1HC7WhBnLfRHp8mQTtCP2khdXXjhuyGyYEoy2Suy",+            "FwfaykN7ACnsEUDHANzGHqTGQZMcGnUSsahAHUqbdPrz",+            "Ep5Y58PaSyALPrdFxDVAdfKtVdP55vApvsWjb3jSmXsG",+            "AzHQ8Bia1grVVbcGyci7wzueSWkgvu7YZVZ4B9rkL5P6",+            "BhvLngiqqKeZ8rpxch2uGjeCiC88zzewoWPRuoxpp1aS",+            "5khMKAcvmsFaAhoKkdg3u5abvKsmjUQNmhTNP624WB1F",+            "FR84wZQy3Y3j2gWz6pgETUiUoJtreMEuWfbg6573UCj9",+            "HCV5dGFJXRrJ3jhDYA4DCeb9TEDTwGGYXtT3wHksu2Zr",+            "JCwT5Ygmq3VeBEbDjL8s8E82Ra2rP9bq45QfZE7Xyaq7",+            "63DtkW7zuARcd185EmHAkfF44bDcC2SiTSEj2spLP3iA",+            "CuatS6njAcfkFHnvai7zXCs7syA9bykXWsDCJEWfhjHG",+            "6nN69B4uZuESZYxr9nrLDjmKRtjDZQXrehwkfQTKw62U",+            "EMhn1U3TMimW3bvWYbPUvN2eZnCfsuBN4LGWhzzYhiWR",+            "BsKsunvENxAraBrL77UfAn1Gi7unVEmQAdCbhsjUN6tU",+            "3iPvAS4xdhYr6SkhVDHCLr7tJjMAFK4wvvHWJxFQVg15",+            "5PLJZLJiRR9vf7d1JCCg7UuWjtyN9nkab9uok6TqSyuP",+            "CQDYc4ET2mbFhVpgj41gXahL6Exn5ZoPcGAzSHuYxwmE",+            "CWeRmXme7LmbaUWTZWFLt6FMnpzLCHaQLuR2TdgFn4Lq",+            "CakcnaRDHka2gXyfbEd2d3xsvkJkqsLw2akB3zsN1D2S",+            "BUjkdqUuH5Lz9XzcMcR4DdEMnFG6r8QzUMBm16Rfau96",+            "E8jcgWvrvV7rwYHJThwfiBeQ8VAH4FgNEEMG9aAuCMAq",+            "CY7X5o3Wi2eQhTocLmUS6JSWyx1NinBfW7AXRrkRCpi8",+            "5D5NxsNVTgXHyVziwV7mDFwVDS6voaBsyyGxUbhQrhNW",+            "8pNBEppa1VcFAsx4Hzq9CpdXUXZjUXbvQwLX2K7QsCwb",+            "EMAY24PrS6rWfvpqffFCsTsFJypeeYYmtUc26wdh3Wup",+            "GK8R4uUmrawcREZ5xJy5dAzVV5V7aFvYg77id37pVTK",+            "3itU5ME8L6FDqtMiRoUiT1F7PwbkTtHBbW51YWD5jtjm",+            "6zw7em7uQdmMpuS9fGz8Nq9TLHa5YQhEKKwPjo5PwDK4",+            "Es13uD2p64UVPFpEWfDtd6SERdoNR2XVgqBQBZcZSLqW",+            "HUAkU5psJXZuw54Lrg1ksbXzHv2fzczQ9sNbmisVMeJU",+            "3bTGcGB9F98XxnrBNftmmm48JGfPgi5sYxDEKiCjQYk3",+            "BuCEvc9ze8UoAQwwsQLy8d447C8sA4zeVtVpc6m5wQeS",+            "8DE8fqPfv1fp9DHyGyDFFaMjpopMgDeXspzoi9jpBJjC",+            "7Np41oeYqPefeNQEHSv1UDhYrehxin3NStELsSKCT4K2",+            "Fgyh8EeYGZtbW8sS33YmNQnzx54WXPrJ5KWNPkCfWPot",+            "CTvhdUVf8KNyMbyEdnvRrBCHJjBKtQwkbj6zwoqcEssG",+            "8rT45mqpuDBR1vcnDc9kwP9DrZAXDR4ZeuKWw3u1gTGa",+            "4vuWt1oHRqLMhf8Nv1zyEXZsYaeK7dipwrfKLoYU9Riq",+            "xQadXQiUTCCFhfHjvQx1hyJK6KVWr1w2fD6DT3cdwj7",+            "GumSE5HsMV5HCwBTv2D2D81yy9x17aDkvobkqAfTRgmo",+            "EAJJD6nDqtXcZ4DnQb19F9XEz8y8bRDHxbWbahatZNbL",+            "AVYpwVou2BhdLivAwLxKPALZQsY7aZNkNmGbP2fZw7RU",+            "GLUmCeJpXB8veNcchPwibkRYwCwvQbKodex5mEjrgToi",+            "9hknftBZAQL4f48tWfk3bUEV5YSLcYYtDRqNmpNnhCWG",+            "3o6xgkJ9sTmDeQWyfj3sxwon18fXJB9PV5LDc8sfgR4a",+            "9S2M3UYPpnPZTBtbcUvehYmiWFK3kBhwfzV2iWuwvaVy",+            "4pV47TiPzZ7SSBPHmgUvSLmH9mMSe8tjyPhQZGbi1zPC",+            "AzVV9ZZDxTgW4wWfJmsG6ytaHpQGSe1yz76Nyy84VbQF",+            "14FUT96s9swbmH7ZjpDvfEDywnAYy9zaNhv4xvezySGu",+            "GmyW1nqYcrw7P7JqrcyP9ivU9hYNbrgZ1r5SYJJH41Fs",+            "9xbcBZoGYFnfJZe81EDuDYKUm8xGkjzW8z4EgnVhNvsv",+            "CsUqV42gVQLJwQsKyjWHqGkfHarxn9hcY4YeSjgaaeTd",+            "H1rt8KvXkNhQExTRfkY8r9wjZbZ8yCih6J4wQ5Fz9HGP",+            "Hm9JW7of5i9dnrboS8pCUCSeoQUPh7JsP1rkbJnW7An4",+            "HKJgYGTTYYR2ZkfJKHbn58w676fKueQXmvbtpyvrSM3N",+            "GNiz4Mq886bTNDT3pijGsu2gbw6it7sqrwncro45USeB",+            "GK2zqSsXLA2rwVZk347RYhh6jJpRsCA69FjLW93ZGi3B",+            "GpxpMVhrBBBEYbEJxdR62w3daWz444V7m6dxYDZKH77D",+            "8UVjvYyoqP6sqcctTso3xpCdCfgTMiv3VRh7vraC2eJk",+            "3ZrsTmNM6AkMcqFfv3ryfhQ2jMfqP64RQbqVyAaxqhrQ",+            "Br3aeVGapRb2xTq17RU2pYZCoJpWA7bq6TKBCcYtMSmt",+            "DrKzW5koKSZp4mg4BdHLwr72MMXscd2kTiWgckCvvPXz",+            "Mc5XB47H3DKJHym5RLa9mPzWv5snERsF3KNv5AauXK8",+            "3fV2GaDKa3pZxyDcpMh5Vrh2FVAMUiWUKbYmnBFv8As3",+            "F9MWFw8cnYVwsRq8Am1PGfFL3cQUZV37mbGoxZftzLjN",+            "GdnSyH3YtwcxFvQrVVJMm1JhTS4QVX7MFsX56uJLUfiZ",+            "DUS1KxwUhUyDKB4A81E8vdnTe3hSahd92Abtn9CXsEcj",+            "EziVYi3Sv5kJWxmU77PnbrT8jmkVuqwdiFLLzZpLVEn7",+            "GvpCiTgq9dmEeojCDBivoLoZqc4AkbUDACpqPMwYLWKh",+            "CND6ZjRTzaCFVdX7pSSWgjTfHZuhxqFDoUBqWBJguNoA",+            "P8aKfWQPeRnsZtpBrwWTYzyAoRk74KMz56xc6NEpC4J",+            "Ab1UcdsFXZVnkSt1Z3vcYU65GQk5MvCbs54SviaiaqHb",+            "GhsotwFMH6XUrRLJCxcx62h7748N2Uq8mf87hUGkmPhg",+            "nGME7HgBT6tAJN1f6YuCCngpqT5cvSTndZUVLjQ4jwA",+            "8vqrX3H2BYLaXVintse3gorPEM4TgTwTFZNN1Fm9TdYs",+            "C7C8odR8oashR5Feyrq2tJKaXL18id1dSj2zbkDGL2C2",+            "AsrYX4FeLXnZcrjcZmrASY2Eq1jvEeQfwxtNTxS5zojA",+            "8W58E8JVJjH1jCy5CeHJQgvwFXTyAVyesuXRZGbcSUGG",+            "H3Ni7vG1CsmJZdTvxF7RkAf9UM5qk4RsohJsmPvtZNnu",+            "CVgyXrbEd1ctEuvq11QdpnCQVnPit8NLdhyqXQHLprM2",+            "Hz9nydgN1k15wnwffKX7CSmZp4VFTnTwLXAEdomFGNXy",+            "5q54XjQ7vDx4y6KphPeE97LUNiYGtP55spjvXAWPGBuf",+            "GpYnVDgB7dzvwSgsjQFeHznjG6Kt1DLBFYrKxjGU1LuD"+          ],+          "total": 503001051202993908+        }+      },+      "id": 1+    }+  },+  {+    "name": "getVoteAccounts",+    "method": "getVoteAccounts",+    "response": {+      "jsonrpc": "2.0",+      "result": {+        "current": [+          {+            "activatedStake": 1000542704216513,+            "commission": 0,+            "epochCredits": [+              [+                32,+                16368,+                15856+              ],+              [+                33,+                16880,+                16368+              ],+              [+                34,+                17392,+                16880+              ],+              [+                35,+                17904,+                17392+              ],+              [+                36,+                18352,+                17904+              ]+            ],+            "epochVoteAccount": true,+            "lastVote": 1178,+            "nodePubkey": "8XmVUUfAbS6kPE9EYK3Pt9JbitW1LYcez4GaEwJhWy13",+            "rootSlot": 1147,+            "votePubkey": "6PE42VjbVgozfFAEu8qMXQgvST4spCnw37FBfeYCrL16"+          }+        ],+        "delinquent": []+      },+      "id": 1+    }+  },+  {+    "name": "getMinimumBalanceForRentExemption",+    "method": "getMinimumBalanceForRentExemption",+    "response": {+      "jsonrpc": "2.0",+      "result": 2039280,+      "id": 1+    },+    "params": [+      165+    ]+  },+  {+    "name": "getBalance",+    "method": "getBalance",+    "response": {+      "jsonrpc": "2.0",+      "result": {+        "context": {+          "apiVersion": "2.1.16",+          "slot": 1179+        },+        "value": 8996469120+      },+      "id": 1+    },+    "params": [+      "9cjs2tCQQft8cR1bDXrRYerzkw1kS2Z2VvrMDhmzUnpU"+    ]+  },+  {+    "name": "getAccountInfo",+    "method": "getAccountInfo",+    "response": {+      "jsonrpc": "2.0",+      "result": {+        "context": {+          "apiVersion": "2.1.16",+          "slot": 1179+        },+        "value": {+          "data": [+            "",+            "base64"+          ],+          "executable": false,+          "lamports": 8996469120,+          "owner": "11111111111111111111111111111111",+          "rentEpoch": 18446744073709551615,+          "space": 0+        }+      },+      "id": 1+    },+    "params": [+      "9cjs2tCQQft8cR1bDXrRYerzkw1kS2Z2VvrMDhmzUnpU",+      {+        "encoding": "base64"+      }+    ]+  },+  {+    "name": "getAccountInfo-missing",+    "method": "getAccountInfo",+    "response": {+      "jsonrpc": "2.0",+      "result": {+        "context": {+          "apiVersion": "2.1.16",+          "slot": 1179+        },+        "value": null+      },+      "id": 1+    },+    "params": [+      "11111111111111111111111111111112",+      {+        "encoding": "base64"+      }+    ]+  },+  {+    "name": "getAccountInfo-base58",+    "method": "getAccountInfo",+    "response": {+      "jsonrpc": "2.0",+      "result": {+        "context": {+          "apiVersion": "2.1.16",+          "slot": 1179+        },+        "value": {+          "data": "DK9N3QzkXWLeDqMwkqAigERVBaD8rZUH5ajoRw1T243TXP5qCRvVNyytYSh3MBnhZ5GhBHo9PwRrMMyuUzCwZLMDTp9CBkrfRMxhwhVtQSKeXqZ",+          "executable": false,+          "lamports": 1461600,+          "owner": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",+          "rentEpoch": 18446744073709551615,+          "space": 82+        }+      },+      "id": 1+    },+    "params": [+      "GLHHB8i4NhGbp5KvPkSb9sWwW5W2M9ExHhFGooF3qjrZ"+    ]+  },+  {+    "name": "getAccountInfo-jsonParsed",+    "method": "getAccountInfo",+    "response": {+      "jsonrpc": "2.0",+      "result": {+        "context": {+          "apiVersion": "2.1.16",+          "slot": 1179+        },+        "value": {+          "data": {+            "parsed": {+              "info": {+                "delegate": "7CJWFMwehUcFsPqFpxbCrcZM5QwzkvFyhqTyMvbLd57t",+                "delegatedAmount": {+                  "amount": "1000000",+                  "decimals": 6,+                  "uiAmount": 1.0,+                  "uiAmountString": "1"+                },+                "isNative": false,+                "mint": "GLHHB8i4NhGbp5KvPkSb9sWwW5W2M9ExHhFGooF3qjrZ",+                "owner": "9cjs2tCQQft8cR1bDXrRYerzkw1kS2Z2VvrMDhmzUnpU",+                "state": "initialized",+                "tokenAmount": {+                  "amount": "42000000",+                  "decimals": 6,+                  "uiAmount": 42.0,+                  "uiAmountString": "42"+                }+              },+              "type": "account"+            },+            "program": "spl-token",+            "space": 165+          },+          "executable": false,+          "lamports": 2039280,+          "owner": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",+          "rentEpoch": 18446744073709551615,+          "space": 165+        }+      },+      "id": 1+    },+    "params": [+      "DsZB2RSj7Ry9e69QzCAbg7oFhAUuNYzzFkPbhXSSGTBX",+      {+        "encoding": "jsonParsed"+      }+    ]+  },+  {+    "name": "getAccountInfo-jsonParsed-fallback",+    "method": "getAccountInfo",+    "response": {+      "jsonrpc": "2.0",+      "result": {+        "context": {+          "apiVersion": "2.1.16",+          "slot": 1179+        },+        "value": {+          "data": [+            "",+            "base64"+          ],+          "executable": false,+          "lamports": 8996469120,+          "owner": "11111111111111111111111111111111",+          "rentEpoch": 18446744073709551615,+          "space": 0+        }+      },+      "id": 1+    },+    "params": [+      "9cjs2tCQQft8cR1bDXrRYerzkw1kS2Z2VvrMDhmzUnpU",+      {+        "encoding": "jsonParsed"+      }+    ]+  },+  {+    "name": "getMultipleAccounts",+    "method": "getMultipleAccounts",+    "response": {+      "jsonrpc": "2.0",+      "result": {+        "context": {+          "apiVersion": "2.1.16",+          "slot": 1179+        },+        "value": [+          {+            "data": [+              "",+              "base64"+            ],+            "executable": false,+            "lamports": 8996469120,+            "owner": "11111111111111111111111111111111",+            "rentEpoch": 18446744073709551615,+            "space": 0+          },+          {+            "data": [+              "",+              "base64"+            ],+            "executable": false,+            "lamports": 1000000000,+            "owner": "11111111111111111111111111111111",+            "rentEpoch": 18446744073709551615,+            "space": 0+          }+        ]+      },+      "id": 1+    },+    "params": [+      [+        "9cjs2tCQQft8cR1bDXrRYerzkw1kS2Z2VvrMDhmzUnpU",+        "7CJWFMwehUcFsPqFpxbCrcZM5QwzkvFyhqTyMvbLd57t"+      ],+      {+        "encoding": "base64"+      }+    ]+  },+  {+    "name": "getLargestAccounts",+    "method": "getLargestAccounts",+    "response": {+      "jsonrpc": "2.0",+      "result": {+        "context": {+          "apiVersion": "2.1.16",+          "slot": 1179+        },+        "value": [+          {+            "address": "594C9C199Zp8fK2zvmXrSveE359gijrM6tsoZLYk9obv",+            "lamports": 500000000000000000+          },+          {+            "address": "ADdiFqTcHJQS8ay9LJqXRcBArpetNszyJcUyoqWL43ge",+            "lamports": 1000542706499393+          },+          {+            "address": "6PE42VjbVgozfFAEu8qMXQgvST4spCnw37FBfeYCrL16",+            "lamports": 1000000000000000+          },+          {+            "address": "9WdyMYjoCgNpLUrYDHLtfMM5grfNrtRfWUAwRGe95gHj",+            "lamports": 999930999905000+          },+          {+            "address": "8XmVUUfAbS6kPE9EYK3Pt9JbitW1LYcez4GaEwJhWy13",+            "lamports": 499994261520+          },+          {+            "address": "9ZugqNkqmCZFqwwBV2L4axN2G3gHrrkhHA7z4aYBkAED",+            "lamports": 9994444840+          },+          {+            "address": "5XbxkDKu1Pig2pL422Ah2ccKTCetsx41KE5aFsSVUXZC",+            "lamports": 8999995000+          },+          {+            "address": "Dn6sL2FccobRZU21p8oXDZa4tUwEcb2ocCkhtyxqkZPt",+            "lamports": 8999993000+          },+          {+            "address": "9cjs2tCQQft8cR1bDXrRYerzkw1kS2Z2VvrMDhmzUnpU",+            "lamports": 8996469120+          },+          {+            "address": "DoU57AYuPFu2QU514RktNPG22QhApEjnKxnBcu4BHDTY",+            "lamports": 3773078640+          },+          {+            "address": "9NZMSPPqaYtM45wGhC2P1UGzpmpr9Txf1YrG67XoEEH4",+            "lamports": 2994444840+          },+          {+            "address": "GHVKVku7jKWF9Wm1vm783JNMo1a7vcFjfjP53ox2kjHq",+            "lamports": 2994429840+          },+          {+            "address": "GpvMHGGKjpXvpEps5ojrQd9PcoexvBrEuoHgZa926svq",+            "lamports": 2798258920+          },+          {+            "address": "A988FuUtUVk8jMUuVc1ccaoTA3VS9CB4dkEf9XUAUqV4",+            "lamports": 2000000000+          },+          {+            "address": "AjHJq8a2rAvpEwPpCwmHSCXm9VNn2rsoGXUSzCueBYsz",+            "lamports": 1999990000+          },+          {+            "address": "5GrkvsgBcLbdwcVimm2WfHDmRxS6AqvDWoaX4UDTCBBa",+            "lamports": 1999990000+          },+          {+            "address": "3DpdBHnNFepEUJTiGrX6GiUUzRQ6GM6VKEHqXgSCV5sz",+            "lamports": 1898537320+          },+          {+            "address": "7ggMA8j7sCgoLTgaa7xSsbSaKvPPT6A614vaQHb6qUa",+            "lamports": 1798532320+          },+          {+            "address": "2VBTd7S7aEbZxc3m28s1h5HArw23m3Z4dQVFjXCW95Us",+            "lamports": 1699994980+          },+          {+            "address": "3PHvKBPpTRd7kiQ5DZKr4RPZJ7NubTTUuq3ksV1b1mYv",+            "lamports": 1599995000+          }+        ]+      },+      "id": 1+    }+  },+  {+    "name": "getProgramAccounts",+    "method": "getProgramAccounts",+    "response": {+      "jsonrpc": "2.0",+      "result": [+        {+          "account": {+            "data": [+              "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAJAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==",+              "base64"+            ],+            "executable": false,+            "lamports": 1000000000,+            "owner": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",+            "rentEpoch": 18446744073709551615,+            "space": 82+          },+          "pubkey": "So11111111111111111111111111111111111111112"+        },+        {+          "account": {+            "data": [+              "f8yB2ezHuSXFgQc9OD5mrJG6yyEdcHOY3W05yMNPeq5/S6eJ+rOjhhlinyaXuJ2wREBn+uyvXRlFifoiZv7FukBUiQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",+              "base64"+            ],+            "executable": false,+            "lamports": 2039280,+            "owner": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",+            "rentEpoch": 18446744073709551615,+            "space": 165+          },+          "pubkey": "nvrNy8RfWcWNd1Hk6UuVZpXKT4DcRH6RJwkkWVtZ2o5"+        },+        {+          "account": {+            "data": [+              "0yYZmkq3kUcUc0zQu6kC8BsygDW6RwjWNkL9SAt/w0A3Z0QZx+w/ZzX78fBDdSj1HQeH0Brvpf+rTq49QT3UW0BCDwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",+              "base64"+            ],+            "executable": false,+            "lamports": 2039280,+            "owner": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",+            "rentEpoch": 18446744073709551615,+            "space": 165+          },+          "pubkey": "3DZ9VmQKPueP2woSUfKK9uXqnBpBvRP7o7tn4HHFdzKh"+        },+        {+          "account": {+            "data": [+              "f8yB2ezHuSXFgQc9OD5mrJG6yyEdcHOY3W05yMNPeq6HzeKdDxux9wMt4eRrjxpvtFa/1y2QS6tYOd8YjYTgQ0BCDwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",+              "base64"+            ],+            "executable": false,+            "lamports": 2039280,+            "owner": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",+            "rentEpoch": 18446744073709551615,+            "space": 165+          },+          "pubkey": "3j5nSERqS4hghMmJZKzrKqPF4LRALodfR3ZZhWjesWwS"+        },+        {+          "account": {+            "data": [+              "AQAAAOMYnftCUoqgz0EA+OL0ou/3PvWB9DEfOZyMqZ+s1A9gQEIPAAAAAAAGAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==",+              "base64"+            ],+            "executable": false,+            "lamports": 1461600,+            "owner": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",+            "rentEpoch": 18446744073709551615,+            "space": 82+          },+          "pubkey": "5JrGPYnmLUQLP4r5itPc2BHL75SdJCDE5yyP99YZ39Zz"+        },+        {+          "account": {+            "data": [+              "QAFTcLSa52Kdj963SMxdpWKdTgzfY2lloOGkvDtroUnjGJ37QlKKoM9BAPji9KLv9z71gfQxHzmcjKmfrNQPYLBxCwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",+              "base64"+            ],+            "executable": false,+            "lamports": 2039280,+            "owner": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",+            "rentEpoch": 18446744073709551615,+            "space": 165+          },+          "pubkey": "5SyXMRLKqg6fZW44kY5Tj1S6da1hd3iWHnj7ZVZoX6r1"+        },+        {+          "account": {+            "data": [+              "0yYZmkq3kUcUc0zQu6kC8BsygDW6RwjWNkL9SAt/w0B8Y0X6Fnbib71unPZy51atANzZdq2s3zAa0I0RTeHjH0BUiQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",+              "base64"+            ],+            "executable": false,+            "lamports": 2039280,+            "owner": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",+            "rentEpoch": 18446744073709551615,+            "space": 165+          },+          "pubkey": "6fR7HMCWnRvvrRFXFMYjnBNpXEoE2sJ5gS6n6jzZPryu"+        },+        {+          "account": {+            "data": [+              "AQAAAH9Lp4n6s6OGGWKfJpe4nbBEQGf67K9dGUWJ+iJm/sW6gJaYAAAAAAAGAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==",+              "base64"+            ],+            "executable": false,+            "lamports": 1461600,+            "owner": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",+            "rentEpoch": 18446744073709551615,+            "space": 82+          },+          "pubkey": "9bseQGaMu7zxy5ecFYBQCkS5R7LKufZg2r17NXNb1o9f"+        },+        {+          "account": {+            "data": [+              "QAFTcLSa52Kdj963SMxdpWKdTgzfY2lloOGkvDtroUk/TVuRlPUmP/2lE0FZ2+oGYoIvlX4TJc2Tha7rd54025DQAwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",+              "base64"+            ],+            "executable": false,+            "lamports": 2039280,+            "owner": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",+            "rentEpoch": 18446744073709551615,+            "space": 165+          },+          "pubkey": "9rDLHt7wMAmAumaAs7C7ssyBuEYGPAjfqwVScjLV1imY"+        },+        {+          "account": {+            "data": [+              "48++7/3R0WIonJxD1BvrGy92zcSxFsK3EIlKgUiPsQKABUnlbzpEmD/B9Mne0UtewK8vglXadt4+j5gxsciGXYDegAIAAAAAAQAAAFwLTXcQ1RvynIymV4tqc6oCAPQkAOGMo+Zz71Y4tzRvAQAAAAAAAAAAAAAAAEBCDwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",+              "base64"+            ],+            "executable": false,+            "lamports": 2039280,+            "owner": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",+            "rentEpoch": 18446744073709551615,+            "space": 165+          },+          "pubkey": "DsZB2RSj7Ry9e69QzCAbg7oFhAUuNYzzFkPbhXSSGTBX"+        },+        {+          "account": {+            "data": [+              "AQAAAHxjRfoWduJvvW6c9nLnVq0A3Nl2razfMBrQjRFN4eMfgJaYAAAAAAAGAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==",+              "base64"+            ],+            "executable": false,+            "lamports": 1461600,+            "owner": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",+            "rentEpoch": 18446744073709551615,+            "space": 82+          },+          "pubkey": "FDEimKKSp1YzmFnTSnj4NMRqqEuP8DW4QtTTphSWJk3h"+        },+        {+          "account": {+            "data": [+              "AQAAAIAFSeVvOkSYP8H0yd7RS17Ary+CVdp23j6PmDGxyIZdgN6AAgAAAAAGAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==",+              "base64"+            ],+            "executable": false,+            "lamports": 1461600,+            "owner": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",+            "rentEpoch": 18446744073709551615,+            "space": 82+          },+          "pubkey": "GLHHB8i4NhGbp5KvPkSb9sWwW5W2M9ExHhFGooF3qjrZ"+        }+      ],+      "id": 1+    },+    "params": [+      "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",+      {+        "encoding": "base64"+      }+    ]+  },+  {+    "name": "getSignaturesForAddress",+    "method": "getSignaturesForAddress",+    "response": {+      "jsonrpc": "2.0",+      "result": [+        {+          "blockTime": 1789304708,+          "confirmationStatus": "finalized",+          "err": null,+          "memo": null,+          "signature": "3C4AHsfWkBC2uWHop2ojYDph6xMYd4VjUEZFa1HToxYc216i9N9NoTPkKS4gPENnBoo4qdSXkUCEce99EMdXjqHx",+          "slot": 1177+        },+        {+          "blockTime": 1789304691,+          "confirmationStatus": "finalized",+          "err": null,+          "memo": null,+          "signature": "7LWBsxy64WXiaaPRWic6Bz71xfN9mCrE9Ai54pHCRHjA1a6f454FFc1FaHafQV7VYvAtd35WpGB7b3CSLPkiimt",+          "slot": 1139+        }+      ],+      "id": 1+    },+    "params": [+      "7CJWFMwehUcFsPqFpxbCrcZM5QwzkvFyhqTyMvbLd57t",+      {+        "limit": 3+      }+    ]+  },+  {+    "name": "getSignatureStatuses",+    "method": "getSignatureStatuses",+    "response": {+      "jsonrpc": "2.0",+      "result": {+        "context": {+          "apiVersion": "2.1.16",+          "slot": 1210+        },+        "value": [+          {+            "confirmationStatus": "finalized",+            "confirmations": null,+            "err": null,+            "slot": 1139,+            "status": {+              "Ok": null+            }+          }+        ]+      },+      "id": 1+    },+    "params": [+      [+        "7LWBsxy64WXiaaPRWic6Bz71xfN9mCrE9Ai54pHCRHjA1a6f454FFc1FaHafQV7VYvAtd35WpGB7b3CSLPkiimt"+      ],+      {+        "searchTransactionHistory": true+      }+    ]+  },+  {+    "name": "getTransaction",+    "method": "getTransaction",+    "response": {+      "jsonrpc": "2.0",+      "result": {+        "blockTime": 1789304691,+        "meta": {+          "computeUnitsConsumed": 150,+          "err": null,+          "fee": 5000,+          "innerInstructions": [],+          "loadedAddresses": {+            "readonly": [],+            "writable": []+          },+          "logMessages": [+            "Program 11111111111111111111111111111111 invoke [1]",+            "Program 11111111111111111111111111111111 success"+          ],+          "postBalances": [+            8999995000,+            1000000000,+            1+          ],+          "postTokenBalances": [],+          "preBalances": [+            10000000000,+            0,+            1+          ],+          "preTokenBalances": [],+          "rewards": [],+          "status": {+            "Ok": null+          }+        },+        "slot": 1139,+        "transaction": {+          "message": {+            "accountKeys": [+              "9cjs2tCQQft8cR1bDXrRYerzkw1kS2Z2VvrMDhmzUnpU",+              "7CJWFMwehUcFsPqFpxbCrcZM5QwzkvFyhqTyMvbLd57t",+              "11111111111111111111111111111111"+            ],+            "header": {+              "numReadonlySignedAccounts": 0,+              "numReadonlyUnsignedAccounts": 1,+              "numRequiredSignatures": 1+            },+            "instructions": [+              {+                "accounts": [+                  0,+                  1+                ],+                "data": "3Bxs3zzLZLuLQEYX",+                "programIdIndex": 2,+                "stackHeight": null+              }+            ],+            "recentBlockhash": "GVHCpiV2HaJBYYxpxTvaA7sHiD9BKfmAc8zPQWUvz37C"+          },+          "signatures": [+            "7LWBsxy64WXiaaPRWic6Bz71xfN9mCrE9Ai54pHCRHjA1a6f454FFc1FaHafQV7VYvAtd35WpGB7b3CSLPkiimt"+          ]+        },+        "version": "legacy"+      },+      "id": 1+    },+    "params": [+      "7LWBsxy64WXiaaPRWic6Bz71xfN9mCrE9Ai54pHCRHjA1a6f454FFc1FaHafQV7VYvAtd35WpGB7b3CSLPkiimt",+      {+        "encoding": "json",+        "maxSupportedTransactionVersion": 0+      }+    ]+  },+  {+    "name": "getFeeForMessage",+    "method": "getFeeForMessage",+    "response": {+      "jsonrpc": "2.0",+      "result": {+        "context": {+          "apiVersion": "2.1.16",+          "slot": 1210+        },+        "value": 5000+      },+      "id": 1+    },+    "params": [+      "AQABA4AFSeVvOkSYP8H0yd7RS17Ary+CVdp23j6PmDGxyIZdXAtNdxDVG/KcjKZXi2pzqgIA9CQA4Yyj5nPvVji3NG8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAL9YWnne+HJfKGpooSWeyzKzvXknewkQk0D2V2wXmj0pAQICAAEMAgAAAEBCDwAAAAAA",+      {+        "commitment": "processed"+      }+    ]+  },+  {+    "name": "getTokenSupply",+    "method": "getTokenSupply",+    "response": {+      "jsonrpc": "2.0",+      "result": {+        "context": {+          "apiVersion": "2.1.16",+          "slot": 1179+        },+        "value": {+          "amount": "42000000",+          "decimals": 6,+          "uiAmount": 42.0,+          "uiAmountString": "42"+        }+      },+      "id": 1+    },+    "params": [+      "GLHHB8i4NhGbp5KvPkSb9sWwW5W2M9ExHhFGooF3qjrZ"+    ]+  },+  {+    "name": "getTokenAccountBalance",+    "method": "getTokenAccountBalance",+    "response": {+      "jsonrpc": "2.0",+      "result": {+        "context": {+          "apiVersion": "2.1.16",+          "slot": 1179+        },+        "value": {+          "amount": "42000000",+          "decimals": 6,+          "uiAmount": 42.0,+          "uiAmountString": "42"+        }+      },+      "id": 1+    },+    "params": [+      "DsZB2RSj7Ry9e69QzCAbg7oFhAUuNYzzFkPbhXSSGTBX"+    ]+  },+  {+    "name": "getTokenLargestAccounts",+    "method": "getTokenLargestAccounts",+    "response": {+      "jsonrpc": "2.0",+      "result": {+        "context": {+          "apiVersion": "2.1.16",+          "slot": 1179+        },+        "value": [+          {+            "address": "DsZB2RSj7Ry9e69QzCAbg7oFhAUuNYzzFkPbhXSSGTBX",+            "amount": "42000000",+            "decimals": 6,+            "uiAmount": 42.0,+            "uiAmountString": "42"+          }+        ]+      },+      "id": 1+    },+    "params": [+      "GLHHB8i4NhGbp5KvPkSb9sWwW5W2M9ExHhFGooF3qjrZ"+    ]+  },+  {+    "name": "getTokenAccountsByOwner",+    "method": "getTokenAccountsByOwner",+    "response": {+      "jsonrpc": "2.0",+      "result": {+        "context": {+          "apiVersion": "2.1.16",+          "slot": 1179+        },+        "value": [+          {+            "account": {+              "data": [+                "48++7/3R0WIonJxD1BvrGy92zcSxFsK3EIlKgUiPsQKABUnlbzpEmD/B9Mne0UtewK8vglXadt4+j5gxsciGXYDegAIAAAAAAQAAAFwLTXcQ1RvynIymV4tqc6oCAPQkAOGMo+Zz71Y4tzRvAQAAAAAAAAAAAAAAAEBCDwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",+                "base64"+              ],+              "executable": false,+              "lamports": 2039280,+              "owner": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",+              "rentEpoch": 18446744073709551615,+              "space": 165+            },+            "pubkey": "DsZB2RSj7Ry9e69QzCAbg7oFhAUuNYzzFkPbhXSSGTBX"+          }+        ]+      },+      "id": 1+    },+    "params": [+      "9cjs2tCQQft8cR1bDXrRYerzkw1kS2Z2VvrMDhmzUnpU",+      {+        "mint": "GLHHB8i4NhGbp5KvPkSb9sWwW5W2M9ExHhFGooF3qjrZ"+      },+      {+        "encoding": "base64"+      }+    ]+  },+  {+    "name": "getTokenAccountsByDelegate",+    "method": "getTokenAccountsByDelegate",+    "response": {+      "jsonrpc": "2.0",+      "result": {+        "context": {+          "apiVersion": "2.1.16",+          "slot": 1179+        },+        "value": [+          {+            "account": {+              "data": [+                "48++7/3R0WIonJxD1BvrGy92zcSxFsK3EIlKgUiPsQKABUnlbzpEmD/B9Mne0UtewK8vglXadt4+j5gxsciGXYDegAIAAAAAAQAAAFwLTXcQ1RvynIymV4tqc6oCAPQkAOGMo+Zz71Y4tzRvAQAAAAAAAAAAAAAAAEBCDwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",+                "base64"+              ],+              "executable": false,+              "lamports": 2039280,+              "owner": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",+              "rentEpoch": 18446744073709551615,+              "space": 165+            },+            "pubkey": "DsZB2RSj7Ry9e69QzCAbg7oFhAUuNYzzFkPbhXSSGTBX"+          }+        ]+      },+      "id": 1+    },+    "params": [+      "7CJWFMwehUcFsPqFpxbCrcZM5QwzkvFyhqTyMvbLd57t",+      {+        "mint": "GLHHB8i4NhGbp5KvPkSb9sWwW5W2M9ExHhFGooF3qjrZ"       },       {         "encoding": "base64"
test/fixtures/transactions.json view
@@ -42,5 +42,13 @@   {     "hex": "01fcffc6cc51a4de7b7259f429eabff20a3a06fd71eaf9463baae5a1955eb6599778cc1576701e78db8398ce733d9c0e0a63021a4e4ce7c7de049f638bc3301002010002058a88e3dd7409f195fd52db2d3cba5d72ca6709bf1d94121bf3748801b40f6f5c02020202020202020202020202020202020202020202020202020202020202022525252525252525252525252525252525252525252525252525252525252525000000000000000000000000000000000000000000000000000000000000000006a7d517192c568ee08a845f73d29788cf035c3145b21ab344d8062ea940000025bdedad9fd7d269184d58d8f80e149acc1ceb154afc3c77363d7859c1febf2e0203030204000404000000030200010c0200000000ca9a3b00000000",     "name": "nonce-transfer-transaction"+  },+  {+    "hex": "0154fc0db41825a24d786eb9f7a47d74fb95afd938f7bfddc62835a7e595b315e1726352424be259ad6ea9aceeed325215927098cc123a81f1bc52909a89cb0b06010001028a88e3dd7409f195fd52db2d3cba5d72ca6709bf1d94121bf3748801b40f6f5c054a535a992921064d24e87160da387c7c35b5ddbc92bb81e41fa8404105448d0909090909090909090909090909090909090909090909090909090909090909010101000a68656c6c6f2d6d656d6f",+    "name": "memo-only-transaction"+  },+  {+    "hex": "01df408945661266863208084cc664f9116851f15f77eaed5f05888d124688922f47416eeb1f0e83ff4d71b4597b73eeb4c88e5f84456f55997e35236c1b8abd0880010001028a88e3dd7409f195fd52db2d3cba5d72ca6709bf1d94121bf3748801b40f6f5c054a535a992921064d24e87160da387c7c35b5ddbc92bb81e41fa8404105448d0909090909090909090909090909090909090909090909090909090909090909010101000a68656c6c6f2d6d656d6f00",+    "name": "v0-memo-only-transaction"   } ]