packages feed

solana-haskell-sdk 1.3.0.0 → 1.4.0.0

raw patch · 13 files changed

+1985/−31 lines, 13 filesPVP ok

version bump matches the API change (PVP)

API changes (from Hackage documentation)

- Network.Solana.Core.Instruction: newtype CompileException
+ Network.Solana.Core.Instruction: CompiledInstruction :: Word8 -> CompactArray Word8 -> CompactArray Word8 -> CompiledInstruction
+ Network.Solana.Core.Instruction: InvalidMessage :: String -> CompileException
+ Network.Solana.Core.Instruction: LimitExceeded :: String -> CompileException
+ Network.Solana.Core.Instruction: [ciAccounts] :: CompiledInstruction -> CompactArray Word8
+ Network.Solana.Core.Instruction: [ciData] :: CompiledInstruction -> CompactArray Word8
+ Network.Solana.Core.Instruction: [ciProgramIdIndex] :: CompiledInstruction -> Word8
+ Network.Solana.Core.Instruction: data CompileException
+ Network.Solana.Core.MessageV1: TransactionConfig :: Maybe Word64 -> Maybe Word32 -> Maybe Word32 -> Maybe Word32 -> TransactionConfig
+ Network.Solana.Core.MessageV1: [tcComputeUnitLimit] :: TransactionConfig -> Maybe Word32
+ Network.Solana.Core.MessageV1: [tcHeapSize] :: TransactionConfig -> Maybe Word32
+ Network.Solana.Core.MessageV1: [tcLoadedAccountsDataSizeLimit] :: TransactionConfig -> Maybe Word32
+ Network.Solana.Core.MessageV1: [tcPriorityFee] :: TransactionConfig -> Maybe Word64
+ Network.Solana.Core.MessageV1: compileV1Message :: SolanaPublicKey -> BlockHash -> [Instruction] -> TransactionConfig -> Either CompileException ByteString
+ Network.Solana.Core.MessageV1: data TransactionConfig
+ Network.Solana.Core.MessageV1: emptyTransactionConfig :: TransactionConfig
+ Network.Solana.Core.MessageV1: instance GHC.Classes.Eq Network.Solana.Core.MessageV1.TransactionConfig
+ Network.Solana.Core.MessageV1: instance GHC.Generics.Generic Network.Solana.Core.MessageV1.TransactionConfig
+ Network.Solana.Core.MessageV1: instance GHC.Show.Show Network.Solana.Core.MessageV1.TransactionConfig
+ Network.Solana.Core.MessageV1: newV1TransactionIntent :: SolanaPublicKey -> [SolanaPrivateKey] -> [Instruction] -> TransactionConfig -> BlockHash -> Either CompileException String

Files

