> ## Documentation Index
> Fetch the complete documentation index at: https://docs.tape.network/llms.txt
> Use this file to discover all available pages before exploring further.

# Write from a Solana Program

Your program can store data on Tapedrive by calling the Tapedrive program. This page carries two complete examples from the `examples` directory of the tape repository, both run end to end. Quill is a delegated writer that stores small named objects. Bundle collects a payload across several transactions and writes it as one track, for payloads larger than one transaction carries.

## How much fits

| Payload               | Path                                                                                                                                      |
| --------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
| Up to about 825 bytes | One transaction. Inline data is certified as soon as the transaction confirms. Quill.                                                     |
| Up to 10 KiB          | A program collects the payload across several transactions and writes it with one call. Still inline, still certified on confirm. Bundle. |
| Larger                | An erasure-coded write from the [SDK](/sdks/writing), off chain.                                                                          |

## What you need

* The Solana toolchain with `cargo build-sbf`, and a recent stable Rust for the client and the tests.
* A wallet with SOL for transaction fees and TAPE for the reservation. If you
  want to use Tapenet, follow the [wallet and funding setup](/tapenet/access).
* The `tape-api`, `tape-core` and `tape-crypto` crates, version 0.4.0, which the manifests below pull from crates.io.

## Quill: a delegated writer

A tape has an owner and an optional delegate, and either one can write to it. Quill does not own a tape. The tape's owner delegates writes to a PDA the program controls, `["quill", tape]`, once. The PDA holds no data. It exists so the program can sign for it.

Each write is then two calls. `build_track_write_ix` from `tape-api` builds the Tapedrive instruction, with a name and a content type so the write is a named object that a gateway can list and serve. `invoke_signed` sends it, signing as the PDA. The caller chooses the name and the bytes, and the program relays them.

Before it sends anything the program checks four things: the fee payer signed, the writer account is its own PDA for that tape, and the Tapedrive program, its system account and the slot hashes sysvar are the real ones. Tapedrive itself checks the tape, its capacity and its expiry.

### Full code

The program is three files.

```toml program/Cargo.toml theme={null}
[package]
name = "tape-quill-program"
version = "0.1.0"
edition = "2021"
description = "Quill: a Solana program that writes named objects to a Tapedrive tape as its delegate."
license = "Apache-2.0"

[lib]
crate-type = ["cdylib", "lib"]
name = "quill"

[dependencies]
tape-api = { version = "0.4.0", features = ["solana"] }
tape-core = { version = "0.4.0", features = ["solana"] }
tape-crypto = { version = "0.4.0", features = ["solana"] }

solana-program = "=4.0.0"
solana-system-interface = { version = "=3.0.0", features = ["bincode"] }
num_enum = "0.7.2"

bytemuck_derive = "1.9.3"
half = "=2.4.1"

[dev-dependencies]
# Mollusk harness preloaded with the Tapedrive program, so `cargo test` runs the CPI
tape-test = "0.4.0"
solana-account = "=3.4.0"

[profile.release]
overflow-checks = true
```

```rust program/src/lib.rs theme={null}
#![allow(unexpected_cfgs)]

//! Quill: a Solana program that writes named objects to a Tapedrive tape as its delegate.
//!
//! The tape owner delegates writes to the `["quill", tape]` PDA once. Each `write`
//! then builds a Tapedrive track write and sends it by CPI, signed as that PDA.
//! Inline data is certified when the transaction confirms. The caller chooses the
//! name, the content type and the bytes.

pub mod error;
pub mod write;

use solana_program::account_info::AccountInfo;
use solana_program::entrypoint;
use solana_program::entrypoint::ProgramResult;
use solana_program::pubkey::Pubkey;

use crate::write::process_write;

entrypoint!(process_instruction);

/// The one instruction: write a caller-named object to the tape
pub fn process_instruction(
    program_id: &Pubkey,
    accounts: &[AccountInfo],
    data: &[u8],
) -> ProgramResult {
    process_write(program_id, accounts, data)
}
```

```rust program/src/error.rs theme={null}
//! Errors returned by quill, surfaced as `ProgramError::Custom`

use solana_program::program_error::ProgramError;

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum QuillError {
    /// Instruction data is too short, the name is empty, or the name length overruns the data
    InvalidWire = 0,

    /// The content type is not a known `ContentType`
    InvalidContentType,

    /// The writer account is not the `["quill", tape]` PDA
    AddressMismatch,

    /// The fee payer did not sign
    MissingSignature,

    /// The Tapedrive program, its system account or the slot hashes sysvar is not the real one
    UnexpectedAccount,

    /// The Tapedrive track write could not be built, for example an oversized name
    TrackWriteBuildFailed,
}

impl From<QuillError> for ProgramError {
    fn from(error: QuillError) -> Self {
        ProgramError::Custom(error as u32)
    }
}
```

```rust program/src/write.rs theme={null}
//! The one instruction: write a caller-named object to a delegated tape by CPI

use solana_program::account_info::{next_account_info, AccountInfo};
use solana_program::entrypoint::ProgramResult;
use solana_program::msg;
use solana_program::program::invoke_signed;
use solana_program::program_error::ProgramError;
use solana_program::pubkey::Pubkey;
use solana_program::sysvar;

use tape_api::instruction::build_track_write_ix;
use tape_api::program::tapedrive::{self, system_pda};
use tape_core::track::data::{BlobData, BlobInfo, TrackObjectInfo};
use tape_core::types::{ContentType, StorageUnits};

use crate::error::QuillError;

/// PDA seed prefix; the delegate PDA is `["quill", tape]`
pub const QUILL_SEED: &[u8] = b"quill";

/// Wire header: `content_type (u16 LE)` then `name_len (u16 LE)`
const WIRE_HEADER_LEN: usize = 4;
const CONTENT_TYPE_OFFSET: usize = 0;
const NAME_LEN_OFFSET: usize = 2;

/// Write one caller-named object to `tape`, signing as the tape's delegate PDA
///
/// Instruction data is `[content_type u16 LE][name_len u16 LE][name][payload]`.
/// Accounts: `[payer, writer, tape, tapedrive_system, slot_hashes, tapedrive_program]`.
pub fn process_write(
    program_id: &Pubkey,
    accounts: &[AccountInfo],
    data: &[u8],
) -> ProgramResult {
    let (content_type, name, payload) = parse(data)?;

    let accounts = &mut accounts.iter();
    let payer = next_account_info(accounts)?;
    let writer = next_account_info(accounts)?;
    let tape = next_account_info(accounts)?;
    let tapedrive_system = next_account_info(accounts)?;
    let slot_hashes = next_account_info(accounts)?;
    let tapedrive_program = next_account_info(accounts)?;

    // the fee payer signs the outer transaction and pays for the CPI
    if !payer.is_signer {
        return Err(QuillError::MissingSignature.into());
    }

    // the writer is this program's delegate PDA for the tape; it holds no data and only signs
    let (address, bump) =
        Pubkey::find_program_address(&[QUILL_SEED, tape.key.as_ref()], program_id);
    if address != *writer.key {
        return Err(QuillError::AddressMismatch.into());
    }

    // the write goes to the real Tapedrive program with its system account and sysvar
    let system_key: Pubkey = system_pda().0.into();
    if *tapedrive_program.key != tapedrive::ID
        || *tapedrive_system.key != system_key
        || *slot_hashes.key != sysvar::slot_hashes::ID
    {
        return Err(QuillError::UnexpectedAccount.into());
    }

    // the caller owns the name and content type; inline data is certified on arrival
    let ix = build_track_write_ix(
        (*payer.key).into(),
        (*writer.key).into(),
        (*tape.key).into(),
        BlobInfo {
            object: Some(TrackObjectInfo {
                name: name.to_vec(),
                content_type,
                logical_size: StorageUnits::from_bytes(payload.len() as u64),
            }),
            data: BlobData::Inline(payload.to_vec()),
        },
    )
    .map_err(|_| QuillError::TrackWriteBuildFailed)?;

    // the PDA seeds sign for the writer, which is what authorizes the delegated write
    invoke_signed(
        &ix,
        &[
            payer.clone(),
            writer.clone(),
            tapedrive_system.clone(),
            tape.clone(),
            slot_hashes.clone(),
            tapedrive_program.clone(),
        ],
        &[&[QUILL_SEED, tape.key.as_ref(), &[bump]]],
    )?;

    msg!(
        "quill: wrote object {} ({} bytes)",
        core::str::from_utf8(name).unwrap_or("<binary>"),
        payload.len()
    );
    Ok(())
}

/// Split instruction data into `(content_type, name, payload)`
fn parse(data: &[u8]) -> Result<(ContentType, &[u8], &[u8]), ProgramError> {
    if data.len() < WIRE_HEADER_LEN {
        return Err(QuillError::InvalidWire.into());
    }

    let content_type_bytes = [data[CONTENT_TYPE_OFFSET], data[CONTENT_TYPE_OFFSET + 1]];
    let content_type = ContentType::try_from(u16::from_le_bytes(content_type_bytes))
        .map_err(|_| QuillError::InvalidContentType)?;
    let name_len =
        u16::from_le_bytes([data[NAME_LEN_OFFSET], data[NAME_LEN_OFFSET + 1]]) as usize;

    let body = &data[WIRE_HEADER_LEN..];
    if name_len == 0 || body.len() < name_len {
        return Err(QuillError::InvalidWire.into());
    }

    let (name, payload) = body.split_at(name_len);
    Ok((content_type, name, payload))
}
```

