packages feed

solana-haskell-sdk-1.3.0.0: test/Test/Core/Account.hs

{-# LANGUAGE OverloadedStrings #-}

module Test.Core.Account (tests) where

import Data.Aeson (eitherDecode, encode)
import Data.Aeson.Types (parseMaybe, withObject, (.:))
import Data.ByteString qualified as BS
import Data.ByteString.Lazy qualified as BL
import Data.Either (isLeft)
import Data.List (isInfixOf)
import Data.Word (Word8)
import Network.Solana.Core.Account
import Test.Tasty
import Test.Tasty.HUnit
import Test.Tasty.QuickCheck

-- | The Rent sysvar's 17 data bytes (the cluster-independent defaults), as
-- Agave returns them: bare base58 under the default encoding, a tagged pair
-- under @base64@.
rentBytes :: BS.ByteString
rentBytes = BS.pack [152, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 64, 50]

-- | A nonce account's @data@ under @encoding: "jsonParsed"@, as Agave 2.1.16
-- returns it.
nonceParsedJson :: BL.ByteString
nonceParsedJson =
  "{\"parsed\":{\"info\":{\"authority\":\"AkagUZWfCDrEDJTtsuxvAGv9AkhNQAgywJLxyzHPnFQ9\",\"blockhash\":\"EKp95XWYEyvEKaNoZMqaBBBiHPuEyodG9ZC4iHiKHMjj\",\"feeCalculator\":{\"lamportsPerSignature\":\"5000\"}},\"type\":\"initialized\"},\"program\":\"nonce\",\"space\":80}"

-- | The Clock sysvar as a whole @getAccountInfo@ value under
-- @encoding: "jsonParsed"@ (the shape that failed at @$.value.data@).
clockAccountJson :: BL.ByteString
clockAccountJson =
  "{\"data\":{\"parsed\":{\"info\":{\"epoch\":0,\"epochStartTimestamp\":1789286435,\"leaderScheduleEpoch\":1,\"slot\":1367,\"unixTimestamp\":1789287077},\"type\":\"clock\"},\"program\":\"sysvar\",\"space\":40},\"executable\":false,\"lamports\":1169280,\"owner\":\"Sysvar1111111111111111111111111111111111111\",\"rentEpoch\":0,\"space\":40}"

tests :: TestTree
tests =
  testGroup
    "account"
    [ testCase "AccountData FromJSON rejects an unsupported encoding tag" $
        case eitherDecode "[\"abcd\",\"unknownEncoding\"]" :: Either String AccountData of
          Left _ -> pure ()
          Right ad -> assertFailure ("expected parse failure, got " <> show ad),
      testCase "AccountData FromJSON rejects invalid base58 in a base58-tagged pair" $
        case eitherDecode "[\"0OIl\",\"base58\"]" :: Either String AccountData of
          Left _ -> pure ()
          Right ad -> assertFailure ("expected parse failure, got " <> show ad),
      testCase "AccountData FromJSON decodes a bare string as base58 (the node's default binary encoding)" $
        eitherDecode "\"2RsdFKVyfoKRJwMEPcvasdsF\"" @?= Right (AccountDataBinary rentBytes),
      testCase "bare string and base58-tagged pair decode identically" $ do
        eitherDecode "\"1111\"" @?= Right (AccountDataBinary (BS.replicate 4 0))
        eitherDecode "[\"1111\",\"base58\"]" @?= Right (AccountDataBinary (BS.replicate 4 0)),
      testCase "base64-tagged pair still decodes as base64" $
        eitherDecode "[\"mA0AAAAAAAAAAAAAAAAAQDI=\",\"base64\"]" @?= Right (AccountDataBinary rentBytes),
      testCase "bare empty string decodes to empty account data" $
        eitherDecode "\"\"" @?= Right (AccountDataBinary BS.empty),
      testCase "AccountData FromJSON rejects a bare string that is not base58" $
        case eitherDecode "\"0OIl\"" :: Either String AccountData of
          Left err -> assertBool ("expected \"invalid base58\" in: " <> err) ("invalid base58" `isInfixOf` err)
          Right ad -> assertFailure ("expected parse failure, got " <> show ad),
      testProperty "AccountData JSON round-trip (ToJSON emits the bare base58 form)" $ \(ws :: [Word8]) ->
        let bs = BS.pack ws
         in eitherDecode (encode (AccountDataBinary bs)) === Right (AccountDataBinary bs),
      testCase "AccountData FromJSON decodes a jsonParsed object into AccountDataJSON" $
        case eitherDecode nonceParsedJson of
          Left err -> assertFailure err
          Right (AccountDataJSON prog parsed space) -> do
            prog @?= "nonce"
            space @?= 80
            parseMaybe (withObject "parsed" (.: "type")) parsed @?= Just ("initialized" :: String)
          Right other -> assertFailure ("expected AccountDataJSON, got " <> show other),
      testCase "AccountData FromJSON decodes the jsonParsed base64 fallback pair as binary" $
        eitherDecode "[\"c3lzdGVtX3Byb2dyYW0=\",\"base64\"]" @?= Right (AccountDataBinary "system_program"),
      testCase "AccountData FromJSON rejects a jsonParsed object missing space" $
        assertBool
          "accepted an object without space"
          (isLeft (eitherDecode "{\"parsed\":{},\"program\":\"nonce\"}" :: Either String AccountData)),
      testCase "AccountData FromJSON rejects the never-emitted [text, \"jsonParsed\"] pair" $
        assertBool
          "accepted a jsonParsed pair"
          (isLeft (eitherDecode "[\"{}\",\"jsonParsed\"]" :: Either String AccountData)),
      testCase "AccountData ToJSON/FromJSON round-trips parsed data" $
        case eitherDecode nonceParsedJson :: Either String AccountData of
          Left err -> assertFailure err
          Right ad -> eitherDecode (encode ad) @?= Right ad,
      testCase "AccountInfo FromJSON accepts a jsonParsed account value" $
        case eitherDecode clockAccountJson :: Either String AccountInfo of
          Left err -> assertFailure err
          Right info -> do
            lamports info @?= Lamport 1169280
            case dataField info of
              AccountDataJSON prog _ space -> (prog @?= "sysvar") >> (space @?= 40)
              other -> assertFailure ("expected AccountDataJSON, got " <> show other)
    ]