Przeglądaj źródła

docs: Add `litesvm` docs to Testing Libraries section (#3565)

Kevin Heavey 7 miesięcy temu
rodzic
commit
0d18bb727e

+ 623 - 0
docs/content/docs/testing/litesvm.mdx

@@ -0,0 +1,623 @@
+---
+title: LiteSVM
+description: Write tests for Solana programs in Rust, TS/JS or Python using LiteSVM.
+---
+
+## Overview
+
+[`litesvm`](https://github.com/LiteSVM/litesvm) is a fast and lightweight library for testing Solana programs.
+It works by creating an in-process Solana VM optimized for program developers.
+This makes it much faster to run and compile than alternatives like `solana-program-test` and `solana-test-validator`.
+`litesvm` is available in Rust, TS/JS and Python (as part of the [`solders`](https://pypi.org/project/solders/) library).
+
+### Installation
+
+import { Tab, Tabs } from 'fumadocs-ui/components/tabs';
+
+<Tabs items={["Rust", "TS/JS", "Python"]}>
+```sh tab="Rust"
+cargo add litesvm --dev
+```
+
+```sh tab="TS/JS"
+npm i litesvm -D
+```
+
+```sh tab="Python"
+uv add solders # from solders import litesvm
+```
+
+</Tabs>
+
+### Minimal Example
+
+<Tabs items={["Rust", "TypeScript", "Python"]}>
+
+```rust tab="Rust"
+use litesvm::LiteSVM;
+use solana_message::Message;
+use solana_pubkey::Pubkey;
+use solana_system_interface::instruction::transfer;
+use solana_keypair::Keypair;
+use solana_signer::Signer;
+use solana_transaction::Transaction;
+
+let from_keypair = Keypair::new();
+let from = from_keypair.pubkey();
+let to = Pubkey::new_unique();
+
+let mut svm = LiteSVM::new();
+svm.airdrop(&from, 10_000).unwrap();
+
+let instruction = transfer(&from, &to, 64);
+let tx = Transaction::new(
+    &[&from_keypair],
+    Message::new(&[instruction], Some(&from)),
+    svm.latest_blockhash(),
+);
+let tx_res = svm.send_transaction(tx).unwrap();
+
+let from_account = svm.get_account(&from);
+let to_account = svm.get_account(&to);
+assert_eq!(from_account.unwrap().lamports, 4936);
+assert_eq!(to_account.unwrap().lamports, 64);
+```
+
+```ts tab="TypeScript"
+import { LiteSVM } from "litesvm";
+import {
+  PublicKey,
+  Transaction,
+  SystemProgram,
+  Keypair,
+  LAMPORTS_PER_SOL,
+} from "@solana/web3.js";
+
+test("one transfer", () => {
+  const svm = new LiteSVM();
+  const payer = new Keypair();
+  svm.airdrop(payer.publicKey, BigInt(LAMPORTS_PER_SOL));
+  const receiver = PublicKey.unique();
+  const blockhash = svm.latestBlockhash();
+  const transferLamports = 1_000_000n;
+  const ixs = [
+    SystemProgram.transfer({
+      fromPubkey: payer.publicKey,
+      toPubkey: receiver,
+      lamports: transferLamports,
+    }),
+  ];
+  const tx = new Transaction();
+  tx.recentBlockhash = blockhash;
+  tx.add(...ixs);
+  tx.sign(payer);
+  svm.sendTransaction(tx);
+  const balanceAfter = svm.getBalance(receiver);
+  expect(balanceAfter).toBe(transferLamports);
+});
+```
+
+```python tab="Python"
+from solders.keypair import Keypair
+from solders.litesvm import LiteSVM
+from solders.message import Message
+from solders.pubkey import Pubkey
+from solders.system_program import transfer
+from solders.transaction import VersionedTransaction
+
+
+def test_transfer() -> None:
+    receiver = Pubkey.new_unique()
+    client = LiteSVM()
+    payer = Keypair()
+    client.airdrop(payer.pubkey(), 1_000_000_000)
+    blockhash = client.latest_blockhash()
+    transfer_lamports = 1_000_000
+    ixs = [
+        transfer(
+            {
+                "from_pubkey": payer.pubkey(),
+                "to_pubkey": receiver,
+                "lamports": transfer_lamports,
+            }
+        )
+    ]
+    msg = Message.new_with_blockhash(ixs, payer.pubkey(), blockhash)
+    tx = VersionedTransaction(msg, [payer])
+    client.send_transaction(tx)
+    balance_after = client.get_balance(receiver)
+    assert balance_after == transfer_lamports
+```
+
+</Tabs>
+
+## Deploying Programs
+
+Most of the time we want to do more than just mess around with token transfers -
+we want to test our own programs.
+
+**Tip**: if you want to pull a Solana program from mainnet or devnet, use the `solana program dump` command from the Solana CLI.
+
+To add a compiled program to our tests we can use [`.add_program_from_file`](https://docs.rs/litesvm/latest/litesvm/struct.LiteSVM.html#method.add_program_from_file).
+
+Here's an example using a [simple program](https://github.com/solana-labs/solana-program-library/tree/bd216c8103cd8eb9f5f32e742973e7afb52f3b81/examples/rust/logging)
+from the Solana Program Library that just does some logging:
+
+<Tabs items={["Rust", "TypeScript", "Python"]}>
+
+```rust tab="Rust"
+use {
+    litesvm::LiteSVM,
+    solana_instruction::{account_meta::AccountMeta, Instruction},
+    solana_keypair::Keypair,
+    solana_pubkey::{pubkey, Pubkey},
+    solana_message::{Message, VersionedMessage},
+    solana_signer::Signer,
+    solana_transaction::VersionedTransaction,
+};
+
+fn test_logging() {
+    let program_id = pubkey!("Logging111111111111111111111111111111111111");
+    let account_meta = AccountMeta {
+        pubkey: Pubkey::new_unique(),
+        is_signer: false,
+        is_writable: true,
+    };
+    let ix = Instruction {
+        program_id,
+        accounts: vec![account_meta],
+        data: vec![5, 10, 11, 12, 13, 14],
+    };
+    let mut svm = LiteSVM::new();
+    let payer = Keypair::new();
+    let bytes = include_bytes!("../../node-litesvm/program_bytes/spl_example_logging.so");
+    svm.add_program(program_id, bytes);
+    svm.airdrop(&payer.pubkey(), 1_000_000_000).unwrap();
+    let blockhash = svm.latest_blockhash();
+    let msg = Message::new_with_blockhash(&[ix], Some(&payer.pubkey()), &blockhash);
+    let tx = VersionedTransaction::try_new(VersionedMessage::Legacy(msg), &[payer]).unwrap();
+    // let's sim it first
+    let sim_res = svm.simulate_transaction(tx.clone()).unwrap();
+    let meta = svm.send_transaction(tx).unwrap();
+    assert_eq!(sim_res.meta, meta);
+    assert_eq!(meta.logs[1], "Program log: static string");
+    assert!(meta.compute_units_consumed < 10_000) // not being precise here in case it changes
+}
+```
+
+```ts tab="TypeScript"
+import { LiteSVM, TransactionMetadata } from "litesvm";
+import {
+  Keypair,
+  LAMPORTS_PER_SOL,
+  PublicKey,
+  Transaction,
+  TransactionInstruction,
+} from "@solana/web3.js";
+
+test("spl logging", () => {
+  const programId = PublicKey.unique();
+  const svm = new LiteSVM();
+  svm.addProgramFromFile(programId, "program_bytes/spl_example_logging.so");
+  const payer = new Keypair();
+  svm.airdrop(payer.publicKey, BigInt(LAMPORTS_PER_SOL));
+  const blockhash = svm.latestBlockhash();
+  const ixs = [
+    new TransactionInstruction({
+      programId,
+      keys: [
+        { pubkey: PublicKey.unique(), isSigner: false, isWritable: false },
+      ],
+    }),
+  ];
+  const tx = new Transaction();
+  tx.recentBlockhash = blockhash;
+  tx.add(...ixs);
+  tx.sign(payer);
+  // let's sim it first
+  const simRes = svm.simulateTransaction(tx);
+  const sendRes = svm.sendTransaction(tx);
+  if (sendRes instanceof TransactionMetadata) {
+    expect(simRes.meta().logs()).toEqual(sendRes.logs());
+    expect(sendRes.logs()[1]).toBe("Program log: static string");
+  } else {
+    throw new Error("Unexpected tx failure");
+  }
+});
+
+```
+
+```python tab="Python"
+from pathlib import Path
+
+from solders.instruction import AccountMeta, Instruction
+from solders.keypair import Keypair
+from solders.litesvm import LiteSVM
+from solders.message import Message
+from solders.pubkey import Pubkey
+from solders.transaction import VersionedTransaction
+from solders.transaction_metadata import TransactionMetadata
+
+
+def test_logging() -> None:
+    program_id = Pubkey.from_string("Logging111111111111111111111111111111111111")
+    ix = Instruction(
+        program_id,
+        bytes([5, 10, 11, 12, 13, 14]),
+        [AccountMeta(Pubkey.new_unique(), is_signer=False, is_writable=True)],
+    )
+    client = LiteSVM()
+    payer = Keypair()
+    client.add_program_from_file(
+        program_id, Path("tests/fixtures/spl_example_logging.so")
+    )
+    client.airdrop(payer.pubkey(), 1_000_000_000)
+    blockhash = client.latest_blockhash()
+    msg = Message.new_with_blockhash([ix], payer.pubkey(), blockhash)
+    tx = VersionedTransaction(msg, [payer])
+    # let's sim it first
+    sim_res = client.simulate_transaction(tx)
+    meta = client.send_transaction(tx)
+    assert isinstance(meta, TransactionMetadata)
+    assert sim_res.meta() == meta
+    assert meta.logs()[1] == "Program log: static string"
+    assert (
+        meta.compute_units_consumed() < 10_000
+    )  # not being precise here in case it changes
+```
+
+</Tabs>
+
+## Time travel
+
+Many programs rely on the `Clock` sysvar: for example, a mint that doesn't become available until after
+a certain time. With `litesvm` you can dynamically overwrite the `Clock` sysvar
+using [`svm.set_sysvar::<Clock>()`](https://docs.rs/litesvm/latest/litesvm/struct.LiteSVM.html#method.set_sysvar)
+(or `.setClock` in TS, or `.set_clock` in Python).
+Here's an example using a program that panics if `clock.unix_timestamp` is greater than 100
+(which is on January 1st 1970):
+
+<Tabs items={["Rust", "TypeScript", "Python"]}>
+```rust tab="Rust"
+use {
+    litesvm::LiteSVM,
+    solana_clock::Clock,
+    solana_instruction::Instruction,
+    use solana_keypair::Keypair,
+    solana_message::{Message, VersionedMessage},
+    solana_pubkey::Pubkey,
+    solana_signer::Signer,
+    solana_transaction::VersionedTransaction,
+};
+
+fn test_set_clock() {
+    let program_id = Pubkey::new_unique();
+    let mut svm = LiteSVM::new();
+    let bytes = include_bytes!("../../node-litesvm/program_bytes/litesvm_clock_example.so");
+    svm.add_program(program_id, bytes);
+    let payer = Keypair::new();
+    let payer_address = payer.pubkey();
+    svm.airdrop(&payer.pubkey(), 1_000_000_000).unwrap();
+    let blockhash = svm.latest_blockhash();
+    let ixs = [Instruction {
+        program_id,
+        data: vec![],
+        accounts: vec![],
+    }];
+    let msg = Message::new_with_blockhash(&ixs, Some(&payer_address), &blockhash);
+    let versioned_msg = VersionedMessage::Legacy(msg);
+    let tx = VersionedTransaction::try_new(versioned_msg, &[&payer]).unwrap();
+    // set the time to January 1st 2000
+    let mut initial_clock = svm.get_sysvar::<Clock>();
+    initial_clock.unix_timestamp = 1735689600;
+    svm.set_sysvar::<Clock>(&initial_clock);
+    // this will fail because it's not January 1970 anymore
+    svm.send_transaction(tx).unwrap_err();
+    // so let's turn back time
+    let mut clock = svm.get_sysvar::<Clock>();
+    clock.unix_timestamp = 50;
+    svm.set_sysvar::<Clock>(&clock);
+    let ixs2 = [Instruction {
+        program_id,
+        data: vec![1], // unused, this is just to dedup the transaction
+        accounts: vec![],
+    }];
+    let msg2 = Message::new_with_blockhash(&ixs2, Some(&payer_address), &blockhash);
+    let versioned_msg2 = VersionedMessage::Legacy(msg2);
+    let tx2 = VersionedTransaction::try_new(versioned_msg2, &[&payer]).unwrap();
+    // now the transaction goes through
+    svm.send_transaction(tx2).unwrap();
+}
+
+```
+
+```ts tab="TypeScript"
+import {
+  FailedTransactionMetadata,
+  LiteSVM,
+  TransactionMetadata,
+} from "litesvm";
+import {
+  Keypair,
+  LAMPORTS_PER_SOL,
+  PublicKey,
+  Transaction,
+  TransactionInstruction,
+} from "@solana/web3.js";
+
+test("clock", () => {
+  const programId = PublicKey.unique();
+  const svm = new LiteSVM();
+  svm.addProgramFromFile(programId, "program_bytes/litesvm_clock_example.so");
+  const payer = new Keypair();
+  svm.airdrop(payer.publicKey, BigInt(LAMPORTS_PER_SOL));
+  const blockhash = svm.latestBlockhash();
+  const ixs = [
+    new TransactionInstruction({ keys: [], programId, data: Buffer.from("") }),
+  ];
+  const tx = new Transaction();
+  tx.recentBlockhash = blockhash;
+  tx.add(...ixs);
+  tx.sign(payer);
+  // set the time to January 1st 2000
+  const initialClock = svm.getClock();
+  initialClock.unixTimestamp = 1735689600n;
+  svm.setClock(initialClock);
+  // this will fail because the contract wants it to be January 1970
+  const failed = svm.sendTransaction(tx);
+  if (failed instanceof FailedTransactionMetadata) {
+    expect(failed.err().toString()).toContain("ProgramFailedToComplete");
+  } else {
+    throw new Error("Expected transaction failure here");
+  }
+  // so let's turn back time
+  const newClock = svm.getClock();
+  newClock.unixTimestamp = 50n;
+  svm.setClock(newClock);
+  const ixs2 = [
+    new TransactionInstruction({
+      keys: [],
+      programId,
+      data: Buffer.from("foobar"), // unused, just here to dedup the tx
+    }),
+  ];
+  const tx2 = new Transaction();
+  tx2.recentBlockhash = blockhash;
+  tx2.add(...ixs2);
+  tx2.sign(payer);
+  // now the transaction goes through
+  const success = svm.sendTransaction(tx2);
+  expect(success).toBeInstanceOf(TransactionMetadata);
+});
+
+```
+
+```python tab="Python"
+from pathlib import Path
+
+from solders.instruction import Instruction
+from solders.keypair import Keypair
+from solders.litesvm import LiteSVM
+from solders.message import Message
+from solders.pubkey import Pubkey
+from solders.transaction import VersionedTransaction
+from solders.transaction_metadata import FailedTransactionMetadata, TransactionMetadata
+
+
+def test_set_clock() -> None:
+    program_id = Pubkey.new_unique()
+    client = LiteSVM()
+    client.add_program_from_file(
+        program_id, Path("tests/fixtures/solders_clock_example.so")
+    )
+    payer = Keypair()
+    client.airdrop(payer.pubkey(), 1_000_000_000)
+    blockhash = client.latest_blockhash()
+    ixs = [Instruction(program_id=program_id, data=b"", accounts=[])]
+    msg = Message.new_with_blockhash(ixs, payer.pubkey(), blockhash)
+    tx = VersionedTransaction(msg, [payer])
+    # set the time to January 1st 2000
+    initial_clock = client.get_clock()
+    initial_clock.unix_timestamp = 1735689600
+    client.set_clock(initial_clock)
+    # this will fail because it's not January 1970 anymore
+    bad_res = client.send_transaction(tx)
+    assert isinstance(bad_res, FailedTransactionMetadata)
+    # so let's turn back time
+    clock = client.get_clock()
+    clock.unix_timestamp = 50
+    client.set_clock(clock)
+    ixs2 = [
+        Instruction(
+            program_id=program_id,
+            data=b"foobar",  # unused, this is just to dedup the transaction
+            accounts=[],
+        )
+    ]
+    msg2 = Message.new_with_blockhash(ixs2, payer.pubkey(), blockhash)
+    tx2 = VersionedTransaction(msg2, [payer])
+    # now the transaction goes through
+    good_res = client.send_transaction(tx2)
+    assert isinstance(good_res, TransactionMetadata)
+
+```
+</Tabs>
+
+See also: [`warp_to_slot`](https://docs.rs/litesvm/latest/litesvm/struct.LiteSVM.html#method.warp_to_slot), which lets you jump to a future slot.
+
+## Writing arbitrary accounts
+
+LiteSVM lets you write any account data you want, regardless of
+whether the account state would even be possible.
+
+Here's an example where we give an account a bunch of USDC,
+even though we don't have the USDC mint keypair. This is
+convenient for testing because it means we don't have to
+work with fake USDC in our tests:
+
+<Tabs items={["Rust", "TypeScript", "Python"]}>
+```rust tab="Rust"
+use {
+    litesvm::LiteSVM,
+    solana_account::Account,
+    solana_program_option::COption,
+    solana_program_pack::Pack,
+    solana_pubkey::{pubkey, Pubkey},
+    spl_associated_token_account_client::address::get_associated_token_address,
+    spl_token::{
+        state::{Account as TokenAccount, AccountState},
+        ID as TOKEN_PROGRAM_ID,
+    },
+};
+
+fn test_infinite_usdc_mint() {
+    let owner = Pubkey::new_unique();
+    let usdc_mint = pubkey!("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v");
+    let ata = get_associated_token_address(&owner, &usdc_mint);
+    let usdc_to_own = 1_000_000_000_000;
+    let token_acc = TokenAccount {
+        mint: usdc_mint,
+        owner: owner,
+        amount: usdc_to_own,
+        delegate: COption::None,
+        state: AccountState::Initialized,
+        is_native: COption::None,
+        delegated_amount: 0,
+        close_authority: COption::None,
+    };
+    let mut svm = LiteSVM::new();
+    let mut token_acc_bytes = [0u8; TokenAccount::LEN];
+    TokenAccount::pack(token_acc, &mut token_acc_bytes).unwrap();
+    svm.set_account(
+        ata,
+        Account {
+            lamports: 1_000_000_000,
+            data: token_acc_bytes.to_vec(),
+            owner: TOKEN_PROGRAM_ID,
+            executable: false,
+            rent_epoch: 0,
+        },
+    )
+    .unwrap();
+    let raw_account = svm.get_account(&ata).unwrap();
+    assert_eq!(
+        TokenAccount::unpack(&raw_account.data).unwrap().amount,
+        usdc_to_own
+    )
+}
+
+```
+
+```ts tab="TypeScript"
+import { LiteSVM } from "litesvm";
+import { PublicKey } from "@solana/web3.js";
+import {
+  getAssociatedTokenAddressSync,
+  AccountLayout,
+  ACCOUNT_SIZE,
+  TOKEN_PROGRAM_ID,
+} from "@solana/spl-token";
+
+test("infinite usdc mint", () => {
+  const owner = PublicKey.unique();
+  const usdcMint = new PublicKey(
+    "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
+  );
+  const ata = getAssociatedTokenAddressSync(usdcMint, owner, true);
+  const usdcToOwn = 1_000_000_000_000n;
+  const tokenAccData = Buffer.alloc(ACCOUNT_SIZE);
+  AccountLayout.encode(
+    {
+      mint: usdcMint,
+      owner,
+      amount: usdcToOwn,
+      delegateOption: 0,
+      delegate: PublicKey.default,
+      delegatedAmount: 0n,
+      state: 1,
+      isNativeOption: 0,
+      isNative: 0n,
+      closeAuthorityOption: 0,
+      closeAuthority: PublicKey.default,
+    },
+    tokenAccData,
+  );
+  const svm = new LiteSVM();
+  svm.setAccount(ata, {
+    lamports: 1_000_000_000,
+    data: tokenAccData,
+    owner: TOKEN_PROGRAM_ID,
+    executable: false,
+  });
+  const rawAccount = svm.getAccount(ata);
+  expect(rawAccount).not.toBeNull();
+  const rawAccountData = rawAccount?.data;
+  const decoded = AccountLayout.decode(rawAccountData);
+  expect(decoded.amount).toBe(usdcToOwn);
+});
+
+```
+
+```python tab="Python"
+from solders.account import Account
+from solders.litesvm import LiteSVM
+from solders.pubkey import Pubkey
+from solders.token import ID as TOKEN_PROGRAM_ID
+from solders.token.associated import get_associated_token_address
+from solders.token.state import TokenAccount, TokenAccountState
+
+
+def test_infinite_usdc_mint() -> None:
+    owner = Pubkey.new_unique()
+    usdc_mint = Pubkey.from_string("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v")
+    ata = get_associated_token_address(owner, usdc_mint)
+    usdc_to_own = 1_000_000_000_000
+    token_acc = TokenAccount(
+        mint=usdc_mint,
+        owner=owner,
+        amount=usdc_to_own,
+        delegate=None,
+        state=TokenAccountState.Initialized,
+        is_native=None,
+        delegated_amount=0,
+        close_authority=None,
+    )
+    client = LiteSVM()
+    client.set_account(
+        ata,
+        Account(
+            lamports=1_000_000_000,
+            data=bytes(token_acc),
+            owner=TOKEN_PROGRAM_ID,
+            executable=False,
+        ),
+    )
+    raw_account = client.get_account(ata)
+    assert raw_account is not None
+    raw_account_data = raw_account.data
+    assert TokenAccount.from_bytes(raw_account_data).amount == usdc_to_own
+
+```
+</Tabs>
+
+## Copying Accounts from a live environment
+
+If you want to copy accounts from mainnet or devnet, you can use the `solana account` command in the Solana CLI to save account data to a file.
+
+## Other features
+
+Other things you can do with `litesvm` include:
+
+* Changing the max compute units and other compute budget behaviour using [`.with_compute_budget`](https://docs.rs/litesvm/latest/litesvm/struct.LiteSVM.html#method.with_compute_budget).
+* Disable transaction signature checking using [`.with_sigverify(false)`](https://docs.rs/litesvm/latest/litesvm/struct.LiteSVM.html#method.with_sigverify).
+* Find previous transactions using [`.get_transaction`](https://docs.rs/litesvm/latest/litesvm/struct.LiteSVM.html#method.get_transaction).
+
+## When should I use `solana-test-validator`?
+
+While `litesvm` is faster and more convenient, it is also less like a real RPC node.
+So `solana-test-validator` is still useful when you need to call RPC methods that LiteSVM
+doesn't support, or when you want to test something that depends on real-life validator behaviour
+rather than just testing your program and client code.
+
+In general though it is recommended to use `litesvm` wherever possible, as it will make your life
+much easier.

+ 1 - 1
docs/content/docs/testing/meta.json

@@ -1,4 +1,4 @@
 {
   "title": "Testing Libraries",
-  "pages": ["mollusk"]
+  "pages": ["litesvm", "mollusk"]
 }