The test module at the end of `write.rs` is left out here. `cargo test` runs the write under Mollusk with the real Tapedrive program loaded, so the cross-program call is exercised, not just compiled.

The client reserves the tape, delegates it, sends one write and reads the tape back, using `solana-client` and the same builders.

<Accordion title="client/Cargo.toml and client/src/main.rs">
  ```toml client/Cargo.toml theme={null}
  [package]
  name = "quill-client"
  version = "0.1.0"
  edition = "2021"
  description = "CLI that drives the quill program: reserve, delegate, write, read back."
  license = "Apache-2.0"

  [[bin]]
  name = "quill"
  path = "src/main.rs"

  [dependencies]
  # The same builders the program uses, off chain this time, to assemble transactions.
  tape-api = "0.4.0"
  tape-core = "0.4.0"
  tape-crypto = "0.4.0"

  # Pinned to the versions tape-api resolves against, so the Pubkey and Instruction
  # types unify with no duplicate solana-program in the graph.
  solana-client = "=4.0.0"
  solana-program = "=4.0.0"
  solana-keypair = "=3.1.0"
  solana-signer = "=3.0.0"
  solana-transaction = "=3.1.0"
  solana-commitment-config = "=3.1.1"
  ```

  ```rust client/src/main.rs theme={null}
  //! CLI that drives the quill program end to end
  //!
  //! Reserve a tape, delegate writes to the program's PDA, write caller-named objects,
  //! and read the tape back, using vanilla `solana-client` plus the same `tape-api`
  //! builders the program uses.
  //!
  //! Config comes from the shared example environment:
  //! `SOLANA_RPC`, `KEYPAIR_PATH`, and optionally `QUILL_PROGRAM_ID`.
  //!
  //! Reserve size/duration and the written object are overridable on the command
  //! line: `--mb`, `--epochs`, `--name`, and either `--message <text>` for a literal
  //! payload or `--file <path>` to write a file's bytes (content type inferred from
  //! its extension, or forced with `--content-type <mime>`).

  use std::env;
  use std::fs;
  use std::path::Path;
  use std::str::FromStr;

  use solana_client::rpc_client::RpcClient;
  use solana_commitment_config::CommitmentConfig;
  use solana_keypair::{read_keypair_file, Keypair};
  use solana_program::instruction::{AccountMeta, Instruction};
  use solana_program::pubkey::Pubkey;
  use solana_program::sysvar;
  use solana_signer::Signer;
  use solana_transaction::Transaction;

  use tape_api::instruction::{build_reserve_tape_ix, build_set_tape_delegate_ix};
  use tape_api::program::tapedrive::{self, system_pda, tape_pda};
  use tape_api::state::{System, Tape};
  use tape_core::types::{ContentType, EpochNumber, StorageUnits};
  use tape_crypto::Address;

  type Result<T> = std::result::Result<T, Box<dyn std::error::Error>>;

  /// PDA seed prefix shared with the program (`["quill", tape]`)
  const QUILL_SEED: &[u8] = b"quill";

  const DEFAULT_MEGABYTES: u64 = 10;
  const DEFAULT_EPOCHS: u64 = 100;

  const DEFAULT_NAME: &str = "greetings/0001";
  const DEFAULT_MESSAGE: &str = "hello, tape";

  /// A whole inline write rides in a single transaction to the quill program, so it
  /// is bound by the Solana packet limit, not the 10 KB CPI ceiling. Keep the framed
  /// object (header + name + payload) within the SDK's proven inline budget (825 B);
  /// past this you'd move to a coded write. (The sibling `loom` example collects
  /// larger inline writes in a buffer account across several transactions.)
  const MAX_INLINE_BYTES: usize = 825;

  /// Run-time values parsed from the command line, each falling back to its default:
  /// reserve size (`--mb`), reserve duration (`--epochs`), and the object written by
  /// `write` / `all`: its `name`, `content_type`, and `payload` bytes (from either
  /// `--message` text or a `--file`).
  struct Options {
      megabytes: u64,
      epochs: u64,
      name: String,
      content_type: ContentType,
      payload: Vec<u8>,
  }

  fn main() -> Result<()> {
      let command = env::args().nth(1).unwrap_or_default();

      match command.as_str() {
          "reserve" | "delegate" | "write" | "show" | "all" => {}
          other => {
              println!("{}", usage(other));
              return Ok(());
          }
      }

      let options = parse_options()?;

      let rpc = RpcClient::new_with_commitment(rpc_url()?, CommitmentConfig::confirmed());
      let payer = read_keypair_file(keypair_path()?)?;
      let payer_key = pubkey_from_bytes(payer.pubkey().to_bytes());

      let quill_id = program_id()?;
      let tape = to_pubkey(&tape_pda(to_address(&payer_key)).0);
      let (writer, _) = Pubkey::find_program_address(&[QUILL_SEED, tape.as_ref()], &quill_id);

      println!("Quill: writing to Tapedrive from a program\n");
      println!("  payer / tape owner    {payer_key}");
      println!("  tape                  {tape}");
      println!("  writer PDA (delegate) {writer}");
      println!("  quill program         {quill_id}");
      println!();

      match command.as_str() {
          "reserve" => reserve(&rpc, &payer, &payer_key, &options)?,
          "delegate" => delegate(&rpc, &payer, &payer_key, &tape, &writer)?,
          "write" => write(&rpc, &payer, &payer_key, &tape, &quill_id, &writer, &options)?,
          "show" => show(&rpc, &tape)?,
          "all" => run_all(&rpc, &payer, &payer_key, &tape, &quill_id, &writer, &options)?,
          other => return Err(usage(other).into()),
      }

      Ok(())
  }

  /// Reserve, delegate, write one object, then print the tape
  fn run_all(
      rpc: &RpcClient,
      payer: &Keypair,
      payer_key: &Pubkey,
      tape: &Pubkey,
      quill_id: &Pubkey,
      writer: &Pubkey,
      options: &Options,
  ) -> Result<()> {
      reserve(rpc, payer, payer_key, options)?;
      delegate(rpc, payer, payer_key, tape, writer)?;
      write(rpc, payer, payer_key, tape, quill_id, writer, options)?;
      show(rpc, tape)
  }

  /// Reserve a tape owned by the payer, active from the current epoch
  fn reserve(rpc: &RpcClient, payer: &Keypair, payer_key: &Pubkey, options: &Options) -> Result<()> {
      let system = fetch_system(rpc)?;
      let activation = system.current_epoch;
      let expiry = EpochNumber(activation.0 + options.epochs);

      let instruction = build_reserve_tape_ix(
          to_address(payer_key),
          to_address(payer_key),
          StorageUnits::mb(options.megabytes),
          activation,
          expiry,
      );

      println!("Reserve a tape you own");
      println!(
          "  Tapedrive reserves [\"cassette\", payer], {} MB, active from this epoch",
          options.megabytes
      );
      send(rpc, payer, payer_key, &[instruction])
  }

  /// Delegate tape writes to the program's `["quill", tape]` PDA
  fn delegate(
      rpc: &RpcClient,
      payer: &Keypair,
      payer_key: &Pubkey,
      tape: &Pubkey,
      writer: &Pubkey,
  ) -> Result<()> {
      let instruction = build_set_tape_delegate_ix(
          to_address(payer_key),
          to_address(payer_key),
          to_address(tape),
          to_address(writer),
      );

      println!("Delegate writes to the program");
      println!("  SetTapeDelegate(delegate = writer PDA), the program may now append tracks");
      send(rpc, payer, payer_key, &[instruction])
  }

  /// Write one caller-named object to the tape via the program's delegate-signed CPI
  fn write(
      rpc: &RpcClient,
      payer: &Keypair,
      payer_key: &Pubkey,
      tape: &Pubkey,
      quill_id: &Pubkey,
      writer: &Pubkey,
      options: &Options,
  ) -> Result<()> {
      let tapedrive_system = to_pubkey(&system_pda().0);
      let data = wire(options.content_type, options.name.as_bytes(), &options.payload);

      let instruction = Instruction {
          program_id: *quill_id,
          accounts: vec![
              AccountMeta::new(*payer_key, true),
              AccountMeta::new_readonly(*writer, false),
              AccountMeta::new(*tape, false),
              AccountMeta::new_readonly(tapedrive_system, false),
              AccountMeta::new_readonly(sysvar::slot_hashes::ID, false),
              AccountMeta::new_readonly(tapedrive::ID, false),
          ],
          data,
      };

      println!(
          "Write object {:?} ({}, {} bytes)",
          options.name,
          options.content_type,
          options.payload.len()
      );
      println!("  the program calls TrackWrite as the delegate PDA (named, inline, certified at once)");
      send(rpc, payer, payer_key, &[instruction])
  }

  /// Read the tape account and print its authority, delegate, and track count
  fn show(rpc: &RpcClient, tape: &Pubkey) -> Result<()> {
      let account = rpc.get_account(tape)?;
      let state = Tape::unpack_with_discriminator(&account.data).map_err(|error| format!("unpack tape: {error:?}"))?;

      println!("Read the tape back");
      println!("  authority {}", to_pubkey(&state.authority));
      println!("  delegate  {}", to_pubkey(&state.delegate));
      println!("  used      {} MB of {} MB", state.used.to_mb(), state.capacity.to_mb());
      println!("  tracks    {} (next #{})", state.tracks.num_tracks, state.tracks.next_number.as_u64());
      Ok(())
  }

  /// Fixed framing ahead of the name: `content_type (u16 LE)` + `name_len (u16 LE)`
  const WIRE_HEADER_LEN: usize = 4;

  /// Frame instruction data: `[content_type u16 LE][name_len u16 LE][name][payload]`
  fn wire(content_type: ContentType, name: &[u8], payload: &[u8]) -> Vec<u8> {
      let mut data = Vec::with_capacity(WIRE_HEADER_LEN + name.len() + payload.len());
      data.extend_from_slice(&u16::from(content_type).to_le_bytes());
      data.extend_from_slice(&(name.len() as u16).to_le_bytes());
      data.extend_from_slice(name);
      data.extend_from_slice(payload);
      data
  }

  /// Sign and submit a transaction, printing the confirmed signature
  fn send(rpc: &RpcClient, payer: &Keypair, payer_key: &Pubkey, instructions: &[Instruction]) -> Result<()> {
      let blockhash = rpc.get_latest_blockhash()?;
      let transaction =
          Transaction::new_signed_with_payer(instructions, Some(payer_key), &[payer], blockhash);

      let signature = rpc.send_and_confirm_transaction(&transaction)?;
      println!("  confirmed  {signature}\n");
      Ok(())
  }

  /// Fetch and decode the Tapedrive System account
  fn fetch_system(rpc: &RpcClient) -> Result<System> {
      let account = rpc.get_account(&to_pubkey(&system_pda().0))?;
      let system = System::unpack_with_discriminator(&account.data).map_err(|error| format!("unpack system: {error:?}"))?;
      Ok(*system)
  }

  fn to_address(pubkey: &Pubkey) -> Address {
      Address::new(pubkey.to_bytes())
  }

  fn to_pubkey(address: &Address) -> Pubkey {
      Pubkey::new_from_array((*address).into())
  }

  fn pubkey_from_bytes(bytes: [u8; 32]) -> Pubkey {
      Pubkey::new_from_array(bytes)
  }

  fn rpc_url() -> Result<String> {
      Ok(env::var("SOLANA_RPC").unwrap_or_else(|_| "http://127.0.0.1:8899".to_string()))
  }

  fn keypair_path() -> Result<String> {
      env::var("KEYPAIR_PATH").map_err(|_| "set KEYPAIR_PATH to your keypair file".into())
  }

  /// Parse the flags after the command, resolving the object to write from either
  /// `--message` text or a `--file`, and defaulting anything unset:
  /// `--mb`, `--epochs`, `--name`, `--message`, `--file`, `--content-type`.
  fn parse_options() -> Result<Options> {
      let mut megabytes = DEFAULT_MEGABYTES;
      let mut epochs = DEFAULT_EPOCHS;
      let mut name: Option<String> = None;
      let mut message: Option<String> = None;
      let mut file: Option<String> = None;
      let mut content_type: Option<ContentType> = None;

      let mut args = env::args().skip(2);
      while let Some(flag) = args.next() {
          match flag.as_str() {
              "--mb" | "--megabytes" => {
                  megabytes = next_value(&mut args, &flag)?
                      .parse()
                      .map_err(|_| format!("invalid value for {flag}: expected a number"))?;
              }
              "--epochs" => {
                  epochs = next_value(&mut args, &flag)?
                      .parse()
                      .map_err(|_| format!("invalid value for {flag}: expected a number"))?;
              }
              "--name" => name = Some(next_value(&mut args, &flag)?),
              "--message" => message = Some(next_value(&mut args, &flag)?),
              "--file" => file = Some(next_value(&mut args, &flag)?),
              "--content-type" => {
                  content_type = Some(ContentType::from_str(&next_value(&mut args, &flag)?));
              }
              other => return Err(format!("unknown option {other:?}").into()),
          }
      }

      if file.is_some() && message.is_some() {
          return Err("pass either --message or --file, not both".into());
      }

      // A file supplies raw bytes (name/content-type inferred from the path); otherwise
      // the payload is `--message` text, sent as text/plain. Explicit flags win either way.
      let (name, content_type, payload) = match file {
          Some(path) => {
              let payload = fs::read(&path).map_err(|error| format!("read {path}: {error}"))?;
              let name = name.unwrap_or_else(|| file_name(&path));
              let content_type = content_type.unwrap_or_else(|| content_type_for(&path));
              (name, content_type, payload)
          }
          None => {
              let text = message.unwrap_or_else(|| DEFAULT_MESSAGE.to_string());
              let name = name.unwrap_or_else(|| DEFAULT_NAME.to_string());
              let content_type = content_type.unwrap_or(ContentType::TextPlain);
              (name, content_type, text.into_bytes())
          }
      };

      let framed = WIRE_HEADER_LEN + name.len() + payload.len();
      if framed > MAX_INLINE_BYTES {
          return Err(format!(
              "object is {framed} B framed (name {} + payload {}), over the {MAX_INLINE_BYTES} B \
               inline limit for a single transaction, use a smaller payload or a coded write",
              name.len(),
              payload.len(),
          )
          .into());
      }

      Ok(Options { megabytes, epochs, name, content_type, payload })
  }

  /// Take the next argument as a flag's value, or error naming the flag
  fn next_value(args: &mut impl Iterator<Item = String>, flag: &str) -> Result<String> {
      args.next().ok_or_else(|| format!("missing value for {flag}").into())
  }

  /// Object name for a file payload: the path's final component (e.g. `photo.png`)
  fn file_name(path: &str) -> String {
      Path::new(path)
          .file_name()
          .map(|name| name.to_string_lossy().into_owned())
          .unwrap_or_else(|| path.to_string())
  }

  /// Guess a `ContentType` from a file's extension, defaulting to binary (octet-stream)
  fn content_type_for(path: &str) -> ContentType {
      let extension = Path::new(path)
          .extension()
          .map(|ext| ext.to_string_lossy().to_ascii_lowercase())
          .unwrap_or_default();

      match extension.as_str() {
          "txt" => ContentType::TextPlain,
          "html" | "htm" => ContentType::TextHtml,
          "css" => ContentType::TextCss,
          "js" => ContentType::TextJavascript,
          "csv" => ContentType::TextCsv,
          "md" | "markdown" => ContentType::TextMarkdown,
          "json" => ContentType::ApplicationJson,
          "xml" => ContentType::ApplicationXml,
          "yaml" | "yml" => ContentType::ApplicationYaml,
          "pdf" => ContentType::ApplicationPdf,
          "zip" => ContentType::ApplicationZip,
          "gz" => ContentType::ApplicationGzip,
          "tar" => ContentType::ApplicationTar,
          "png" => ContentType::ImagePng,
          "jpg" | "jpeg" => ContentType::ImageJpeg,
          "gif" => ContentType::ImageGif,
          "webp" => ContentType::ImageWebp,
          _ => ContentType::Unknown,
      }
  }

  /// Resolve the deployed program id from `QUILL_PROGRAM_ID`, or the build keypair
  fn program_id() -> Result<Pubkey> {
      if let Ok(value) = env::var("QUILL_PROGRAM_ID") {
          return Ok(Pubkey::from_str(&value)?);
      }

      let keypair = read_keypair_file("../program/target/deploy/quill-keypair.json")
          .map_err(|_| "set QUILL_PROGRAM_ID or build the program first (cargo build-sbf)")?;
      Ok(pubkey_from_bytes(keypair.pubkey().to_bytes()))
  }

  fn usage(command: &str) -> String {
      let prefix = if command.is_empty() {
          String::from("usage")
      } else {
          format!("unknown command {command:?}")
      };
      format!(
          "{prefix}: reserve | delegate | write | show | all\
          \n  --mb <n>            reserve size in MB (default {DEFAULT_MEGABYTES})\
          \n  --epochs <n>        reserve duration in epochs (default {DEFAULT_EPOCHS})\
          \n  --name <s>          object name (default {DEFAULT_NAME:?}, or the file name)\
          \n  --message <s>       object payload as text (default {DEFAULT_MESSAGE:?})\
          \n  --file <path>       object payload from a file (bytes; overrides --message)\
          \n  --content-type <m>  MIME type override, e.g. image/png (default: inferred)"
      )
  }
  ```