CHANGELOG.md view
@@ -1,5 +1,59 @@ # Revision history for solana-haskell-sdk +## 1.4.0.0 -- 2026-10-19++* Added v1 transaction support (SIMD-0385), in the new+  `Network.Solana.Core.MessageV1`: `compileV1Message`,+  `newV1TransactionIntent`, and the `TransactionConfig` record (with+  `emptyTransactionConfig`) carrying a v1 message's priority fee,+  compute-unit limit, loaded-accounts data size limit and heap size.+  v1 lists every address inline (it has no address lookup tables), moves+  resource requests out of `ComputeBudget` instructions and into a header+  configuration mask, places the signatures after the message rather than+  before it, and raises the transaction size limit to 4096 bytes.++  Both entry points take the fee payer explicitly, unlike their legacy and+  v0 counterparts, which derive it from the instruction list. The reference+  implementation's only v1 compile path does the same, and the implicit+  form is a trap in this format: the priority fee travels inside the+  message, so instruction order alone would decide who pays it.++  Compilation rejects a message the network would refuse, or would accept+  and charge for without it being able to do any work:+  `LimitExceeded` for more than 64 addresses, more than 64 instructions,+  more than 12 required signatures, an instruction with more than 255+  accounts or 65535 data bytes, or a transaction over 4096 bytes once+  signed; `InvalidMessage` for no instructions at all, the fee payer named+  as a program id, a compute-unit or loaded-accounts request of zero, or a+  heap request that is not a multiple of 1024 within [32768, 262144].++  Note that in v1 an absent resource request means the *minimum*, not the+  generous default earlier formats use: an absent compute-unit limit means+  0 rather than a per-instruction allowance, and an absent loaded-accounts+  data size limit means 0 rather than 64 MiB. `emptyTransactionConfig` is+  therefore a base for record update rather than a usable configuration,+  and compiling with it unchanged fails.+* Changed (breaking): `CompileException` is no longer a newtype over a+  single `MissingIndex` constructor. It gains `LimitExceeded` and+  `InvalidMessage`, so the v1 checks above report something other than a+  missing index, and callers can discriminate without matching on the+  message text. Existing `MissingIndex` matches still compile but are no+  longer exhaustive.+* Changed (behavioural): `getBlock` and `getTransaction` now send+  `maxSupportedTransactionVersion: 1` instead of `0`. Without it a node+  refuses any block holding a v1 transaction with error -32015, which now+  covers most blocks. A v1 message's `transactionConfig` is not surfaced by+  `CompiledMessage`, which drops it as it already drops a v0 message's+  `addressTableLookups` — and, unlike a legacy transaction's+  `ComputeBudget` instructions, it is not recoverable from the parsed+  instruction list either.+* Added: `Network.Solana.Core.Instruction` now also exports+  `CompiledInstruction`'s constructor and fields (`ciProgramIdIndex`,+  `ciAccounts`, `ciData`), which the v1 serializer needs, since v1 writes+  the fixed-width instruction headers apart from the variable-width+  payloads. Code importing that module unqualified and defining names of+  its own that collide will need a `hiding` clause.+ ## 1.3.0.0 -- 2026-09-13  * Fixed (security): `readSigningKeyFromFile` accepted key files of any length
README.md view
@@ -30,7 +30,7 @@  ## Documentation -API documentation is generated with Haddock (`cabal haddock`). Serialization correctness methodology: every instruction, message, and transaction encoder is asserted byte-identical to vectors generated from the official Rust crates (solana-sdk, spl-token, mpl-token-metadata) — see [tools/README.md](https://github.com/mariusgeorgescu/solana-haskell-sdk/blob/main/tools/README.md).+API documentation is generated with Haddock (`cabal haddock`). Serialization correctness methodology: each instruction, message, and transaction encoder is asserted byte-identical to a golden vector — generated from the official Rust crates (solana-sdk, spl-token, mpl-token-metadata), except for the v1 transaction format, whose vector is a transaction recorded from devnet (the pinned Rust crates predate it) — see [tools/README.md](https://github.com/mariusgeorgescu/solana-haskell-sdk/blob/main/tools/README.md).  ## Integration tests @@ -67,6 +67,7 @@ - [x] Transaction building, signing and submission   - [x] Priority fees via Compute Budget (`setComputeUnitLimit`, `setComputeUnitPrice`)   - [x] Versioned (v0) transactions and Address Lookup Tables+  - [x] v1 transactions (SIMD-0385): inline addresses, header config mask, 4096-byte limit   - [x] Clients for native programs     - [x] Address Lookup Table     - [x] BPF Loader (upgradeable)@@ -82,11 +83,11 @@   - [x] Metaplex Token Metadata (create/update metadata, master edition) - [x] On-chain account state decoders (SPL token accounts and mints, lookup tables, stake and nonce accounts, Metaplex metadata) - [x] PubSub (WebSocket) signature and account subscriptions, with push-based confirmation-- [x] Serialization verified byte-for-byte against the official Rust crates (solana-sdk, spl-token, mpl-token-metadata) (golden-vector tests)+- [x] Serialization verified byte-for-byte by golden-vector tests, against the official Rust crates (solana-sdk, spl-token, mpl-token-metadata) — and, for v1, against a transaction recorded from devnet  ### Release status -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)).+The current stable release is **v1.4.0.0**. Serialization is verified byte-for-byte by golden-vector tests — against the official Rust SDK, and for the v1 format against a transaction recorded from devnet (see test/fixtures/ and [tools/README.md](https://github.com/mariusgeorgescu/solana-haskell-sdk/blob/main/tools/README.md)).  ## Usage Examples 
solana-haskell-sdk.cabal view
@@ -1,6 +1,6 @@ cabal-version:   3.0 name:            solana-haskell-sdk-version:         1.3.0.0+version:         1.4.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@@ -12,7 +12,9 @@    This library is aimed at developers building Solana dApps, tools, or infrastructure in Haskell. -  All serialization is verified byte-for-byte against the official Rust SDK by golden-vector tests.+  Serialization is verified byte-for-byte by golden-vector tests: against vectors generated+  from the official Rust crates, except the v1 (SIMD-0385) transaction format, whose vector is+  a transaction recorded from devnet because the pinned Rust crates predate the format.  license:         Apache-2.0 license-file:    LICENSE@@ -58,6 +60,7 @@     Network.Solana.Core.Crypto     Network.Solana.Core.Instruction     Network.Solana.Core.Message+    Network.Solana.Core.MessageV1     Network.Solana.Core.Pda     Network.Solana.Core.Transaction     Network.Solana.Core.VersionedMessage@@ -180,6 +183,7 @@     Test.Core.Crypto     Test.Core.Instruction     Test.Core.Message+    Test.Core.MessageV1     Test.Core.Pda     Test.Core.VersionedMessage     Test.Fixtures
src/Network/Solana/Core/Instruction.hs view
@@ -16,7 +16,7 @@     AccountMeta (..),     InstructionData (..),     compileInstruction,-    CompiledInstruction,+    CompiledInstruction (..),     CompileException (..),   ) where@@ -189,11 +189,24 @@ -- *** CompileException  --------------------------------------------------------------------------------------------------- | 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+-- | Compilation failure. Each constructor carries a human-readable+-- explanation; the constructor says which kind of problem it is, so callers+-- can react without matching on the text.+data CompileException+  = -- | 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.+    MissingIndex String+  | -- | A count or size exceeds what the transaction format allows: how+    -- many addresses, instructions or signatures a message may carry, how+    -- large a single instruction may be, or how large the whole signed+    -- transaction may be.+    LimitExceeded String+  | -- | The message is structurally well-formed, but the network would+    -- reject it at sanitize time -- or would accept it, charge the fee+    -- payer, and run a transaction that cannot do any work.+    InvalidMessage String   deriving (Show)  instance Exception CompileException
+ src/Network/Solana/Core/MessageV1.hs view
@@ -0,0 +1,352 @@+{-# LANGUAGE RecordWildCards #-}++-- |+-- Module      : Network.Solana.Core.MessageV1+-- Description : v1 message compilation (SIMD-0385) and v1 transaction signing.+--+-- The v1 transaction format carries its resource requests in a header+-- configuration mask instead of @ComputeBudget@ instructions, lists every+-- address inline (it has no address lookup tables), and places the+-- signatures /after/ the message rather than before it. See+-- "Network.Solana.Core.VersionedMessage" for v0, which is the format to use+-- when lookup tables are needed.+--+-- Unlike the legacy and v0 builders, both entry points here take the fee+-- payer explicitly. The reference implementation's only v1 compile path+-- does the same (@v1::Message::try_compile_with_config(payer, ..)@), and+-- deriving the payer implicitly from the instruction list is a trap this+-- format makes expensive: the priority fee travels inside the message, so+-- whoever lands in account 0 pays it.+module Network.Solana.Core.MessageV1+  ( TransactionConfig (..),+    emptyTransactionConfig,+    compileV1Message,+    newV1TransactionIntent,+  )+where++import Data.Binary (Word16, Word32, Word64, Word8, put)+import Data.Binary.Put+import Data.Bits ((.|.))+import Data.ByteString qualified as BS+import Data.ByteString.Lazy qualified as BL+import GHC.Generics (Generic)+import Network.Solana.Core.Block (BlockHash)+import Network.Solana.Core.Compact (unCompact)+import Network.Solana.Core.Crypto+  ( SolanaPrivateKey,+    SolanaPublicKey,+    dsign,+    getSolanaSignatureRaw,+    toBase64String,+  )+import Network.Solana.Core.Instruction+import Network.Solana.Core.Message++------------------------------------------------------------------------------------------------++-- * TransactionConfig++------------------------------------------------------------------------------------------------++-- | The resource requests a v1 message carries in place of the+-- @ComputeBudget@ instructions earlier formats use. Each field present+-- sets its bit in the message's configuration mask and appends its value.+--+-- __An absent field does not mean \"the usual default\"__: SIMD-0385 uses+-- the /minimum/ value instead, which for two of these fields is zero. A+-- transaction requesting zero compute units, or zero bytes of loaded+-- account data, is still accepted into a block and still charges its fee+-- payer -- it simply fails without doing anything. 'compileV1Message'+-- therefore refuses to build one; see 'emptyTransactionConfig'.+--+-- @ComputeBudget@ instructions placed in a v1 transaction are ignored for+-- configuration purposes -- they still execute, as successful no-ops that+-- consume compute units -- so resource requests must be made here.+data TransactionConfig = TransactionConfig+  { -- | Total priority fee, in lamports; absent means 0.+    --+    -- __Not the same unit as @ComputeBudget@__: @SetComputeUnitPrice@ takes+    -- micro-lamports /per compute unit/, while this is a flat total. A+    -- value carried across unconverted overpays by a factor of up to a+    -- million, and the network accepts it. To port one, multiply the price+    -- by the compute-unit limit and divide by 1e6.+    tcPriorityFee :: Maybe Word64,+    -- | Maximum compute units the transaction may consume. Absent means 0,+    -- which cannot execute anything, so this is effectively required.+    tcComputeUnitLimit :: Maybe Word32,+    -- | Maximum bytes of account data the transaction may load. Absent+    -- means 0 -- not the 64 MiB earlier formats default to -- so this is+    -- effectively required too.+    tcLoadedAccountsDataSizeLimit :: Maybe Word32,+    -- | Heap size in bytes; absent means 32 KiB. When given it must be a+    -- multiple of 1024 and within [32768, 262144]; 'compileV1Message'+    -- rejects anything else.+    tcHeapSize :: Maybe Word32+  }+  deriving (Show, Eq, Generic)++-- | A configuration requesting nothing at all.+--+-- This is a base for record update, not a usable configuration:+-- 'compileV1Message' rejects it, because a v1 message with no compute-unit+-- limit and no loaded-accounts limit requests zero of both and so is+-- guaranteed to be charged for and then fail (see 'TransactionConfig').+-- Fill in at least those two fields:+--+-- > emptyTransactionConfig+-- >   { tcComputeUnitLimit = Just 200000,+-- >     tcLoadedAccountsDataSizeLimit = Just 131072+-- >   }+emptyTransactionConfig :: TransactionConfig+emptyTransactionConfig =+  TransactionConfig+    { tcPriorityFee = Nothing,+      tcComputeUnitLimit = Nothing,+      tcLoadedAccountsDataSizeLimit = Nothing,+      tcHeapSize = Nothing+    }++------------------------------------------------------------------------------------------------++-- * Compile v1 message++------------------------------------------------------------------------------------------------++-- | Compile a v1 message for the given fee payer: assemble the account keys+-- exactly as the legacy and v0 builders do (privilege union, then canonical+-- ordering, via 'mkNewMessageWithPayer', which seeds the payer as a writable+-- signer and pins it to account 0), then serialize them in the v1 layout+-- behind the @129@ version byte. Naming the payer also supports sponsored+-- fees: it need not appear in any instruction.+--+-- Fails with a 'CompileException' when the result would not be a usable v1+-- message:+--+-- * 'LimitExceeded' -- more than 64 addresses, more than 64 instructions,+--   more than 12 required signatures, an instruction with more than 255+--   accounts or more than 65535 data bytes, or a transaction that would+--   exceed 4096 bytes once signed.+-- * 'InvalidMessage' -- no instructions at all, the fee payer used as a+--   program id, a compute-unit or loaded-accounts request of zero (see+--   'TransactionConfig'), or a heap request that is not a multiple of 1024+--   within [32768, 262144].+compileV1Message ::+  SolanaPublicKey -> BlockHash -> [Instruction] -> TransactionConfig -> Either CompileException BS.ByteString+compileV1Message payer bh instructions =+  compileV1FromLegacy (mkNewMessageWithPayer payer bh instructions)++-- | Build and sign a v1 transaction for the given fee payer, returning it+-- Base64-encoded. A v1 transaction puts the message first and the+-- signatures last, as a bare sequence of 64-byte signatures with no length+-- prefix -- the reverse of the legacy and v0 envelopes, and the reason the+-- signature count has to match the message's required signers exactly:+-- there is no count on the wire to describe a mismatch.+--+-- The given private keys may be listed in any order. They are matched+-- against the compiled message's required signers and placed in message+-- order automatically, so a key for the wrong account, a missing key or a+-- spare key is a 'MissingIndex' failure rather than a silently malformed+-- transaction.+newV1TransactionIntent ::+  SolanaPublicKey ->+  [SolanaPrivateKey] ->+  [Instruction] ->+  TransactionConfig ->+  BlockHash ->+  Either CompileException String+newV1TransactionIntent payer signingKeys instructions config bh = do+  let legacy = mkNewMessageWithPayer payer bh instructions+  msg <- compileV1FromLegacy legacy config+  orderedKeys <- orderSigningKeys legacy signingKeys+  return $ toBase64String (msg <> signaturesOver msg orderedKeys)++------------------------------------------------------------------------------------------------++-- * Internal helpers++------------------------------------------------------------------------------------------------++-- | Serialize an assembled legacy message in the v1 layout.+--+-- The header's @numReadonlySignedAccounts < numRequiredSignatures@ sanitize+-- rule needs no check here: every caller routes through+-- 'mkNewMessageWithPayer', which puts the payer in the writable-signer+-- section and never demotes it, so that section is never empty.+compileV1FromLegacy :: Message -> TransactionConfig -> Either CompileException BS.ByteString+compileV1FromLegacy legacy config = do+  compiled <- mapM (compileInstruction keys) (mInstructions legacy)+  checkLimits legacy compiled config+  let bytes = BL.toStrict . runPut $ do+        putWord8 v1Prefix+        put (mHeader legacy)+        putWord32le (configMask config)+        put (mRecentBlockhash legacy)+        putWord8 (fromIntegral (length compiled))+        putWord8 (fromIntegral (length keys))+        mapM_ put keys+        putConfigValues config+        mapM_ putInstructionHeader compiled+        mapM_ putInstructionPayload compiled+  checkTransactionSize legacy bytes+  return bytes+  where+    keys = mAccountKeys legacy++-- | The trailing signature array: each signer's 64 raw signature bytes over+-- the whole serialized message, concatenated without a length prefix.+signaturesOver :: BS.ByteString -> [SolanaPrivateKey] -> BS.ByteString+signaturesOver msg signers = mconcat (getSolanaSignatureRaw . flip dsign msg <$> signers)++-- | The version byte a v1 transaction opens with.+v1Prefix :: Word8+v1Prefix = 129++-- | The configuration mask: one bit per requested field, except the+-- priority fee, which occupies two (both must be set, and its value is+-- eight bytes wide rather than four).+--+-- The compute-unit and loaded-accounts bits are only distinguishable from+-- each other when exactly one of the two is present, which 'checkLimits'+-- forbids -- so no test can pin them apart while that guard stands. Relax+-- the guard and they need pinning.+configMask :: TransactionConfig -> Word32+configMask TransactionConfig {..} =+  bitFor tcPriorityFee 0b11+    .|. bitFor tcComputeUnitLimit 0b100+    .|. bitFor tcLoadedAccountsDataSizeLimit 0b1000+    .|. bitFor tcHeapSize 0b10000+  where+    bitFor field bits = maybe 0 (const bits) field++-- | The requested values, in field order, each present only when its mask+-- bit is set.+putConfigValues :: TransactionConfig -> Put+putConfigValues TransactionConfig {..} = do+  mapM_ putWord64le tcPriorityFee+  mapM_ putWord32le tcComputeUnitLimit+  mapM_ putWord32le tcLoadedAccountsDataSizeLimit+  mapM_ putWord32le tcHeapSize++-- | One instruction's fixed-width header: program id index, account count,+-- and data length. All headers precede all payloads on the wire.+putInstructionHeader :: CompiledInstruction -> Put+putInstructionHeader CompiledInstruction {..} = do+  putWord8 ciProgramIdIndex+  putWord8 (fromIntegral (length (unCompact ciAccounts)))+  putWord16le (fromIntegral (length (unCompact ciData)))++-- | One instruction's variable-width payload: its account indices followed+-- by its data bytes, neither length-prefixed (the header carries both+-- lengths).+putInstructionPayload :: CompiledInstruction -> Put+putInstructionPayload CompiledInstruction {..} = do+  mapM_ putWord8 (unCompact ciAccounts)+  mapM_ putWord8 (unCompact ciData)++-- | Everything the builder can check before serializing: the format's count+-- caps, the sanitize rules reachable from here, and the resource requests+-- that would make the transaction pay to do nothing.+checkLimits :: Message -> [CompiledInstruction] -> TransactionConfig -> Either CompileException ()+checkLimits legacy compiled TransactionConfig {..} = do+  limit "addresses" maxAddresses (length (mAccountKeys legacy))+  limit "instructions" maxInstructions (length compiled)+  limit "required signatures" maxSignatures (fromIntegral (numRequiredSignatures (mHeader legacy)))+  checkNonEmpty+  mapM_ checkInstruction compiled+  checkRequest "compute-unit limit" tcComputeUnitLimit+  checkRequest "loaded-accounts data size limit" tcLoadedAccountsDataSizeLimit+  mapM_ checkHeapSize tcHeapSize+  where+    limit what cap actual+      | actual <= cap = Right ()+      | otherwise =+          Left+            ( LimitExceeded+                ("v1 message has " <> show actual <> " " <> what <> ", more than the " <> show cap <> " allowed")+            )+    checkNonEmpty+      | null compiled = Left (InvalidMessage "v1 message has no instructions, so it would pay a fee to do nothing")+      | otherwise = Right ()+    checkInstruction CompiledInstruction {..} = do+      limit "accounts in one instruction" maxInstructionAccounts (length (unCompact ciAccounts))+      limit "data bytes in one instruction" maxInstructionData (length (unCompact ciData))+      if ciProgramIdIndex == 0+        then Left (InvalidMessage "v1 instruction invokes account 0, but the fee payer cannot be a program id")+        else Right ()+    -- Absent or zero are the same request on the wire, and both guarantee+    -- an included, charged, failing transaction -- so both are refused.+    checkRequest what request+      | request `elem` [Nothing, Just 0] =+          Left+            ( InvalidMessage+                ( "v1 message requests a "+                    <> what+                    <> " of 0, which cannot execute anything; set it in the TransactionConfig"+                )+            )+      | otherwise = Right ()+    checkHeapSize n+      | n `mod` 1024 /= 0 =+          Left (InvalidMessage ("v1 heap size " <> show n <> " is not a multiple of 1024"))+      | n < minHeapSize =+          Left (InvalidMessage ("v1 heap size " <> show n <> " is below the minimum of " <> show minHeapSize))+      | n > maxHeapSize =+          Left (InvalidMessage ("v1 heap size " <> show n <> " is above the maximum of " <> show maxHeapSize))+      | otherwise = Right ()++-- | Reject a message whose transaction -- the message plus one 64-byte+-- signature per required signer -- would exceed the v1 size cap. Checked on+-- the message, since the header fixes the signature count and+-- 'newV1TransactionIntent' signs exactly that many.+checkTransactionSize :: Message -> BS.ByteString -> Either CompileException ()+checkTransactionSize legacy bytes+  | total <= maxTransactionSize = Right ()+  | otherwise =+      Left+        ( LimitExceeded+            ( "v1 transaction is "+                <> show total+                <> " bytes, more than the "+                <> show maxTransactionSize+                <> " allowed"+            )+        )+  where+    signatures = signatureSize * fromIntegral (numRequiredSignatures (mHeader legacy))+    total = BS.length bytes + signatures++-- | Addresses are counted by a single byte and capped at 64 by SIMD-0385.+maxAddresses :: Int+maxAddresses = 64++-- | Instructions are counted by a single byte and capped at 64.+maxInstructions :: Int+maxInstructions = 64++-- | A v1 transaction may carry at most 12 signatures.+maxSignatures :: Int+maxSignatures = 12++-- | An instruction's account count is a single byte.+maxInstructionAccounts :: Int+maxInstructionAccounts = fromIntegral (maxBound :: Word8)++-- | An instruction's data length is a 'Word16'.+maxInstructionData :: Int+maxInstructionData = fromIntegral (maxBound :: Word16)++-- | A v1 transaction may be at most 4096 bytes on the wire (SIMD-0296),+-- against the 1232 of earlier formats.+maxTransactionSize :: Int+maxTransactionSize = 4096++-- | One ed25519 signature.+signatureSize :: Int+signatureSize = 64++minHeapSize :: Word32+minHeapSize = 32 * 1024++maxHeapSize :: Word32+maxHeapSize = 256 * 1024
src/Network/Solana/RPC/HTTP/Block.hs view
@@ -46,16 +46,24 @@ {-# INLINE getBlock' #-}  -- | Returns identity and transaction information about a confirmed block in--- the ledger. Sets @maxSupportedTransactionVersion@ so that blocks--- containing versioned (v0) transactions can be decoded (without it, such--- blocks make the node respond with error -32015); as a result, v0--- transactions are returned without their address-lookup-table data, so--- instruction account indices may reference addresses not present in the--- parsed account keys.+-- the ledger. Sets @maxSupportedTransactionVersion@ to 1, so that blocks+-- containing v0 or v1 (SIMD-0385) transactions can be decoded -- without a+-- high enough value such blocks make the node respond with error -32015,+-- and v1 transactions are now a routine part of blocks.+--+-- Versioned transactions are returned without the message parts+-- t'Network.Solana.Core.Message.CompiledMessage' does not model: a v0+-- transaction's address-lookup-table data (so instruction account indices+-- may reference addresses not present in the parsed account keys), and a v1+-- transaction's @transactionConfig@ resource requests (its priority fee,+-- compute-unit limit, loaded-accounts size limit and heap size -- which in+-- v1 replace the @ComputeBudget@ instructions earlier formats carry, so+-- unlike a legacy transaction's, they are not visible among the parsed+-- instructions either). -- 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})+getBlock slot = getBlock' slot (defaultConfigObject {encoding = Just "json", maxSupportedTransactionVersion = Just 1}) {-# INLINE getBlock #-}  -- | Contains identity and transaction information for a block.
src/Network/Solana/RPC/HTTP/Transaction.hs view
@@ -55,14 +55,19 @@ {-# INLINE getTransaction' #-}  -- | Returns metadata and content for a confirmed transaction identified by--- its signature. Sets @maxSupportedTransactionVersion@ so that versioned--- (v0) transactions can be decoded (without it, the node responds with--- error -32015); as a result, v0 transactions are returned without their--- address-lookup-table data, so instruction account indices may reference--- addresses not present in the parsed account keys.+-- its signature. Sets @maxSupportedTransactionVersion@ to 1, so that v0 and+-- v1 (SIMD-0385) transactions can be decoded -- without a high enough value+-- the node responds with error -32015.+--+-- Versioned transactions are returned without the message parts+-- t'Network.Solana.Core.Message.CompiledMessage' does not model: a v0+-- transaction's address-lookup-table data (so instruction account indices+-- may reference addresses not present in the parsed account keys), and a v1+-- transaction's @transactionConfig@ resource requests (see+-- 'Network.Solana.RPC.HTTP.Block.getBlock'). -- Returns 'Nothing' if the signature is unknown or has expired. getTransaction :: (JsonRpc m) => SolanaSignature -> m (Maybe TransactionResult)-getTransaction sig = getTransaction' sig (defaultConfigObject {encoding = Just "json", maxSupportedTransactionVersion = Just 0})+getTransaction sig = getTransaction' sig (defaultConfigObject {encoding = Just "json", maxSupportedTransactionVersion = Just 1}) {-# INLINE getTransaction #-}  -- | Contains metadata and transaction information returned by 'getTransaction'.
src/Network/Solana/RPC/HTTP/Types.hs view
@@ -94,9 +94,14 @@     minContextSlot :: Maybe Int,     -- | Whether to search older confirmed transaction history.     searchTransactionHistory :: Maybe Bool,-    -- | The maximum transaction version to return in responses. Required to-    -- fetch blocks or transactions containing versioned (v0) transactions;-    -- omitting it causes the RPC to reject them with error -32015.+    -- | The maximum transaction version to return in responses. A node+    -- rejects a block or transaction whose version exceeds this with error+    -- -32015, and omitting it admits legacy transactions only, so fetching+    -- v0 requires at least @0@ and v1 (SIMD-0385) at least @1@. The+    -- bindings' own defaults send @1@.+    --+    -- A value of @1@ or above cannot be combined with @base58@ encoding;+    -- a current node answers that combination with @invalid_params@.     maxSupportedTransactionVersion :: Maybe Int   }   deriving (Generic, Show)
test/Main.hs view
@@ -7,6 +7,7 @@ import Test.Core.Crypto qualified import Test.Core.Instruction qualified import Test.Core.Message qualified+import Test.Core.MessageV1 qualified import Test.Core.Pda qualified import Test.Core.VersionedMessage qualified import Test.Metaplex.TokenMetadata qualified@@ -40,6 +41,7 @@           Test.Core.Crypto.tests,           Test.Core.Instruction.tests,           Test.Core.Message.tests,+          Test.Core.MessageV1.tests,           Test.Core.Pda.tests,           Test.Core.VersionedMessage.tests,           Test.Metaplex.TokenMetadata.tests,
+ test/Test/Core/MessageV1.hs view
@@ -0,0 +1,475 @@+{-# LANGUAGE LambdaCase #-}+{-# LANGUAGE OverloadedStrings #-}++-- | Golden and behaviour tests for v1 (SIMD-0385) message compilation.+--+-- The golden vector is a real transaction recorded from devnet+-- (@test\/fixtures\/v1_transactions.json@), not a hand-written sample: its+-- inputs are fed back through the builder and the result must match the+-- bytes the network actually carried. That is how the layout stays pinned+-- while @tools\/fixture-gen@ remains on its solana-sdk 1.18 pin, which+-- predates the format and so cannot generate a vector for it.+--+-- One recorded transaction cannot pin everything, so the cases after it+-- cover what it structurally cannot: it has a single instruction (so it+-- cannot tell all-headers-then-all-payloads apart from interleaved), a+-- single signer (so it cannot tell signature slots apart), and no heap+-- request. Each limit is pinned by a pair of cases straddling it rather+-- than by matching a number in an error message.+module Test.Core.MessageV1 (tests) where++import Data.Binary (Binary (..), Word8)+import Data.Binary.Put (putByteString)+import Data.ByteString qualified as BS+import Data.List (isInfixOf)+import Network.Solana.Core.Block (BlockHash (..))+import Network.Solana.Core.Crypto+import Network.Solana.Core.Instruction (AccountMeta (..), CompileException (..), Instruction, mkInstruction)+import Network.Solana.Core.MessageV1+import Network.Solana.NativePrograms.SystemProgram qualified as SP+import Test.Fixtures+import Test.Tasty+import Test.Tasty.HUnit++-- | Instruction data passed through verbatim; 'mkInstruction' encodes its+-- payload with 'Binary', and the standard instances for 'BS.ByteString' and+-- lists prepend a length the wire format must not carry.+newtype RawData = RawData BS.ByteString++instance Binary RawData where+  put (RawData bs) = putByteString bs+  get = fail "RawData: decoding not used"++------------------------------------------------------------------------------------------------+-- The recorded devnet transaction, as its own inputs.+------------------------------------------------------------------------------------------------++-- | Fee payer: the single required signer, writable.+devnetPayer :: SolanaPublicKey+devnetPayer = unsafeSolanaPublicKey "4QVtKaZkDhEwKszzJWa7LJPZBFkkcgUVRG4xE3fkWNGt"++-- | The one writable unsigned account.+devnetWritable :: SolanaPublicKey+devnetWritable = unsafeSolanaPublicKey "55gx5ra96qkYXXMGShkVjwQz1k4j9bS26HhdksFa9buL"++-- | The invoked program.+devnetProgram :: SolanaPublicKey+devnetProgram = unsafeSolanaPublicKey "MktiyFj9mVdLQgj45AXvJdoaHvF9PkuU1UmA2uwPJWM"++devnetClock :: SolanaPublicKey+devnetClock = unsafeSolanaPublicKey "SysvarC1ock11111111111111111111111111111111"++devnetBlockhash :: BlockHash+devnetBlockhash = BlockHash (requireBase58 "4iSS7zQNeY8BTSWZ9VRMBo8B17AyijyLDD8C2EycoW19")++-- | The recorded instruction's 90 data bytes.+devnetInstructionData :: BS.ByteString+devnetInstructionData =+  requireBase58+    "2isQdwGHZhqBC7JtD5qgwY3fpHY2d1ciRmLgt9KN5x75VY2UxdpWv67RMmcvfJCqk5SAeAV4zDVqkkrgrJga5gREAVsRKJUhgqq5ZxqV2rNZhRkuobuQtQubPy9"++-- | @priorityFee = 0@, @computeUnitLimit = 324@,+-- @loadedAccountsDataSizeLimit = 43008@, no heap request -- config mask 0xF.+devnetConfig :: TransactionConfig+devnetConfig =+  TransactionConfig+    { tcPriorityFee = Just 0,+      tcComputeUnitLimit = Just 324,+      tcLoadedAccountsDataSizeLimit = Just 43008,+      tcHeapSize = Nothing+    }++requireBase58 :: String -> BS.ByteString+requireBase58 s = maybe (error ("bad base58: " <> s)) id (fromBase58String s)++tests :: TestTree+tests =+  testGroup+    "v1 message compilation (SIMD-0385)"+    [ testCase "message bytes match a transaction recorded from devnet" $ do+        (expectedMessage, signature) <- devnetGolden+        let ix =+              mkInstruction+                devnetProgram+                [ AccountMeta devnetPayer True True,+                  AccountMeta devnetClock False False,+                  AccountMeta devnetWritable False True+                ]+                (RawData devnetInstructionData)+        bytes <- requireRight (compileV1Message devnetPayer devnetBlockhash [ix] devnetConfig)+        bytes @?= expectedMessage+        -- Cross-check the fixture against itself: the bytes the golden+        -- compares to must be exactly what the recorded signature signs, so+        -- a re-recording with a different signer count cannot silently+        -- shift the split point.+        assertBool+          "the recorded signature verifies over the recorded message"+          (dverify devnetPayer expectedMessage signature),+      testGroup "wire layout the recorded transaction cannot pin" layoutTests,+      testGroup "configuration mask" maskTests,+      testGroup "signatures" signatureTests,+      testGroup "limits" limitTests,+      testGroup "refused as unusable" unusableTests+    ]++-- | The recorded transaction, split into the message and its one signature+-- using the signer count the header itself declares (byte 1) rather than a+-- hardcoded offset.+devnetGolden :: IO (BS.ByteString, SolanaSignature)+devnetGolden = do+  fs <- loadFixtures "test/fixtures/v1_transactions.json"+  let wire = requireFixture "devnet-v1-transaction" fs+      signers = fromIntegral (BS.index wire 1)+      msgLen = BS.length wire - 64 * signers+  signers @?= (1 :: Int)+  pure (BS.take msgLen wire, unsafeSigFromString (toBase58String (BS.drop msgLen wire)))++------------------------------------------------------------------------------------------------+-- Layout+------------------------------------------------------------------------------------------------++layoutTests :: [TestTree]+layoutTests =+  [ testCase "all instruction headers precede all payloads" $ do+      -- Three instructions with distinct account counts (1, 2, 3) and+      -- distinct data lengths (1, 2, 3). Interleaving each header with its+      -- own payload -- the legacy and v0 layout, and so the likeliest wrong+      -- edit -- puts an account index where the second header belongs, and+      -- this assertion fails.+      let ix n dat =+            mkInstruction+              layoutProgram+              (AccountMeta payerPk True True : [AccountMeta k False True | k <- take (n - 1) [keyA, keyB]])+              (RawData (BS.pack dat))+          instrs = [ix 1 [0xA0], ix 2 [0xB0, 0xB1], ix 3 [0xC0, 0xC1, 0xC2]]+      bytes <- requireRight (compileV1Message payerPk fixedBlockhash instrs minimalConfig)+      -- payer, keyA, keyB are accounts 0..2 and the program is account 3.+      let headers = [3, 1, 1, 0, 3, 2, 2, 0, 3, 3, 3, 0]+          payloads = [0, 0xA0, 0, 1, 0xB0, 0xB1, 0, 1, 2, 0xC0, 0xC1, 0xC2]+          expected = BS.pack (headers <> payloads)+      BS.drop (BS.length bytes - BS.length expected) bytes @?= expected+  ]++------------------------------------------------------------------------------------------------+-- Configuration mask+------------------------------------------------------------------------------------------------++-- | The mask bytes and the config values sit immediately after the+-- addresses; @addressesEnd@ is where the values begin.+maskTests :: [TestTree]+maskTests =+  [ maskCase "compute-unit and loaded-accounts limits only" minimalConfig 0b1100 8,+    maskCase "with a priority fee (two bits, eight bytes)" minimalConfig {tcPriorityFee = Just 7} 0b1111 16,+    maskCase "with a heap request" minimalConfig {tcHeapSize = Just (64 * 1024)} 0b11100 12,+    maskCase+      "with every field set"+      minimalConfig {tcPriorityFee = Just 7, tcHeapSize = Just (64 * 1024)}+      0b11111+      20,+    testCase "values are little-endian and in field order" $ do+      let full =+            TransactionConfig+              { tcPriorityFee = Just 0x1122334455667788,+                tcComputeUnitLimit = Just 0x0A0B0C0D,+                tcLoadedAccountsDataSizeLimit = Just 0x01020304,+                tcHeapSize = Just (64 * 1024)+              }+      bytes <- requireRight (compileV1Message payerPk fixedBlockhash [simpleInstruction] full)+      assertEqual+        "priority fee u64, then three u32s"+        ( BS.pack+            [ 0x88, 0x77, 0x66, 0x55, 0x44, 0x33, 0x22, 0x11,+              0x0d, 0x0c, 0x0b, 0x0a,+              0x04, 0x03, 0x02, 0x01,+              0x00, 0x00, 0x01, 0x00+            ]+        )+        (BS.take 20 (BS.drop (valuesOffset bytes) bytes))+  ]++-- | Assert a configuration's mask bytes and the number of value bytes it+-- appends. The byte count is what distinguishes the two-bit, eight-byte+-- priority fee from the one-bit, four-byte fields.+maskCase :: String -> TransactionConfig -> Word -> Int -> TestTree+maskCase name config mask valueBytes =+  testCase name $ do+    bytes <- requireRight (compileV1Message payerPk fixedBlockhash [simpleInstruction] config)+    assertEqual "mask" (BS.pack [fromIntegral mask, fromIntegral (mask `div` 256), 0, 0]) (BS.take 4 (BS.drop 4 bytes))+    bare <- requireRight (compileV1Message payerPk fixedBlockhash [simpleInstruction] minimalConfig)+    -- Every configuration here differs from the baseline only in its value+    -- bytes, so the length difference is exactly the extra values.+    BS.length bytes - BS.length bare @?= valueBytes - 8++-- | Where the config values start: the 42-byte fixed header followed by the+-- address array, whose length byte sits at offset 41.+valuesOffset :: BS.ByteString -> Int+valuesOffset bytes = 42 + 32 * fromIntegral (BS.index bytes 41)++------------------------------------------------------------------------------------------------+-- Signatures+------------------------------------------------------------------------------------------------++signatureTests :: [TestTree]+signatureTests =+  [ testCase "one signature follows the message, unprefixed, and verifies over it" $ do+      msg <- requireRight (compileV1Message payerPk fixedBlockhash [simpleInstruction] minimalConfig)+      signed <- requireRight (newV1TransactionIntent payerPk [payerSk] [simpleInstruction] minimalConfig fixedBlockhash)+      let wire = fromBase64String signed+      BS.length wire @?= BS.length msg + 64+      assertEqual "message comes first" msg (BS.take (BS.length msg) wire)+      assertBool+        "signature verifies over the message bytes"+        (dverify payerPk msg (sigAt msg wire 0)),+      testCase "two signatures occupy their signers' account slots, in message order" $ do+        -- co-signer is seeded writable so both are writable signers; the+        -- payer is pinned to account 0 and the other follows.+        let ix = mkInstruction layoutProgram [AccountMeta payerPk True True, AccountMeta coPk True True] ()+        msg <- requireRight (compileV1Message payerPk fixedBlockhash [ix] minimalConfig)+        signed <- requireRight (newV1TransactionIntent payerPk [payerSk, coSk] [ix] minimalConfig fixedBlockhash)+        let wire = fromBase64String signed+        BS.length wire @?= BS.length msg + 128+        assertBool "slot 0 is the fee payer's" (dverify payerPk msg (sigAt msg wire 0))+        assertBool "slot 1 is the co-signer's" (dverify coPk msg (sigAt msg wire 1))+        assertBool "slot 0 is not the co-signer's" (not (dverify coPk msg (sigAt msg wire 0)))+        -- The keys are matched to the message's signers, so the order they+        -- are supplied in cannot change the bytes.+        reversed <-+          requireRight (newV1TransactionIntent payerPk [coSk, payerSk] [ix] minimalConfig fixedBlockhash)+        reversed @?= signed,+      testCase "a key for no required signer is refused" $+        assertRejected+          "unused signing key"+          (newV1TransactionIntent payerPk [payerSk, coSk] [simpleInstruction] minimalConfig fixedBlockhash),+      testCase "a missing signer is refused" $ do+        let ix = mkInstruction layoutProgram [AccountMeta payerPk True True, AccountMeta coPk True True] ()+        assertRejected "missing signer" (newV1TransactionIntent payerPk [payerSk] [ix] minimalConfig fixedBlockhash)+  ]++-- | The @n@th 64-byte signature slot of a signed transaction.+sigAt :: BS.ByteString -> BS.ByteString -> Int -> SolanaSignature+sigAt msg wire n =+  unsafeSigFromString (toBase58String (BS.take 64 (BS.drop (BS.length msg + 64 * n) wire)))++------------------------------------------------------------------------------------------------+-- Limits, each pinned by a pair of cases straddling it+------------------------------------------------------------------------------------------------++limitTests :: [TestTree]+limitTests =+  [ testCase "64 addresses compile, 65 do not" $ do+      _ <- requireRight (compileV1Message payerPk fixedBlockhash [wideInstruction 62] minimalConfig)+      assertRejected "addresses" (compileV1Message payerPk fixedBlockhash [wideInstruction 63] minimalConfig),+    testCase "64 instructions compile, 65 do not" $ do+      _ <- requireRight (compileV1Message payerPk fixedBlockhash (replicate 64 simpleInstruction) minimalConfig)+      assertRejected+        "instructions"+        (compileV1Message payerPk fixedBlockhash (replicate 65 simpleInstruction) minimalConfig),+    testCase "255 accounts in one instruction compile, 256 do not" $ do+      _ <- requireRight (compileV1Message payerPk fixedBlockhash [repeatedAccounts 255] minimalConfig)+      assertRejected+        "accounts in one instruction"+        (compileV1Message payerPk fixedBlockhash [repeatedAccounts 256] minimalConfig),+    testCase "12 signatures compile, 13 do not" $ do+      _ <- requireRight (compileV1Message payerPk fixedBlockhash [manySigners 12] minimalConfig)+      assertRejected+        "required signatures"+        (compileV1Message payerPk fixedBlockhash [manySigners 13] minimalConfig),+    testCase "a transaction of exactly 4096 bytes compiles, 4097 does not" $ do+      bytes <- requireRight (compileV1Message payerPk fixedBlockhash [bigDataInstruction atCap] minimalConfig)+      -- One required signer, so the signed transaction is 64 bytes longer.+      BS.length bytes + 64 @?= 4096+      assertRejected+        "4096"+        (compileV1Message payerPk fixedBlockhash [bigDataInstruction (atCap + 1)] minimalConfig),+    testCase "the size cap counts one signature per required signer" $ do+      -- With 12 signers the signature array is 768 bytes, so a message that+      -- would fit comfortably on its own must still be refused. A size+      -- check that ignored the signer count would accept this.+      let twelve n = mkInstruction layoutProgram (signerMetas 12) (RawData (BS.replicate n 7))+      big <- pure (compileV1Message payerPk fixedBlockhash [twelve 3000] minimalConfig)+      assertRejected "4096" big+      small <- requireRight (compileV1Message payerPk fixedBlockhash [twelve 2000] minimalConfig)+      assertBool "the accepted one leaves room for 12 signatures" (BS.length small + 12 * 64 <= 4096),+    testCase "the heap request's bounds are inclusive" $ do+      _ <- requireRight (compileV1Message payerPk fixedBlockhash [simpleInstruction] (heap 32768))+      _ <- requireRight (compileV1Message payerPk fixedBlockhash [simpleInstruction] (heap 262144))+      assertRejected "below the minimum" (compileV1Message payerPk fixedBlockhash [simpleInstruction] (heap 31744))+      assertRejected "above the maximum" (compileV1Message payerPk fixedBlockhash [simpleInstruction] (heap 263168))+      assertRejected+        "multiple of 1024"+        (compileV1Message payerPk fixedBlockhash [simpleInstruction] (heap 33000))+  ]+  where+    heap n = minimalConfig {tcHeapSize = Just n}++-- | The instruction data length that makes the signed transaction exactly+-- 4096 bytes: the cap, less one signature, the 42-byte fixed header, three+-- 32-byte addresses, eight config bytes, one instruction header and the two+-- account indices.+atCap :: Int+atCap = 4096 - 64 - 42 - 3 * 32 - 8 - 4 - 2++------------------------------------------------------------------------------------------------+-- Refused as unusable+------------------------------------------------------------------------------------------------++unusableTests :: [TestTree]+unusableTests =+  [ testCase "an empty instruction list" $+      assertRejected "no instructions" (compileV1Message payerPk fixedBlockhash [] minimalConfig),+    testCase "a config requesting nothing" $+      assertRejected+        "compute-unit limit of 0"+        (compileV1Message payerPk fixedBlockhash [simpleInstruction] emptyTransactionConfig),+    testCase "an absent compute-unit limit" $+      assertRejected+        "compute-unit limit of 0"+        ( compileV1Message+            payerPk+            fixedBlockhash+            [simpleInstruction]+            minimalConfig {tcComputeUnitLimit = Nothing}+        ),+    testCase "an explicitly zero compute-unit limit" $+      assertRejected+        "compute-unit limit of 0"+        ( compileV1Message+            payerPk+            fixedBlockhash+            [simpleInstruction]+            minimalConfig {tcComputeUnitLimit = Just 0}+        ),+    testCase "an absent loaded-accounts data size limit" $+      assertRejected+        "loaded-accounts data size limit of 0"+        ( compileV1Message+            payerPk+            fixedBlockhash+            [simpleInstruction]+            minimalConfig {tcLoadedAccountsDataSizeLimit = Nothing}+        ),+    testCase "an explicitly zero loaded-accounts data size limit" $+      assertRejected+        "loaded-accounts data size limit of 0"+        ( compileV1Message+            payerPk+            fixedBlockhash+            [simpleInstruction]+            minimalConfig {tcLoadedAccountsDataSizeLimit = Just 0}+        ),+    testCase "the fee payer named as a program id" $+      assertRejected+        "cannot be a program id"+        ( compileV1Message+            layoutProgram+            fixedBlockhash+            [mkInstruction layoutProgram [AccountMeta payerPk False True] ()]+            minimalConfig+        )+  ]++------------------------------------------------------------------------------------------------+-- Fixtures and helpers+------------------------------------------------------------------------------------------------++-- | The smallest configuration the builder accepts: both effectively+-- required fields set, nothing else.+minimalConfig :: TransactionConfig+minimalConfig =+  emptyTransactionConfig+    { tcComputeUnitLimit = Just 200000,+      tcLoadedAccountsDataSizeLimit = Just 131072+    }++-- | An instruction touching @n@ distinct writable accounts, so the compiled+-- message carries @n + 2@ addresses (the payer and the program).+wideInstruction :: Int -> Instruction+wideInstruction n =+  mkInstruction+    layoutProgram+    (AccountMeta payerPk True True : [AccountMeta (numberedKey i) False True | i <- [1 .. n]])+    ()++-- | An instruction naming the same account @n@ times, so its account-index+-- count can exceed the message's address count.+repeatedAccounts :: Int -> Instruction+repeatedAccounts n =+  mkInstruction layoutProgram (AccountMeta payerPk True True : replicate (n - 1) (AccountMeta keyA False True)) ()++-- | An instruction requiring @n@ signatures.+manySigners :: Int -> Instruction+manySigners n = mkInstruction layoutProgram (signerMetas n) ()++signerMetas :: Int -> [AccountMeta]+signerMetas n = AccountMeta payerPk True True : [AccountMeta (numberedKey i) True True | i <- [1 .. n - 1]]++-- | An instruction carrying @n@ data bytes and two account indices.+bigDataInstruction :: Int -> Instruction+bigDataInstruction n =+  mkInstruction+    layoutProgram+    [AccountMeta payerPk True True, AccountMeta keyA False True]+    (RawData (BS.replicate n 7))++simpleInstruction :: Instruction+simpleInstruction = SP.transfer payerPk keyA 1++payerKeys :: (SolanaPublicKey, SolanaPrivateKey)+payerKeys = seededKeypair 1++coSignerKeys :: (SolanaPublicKey, SolanaPrivateKey)+coSignerKeys = seededKeypair 2++payerPk :: SolanaPublicKey+payerPk = fst payerKeys++payerSk :: SolanaPrivateKey+payerSk = snd payerKeys++coPk :: SolanaPublicKey+coPk = fst coSignerKeys++coSk :: SolanaPrivateKey+coSk = snd coSignerKeys++seededKeypair :: Word8 -> (SolanaPublicKey, SolanaPrivateKey)+seededKeypair seed =+  case createSolanaKeypairFromSeed (BS.replicate 32 seed) of+    Just kp -> kp+    Nothing -> error ("failed to derive keypair from seed " <> show seed)++-- | Two ordinary accounts, chosen so that their raw bytes sort in this+-- order (the layout test depends on their account indices).+keyA, keyB :: SolanaPublicKey+keyA = unsafeSolanaPublicKeyRaw (5 : replicate 31 0)+keyB = unsafeSolanaPublicKeyRaw (6 : replicate 31 0)++-- | A program id sorting after 'keyA' and 'keyB', so it is the last+-- address.+layoutProgram :: SolanaPublicKey+layoutProgram = unsafeSolanaPublicKeyRaw (9 : replicate 31 0)++fixedBlockhash :: BlockHash+fixedBlockhash = BlockHash (BS.replicate 32 9)++-- | A distinct 32-byte key per number, for large key sets.+numberedKey :: Int -> SolanaPublicKey+numberedKey b = unsafeSolanaPublicKeyRaw (3 : fromIntegral (b `div` 256) : fromIntegral (b `mod` 256) : replicate 29 0)++requireRight :: Either CompileException a -> IO a+requireRight = either (assertFailure . show) pure++-- | Assert a compile failed, and that its message names the reason -- so a+-- rejection for an unrelated reason cannot pass for the right one.+assertRejected :: String -> Either CompileException a -> Assertion+assertRejected expected = \case+  Right _ -> assertFailure ("expected rejection mentioning " <> show expected)+  Left err ->+    let msg = compileExceptionMessage err+     in assertBool ("expected " <> show expected <> " in: " <> msg) (expected `isInfixOf` msg)++compileExceptionMessage :: CompileException -> String+compileExceptionMessage = \case+  MissingIndex m -> m+  LimitExceeded m -> m+  InvalidMessage m -> m
test/Test/RPC/Parsers.hs view
@@ -5,8 +5,10 @@  -- | Golden tests for the RPC response parsers. ----- Every fixture here is a real response captured from a local validator by--- @tools/rpc-record/record.py@ -- not a hand-written sample -- so these tests+-- Every fixture here is a real response captured from a node -- not a+-- hand-written sample: the local validator via+-- @tools/rpc-record/record.py@, and devnet for the v1 responses the pinned+-- validator cannot produce -- so these tests -- pin the @FromJSON@ instances against JSON a node actually produces, -- including the fields it omits. That is the one thing the Rust-generated -- byte vectors cannot cover, since the RPC layer's input is JSON rather than@@ -17,7 +19,7 @@ -- parser wired to the wrong key. module Test.RPC.Parsers (tests) where -import Data.Aeson (FromJSON, Value)+import Data.Aeson (FromJSON, Key, Value, toJSON, withObject, (.:), (.:?)) import Data.Aeson.Types (parseEither, parseJSON) import Data.Map.Strict qualified as M import Data.Maybe (isJust)@@ -31,7 +33,10 @@     lamports,   ) import Network.Solana.Core.Block (BlockHash, BlockHeight)+import Network.Solana.Core.Transaction (Transaction (..))+import Network.Solana.Core.Compact (unCompact) import Network.Solana.Core.Crypto (SolanaPublicKey)+import Network.Solana.Core.Instruction (CompiledInstruction (..)) import Network.Solana.RPC.HTTP.Account hiding (lamports) import Network.Solana.RPC.HTTP.Account qualified as RpcAccount import Network.Solana.RPC.HTTP.Block@@ -75,8 +80,80 @@       testGroup "tokenomics" tokenomicsTests,       testGroup "accounts" accountTests,       testGroup "transactions" transactionTests,-      testGroup "SPL token" tokenTests+      testGroup "SPL token" tokenTests,+      testGroup "v1 transactions (SIMD-0385)" v1Tests     ]++-- | Responses carrying v1 transactions, recorded from devnet (which runs+-- them) rather than from the local validator, since the pinned+-- @solana-test-validator@ predates the format. A v1 message reports its+-- resource requests in a @transactionConfig@ object and carries no+-- @addressTableLookups@; these pin that neither shape breaks the parsers.+v1Tests :: [TestTree]+v1Tests =+  [ testCase "getTransaction decodes a v1 transaction" $ withV1Result @(Maybe TransactionResult) "getTransaction-v1" $ \case+      Nothing -> assertFailure "expected a transaction"+      Just result -> do+        slotTx result @?= 512843755+        map show (signatures (transactionTx result))+          @?= ["Mq9Qb7wwtfyRtrtGFF4YjjCaUAhaG9iddBu7DquQ84hKe4D4R7ixgkKjz3DxbvS7UpC7Q2SmSK4htWcwu2wjcGZ"]+        keys <- messageField @[SolanaPublicKey] "accountKeys" (transactionTx result)+        length keys @?= 4+        blockhash <- messageField @BlockHash "recentBlockhash" (transactionTx result)+        show blockhash @?= "4iSS7zQNeY8BTSWZ9VRMBo8B17AyijyLDD8C2EycoW19"+        -- Reach into the v1-specific part: the compiled instruction. These+        -- are the same values Test.Core.MessageV1 transcribes by hand from+        -- the wire bytes, so agreement here cross-checks that transcription+        -- against the node's own JSON rendering of the same transaction.+        ixs <- messageField @[CompiledInstruction] "instructions" (transactionTx result)+        case ixs of+          [ix] -> do+            ciProgramIdIndex ix @?= 2+            unCompact (ciAccounts ix) @?= [0, 3, 1]+            length (unCompact (ciData ix)) @?= 90+          other -> assertFailure ("expected one instruction, got " <> show (length other))+        -- Pin the documented loss: the recorded response does carry a+        -- transactionConfig object, and CompiledMessage drops it.+        config <- optionalMessageField @Value "transactionConfig" (transactionTx result)+        assertEqual "transactionConfig is not surfaced" Nothing config,+    testCase "getBlock decodes a block mixing legacy, v0 and v1 transactions" $+      withV1Result @(Maybe BlockInfo) "getBlock-v1-trimmed" $ \case+        Nothing -> assertFailure "expected a block"+        Just block -> do+          -- The fixture keeps two v1, two legacy and one v0 transaction, in+          -- that recorded order. Asserting each one's account-key count+          -- distinguishes the entries from one another, so a mis-trimmed+          -- fixture or a parser that silently defaulted a message cannot+          -- pass on the count alone.+          counts <-+            mapM+              (fmap length . messageField @[SolanaPublicKey] "accountKeys" . transaction)+              (transactionsBI block)+          counts @?= [4, 4, 10, 19, 10]+  ]++-- | As 'withResult', against the devnet-recorded v1 fixtures.+withV1Result :: forall a. (FromJSON a) => String -> (a -> Assertion) -> Assertion+withV1Result name check = do+  fs <- loadRpcFixtures "test/fixtures/v1_rpc_responses.json"+  case parseEither (parseJSON @a) (requireRpcResult name fs) of+    Left err -> assertFailure ("failed to parse recorded " <> name <> " response: " <> err)+    Right parsed -> check parsed++-- | Read one field of a parsed transaction's message. 'CompiledMessage' is+-- abstract, so its contents are only reachable through its 'ToJSON'+-- rendering -- which is also all a caller of the RPC bindings gets.+messageField :: forall a. (FromJSON a) => Key -> Transaction -> IO a+messageField name tx =+  either (assertFailure . ((show name <> ": ") <>)) pure $+    parseEither (withObject "message" (.: name)) (toJSON (message tx))++-- | As 'messageField', but 'Nothing' when the rendered message has no such+-- field -- for asserting that something is absent.+optionalMessageField :: forall a. (FromJSON a) => Key -> Transaction -> IO (Maybe a)+optionalMessageField name tx =+  either (assertFailure . ((show name <> ": ") <>)) pure $+    parseEither (withObject "message" (.:? name)) (toJSON (message tx))  clusterTests :: [TestTree] clusterTests =
+ test/fixtures/v1_rpc_responses.json view
@@ -0,0 +1,952 @@+[+ {+  "method": "getTransaction",+  "name": "getTransaction-v1",+  "response": {+   "id": 1,+   "jsonrpc": "2.0",+   "result": {+    "blockTime": 1792423794,+    "meta": {+     "computeUnitsConsumed": 182,+     "costUnits": 1532,+     "err": null,+     "fee": 5000,+     "innerInstructions": [],+     "loadedAddresses": {+      "readonly": [],+      "writable": []+     },+     "logMessages": [+      "Program MktiyFj9mVdLQgj45AXvJdoaHvF9PkuU1UmA2uwPJWM invoke [1]",+      "Program MktiyFj9mVdLQgj45AXvJdoaHvF9PkuU1UmA2uwPJWM consumed 182 of 324 compute units",+      "Program MktiyFj9mVdLQgj45AXvJdoaHvF9PkuU1UmA2uwPJWM success"+     ],+     "postBalances": [+      3539480000,+      11054080,+      833120,+      1169280+     ],+     "postTokenBalances": [],+     "preBalances": [+      3539485000,+      11054080,+      833120,+      1169280+     ],+     "preTokenBalances": [],+     "rewards": [],+     "status": {+      "Ok": null+     }+    },+    "slot": 512843755,+    "transaction": {+     "message": {+      "accountKeys": [+       "4QVtKaZkDhEwKszzJWa7LJPZBFkkcgUVRG4xE3fkWNGt",+       "55gx5ra96qkYXXMGShkVjwQz1k4j9bS26HhdksFa9buL",+       "MktiyFj9mVdLQgj45AXvJdoaHvF9PkuU1UmA2uwPJWM",+       "SysvarC1ock11111111111111111111111111111111"+      ],+      "header": {+       "numReadonlySignedAccounts": 0,+       "numReadonlyUnsignedAccounts": 2,+       "numRequiredSignatures": 1+      },+      "instructions": [+       {+        "accounts": [+         0,+         3,+         1+        ],+        "data": "2isQdwGHZhqBC7JtD5qgwY3fpHY2d1ciRmLgt9KN5x75VY2UxdpWv67RMmcvfJCqk5SAeAV4zDVqkkrgrJga5gREAVsRKJUhgqq5ZxqV2rNZhRkuobuQtQubPy9",+        "programIdIndex": 2,+        "stackHeight": 1+       }+      ],+      "recentBlockhash": "4iSS7zQNeY8BTSWZ9VRMBo8B17AyijyLDD8C2EycoW19",+      "transactionConfig": {+       "computeUnitLimit": 324,+       "heapSize": null,+       "loadedAccountsDataSizeLimit": 43008,+       "priorityFee": 0+      }+     },+     "signatures": [+      "Mq9Qb7wwtfyRtrtGFF4YjjCaUAhaG9iddBu7DquQ84hKe4D4R7ixgkKjz3DxbvS7UpC7Q2SmSK4htWcwu2wjcGZ"+     ]+    },+    "transactionIndex": 0,+    "version": 1+   }+  }+ },+ {+  "method": "getBlock",+  "name": "getBlock-v1-trimmed",+  "response": {+   "id": 1,+   "jsonrpc": "2.0",+   "result": {+    "blockHeight": 500055305,+    "blockTime": 1792423794,+    "blockhash": "DmA3KRYyXZVKsb3QneRspksx4d63SPx9JWdRPh5tTCTA",+    "parentSlot": 512843754,+    "previousBlockhash": "G357hNHZs3tep1pYUXgvuQt1SAspAu3e4wTLVRU9MuLN",+    "transactions": [+     {+      "meta": {+       "computeUnitsConsumed": 182,+       "costUnits": 1532,+       "err": null,+       "fee": 5000,+       "innerInstructions": [],+       "loadedAddresses": {+        "readonly": [],+        "writable": []+       },+       "logMessages": [+        "Program MktiyFj9mVdLQgj45AXvJdoaHvF9PkuU1UmA2uwPJWM invoke [1]",+        "Program MktiyFj9mVdLQgj45AXvJdoaHvF9PkuU1UmA2uwPJWM consumed 182 of 324 compute units",+        "Program MktiyFj9mVdLQgj45AXvJdoaHvF9PkuU1UmA2uwPJWM success"+       ],+       "postBalances": [+        3539480000,+        11054080,+        833120,+        1169280+       ],+       "postTokenBalances": [],+       "preBalances": [+        3539485000,+        11054080,+        833120,+        1169280+       ],+       "preTokenBalances": [],+       "rewards": null,+       "status": {+        "Ok": null+       }+      },+      "transaction": {+       "message": {+        "accountKeys": [+         "4QVtKaZkDhEwKszzJWa7LJPZBFkkcgUVRG4xE3fkWNGt",+         "55gx5ra96qkYXXMGShkVjwQz1k4j9bS26HhdksFa9buL",+         "MktiyFj9mVdLQgj45AXvJdoaHvF9PkuU1UmA2uwPJWM",+         "SysvarC1ock11111111111111111111111111111111"+        ],+        "header": {+         "numReadonlySignedAccounts": 0,+         "numReadonlyUnsignedAccounts": 2,+         "numRequiredSignatures": 1+        },+        "instructions": [+         {+          "accounts": [+           0,+           3,+           1+          ],+          "data": "2isQdwGHZhqBC7JtD5qgwY3fpHY2d1ciRmLgt9KN5x75VY2UxdpWv67RMmcvfJCqk5SAeAV4zDVqkkrgrJga5gREAVsRKJUhgqq5ZxqV2rNZhRkuobuQtQubPy9",+          "programIdIndex": 2,+          "stackHeight": 1+         }+        ],+        "recentBlockhash": "4iSS7zQNeY8BTSWZ9VRMBo8B17AyijyLDD8C2EycoW19",+        "transactionConfig": {+         "computeUnitLimit": 324,+         "heapSize": null,+         "loadedAccountsDataSizeLimit": 43008,+         "priorityFee": 0+        }+       },+       "signatures": [+        "Mq9Qb7wwtfyRtrtGFF4YjjCaUAhaG9iddBu7DquQ84hKe4D4R7ixgkKjz3DxbvS7UpC7Q2SmSK4htWcwu2wjcGZ"+       ]+      },+      "version": 1+     },+     {+      "meta": {+       "computeUnitsConsumed": 182,+       "costUnits": 1532,+       "err": null,+       "fee": 5000,+       "innerInstructions": [],+       "loadedAddresses": {+        "readonly": [],+        "writable": []+       },+       "logMessages": [+        "Program MktiyFj9mVdLQgj45AXvJdoaHvF9PkuU1UmA2uwPJWM invoke [1]",+        "Program MktiyFj9mVdLQgj45AXvJdoaHvF9PkuU1UmA2uwPJWM consumed 182 of 324 compute units",+        "Program MktiyFj9mVdLQgj45AXvJdoaHvF9PkuU1UmA2uwPJWM success"+       ],+       "postBalances": [+        3539475000,+        11054080,+        833120,+        1169280+       ],+       "postTokenBalances": [],+       "preBalances": [+        3539480000,+        11054080,+        833120,+        1169280+       ],+       "preTokenBalances": [],+       "rewards": null,+       "status": {+        "Ok": null+       }+      },+      "transaction": {+       "message": {+        "accountKeys": [+         "4QVtKaZkDhEwKszzJWa7LJPZBFkkcgUVRG4xE3fkWNGt",+         "55gx5ra96qkYXXMGShkVjwQz1k4j9bS26HhdksFa9buL",+         "MktiyFj9mVdLQgj45AXvJdoaHvF9PkuU1UmA2uwPJWM",+         "SysvarC1ock11111111111111111111111111111111"+        ],+        "header": {+         "numReadonlySignedAccounts": 0,+         "numReadonlyUnsignedAccounts": 2,+         "numRequiredSignatures": 1+        },+        "instructions": [+         {+          "accounts": [+           0,+           3,+           1+          ],+          "data": "2ctyYDVbUY5FNMbayMXaPmLmKVPMDm4zcowcVkjfi1EDA8eeMr3gjJzYPP4fYrwKgyGDfUfjyze6sp8bfm7jGzqUv1hGWbzGr68X4UcbM8CZRQpmw58SmMqdTfV",+          "programIdIndex": 2,+          "stackHeight": 1+         }+        ],+        "recentBlockhash": "4iSS7zQNeY8BTSWZ9VRMBo8B17AyijyLDD8C2EycoW19",+        "transactionConfig": {+         "computeUnitLimit": 324,+         "heapSize": null,+         "loadedAccountsDataSizeLimit": 43008,+         "priorityFee": 0+        }+       },+       "signatures": [+        "4WqBxBMEJ4HU29xfBB9Mx2ckZBo54dhD7cDPMaFTMU3ZRQtdK4X5AikT57ZTeqSdtwFkHxro31yfBWCPD7ANyUh2"+       ]+      },+      "version": 1+     },+     {+      "meta": {+       "computeUnitsConsumed": 4723,+       "costUnits": 7421,+       "err": null,+       "fee": 10000,+       "innerInstructions": [],+       "loadedAddresses": {+        "readonly": [],+        "writable": []+       },+       "logMessages": [+        "Program ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL invoke [1]",+        "Program log: CreateIdempotent",+        "Program ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL consumed 4618 of 400000 compute units",+        "Program ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL success",+        "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA invoke [1]",+        "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA consumed 105 of 395382 compute units",+        "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA success"+       ],+       "postBalances": [+        7946257653,+        0,+        2039280,+        2039280,+        1,+        1009200,+        20381277856,+        454525905289,+        5938070540,+        0+       ],+       "postTokenBalances": [+        {+         "accountIndex": 2,+         "mint": "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU",+         "owner": "Cf9abfrZbkkcKthpkKqwmA8H3LiGtGZ2ZL8NsfQzYLQP",+         "programId": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",+         "uiTokenAmount": {+          "amount": "115055020000",+          "decimals": 6,+          "uiAmount": 115055.02,+          "uiAmountString": "115055.02"+         }+        },+        {+         "accountIndex": 3,+         "mint": "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU",+         "owner": "D7d6ZP2KcmHzhEh4nGrpe7zMKFjXP8wtvceecGB3fKVp",+         "programId": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",+         "uiTokenAmount": {+          "amount": "4114000000",+          "decimals": 6,+          "uiAmount": 4114.0,+          "uiAmountString": "4114"+         }+        }+       ],+       "preBalances": [+        7946267653,+        0,+        2039280,+        2039280,+        1,+        1009200,+        20381277856,+        454525905289,+        5938070540,+        0+       ],+       "preTokenBalances": [+        {+         "accountIndex": 2,+         "mint": "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU",+         "owner": "Cf9abfrZbkkcKthpkKqwmA8H3LiGtGZ2ZL8NsfQzYLQP",+         "programId": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",+         "uiTokenAmount": {+          "amount": "115054020000",+          "decimals": 6,+          "uiAmount": 115054.02,+          "uiAmountString": "115054.02"+         }+        },+        {+         "accountIndex": 3,+         "mint": "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU",+         "owner": "D7d6ZP2KcmHzhEh4nGrpe7zMKFjXP8wtvceecGB3fKVp",+         "programId": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",+         "uiTokenAmount": {+          "amount": "4115000000",+          "decimals": 6,+          "uiAmount": 4115.0,+          "uiAmountString": "4115"+         }+        }+       ],+       "rewards": null,+       "status": {+        "Ok": null+       }+      },+      "transaction": {+       "message": {+        "accountKeys": [+         "DmxEUYwgTX2qYnsf29LXBBScxYkKp1yn1Doodqf95SP7",+         "D7d6ZP2KcmHzhEh4nGrpe7zMKFjXP8wtvceecGB3fKVp",+         "5iv62nJJJHsV7pgJcA3sf9kp98uWaQcjyKtxFZ5dEbcW",+         "GXGc5RJU7W4j8FrH38vfGbryht5av3zeiZCmhDN7yRPU",+         "11111111111111111111111111111111",+         "SysvarRent111111111111111111111111111111111",+         "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",+         "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU",+         "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL",+         "Cf9abfrZbkkcKthpkKqwmA8H3LiGtGZ2ZL8NsfQzYLQP"+        ],+        "header": {+         "numReadonlySignedAccounts": 0,+         "numReadonlyUnsignedAccounts": 6,+         "numRequiredSignatures": 2+        },+        "instructions": [+         {+          "accounts": [+           0,+           2,+           9,+           7,+           4,+           6,+           5+          ],+          "data": "2",+          "programIdIndex": 8,+          "stackHeight": 1+         },+         {+          "accounts": [+           3,+           7,+           2,+           1+          ],+          "data": "gvPShZQhKrzGM",+          "programIdIndex": 6,+          "stackHeight": 1+         }+        ],+        "recentBlockhash": "6CqcYVxH594qe8fuXWJbLtbihbfiZZvR7LgwbeGGYWf1"+       },+       "signatures": [+        "3pEGq7f179YAoezTopf5eDW6g2eArox5eQFee5Gn86JvqCdUvoUTfitckqU5nDuYut6cKkGPqKahBSxKaDsB7w1W",+        "Y9k8CevfXqQGkDPorJ13wr5HYrkUMugo8Lk7QgkBZqEaTCuM8YMUwgH3XKbeenUNzq7qti6RTfAvQ95dN8XYP3Y"+       ]+      },+      "version": "legacy"+     },+     {+      "meta": {+       "computeUnitsConsumed": 83720,+       "costUnits": 87635,+       "err": null,+       "fee": 5000,+       "innerInstructions": [+        {+         "index": 3,+         "instructions": [+          {+           "accounts": [+            1,+            6,+            0+           ],+           "data": "3dStPEjaYDXV",+           "programIdIndex": 12,+           "stackHeight": 2+          },+          {+           "accounts": [+            1,+            7,+            0+           ],+           "data": "3Rk7VTcM4FDy",+           "programIdIndex": 12,+           "stackHeight": 2+          },+          {+           "accounts": [+            8,+            2,+            17+           ],+           "data": "3rsvjYLBF3B5",+           "programIdIndex": 12,+           "stackHeight": 2+          },+          {+           "accounts": [+            4,+            3,+            16+           ],+           "data": "6pA1XnvgKh8j",+           "programIdIndex": 12,+           "stackHeight": 2+          },+          {+           "accounts": [+            18+           ],+           "data": "5PNV38RXxo7nDN1FxWX2okYSSYqwNQvBzFVDE7E7mJrgW3ShtoatTrZNqVjc22Zkevpkh3i7Wihh1JHuJTGN2211EijwSVekwENhxWS5Tbt8bswfQB7ExHPFGLyi4gWCU5E2vcMzUKR8r2qLSr53cRLNvgzRayLE6yZjtcSfAmbw1SwufEav2aaa7qfWtVQqTvC4qeevtfDv9UrQkTy9QXpPWitxEQqRPHnFXapyGkrGkeFrbMfYHQBPcg6PmgqSD8NMyYrwfr1pD5diBWy1gAhm92FZFmvSRLLELAj5pE8KMRByatxkpG8ipxyZgjihoMUcz95",+           "programIdIndex": 14,+           "stackHeight": 2+          }+         ]+        }+       ],+       "loadedAddresses": {+        "readonly": [],+        "writable": []+       },+       "logMessages": [+        "Program ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL invoke [1]",+        "Program log: CreateIdempotent",+        "Program ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL consumed 4338 of 800000 compute units",+        "Program ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL success",+        "Program ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL invoke [1]",+        "Program log: CreateIdempotent",+        "Program ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL consumed 7338 of 795662 compute units",+        "Program ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL success",+        "Program ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL invoke [1]",+        "Program log: CreateIdempotent",+        "Program ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL consumed 7337 of 788324 compute units",+        "Program ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL success",+        "Program XuXkWAz3xRktfEjAfFDdkFksYBqFzet8JPkQxZMoRfi invoke [1]",+        "Program log: Instruction: Buy",+        "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA invoke [2]",+        "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA consumed 76 of 734476 compute units",+        "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA success",+        "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA invoke [2]",+        "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA consumed 76 of 732583 compute units",+        "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA success",+        "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA invoke [2]",+        "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA consumed 76 of 730762 compute units",+        "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA success",+        "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA invoke [2]",+        "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA consumed 120 of 728966 compute units",+        "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA success",+        "Program XuXkWAz3xRktfEjAfFDdkFksYBqFzet8JPkQxZMoRfi invoke [2]",+        "Program XuXkWAz3xRktfEjAfFDdkFksYBqFzet8JPkQxZMoRfi consumed 424 of 721485 compute units",+        "Program XuXkWAz3xRktfEjAfFDdkFksYBqFzet8JPkQxZMoRfi success",+        "Program XuXkWAz3xRktfEjAfFDdkFksYBqFzet8JPkQxZMoRfi consumed 64707 of 780987 compute units",+        "Program XuXkWAz3xRktfEjAfFDdkFksYBqFzet8JPkQxZMoRfi success"+       ],+       "postBalances": [+        3720280,+        1488440,+        1488440,+        1488440,+        1066800,+        4231640,+        1488440,+        1488440,+        1488440,+        5938070540,+        1066800,+        1,+        20381277856,+        1066800,+        11141440,+        1767840,+        0,+        0,+        0+       ],+       "postTokenBalances": [+        {+         "accountIndex": 1,+         "mint": "GwMYubfhtHzP2vNcoDVRbt64T5ruHRpq5w2wCXGMNTB7",+         "owner": "AAHy9ocUHXcdg7xpUE8DTvJCLaHqgmgJYeRVHomiKMFD",+         "programId": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",+         "uiTokenAmount": {+          "amount": "8918007091",+          "decimals": 6,+          "uiAmount": 8918.007091,+          "uiAmountString": "8918.007091"+         }+        },+        {+         "accountIndex": 2,+         "mint": "BYRX2nT98QKcKvPf6gpHtBEcF9DBHrmY3FiTQwWVEaNj",+         "owner": "AAHy9ocUHXcdg7xpUE8DTvJCLaHqgmgJYeRVHomiKMFD",+         "programId": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",+         "uiTokenAmount": {+          "amount": "18985407",+          "decimals": 6,+          "uiAmount": 18.985407,+          "uiAmountString": "18.985407"+         }+        },+        {+         "accountIndex": 3,+         "mint": "FTWhjeTdTA4k4Ak89saHKTwnizDaMZfQNJQdMJgYNC4K",+         "owner": "AAHy9ocUHXcdg7xpUE8DTvJCLaHqgmgJYeRVHomiKMFD",+         "programId": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",+         "uiTokenAmount": {+          "amount": "18985407",+          "decimals": 6,+          "uiAmount": 18.985407,+          "uiAmountString": "18.985407"+         }+        },+        {+         "accountIndex": 6,+         "mint": "GwMYubfhtHzP2vNcoDVRbt64T5ruHRpq5w2wCXGMNTB7",+         "owner": "EQ5bGv95Ptt4A8nJMJv1SCuma5dAcrAbw9E8eoamrLPp",+         "programId": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",+         "uiTokenAmount": {+          "amount": "5601370072",+          "decimals": 6,+          "uiAmount": 5601.370072,+          "uiAmountString": "5601.370072"+         }+        },+        {+         "accountIndex": 7,+         "mint": "GwMYubfhtHzP2vNcoDVRbt64T5ruHRpq5w2wCXGMNTB7",+         "owner": "8u5r3QSngjMoX7XbBwuRbwTkrwiiSdoEZ3o4JxRp5Uyt",+         "programId": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",+         "uiTokenAmount": {+          "amount": "3920386062232",+          "decimals": 6,+          "uiAmount": 3920386.062232,+          "uiAmountString": "3920386.062232"+         }+        },+        {+         "accountIndex": 8,+         "mint": "BYRX2nT98QKcKvPf6gpHtBEcF9DBHrmY3FiTQwWVEaNj",+         "owner": "EQ5bGv95Ptt4A8nJMJv1SCuma5dAcrAbw9E8eoamrLPp",+         "programId": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",+         "uiTokenAmount": {+          "amount": "345584742",+          "decimals": 6,+          "uiAmount": 345.584742,+          "uiAmountString": "345.584742"+         }+        }+       ],+       "preBalances": [+        3725280,+        1488440,+        1488440,+        1488440,+        1066800,+        4231640,+        1488440,+        1488440,+        1488440,+        5938070540,+        1066800,+        1,+        20381277856,+        1066800,+        11141440,+        1767840,+        0,+        0,+        0+       ],+       "preTokenBalances": [+        {+         "accountIndex": 1,+         "mint": "GwMYubfhtHzP2vNcoDVRbt64T5ruHRpq5w2wCXGMNTB7",+         "owner": "AAHy9ocUHXcdg7xpUE8DTvJCLaHqgmgJYeRVHomiKMFD",+         "programId": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",+         "uiTokenAmount": {+          "amount": "8964135691",+          "decimals": 6,+          "uiAmount": 8964.135691,+          "uiAmountString": "8964.135691"+         }+        },+        {+         "accountIndex": 2,+         "mint": "BYRX2nT98QKcKvPf6gpHtBEcF9DBHrmY3FiTQwWVEaNj",+         "owner": "AAHy9ocUHXcdg7xpUE8DTvJCLaHqgmgJYeRVHomiKMFD",+         "programId": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",+         "uiTokenAmount": {+          "amount": "13498592",+          "decimals": 6,+          "uiAmount": 13.498592,+          "uiAmountString": "13.498592"+         }+        },+        {+         "accountIndex": 3,+         "mint": "FTWhjeTdTA4k4Ak89saHKTwnizDaMZfQNJQdMJgYNC4K",+         "owner": "AAHy9ocUHXcdg7xpUE8DTvJCLaHqgmgJYeRVHomiKMFD",+         "programId": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",+         "uiTokenAmount": {+          "amount": "13498592",+          "decimals": 6,+          "uiAmount": 13.498592,+          "uiAmountString": "13.498592"+         }+        },+        {+         "accountIndex": 6,+         "mint": "GwMYubfhtHzP2vNcoDVRbt64T5ruHRpq5w2wCXGMNTB7",+         "owner": "EQ5bGv95Ptt4A8nJMJv1SCuma5dAcrAbw9E8eoamrLPp",+         "programId": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",+         "uiTokenAmount": {+          "amount": "5555467593",+          "decimals": 6,+          "uiAmount": 5555.467593,+          "uiAmountString": "5555.467593"+         }+        },+        {+         "accountIndex": 7,+         "mint": "GwMYubfhtHzP2vNcoDVRbt64T5ruHRpq5w2wCXGMNTB7",+         "owner": "8u5r3QSngjMoX7XbBwuRbwTkrwiiSdoEZ3o4JxRp5Uyt",+         "programId": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",+         "uiTokenAmount": {+          "amount": "3920385836111",+          "decimals": 6,+          "uiAmount": 3920385.836111,+          "uiAmountString": "3920385.836111"+         }+        },+        {+         "accountIndex": 8,+         "mint": "BYRX2nT98QKcKvPf6gpHtBEcF9DBHrmY3FiTQwWVEaNj",+         "owner": "EQ5bGv95Ptt4A8nJMJv1SCuma5dAcrAbw9E8eoamrLPp",+         "programId": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",+         "uiTokenAmount": {+          "amount": "351071557",+          "decimals": 6,+          "uiAmount": 351.071557,+          "uiAmountString": "351.071557"+         }+        }+       ],+       "rewards": null,+       "status": {+        "Ok": null+       }+      },+      "transaction": {+       "message": {+        "accountKeys": [+         "AAHy9ocUHXcdg7xpUE8DTvJCLaHqgmgJYeRVHomiKMFD",+         "F5hYRkGysQaBgLQt28pQD7Qfe1iayQcSd67mSjw6wtVp",+         "J5BSHuWBq8VueexXADTJRFEpxqw7wLbw7goYkcTcBAyc",+         "7sAVWnpuqRSk6qSVuDgG2P8Ls7ELHkt3ioSwCWGfnmWa",+         "FTWhjeTdTA4k4Ak89saHKTwnizDaMZfQNJQdMJgYNC4K",+         "9bcaAahcx6VV6mrs2ceT5DUtWVCKE1T4sHyXoXFyuum9",+         "HmmT5ZGx4nvUpSRsQramrqZYAwx2z7r5unt5V1krwa48",+         "8u5r3QSngjMoX7XbBwuRbwTkrwiiSdoEZ3o4JxRp5Uyt",+         "EDuLkMZRSYg5JfdMUjiaN7nQiTgb3KYBAfCZB5Xy8XAX",+         "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL",+         "GwMYubfhtHzP2vNcoDVRbt64T5ruHRpq5w2wCXGMNTB7",+         "11111111111111111111111111111111",+         "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",+         "BYRX2nT98QKcKvPf6gpHtBEcF9DBHrmY3FiTQwWVEaNj",+         "XuXkWAz3xRktfEjAfFDdkFksYBqFzet8JPkQxZMoRfi",+         "4J2Vd4GTJAvpg9NWP4BdMoEbaeThp1Tw7KFeMqmSMNPT",+         "9ee77R4PK6qkvrNj4WwKDVQd9t3YFMWCBenaoHH2iEdW",+         "EQ5bGv95Ptt4A8nJMJv1SCuma5dAcrAbw9E8eoamrLPp",+         "HgmY1JtUBoLymMdSHEUUz6ktsDMk7W9SQR76rDGgfwrZ"+        ],+        "addressTableLookups": [],+        "header": {+         "numReadonlySignedAccounts": 0,+         "numReadonlyUnsignedAccounts": 10,+         "numRequiredSignatures": 1+        },+        "instructions": [+         {+          "accounts": [+           0,+           1,+           0,+           10,+           11,+           12+          ],+          "data": "2",+          "programIdIndex": 9,+          "stackHeight": 1+         },+         {+          "accounts": [+           0,+           2,+           0,+           13,+           11,+           12+          ],+          "data": "2",+          "programIdIndex": 9,+          "stackHeight": 1+         },+         {+          "accounts": [+           0,+           3,+           0,+           4,+           11,+           12+          ],+          "data": "2",+          "programIdIndex": 9,+          "stackHeight": 1+         },+         {+          "accounts": [+           0,+           5,+           15,+           10,+           13,+           4,+           16,+           17,+           6,+           7,+           1,+           2,+           3,+           12,+           8,+           14,+           18,+           14+          ],+          "data": "48CS5rNxJb96c6eTcZQYYvRDkB4Uz8pTkB3m",+          "programIdIndex": 14,+          "stackHeight": 1+         }+        ],+        "recentBlockhash": "6Y9h3uJmp6eFM7xzSLC6UYw498ZajgZATErWovuPg849"+       },+       "signatures": [+        "7TvEfsbrjHodqtCZMc7vduuuuB5Hu4SxfHCfxWw6pkxx5vSUmLTxvNuYFnQ6XdV1a7d6bxfvAFvAvvVBSRLh1QY"+       ]+      },+      "version": 0+     },+     {+      "meta": {+       "computeUnitsConsumed": 28793,+       "costUnits": 33377,+       "err": null,+       "fee": 10000,+       "innerInstructions": [+        {+         "index": 2,+         "instructions": [+          {+           "accounts": [+            0,+            4,+            2,+            3,+            9+           ],+           "data": "6VhAuisxVjhwQuJTnG8UEz2TNTdjsRwFB2WKsBAXkyqBwCqVPHMexZQRaA5ySszVoo54NkjkQJyStT3graU7JCcW4AD2PtvAbyfjQKzGA3kwFM8TwwfrPfz3VjjbrVPxHVsJ1PL9G4zLAHHDEK7X83DGkQAa9BuAnQr4xp82AmhmJmizVBhmqhqqbpDx3Ex34CeC8HcGLk6KjXYpGmvab7HyuhZ",+           "programIdIndex": 7,+           "stackHeight": 2+          },+          {+           "accounts": [+            0,+            2+           ],+           "data": "3Bxs412MvVNQj175",+           "programIdIndex": 3,+           "stackHeight": 3+          }+         ]+        }+       ],+       "loadedAddresses": {+        "readonly": [],+        "writable": []+       },+       "logMessages": [+        "Program ComputeBudget111111111111111111111111111111 invoke [1]",+        "Program ComputeBudget111111111111111111111111111111 success",+        "Program q8WWu9FefmPYtLrz4mKRBYVEX5apB4UmRW1F3TxmaW3 invoke [1]",+        "Program log: Instruction: TouchOracle",+        "Program pytd2yyk641x7ak7mkaasSJVXh6YYZnC7wTmtgAyxPt invoke [2]",+        "Program log: Instruction: VerifyMessage",+        "Program 11111111111111111111111111111111 invoke [3]",+        "Program 11111111111111111111111111111111 success",+        "Program pytd2yyk641x7ak7mkaasSJVXh6YYZnC7wTmtgAyxPt consumed 12401 of 35714 compute units",+        "Program return: pytd2yyk641x7ak7mkaasSJVXh6YYZnC7wTmtgAyxPt gO/B9IDFYVrz+2c9Qih+mT2p+8NQa25B36MpUIIMLmwrAAAAddPHkwBbjS8zXgYAAgECAAAABABiLfosOAAAAAT4/wW+SLcMAAAAAAMZAA==",+        "Program pytd2yyk641x7ak7mkaasSJVXh6YYZnC7wTmtgAyxPt success",+        "Program data: KslRnrd07eUB4JbIVKEBAADUxcXBMQIAAFCVyFShAQAA61+RHgAAAAAzm8MQdZvMAxMmy+gJxC/3R0tMuVl3NcUtPx5/WLPK2A==",+        "Program q8WWu9FefmPYtLrz4mKRBYVEX5apB4UmRW1F3TxmaW3 consumed 28643 of 44850 compute units",+        "Program return: q8WWu9FefmPYtLrz4mKRBYVEX5apB4UmRW1F3TxmaW3 gO/B9IDFYVrz+2c9Qih+mT2p+8NQa25B36MpUIIMLmwrAAAAddPHkwBbjS8zXgYAAgECAAAABABiLfosOAAAAAT4/wW+SLcMAAAAAAMZAA==",+        "Program q8WWu9FefmPYtLrz4mKRBYVEX5apB4UmRW1F3TxmaW3 success"+       ],+       "postBalances": [+        5110523511,+        5821680,+        27042354983,+        1,+        3542640,+        1,+        1,+        1141440,+        833120,+        0+       ],+       "postTokenBalances": [],+       "preBalances": [+        5110533512,+        5821680,+        27042354982,+        1,+        3542640,+        1,+        1,+        1141440,+        833120,+        0+       ],+       "preTokenBalances": [],+       "returnData": {+        "data": [+         "gO/B9IDFYVrz+2c9Qih+mT2p+8NQa25B36MpUIIMLmwrAAAAddPHkwBbjS8zXgYAAgECAAAABABiLfosOAAAAAT4/wW+SLcMAAAAAAMZAA==",+         "base64"+        ],+        "programId": "pytd2yyk641x7ak7mkaasSJVXh6YYZnC7wTmtgAyxPt"+       },+       "rewards": null,+       "status": {+        "Ok": null+       }+      },+      "transaction": {+       "message": {+        "accountKeys": [+         "4UTYriXvwcpVTuzwGYUx1DJNposddSYSRvijXVaMhRdq",+         "HCayMtvKa5smx9aoXDoA3icaftHYCy8gkiULzy7fawpX",+         "opsLibxVY7Vz5eYMmSfX8cLFCFVYTtH6fr6MiifMpA7",+         "11111111111111111111111111111111",+         "3rdJbqfnagQ4yx9HXJViD4zc4xpiSqmFsKpPuSCQVyQL",+         "ComputeBudget111111111111111111111111111111",+         "Ed25519SigVerify111111111111111111111111111",+         "pytd2yyk641x7ak7mkaasSJVXh6YYZnC7wTmtgAyxPt",+         "q8WWu9FefmPYtLrz4mKRBYVEX5apB4UmRW1F3TxmaW3",+         "Sysvar1nstructions1111111111111111111111111"+        ],+        "header": {+         "numReadonlySignedAccounts": 0,+         "numReadonlyUnsignedAccounts": 7,+         "numRequiredSignatures": 1+        },+        "instructions": [+         {+          "accounts": [],+          "data": "8AWWfCk8qZYPYNvxfj8Xh",+          "programIdIndex": 6,+          "stackHeight": 1+         },+         {+          "accounts": [],+          "data": "KDaxvT",+          "programIdIndex": 5,+          "stackHeight": 1+         },+         {+          "accounts": [+           0,+           1,+           4,+           7,+           2,+           3,+           9+          ],+          "data": "EM44qLUkPBifopW5RXDNSmDSiyMndC41PxvuzuzYaccmoDFqZNqNXpMDxgoqHsuHFUnWMsWBZ4zLKAZhZ6FXe6x98amBNN8KsjdXK68h9rEj5Jx7b8rnVXTJZNHfVkdHN44JDmmzxcApVAaV7ufSmz84nkQhnPuKr7KfQUUiJCpkVJqCprM1KBxMZkk7muGeqSd5tDUmThE7256ku3UiXxCf",+          "programIdIndex": 8,+          "stackHeight": 1+         }+        ],+        "recentBlockhash": "EPNGy384xLyceSrG7nDNXx7sCeN29WFh7KJ7mNpoC4Me"+       },+       "signatures": [+        "63hTA2JgPTUrnKuJbJjeKqubt651VUkf6YMxrnXHmyXJ3VeZcXqUB9xv4nPMHEYf6rvyDMskpVMEenoCKXVCJktW"+       ]+      },+      "version": "legacy"+     }+    ]+   }+  }+ }+]
+ test/fixtures/v1_transactions.json view
@@ -0,0 +1,6 @@+[+  {+    "hex": "810100020f0000003730a19a717b2d94ccb9d7904cc55053fc510d69ab42dc2405b27efa3a38713c01043298145ee11647cb918dafa3a779ccab852090bc53e28737e12bdbce9f99ab853ca23e02fb0274b1b2ab5896f53140b233e6690151de172698c66c74a23ce1b305513eaa1518a123f4723dd48d606f905017a2f3d82692f71c2bbd61ab12521a06a7d51718c774c928566398691d5eb68b5eb8a39b4b6d5c73555b210000000000000000000000004401000000a8000002035a000003010afc8eafb5f1f7df180000000000804f1e166a41700100000010271027d78f0000e08f000000000000000000802a4d61b6f1f7df1800000000003090a0f83142700100000010271027d78f0000e08f000000000000000000800011f68c121d83a11de76d99261b406d7e8c93ddd47cc19bcae28a3664cc19fd1f16414ba804c83b9f10f8630cf8fea1c5149664535c516616e9658e937d67be02",+    "name": "devnet-v1-transaction"+  }+]