packages feed

solana-haskell-sdk-1.3.0.0: test-integration/Test/Integration/Nonce.hs

-- | Live durable-nonce coverage: create and initialize a nonce account, use
-- it via 'newNonceTransaction' to send a transfer, and check the durable
-- nonce advances and the transfer lands -- both after finalization and, as
-- the helper promises, immediately after a merely-confirmed create or
-- advance.
module Test.Integration.Nonce (tests) where

import Control.Exception (throwIO)
import Control.Monad.IO.Class (liftIO)
import Network.Solana.Core.Account (AccountData (..), AccountInfo (dataField))
import Network.Solana.Core.Block (BlockHash)
import Network.Solana.Core.Crypto (SolanaPublicKey, createSolanaKeyPair)
import Network.Solana.NativePrograms.SystemProgram qualified as SystemProgram
import Network.Solana.RPC.HTTP.Account (getAccountInfo, getAccountInfo', getBalance)
import Network.Solana.RPC.HTTP.Tokenomics (getMinimumBalanceForRentExemption)
import Network.Solana.RPC.HTTP.Types (defaultConfigObject, encoding, value)
import Network.Solana.SolanaWeb3 (accountDataBytes, cfgNonceAccountConfirmed, getNonceAccount, newNonceTransaction)
import Network.Web3.Provider (Web3)
import Test.Integration.Setup
import Test.Tasty
import Test.Tasty.HUnit

tests :: TestTree
tests =
  testGroup
    "Nonce"
    [ testCase "durable-nonce flow: create, use via newNonceTransaction, nonce advances" (run nonceFlow),
      testCase
        "newNonceTransaction works immediately after a confirmed create and a confirmed advance"
        (run nonceImmediateUse)
    ]

-- | Creates and initializes a nonce account (payer as authority), checks its
-- decoded state before use, sends a transfer through 'newNonceTransaction',
-- and checks the durable nonce advanced and the transfer landed with the
-- exact amount.
--
-- 'getNonceAccount' reads at the node's default @finalized@ commitment, so
-- the create transaction is finalized before its state is read;
-- 'newNonceTransaction' itself needs only @confirmed@ state (see
-- 'nonceImmediateUse').
nonceFlow :: Web3 ()
nonceFlow = do
  (payerPk, payerSk) <- fundedKeypair 2_000_000_000
  (noncePk, nonceSk) <- liftIO createSolanaKeyPair
  rent <- getMinimumBalanceForRentExemption 80
  createSig <-
    sendAndConfirm
      [payerSk, nonceSk]
      [ SystemProgram.createAccount payerPk noncePk rent 80 SystemProgram.systemProgramId,
        SystemProgram.initializeNonceAccount noncePk payerPk
      ]
  confirmFinalized createSig -- getNonceAccount reads the finalized bank
  -- The node's default encoding (a bare base58 string) and the explicit
  -- base64 pair must decode to the same 80 bytes.
  raw <- value <$> getAccountInfo' noncePk defaultConfigObject
  b64 <- getAccountInfo noncePk
  liftIO (fmap dataField raw @?= fmap dataField b64)
  -- Under encoding "jsonParsed" the node returns its parsed view for accounts
  -- it has a parser for (the nonce account) and the base64 pair otherwise
  -- (the payer, a plain system account with no data).
  parsed <- value <$> getAccountInfo' noncePk (defaultConfigObject {encoding = Just "jsonParsed"})
  case dataField <$> parsed of
    Just (AccountDataJSON prog _ space) -> liftIO ((prog @?= "nonce") >> (space @?= 80))
    other -> liftIO (assertFailure ("expected parsed nonce data, got " <> show other))
  fallback <- value <$> getAccountInfo' payerPk (defaultConfigObject {encoding = Just "jsonParsed"})
  liftIO (fmap dataField fallback @?= Just (AccountDataBinary mempty))
  before <- requireJust "nonce account (before)" =<< getNonceAccount noncePk
  nonceBefore <- case before of
    SystemProgram.NonceInitialized auth dn _ -> liftIO (auth @?= payerPk) >> pure dn
    _ -> liftIO (throwIO (userError "nonce account not initialized"))
  recipient <- fst <$> liftIO createSolanaKeyPair
  sig <- newNonceTransaction payerPk [payerSk] noncePk [SystemProgram.transfer payerPk recipient 100_000_000]
  confirmFinalized sig
  afterAcc <- requireJust "nonce account (after)" =<< getNonceAccount noncePk
  case afterAcc of
    SystemProgram.NonceInitialized _ dn _ -> liftIO (assertBool "durable nonce advanced" (dn /= nonceBefore))
    _ -> liftIO (throwIO (userError "nonce account no longer initialized"))
  bal <- getBalance recipient
  liftIO (bal @?= 100_000_000)

-- | The helper's contract: usable as soon as the nonce account's creation or
-- last advance is merely @confirmed@, with no finalization wait. Sends two
-- transfers back to back, the second within the finalization window of the
-- first one's nonce advance.
nonceImmediateUse :: Web3 ()
nonceImmediateUse = do
  (payerPk, payerSk) <- fundedKeypair 2_000_000_000
  (noncePk, nonceSk) <- liftIO createSolanaKeyPair
  rent <- getMinimumBalanceForRentExemption 80
  _ <-
    sendAndConfirm
      [payerSk, nonceSk]
      [ SystemProgram.createAccount payerPk noncePk rent 80 SystemProgram.systemProgramId,
        SystemProgram.initializeNonceAccount noncePk payerPk
      ]
  nonce0 <- durableNonceAtConfirmed noncePk
  recipient <- fst <$> liftIO createSolanaKeyPair
  sig1 <- newNonceTransaction payerPk [payerSk] noncePk [SystemProgram.transfer payerPk recipient 100_000_000]
  confirmOrFail "first nonce transaction" sig1
  nonce1 <- durableNonceAtConfirmed noncePk
  liftIO (assertBool "durable nonce advanced after the first use" (nonce1 /= nonce0))
  sig2 <- newNonceTransaction payerPk [payerSk] noncePk [SystemProgram.transfer payerPk recipient 100_000_000]
  confirmFinalized sig2
  afterAcc <- requireJust "nonce account (after)" =<< getNonceAccount noncePk
  case afterAcc of
    SystemProgram.NonceInitialized _ dn _ ->
      liftIO (assertBool "durable nonce advanced after the second use" (dn /= nonce1))
    _ -> liftIO (throwIO (userError "nonce account no longer initialized"))
  bal <- getBalance recipient
  liftIO (bal @?= 200_000_000)

-- | The nonce account's current durable nonce read at @confirmed@
-- commitment -- the same read 'newNonceTransaction' performs.
-- 'getNonceAccount' reads at the node's default @finalized@ commitment and
-- would still report the pre-advance value here.
durableNonceAtConfirmed :: SolanaPublicKey -> Web3 BlockHash
durableNonceAtConfirmed noncePk = do
  acc <- requireJust "nonce account (confirmed)" . value =<< getAccountInfo' noncePk cfgNonceAccountConfirmed
  state <-
    either
      (liftIO . throwIO . userError)
      pure
      (accountDataBytes (dataField acc) >>= SystemProgram.decodeNonceAccount)
  case state of
    SystemProgram.NonceInitialized _ dn _ -> pure dn
    _ -> liftIO (throwIO (userError "nonce account not initialized"))