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 +54/−0
- README.md +4/−3
- solana-haskell-sdk.cabal +6/−2
- src/Network/Solana/Core/Instruction.hs +19/−6
- src/Network/Solana/Core/MessageV1.hs +352/−0
- src/Network/Solana/RPC/HTTP/Block.hs +15/−7
- src/Network/Solana/RPC/HTTP/Transaction.hs +11/−6
- src/Network/Solana/RPC/HTTP/Types.hs +8/−3
- test/Main.hs +2/−0
- test/Test/Core/MessageV1.hs +475/−0
- test/Test/RPC/Parsers.hs +81/−4
- test/fixtures/v1_rpc_responses.json +952/−0
- test/fixtures/v1_transactions.json +6/−0
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"+ }+]