</Accordion>

### Build and deploy

```bash theme={null}
cd examples/quill/program
cargo build-sbf
cargo test
solana program deploy --program-id target/deploy/quill-keypair.json target/deploy/quill.so
```

`solana program deploy` prints the program id. It deploys to whichever cluster your Solana CLI is configured for.

### Run it

The client reads three environment variables. `all` runs reserve, delegate, write and show in one go, and each step is also its own command.

```bash theme={null}
cd ../client
export SOLANA_RPC=<rpc url>
export KEYPAIR_PATH=~/.config/solana/id.json
export QUILL_PROGRAM_ID=<your program id>
cargo run --release -- all
```

```text Output theme={null}
Quill: writing to Tapedrive from a program

  payer / tape owner    7CuwSm4eYdxPNVNVPBMhzDWtPa9zN6sUYcz8WvwtDE5h
  tape                  9TpyETXaV5REGLZw3Dqz7eSMi8tNmwSCJzaH5SF99uZG
  writer PDA (delegate) Ceik5EbjgVwC25WfiAzqGx6ovv54qub5dzvpGdd5hkms
  quill program         6C7LAhBBp5jRz7hDBCL2JG2PYEnhY8Ado9h61zNfocNt

Reserve a tape you own
  Tapedrive reserves ["cassette", payer], 10 MB, active from this epoch
  confirmed  JSQTG27fsai4TPkco2TTvwyZTeaG1FnFxB1xQn6gEdnnsdGdan4aaRCWt57Ue7KCyVMqPAWV1VFx6NbLo2vGb5A

Delegate writes to the program
  SetTapeDelegate(delegate = writer PDA), the program may now append tracks
  confirmed  3M1KsHRqXhTTuvdk3DrazogECyyuyVHU5KGitVVXrwhbSLDxYgs88vtU7Z34zA5ZoC4gRzCT4cQmJGMcfn2ipfNe

Write object "greetings/0001" (text/plain, 11 bytes)
  the program calls TrackWrite as the delegate PDA (named, inline, certified at once)
  confirmed  4ugWqynQTiERSyL9zPLJDkdAxN9QnXTUNJiBPyrkBG7ybHxsgq7WPmd4KDbayqb8guBx6vv7Sp4bWLtyqe14p25q

Read the tape back
  authority 7CuwSm4eYdxPNVNVPBMhzDWtPa9zN6sUYcz8WvwtDE5h
  delegate  Ceik5EbjgVwC25WfiAzqGx6ovv54qub5dzvpGdd5hkms
  used      1 MB of 10 MB
  tracks    1 (next #1)
```

