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 +121/−1
- README.md +56/−21
- app/Main.hs +7/−7
- app/PriorityFee.hs +33/−0
- app/SplTransfer.hs +69/−0
- solana-haskell-sdk.cabal +30/−1
- src/Network/Solana/Core/Account.hs +31/−8
- src/Network/Solana/Core/Crypto.hs +43/−24
- src/Network/Solana/Core/Instruction.hs +30/−7
- src/Network/Solana/Core/Message.hs +26/−10
- src/Network/Solana/Core/VersionedMessage.hs +41/−5
- src/Network/Solana/Metaplex/TokenMetadata.hs +3/−2
- src/Network/Solana/NativePrograms/ComputeBudget.hs +4/−0
- src/Network/Solana/NativePrograms/SystemProgram.hs +4/−0
- src/Network/Solana/RPC/HTTP/Block.hs +12/−3
- src/Network/Solana/RPC/HTTP/Chain.hs +7/−5
- src/Network/Solana/RPC/HTTP/Types.hs +7/−4
- src/Network/Solana/RPC/WebSocket.hs +6/−4
- src/Network/Solana/SolanaWeb3.hs +74/−16
- src/Network/Solana/SplPrograms/Memo.hs +3/−0
- test-integration/Main.hs +7/−2
- test-integration/Test/Integration/Alt.hs +20/−2
- test-integration/Test/Integration/Block.hs +29/−0
- test-integration/Test/Integration/Nonce.hs +80/−9
- test-integration/Test/Integration/PriorityFee.hs +23/−2
- test-integration/Test/Integration/Setup.hs +6/−25
- test-integration/Test/Integration/Token.hs +38/−2
- test-integration/Test/Integration/Transfer.hs +57/−5
- test/Main.hs +6/−0
- test/Test/Core/Account.hs +73/−2
- test/Test/Core/Crypto.hs +90/−1
- test/Test/Core/Instruction.hs +31/−2
- test/Test/Core/Message.hs +63/−1
- test/Test/Core/VersionedMessage.hs +80/−1
- test/Test/Metaplex/TokenMetadata.hs +28/−30
- test/Test/RPC/Chain.hs +44/−2
- test/Test/RPC/Parsers.hs +78/−7
- test/Test/RPC/Types.hs +35/−0
- test/Test/RPC/WebSocket.hs +18/−2
- test/Test/Readme.hs +44/−0
- test/Test/SolanaWeb3.hs +24/−0
- test/fixtures/keypair.json +1/−0
- test/fixtures/keypair_out_of_range.json +1/−0
- test/fixtures/keypair_syntax_error.json +1/−0
- test/fixtures/keypair_truncated.json +1/−0
- test/fixtures/metadata_instruction_data.json +50/−0
- test/fixtures/rpc_responses.json +2052/−1137
- test/fixtures/transactions.json +8/−0
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" } ]