The tape is the one owned by your wallet key itself, at `["cassette", payer]`, so `reserve` runs once per wallet. After that, `write` appends as many objects as you like:

```bash theme={null}
cargo run --release -- write --name greetings/0002 --message "second"
cargo run --release -- write --file ./hello.txt
```

### Read it back

The object is readable the moment the write confirms. Use the tape address the client printed:

```bash theme={null}
tape object ls --bucket 9TpyETXaV5REGLZw3Dqz7eSMi8tNmwSCJzaH5SF99uZG
tape object get --bucket 9TpyETXaV5REGLZw3Dqz7eSMi8tNmwSCJzaH5SF99uZG greetings/0001 -
```

```text Output theme={null}
TYPE            SIZE  CONTENT-TYPE  NAME
object            11  text/plain    greetings/0001
hello, tape
```

Tapenet's gateway serves the same object by name at
`https://gw.tape.network/site/<tape-address>/greetings/0001`. Replace `<tape-address>`
with the tape address your client printed.

### Using this in your own program

* Depend on `tape-api`. Its builders are pure and need no RPC.
* Get write access to a tape: own it, or have the owner delegate to your PDA. From the CLI that is [`tape delegate <pda>`](/tools/cli/commands/tape-management#delegate). From code it is `build_set_tape_delegate_ix`, as in the client above.
* For each write, decide two things. `data`: inline for small payloads that must be readable at once, coded for large ones. `object`: `Some` with a name to make the write listable and servable, `None` for a bare track keyed by its hash.
* Build with `build_track_write_ix`, send with `invoke_signed`. The instruction wants five accounts in order: fee payer, signer (your PDA), the Tapedrive system account, the tape, and the slot hashes sysvar. The builder lays them out. Check that the Tapedrive program and sysvar accounts the caller passed are the real ones, as `write.rs` does.
* The PDA needs no state, so there is nothing to initialise and nothing to close.
* Anchor and native programs use the code as shown. A Pinocchio program reuses the instruction bytes and the five-account order with its own types.

## Bundle: a payload larger than one transaction

A transaction from a wallet fits about 825 bytes of inline data, because the whole payload has to fit one packet. A cross-program call is bound by a different limit, Solana's 10 KiB cap on instruction data, which is also Tapedrive's inline maximum. Bundle uses that gap.

It keeps one buffer account per tape and payer, at `["bundle", tape, payer]`. `append` adds the next piece of the payload and creates the buffer on the first call. `flush` writes the finished buffer to Tapedrive with one call. `close` returns the buffer's rent. The buffer holds the exact bytes of the track write instruction, so `flush` points the call straight at the account with no copy. The tape owner sets the buffer PDA as the tape's delegate once, so it can sign the write.

The payer is part of the buffer's seeds, so only the payer who fills a buffer can append to it, flush it or close it, and `flush` accepts only the real Tapedrive program as its target. The result is an ordinary inline track, certified on confirm, for payloads between 825 bytes and 10 KiB. Above 10 KiB there is no trick. Use a coded write.

### Full code

The program is written with [Quasar](https://github.com/blueshift-gg/quasar), a zero-copy `no_std` framework, and builds with the standard Solana toolchain.

```toml program/Cargo.toml theme={null}
[package]
name = "tape-bundle-program"
version = "0.1.0"
edition = "2021"

# Detached from any parent workspace: Quasar pins bleeding-edge SDK crates that
# must not perturb tape-internal's or tape-public's dependency resolution.
[workspace]

[lib]
name = "bundle"
crate-type = ["cdylib", "lib"]
doctest = false

[features]
alloc = []
# Declared so the cfgs the Quasar derive macros emit are recognized; unused here.
debug = []
idl-build = []

[dependencies]
quasar-lang = { git = "https://github.com/blueshift-gg/quasar", rev = "37e8a6b48192cc44ab6c8b74cdd83d84c345575c" }

[lints.rust]
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(target_os, values("solana"))'] }
```

```rust program/src/lib.rs theme={null}
//! Bundle: collects a payload across several transactions, then writes it to
//! Tapedrive as one inline track by CPI, so the payload can be larger than one
//! transaction carries.

#![no_std]
// Some account fields are only validated or loaded by the framework and never
// read here, which is normal for Quasar programs.
#![allow(dead_code)]

use quasar_lang::prelude::*;

pub mod buffer;
pub mod error;
pub mod wire;

mod instructions;

use instructions::{Append, Close, Flush};

declare_id!("4RepWD31ARA7QTqmm3u3weNUR2v6uUX3vAMPjkk5U9Wo");

#[program]
mod bundle {
    use super::*;

    /// Add the next piece of payload, creating the buffer on the first call
    #[instruction(discriminator = 0)]
    pub fn append(ctx: Ctx<Append>) -> Result<(), ProgramError> {
        let data = ctx.data;
        ctx.accounts.run(data, &ctx.bumps)
    }

    /// Write the finished buffer to Tapedrive in one CPI
    #[instruction(discriminator = 1)]
    pub fn flush(ctx: Ctx<Flush>) -> Result<(), ProgramError> {
        ctx.accounts.run(&ctx.bumps)
    }

    /// Return the buffer rent to the payer
    #[instruction(discriminator = 2)]
    pub fn close(ctx: Ctx<Close>) -> Result<(), ProgramError> {
        ctx.accounts.run()
    }
}
```

```rust program/src/wire.rs theme={null}
//! Constants for the Tapedrive track write instruction that flush sends
//!
//! An inline write is the instruction byte, a kind byte, a two byte length,
//! then the payload. These must match Tapedrive's parser.

use quasar_lang::prelude::*;

/// The Tapedrive program that flush calls into
pub const TAPEDRIVE_ID: Address = address!("4iG5wyAy1u4gCqHm5jYJNU1hsYw7PtQ1N6dC8rZQKmPB");

/// Instruction byte for a Tapedrive track write
pub const TRACK_WRITE_DISCRIMINATOR: u8 = 0xB0;

/// Kind byte marking an inline track
pub const TRACK_KIND_INLINE: u8 = 0;

/// Bytes before the payload: instruction, kind, and a two byte length
pub const WIRE_PREFIX_LEN: usize = 4;

/// Offset of the two byte payload length within the prefix
pub const DATA_LEN_OFFSET: usize = 2;

/// Offset where the payload begins
pub const PAYLOAD_OFFSET: usize = WIRE_PREFIX_LEN;

/// The most instruction data one CPI can carry, matched by Tapedrive
pub const MAX_CPI_INSTRUCTION_DATA_LEN: usize = 10 * 1024;

/// Largest payload that still fits once the prefix is added
pub const MAX_PAYLOAD: usize = MAX_CPI_INSTRUCTION_DATA_LEN - WIRE_PREFIX_LEN;
```

```rust program/src/buffer.rs theme={null}
//! The buffer account: its seeds and size
//!
//! One buffer per tape and payer. It holds exactly the track write bytes, the
//! prefix followed by the payload appended so far, and grows with each append,
//! so flush can hand the whole account to the CPI without copying it. The
//! payer is part of the seeds, so only the payer who filled a buffer can
//! append to it, flush it or close it.

use quasar_lang::prelude::*;

use crate::wire::{MAX_CPI_INSTRUCTION_DATA_LEN, MAX_PAYLOAD, PAYLOAD_OFFSET, WIRE_PREFIX_LEN};

/// Seed prefix for the buffer address
pub const BUNDLE_SEED: &[u8] = b"bundle";

/// Account size needed to hold a payload of the given length
pub fn account_size(payload_len: usize) -> usize {
    PAYLOAD_OFFSET + payload_len
}

/// The buffer address, derived from the tape it serves and the payer filling it
#[derive(Seeds)]
#[seeds(b"bundle", tape: Address, payer: Address)]
pub struct Bundle;

// the payload must fit one inline track write
const _: () = assert!(MAX_PAYLOAD + WIRE_PREFIX_LEN <= MAX_CPI_INSTRUCTION_DATA_LEN);
```

```rust program/src/error.rs theme={null}
//! Errors returned by bundle instructions

use quasar_lang::prelude::*;

/// Ways a buffer can fail
#[error_code]
pub enum BundleError {
    /// Appending this piece would push the payload past the inline maximum
    PayloadTooLarge,

    /// Flush was called before anything was appended
    Empty,
}
```

```rust program/src/instructions/mod.rs theme={null}
//! Instruction handlers

mod append;
mod close;
mod flush;

pub use append::Append;
pub use close::Close;
pub use flush::Flush;
```

```rust program/src/instructions/append.rs theme={null}
//! Adds the next piece of payload, creating the buffer on the first call
//!
//! There is no separate init step. The first append creates the buffer sized to
//! the first piece, and each later append grows it. The instruction data is just
//! the piece bytes. The tape owner must set the buffer as the tape delegate
//! first so flush can sign the write. Only the payer in the seeds can append.

use quasar_lang::accounts::realloc_account;
use quasar_lang::cpi::{system, Seed};
use quasar_lang::prelude::*;

use crate::error::BundleError;
use crate::buffer::{account_size, Bundle, BUNDLE_SEED};
use crate::wire::{
    DATA_LEN_OFFSET, MAX_PAYLOAD, PAYLOAD_OFFSET, TRACK_KIND_INLINE, TRACK_WRITE_DISCRIMINATOR,
};
use crate::ID;

/// Accounts for append: the payer, the tape, the buffer, and system
#[derive(Accounts)]
pub struct Append {
    #[account(mut)]
    pub payer: Signer,

    pub tape: UncheckedAccount,

    #[account(mut, address = Bundle::seeds(tape.address(), payer.address()))]
    pub buffer: UncheckedAccount,

    pub rent: Sysvar<Rent>,
    pub system_program: Program<SystemProgram>,
}

impl Append {
    #[inline(always)]
    pub fn run(&mut self, piece: &[u8], bumps: &AppendBumps) -> Result<(), ProgramError> {
        if self.buffer.to_account_view().lamports() == 0 {
            self.create(piece, bumps)
        } else {
            self.grow(piece)
        }
    }

    #[inline(always)]
    fn create(&mut self, piece: &[u8], bumps: &AppendBumps) -> Result<(), ProgramError> {
        if piece.len() > MAX_PAYLOAD {
            return Err(BundleError::PayloadTooLarge.into());
        }

        let space = account_size(piece.len());
        let lamports = self.rent.get().try_minimum_balance(space)?;
        let bump = [bumps.buffer];
        let seeds = [
            Seed::from(BUNDLE_SEED),
            Seed::from(self.tape.address().as_ref()),
            Seed::from(self.payer.address().as_ref()),
            Seed::from(bump.as_ref()),
        ];
        system::create_account(
            self.payer.to_account_view(),
            self.buffer.to_account_view(),
            lamports,
            space as u64,
            &ID,
        )
        .invoke_signed(&seeds)?;

        self.buffer
            .write_bytes(0, &[TRACK_WRITE_DISCRIMINATOR, TRACK_KIND_INLINE])?;
        self.write_len(piece.len())?;
        self.buffer.write_bytes(PAYLOAD_OFFSET, piece)
    }

    #[inline(always)]
    fn grow(&mut self, piece: &[u8]) -> Result<(), ProgramError> {
        let filled = self.buffer.to_account_view().data_len() - PAYLOAD_OFFSET;
        let total = filled
            .checked_add(piece.len())
            .ok_or(BundleError::PayloadTooLarge)?;
        if total > MAX_PAYLOAD {
            return Err(BundleError::PayloadTooLarge.into());
        }

        self.resize(account_size(total))?;
        self.buffer.write_bytes(PAYLOAD_OFFSET + filled, piece)?;
        self.write_len(total)
    }

    #[inline(always)]
    fn resize(&mut self, space: usize) -> Result<(), ProgramError> {
        let rent = self.rent.get();
        let payer = self.payer.to_account_view();
        // SAFETY: we hold &mut self, so this is the only reference to the buffer.
        let account = unsafe { self.buffer.to_account_view_mut() };
        realloc_account(account, space, payer, Some(rent))
    }

    #[inline(always)]
    fn write_len(&mut self, payload_len: usize) -> Result<(), ProgramError> {
        self.buffer
            .write_bytes(DATA_LEN_OFFSET, &(payload_len as u16).to_le_bytes())
    }
}
```

```rust program/src/instructions/flush.rs theme={null}
//! Writes the finished buffer to Tapedrive in one CPI
//!
//! The buffer is the tape delegate, so it signs the write through its PDA seeds.
//! The payload is too large to copy onto the stack, so the call points straight
//! at the buffer bytes with the low level invoke. Only the payer in the seeds
//! can flush, and the call can only go to the real Tapedrive program.

use quasar_lang::cpi::{
    invoke_raw, result_from_raw, CpiAccount, InstructionAccount, Seed, Signer as CpiSigner,
};
use quasar_lang::prelude::*;

use crate::error::BundleError;
use crate::buffer::{Bundle, BUNDLE_SEED};
use crate::wire::{TAPEDRIVE_ID, WIRE_PREFIX_LEN};

/// Accounts for flush: the five Tapedrive write accounts in order, then the
/// Tapedrive program so the runtime loads it
#[derive(Accounts)]
pub struct Flush {
    #[account(mut)]
    pub payer: Signer,

    #[account(mut)]
    pub tape: UncheckedAccount,

    // Read only: it signs through its PDA seeds, not as a writable account.
    #[account(address = Bundle::seeds(tape.address(), payer.address()))]
    pub buffer: UncheckedAccount,

    pub tapedrive_system: UncheckedAccount,
    pub slot_hashes: UncheckedAccount,

    #[account(address = TAPEDRIVE_ID)]
    pub tapedrive_program: UncheckedAccount,
}

impl Flush {
    #[inline(always)]
    pub fn run(&self, bumps: &FlushBumps) -> Result<(), ProgramError> {
        let buffer = self.buffer.to_account_view();

        // the whole account is the track write bytes, so its length is the wire
        // length; anything at or below the prefix means nothing was appended
        let wire_len = buffer.data_len();
        if wire_len <= WIRE_PREFIX_LEN {
            return Err(BundleError::Empty.into());
        }

        let payer = self.payer.to_account_view();
        let system = self.tapedrive_system.to_account_view();
        let tape = self.tape.to_account_view();
        let slot_hashes = self.slot_hashes.to_account_view();

        let metas = [
            InstructionAccount::writable_signer(payer.address()),
            InstructionAccount::readonly_signer(buffer.address()),
            InstructionAccount::readonly(system.address()),
            InstructionAccount::writable(tape.address()),
            InstructionAccount::readonly(slot_hashes.address()),
        ];
        let cpi_accounts = [
            CpiAccount::from(payer),
            CpiAccount::from(buffer),
            CpiAccount::from(system),
            CpiAccount::from(tape),
            CpiAccount::from(slot_hashes),
        ];

        let bump = [bumps.buffer];
        let seeds = [
            Seed::from(BUNDLE_SEED),
            Seed::from(tape.address().as_ref()),
            Seed::from(payer.address().as_ref()),
            Seed::from(bump.as_ref()),
        ];
        let signer = CpiSigner::from(&seeds);

        // the whole account is the payload, and it is read only in the call so
        // nothing else can write it
        let wire_ptr = buffer.data_ptr();

        // SAFETY: every pointer and length is in bounds and lives for the call.
        let result = unsafe {
            invoke_raw(
                &TAPEDRIVE_ID,
                metas.as_ptr(),
                metas.len(),
                wire_ptr,
                wire_len,
                cpi_accounts.as_ptr(),
                cpi_accounts.len(),
                core::slice::from_ref(&signer),
            )
        };

        result_from_raw(result)
    }
}
```

```rust program/src/instructions/close.rs theme={null}
//! Returns the buffer rent to the payer who filled it
//!
//! The payer is in the buffer's seeds, so nobody else can close it. This only
//! moves the lamports out. A production version would also clear the data and
//! reassign the account to stop it being reused in the same transaction.

use quasar_lang::prelude::*;

use crate::buffer::Bundle;

/// Accounts for close: the refund recipient, the tape, and its buffer
#[derive(Accounts)]
pub struct Close {
    #[account(mut)]
    pub payer: Signer,

    pub tape: UncheckedAccount,

    #[account(mut, address = Bundle::seeds(tape.address(), payer.address()))]
    pub buffer: UncheckedAccount,
}

impl Close {
    #[inline(always)]
    pub fn run(&self) -> Result<(), ProgramError> {
        let buffer = self.buffer.to_account_view();
        let payer = self.payer.to_account_view();

        let reclaimed = buffer.lamports();
        let payer_total = payer
            .lamports()
            .checked_add(reclaimed)
            .ok_or(ProgramError::ArithmeticOverflow)?;

        set_lamports(buffer, 0);
        set_lamports(payer, payer_total);
        Ok(())
    }
}
```

The tests drive the real sequence under Mollusk: several appends, then a flush into the loaded Tapedrive program. They also show the instruction layouts a client sends. `append` is the byte `0` followed by the piece, with the payer, the tape, the buffer, the rent sysvar and the system program. `flush` is the byte `1`, with the payer, the tape, the buffer, the Tapedrive system account, the slot hashes sysvar and the Tapedrive program. `close` is the byte `2`, with the payer, the tape and the buffer.

<Accordion title="tests/Cargo.toml and tests/src/lib.rs">
  ```toml tests/Cargo.toml theme={null}
  [package]
  name = "bundle-tests"
  version = "0.1.0"
  edition = "2021"
  publish = false

  # Standalone: host-built Mollusk tests, resolved independently of the on-chain
  # program crate (which builds under a different, SBF, toolchain).
  [workspace]

  [lib]
  path = "src/lib.rs"

  [dev-dependencies]
  # Tapedrive types for building the on-chain fixtures the CPI writes into.
  tape-api = { version = "0.4.0", features = ["solana"] }
  tape-core = { version = "0.4.0", features = ["solana"] }
  tape-crypto = "0.4.0"

  # Versions pinned to the monorepo's lockfile so tapedrive.so loads cleanly.
  mollusk-svm = "0.13.4"
  bincode = "1.3.3"
  solana-account = "3.4.0"
  solana-instruction = "3.2.0"
  solana-packet = "4.1.0"
  solana-program-error = "3.0.1"
  solana-pubkey = "3.0.0"
  solana-rent = "3.1.0"
  solana-system-interface = "2.0.0"
  solana-transaction = { version = "3.1.0", features = ["bincode"] }
  ```

  ```rust tests/src/lib.rs theme={null}
  //! Host-side Mollusk tests for the bundle program
  #![cfg(test)]

  // bundle buffers a large payload and writes it to Tapedrive with one CPI,
  // proving a payload bigger than one transaction can go inline.

  use mollusk_svm::program::create_program_account_loader_v2;
  use mollusk_svm::program::keyed_account_for_system_program;
  use mollusk_svm::program::loader_keys::LOADER_V3;
  use mollusk_svm::result::Check;
  use mollusk_svm::sysvar::Sysvars;
  use mollusk_svm::Mollusk;

  use solana_account::Account;
  use solana_instruction::{AccountMeta, Instruction};
  use solana_packet::PACKET_DATA_SIZE;
  use solana_program_error::ProgramError;
  use solana_pubkey::Pubkey;
  use solana_transaction::Transaction;

  use tape_api::program::prelude::*;
  use tape_core::track::archive::TrackArchive;
  use tape_core::track::TRACK_TREE_HEIGHT;
  use tape_crypto::merkle::MerkleTree;

  const BUNDLE_ELF: &[u8] = include_bytes!(concat!(
      env!("CARGO_MANIFEST_DIR"),
      "/../program/target/deploy/bundle.so"
  ));
  const TAPEDRIVE_ELF: &[u8] =
      include_bytes!(concat!(env!("CARGO_MANIFEST_DIR"), "/fixtures/tapedrive.so"));

  const APPEND: u8 = 0;
  const FLUSH: u8 = 1;

  const SEED: &[u8] = b"bundle";

  // the largest piece that keeps an append within one packet (see the size test)
  const PIECE: usize = 928;

  // wire prefix that sits before the payload in a buffer account
  const BUFFER_OVERHEAD: usize = 4;

  // custom error code for Empty, the second variant in the program error enum
  const EMPTY_ERROR: u32 = 1;

  fn bundle_id() -> Pubkey {
      "4RepWD31ARA7QTqmm3u3weNUR2v6uUX3vAMPjkk5U9Wo"
          .parse()
          .expect("valid pubkey")
  }

  // a system-owned wallet holding lamports
  fn wallet(key: Pubkey, lamports: u64) -> (Pubkey, Account) {
      (
          key,
          Account {
              lamports,
              data: vec![],
              owner: solana_system_interface::program::ID,
              executable: false,
              rent_epoch: 0,
          },
      )
  }

  // a Tapedrive-owned account holding packed state
  fn owned(key: Pubkey, data: Vec<u8>) -> (Pubkey, Account) {
      let lamports = solana_rent::Rent::default().minimum_balance(data.len());
      (
          key,
          Account {
              lamports,
              data,
              owner: tapedrive::ID,
              executable: false,
              rent_epoch: 0,
          },
      )
  }

  // the slot hashes sysvar with a single entry
  fn slot_hashes() -> (Pubkey, Account) {
      let mut data = vec![0u8; 48];
      data[0] = 1;
      (
          sysvar::slot_hashes::ID,
          Account {
              lamports: 1,
              data,
              owner: sysvar::ID,
              executable: false,
              rent_epoch: 0,
          },
      )
  }

  // the shared fixtures every test starts from
  struct Env {
      payer: Pubkey,
      tape_key: Pubkey,
      buffer: Pubkey,
      system_key: Pubkey,
      rent_id: Pubkey,
      system_program_id: Pubkey,
      accounts: Vec<(Pubkey, Account)>,
      initial_tape: Vec<u8>,
  }

  fn setup() -> (Mollusk, Env) {
      let mut mollusk = Mollusk::default();
      mollusk.add_program_with_loader_and_elf(&bundle_id(), &LOADER_V3, BUNDLE_ELF);
      mollusk.add_program_with_loader_and_elf(&tapedrive::ID, &LOADER_V3, TAPEDRIVE_ELF);

      let payer = Pubkey::new_unique();
      let authority = Pubkey::new_unique();

      let (tape_addr, _) = tape_pda(authority.into());
      let tape_key: Pubkey = tape_addr.into();
      let (buffer, _) =
          Pubkey::find_program_address(&[SEED, tape_key.as_ref(), payer.as_ref()], &bundle_id());
      let (system_addr, _) = system_pda();
      let system_key: Pubkey = system_addr.into();

      // the tape delegates to the buffer so bundle can sign the write
      let tape = Tape {
          id: TapeNumber(1),
          authority: authority.into(),
          delegate: buffer.into(),
          capacity: StorageUnits::mb(1000),
          active_epoch: EpochNumber(0),
          expiry_epoch: EpochNumber(100),
          tracks: TrackArchive {
              tree: MerkleTree::<TRACK_TREE_HEIGHT>::new(),
              next_number: TrackNumber(0),
              num_tracks: 0,
          },
          ..Tape::zeroed()
      };
      let system = System {
          current_epoch: EpochNumber(0),
          live_group_count: 50,
          ..System::zeroed()
      };
      let initial_tape = tape.pack();

      let rent = Sysvars::default().keyed_account_for_rent_sysvar();
      let rent_id = rent.0;
      let system_program = keyed_account_for_system_program();
      let system_program_id = system_program.0;

      let accounts = vec![
          wallet(payer, 100_000_000_000),
          owned(tape_key, initial_tape.clone()),
          (buffer, Account::default()),
          owned(system_key, system.pack()),
          rent,
          system_program,
          slot_hashes(),
          (tapedrive::ID, create_program_account_loader_v2(TAPEDRIVE_ELF)),
      ];

      (
          mollusk,
          Env {
              payer,
              tape_key,
              buffer,
              system_key,
              rent_id,
              system_program_id,
              accounts,
              initial_tape,
          },
      )
  }

  fn append_ix(env: &Env, piece: &[u8]) -> Instruction {
      let mut data = vec![APPEND];
      data.extend_from_slice(piece);
      Instruction {
          program_id: bundle_id(),
          accounts: vec![
              AccountMeta::new(env.payer, true),
              AccountMeta::new_readonly(env.tape_key, false),
              AccountMeta::new(env.buffer, false),
              AccountMeta::new_readonly(env.rent_id, false),
              AccountMeta::new_readonly(env.system_program_id, false),
          ],
          data,
      }
  }

  fn flush_ix(env: &Env) -> Instruction {
      Instruction {
          program_id: bundle_id(),
          accounts: vec![
              AccountMeta::new(env.payer, true),
              AccountMeta::new(env.tape_key, false),
              AccountMeta::new_readonly(env.buffer, false),
              AccountMeta::new_readonly(env.system_key, false),
              AccountMeta::new_readonly(sysvar::slot_hashes::ID, false),
              AccountMeta::new_readonly(tapedrive::ID, false),
          ],
          data: vec![FLUSH],
      }
  }

  // a 4 KB payload collected over several appends lands as one inline track
  #[test]
  fn collect_large_payload() {
      let (mollusk, env) = setup();
      let payload = vec![0xABu8; 4096];

      let mut instructions = vec![];
      let mut offset = 0;
      while offset < payload.len() {
          let end = (offset + PIECE).min(payload.len());
          instructions.push(append_ix(&env, &payload[offset..end]));
          offset = end;
      }
      instructions.push(flush_ix(&env));

      let success = [Check::success()];
      let steps: Vec<(&Instruction, &[Check])> =
          instructions.iter().map(|ix| (ix, success.as_slice())).collect();

      let result = mollusk.process_and_validate_instruction_chain(&steps, &env.accounts);

      // every step succeeded, including the flush CPI into Tapedrive
      let final_tape = result
          .get_account(&env.tape_key)
          .expect("tape account present after flush");
      assert_ne!(
          final_tape.data, env.initial_tape,
          "the tape should have gained a track"
      );
  }

  // the first append creates the buffer and each later append grows it
  #[test]
  fn buffer_grows() {
      let (mollusk, env) = setup();

      let first = mollusk.process_and_validate_instruction(
          &append_ix(&env, &[0u8; 1000]),
          &env.accounts,
          &[Check::success()],
      );
      let buffer = first.get_account(&env.buffer).expect("buffer after first append");
      assert_eq!(buffer.data.len(), BUFFER_OVERHEAD + 1000);

      let second = mollusk.process_and_validate_instruction(
          &append_ix(&env, &[0u8; 500]),
          &first.resulting_accounts,
          &[Check::success()],
      );
      let buffer = second.get_account(&env.buffer).expect("buffer after second append");
      assert_eq!(buffer.data.len(), BUFFER_OVERHEAD + 1500);
  }

  // flush with nothing appended fails instead of writing an empty track
  #[test]
  fn flush_before_append() {
      let (mollusk, env) = setup();

      mollusk.process_and_validate_instruction(
          &flush_ix(&env),
          &env.accounts,
          &[Check::err(ProgramError::Custom(EMPTY_ERROR))],
      );
  }

  // the serialized size of an append transaction carrying a piece of this length
  fn append_tx_size(env: &Env, piece_len: usize) -> usize {
      let ix = append_ix(env, &vec![0u8; piece_len]);
      let tx = Transaction::new_with_payer(&[ix], Some(&env.payer));
      bincode::serialize(&tx).expect("serialize transaction").len()
  }

  // an append transaction must fit one packet: a realistic piece fits, 1024 does not
  #[test]
  fn append_fits_one_packet() {
      let (_, env) = setup();

      let mut max_piece = 0;
      while append_tx_size(&env, max_piece + 1) <= PACKET_DATA_SIZE {
          max_piece += 1;
      }

      println!("append overhead: {} bytes", append_tx_size(&env, 0));
      println!("largest piece in one packet: {} bytes", max_piece);

      assert!(append_tx_size(&env, 900) <= PACKET_DATA_SIZE);
      assert!(append_tx_size(&env, 1024) > PACKET_DATA_SIZE);
  }
  ```
</Accordion>

### Build and test

```bash theme={null}
cd examples/bundle/program
cargo build-sbf
cd ../tests
cargo test
```

All four tests pass: a 4 KB payload collected over several appends lands as one inline track, the buffer grows with each append, a flush before any append fails, and an append transaction with a 928-byte piece fits one packet while 1024 does not.

Two things in this example track the deployed Tapedrive program and have to be refreshed after a Tapedrive redeploy: `TAPEDRIVE_ID` in `wire.rs`, and the prebuilt `tests/fixtures/tapedrive.so` the tests load.

## Troubleshooting

| Symptom                                                | Cause and fix                                                                                                      |
| ------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------ |
| `custom program error: 0x2` from quill                 | The writer account is not `["quill", tape]` for the program id you sent to. Derive it from the deployed id.        |
| `custom program error: 0x4` from quill                 | The Tapedrive program, system account or slot hashes sysvar you passed is not the real one.                        |
| `Unknown program ...` in the bundle test log           | `TAPEDRIVE_ID` in `wire.rs` is from an earlier Tapedrive deployment. Set it to the current program id and rebuild. |
| `incorrect program id for instruction` from the flush  | `tests/fixtures/tapedrive.so` is from an earlier deployment. Replace it with the current build.                    |
| `object is N B framed ... over the 825 B inline limit` | The payload does not fit one transaction. Use bundle, or a coded write.                                            |
| `insufficient capacity`                                | The tape is full. Grow it with `tape resize --add 10m`.                                                            |
