> ## Documentation Index
> Fetch the complete documentation index at: https://luminouslabs-cc5545c6-indexing-tokens.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Transfer Interface

> Program and client guide for transfers between light-token and SPL token accounts. The interface detects account types and invokes the right programs.

export const lightTransferCode = ["// light-token transfer", 'import { transferInterface } from "@lightprotocol/compressed-token";', "", "const tx = await transferInterface(", "  rpc,", "  payer,", "  sourceAta,", "  mint,", "  destinationAta,", "  owner,", "  amount", ");"].join("\n");

export const splTransferCode = ["// SPL transfer", 'import { transfer } from "@solana/spl-token";', "", "const tx = await transfer(", "  connection,", "  payer,", "  sourceAta,", "  destinationAta,", "  owner,", "  amount", ");"].join("\n");

export const CodeCompare = ({firstCode = "", secondCode = "", firstLabel = "Light Token", secondLabel = "SPL"}) => {
  const [sliderPercent, setSliderPercent] = useState(0);
  const [isDragging, setIsDragging] = useState(false);
  const [isAnimating, setIsAnimating] = useState(false);
  const containerRef = useRef(null);
  const animationRef = useRef(null);
  const isLightMode = sliderPercent > 50;
  const highlightCode = code => {
    let escaped = code.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
    const pattern = /(\/\/.*$)|(["'`])(?:(?!\2)[^\\]|\\.)*?\2|\b(const|let|var|await|async|import|from|export|return|if|else|function|class|new|throw|try|catch)\b|\.([a-zA-Z_][a-zA-Z0-9_]*)\b|\b([a-zA-Z_][a-zA-Z0-9_]*)\s*(?=\()/gm;
    return escaped.replace(pattern, (match, comment, stringQuote, keyword, property, func) => {
      if (comment) return `<span style="color:#6b7280;font-style:italic">${match}</span>`;
      if (stringQuote) return `<span style="color:#059669">${match}</span>`;
      if (keyword) return `<span style="color:#db2777">${match}</span>`;
      if (property) return `.<span style="color:#0891b2">${property}</span>`;
      if (func) return `<span style="color:#2563eb">${match}</span>`;
      return match;
    });
  };
  const animateTo = target => {
    if (animationRef.current) cancelAnimationFrame(animationRef.current);
    setIsAnimating(true);
    const start = sliderPercent;
    const startTime = performance.now();
    const duration = 400;
    const animate = currentTime => {
      const elapsed = currentTime - startTime;
      const progress = Math.min(elapsed / duration, 1);
      const eased = 1 - Math.pow(1 - progress, 3);
      const current = start + (target - start) * eased;
      setSliderPercent(current);
      if (progress < 1) {
        animationRef.current = requestAnimationFrame(animate);
      } else {
        setSliderPercent(target);
        setIsAnimating(false);
        animationRef.current = null;
      }
    };
    animationRef.current = requestAnimationFrame(animate);
  };
  const handleToggle = () => {
    animateTo(isLightMode ? 0 : 100);
  };
  const handleMouseDown = e => {
    if (isAnimating) {
      cancelAnimationFrame(animationRef.current);
      setIsAnimating(false);
    }
    e.preventDefault();
    setIsDragging(true);
  };
  const handleMouseUp = () => {
    setIsDragging(false);
  };
  const handleMouseMove = e => {
    if (!isDragging || !containerRef.current) return;
    const rect = containerRef.current.getBoundingClientRect();
    const x = e.clientX - rect.left;
    const percent = Math.max(0, Math.min(100, x / rect.width * 100));
    setSliderPercent(percent);
  };
  const handleTouchMove = e => {
    if (!containerRef.current) return;
    if (isAnimating) {
      cancelAnimationFrame(animationRef.current);
      setIsAnimating(false);
    }
    const rect = containerRef.current.getBoundingClientRect();
    const x = e.touches[0].clientX - rect.left;
    const percent = Math.max(0, Math.min(100, x / rect.width * 100));
    setSliderPercent(percent);
  };
  const handleKeyDown = e => {
    if (e.key === "ArrowLeft") {
      setSliderPercent(p => Math.max(0, p - 5));
    } else if (e.key === "ArrowRight") {
      setSliderPercent(p => Math.min(100, p + 5));
    }
  };
  useEffect(() => {
    if (isDragging) {
      document.addEventListener("mousemove", handleMouseMove);
      document.addEventListener("mouseup", handleMouseUp);
      return () => {
        document.removeEventListener("mousemove", handleMouseMove);
        document.removeEventListener("mouseup", handleMouseUp);
      };
    }
  }, [isDragging]);
  useEffect(() => {
    return () => {
      if (animationRef.current) cancelAnimationFrame(animationRef.current);
    };
  }, []);
  return <>
      <div className="rounded-3xl not-prose mt-4 backdrop-blur-xl border overflow-hidden" style={{
    fontFamily: "Inter, sans-serif",
    borderColor: "#d4d4d8"
  }}>
        {}
        <div className="flex items-center justify-between px-4 py-3 border-b" style={{
    background: "linear-gradient(to bottom, #f8f9fa, #f1f3f4)",
    borderColor: "#e4e4e7"
  }}>
          <span className="text-sm font-medium" style={{
    color: "#52525b"
  }}>
            {isLightMode ? secondLabel : firstLabel}
          </span>

          {}
          <div onClick={handleToggle} style={{
    position: "relative",
    width: "56px",
    height: "28px",
    background: "#e0e0e0",
    borderRadius: "14px",
    boxShadow: "inset -2px -2px 4px #ffffff, inset 2px 2px 4px #b0b0b0",
    cursor: "pointer",
    transition: "background 0.3s ease, box-shadow 0.3s ease"
  }}>
            {}
            <div style={{
    position: "absolute",
    width: "24px",
    height: "24px",
    background: "linear-gradient(145deg, #f5f5f5, #e0e0e0)",
    borderRadius: "12px",
    top: "2px",
    left: isLightMode ? "30px" : "2px",
    boxShadow: "-2px -2px 4px #ffffff, 2px 2px 4px #b0b0b0",
    transition: "all 0.3s ease-in-out",
    display: "flex",
    alignItems: "center",
    justifyContent: "center"
  }}>
              {}
              <div style={{
    width: "6px",
    height: "6px",
    background: isLightMode ? "#0066ff" : "#999",
    borderRadius: "50%",
    boxShadow: isLightMode ? "0 0 8px 2px #0066ff" : "0 0 4px 1px rgba(0, 0, 0, 0.1)",
    transition: "all 0.3s ease-in-out"
  }} />
            </div>
          </div>
        </div>

        {}
        <div ref={containerRef} className="p-0" style={{
    cursor: isDragging ? "grabbing" : "default"
  }} onTouchMove={handleTouchMove} tabIndex={0} onKeyDown={handleKeyDown} role="slider" aria-valuenow={sliderPercent} aria-valuemin={0} aria-valuemax={100} aria-label="Code comparison slider">
          <div className="relative" style={{
    minHeight: "140px",
    overflowX: "auto"
  }}>
            <div style={{
    display: "grid"
  }}>
              {}
              <pre className="m-0 p-4 text-zinc-700 dark:text-white/80 bg-transparent" style={{
    gridArea: "1/1",
    fontFamily: "ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace",
    fontSize: "13px",
    lineHeight: "1.6",
    whiteSpace: "pre",
    zIndex: 1
  }} dangerouslySetInnerHTML={{
    __html: highlightCode(firstCode)
  }} />

              {}
              <pre className="m-0 p-4 text-zinc-700 dark:text-white/80 bg-white dark:bg-zinc-900" style={{
    gridArea: "1/1",
    fontFamily: "ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace",
    fontSize: "13px",
    lineHeight: "1.6",
    whiteSpace: "pre",
    zIndex: 2,
    clipPath: `inset(0 ${100 - sliderPercent}% 0 0)`
  }} dangerouslySetInnerHTML={{
    __html: highlightCode(secondCode)
  }} />
            </div>

            {}
            <div className="absolute top-0 bottom-0 flex items-center justify-center pointer-events-none" style={{
    left: `${sliderPercent}%`,
    transform: "translateX(-50%)",
    zIndex: 30
  }}>
              <div className="absolute top-0 bottom-0 w-px bg-zinc-400 dark:bg-white/30" />

              <div className="absolute top-0 bottom-0" style={{
    right: "50%",
    width: "80px",
    background: "linear-gradient(to left, rgba(0, 102, 255, 0.15) 0%, transparent 100%)"
  }} />

              {}
              <div onMouseDown={handleMouseDown} className="pointer-events-auto cursor-grab flex items-center justify-center gap-px transition-transform" style={{
    width: "20px",
    height: "32px",
    borderRadius: "4px",
    background: "#f8fafc",
    border: "1px solid #d1d5db",
    boxShadow: "0 1px 2px rgba(0,0,0,0.05)",
    transform: isDragging ? "scale(1.08)" : "scale(1)"
  }}>
                <div className="flex flex-col gap-0.5">
                  {[0, 1, 2].map(i => <div key={i} style={{
    width: "3px",
    height: "3px",
    borderRadius: "50%",
    background: "#0066ff"
  }} />)}
                </div>
                <div className="flex flex-col gap-0.5">
                  {[0, 1, 2].map(i => <div key={i} style={{
    width: "3px",
    height: "3px",
    borderRadius: "50%",
    background: "#0066ff"
  }} />)}
                </div>
              </div>
            </div>
          </div>
        </div>
      </div>
    </>;
};

export const CompressibleRentCalculator = () => {
  const [hours, setHours] = useState(24);
  const [lamportsPerWrite, setLamportsPerWrite] = useState(766);
  const [showCustomHours, setShowCustomHours] = useState(false);
  const [showCustomLamports, setShowCustomLamports] = useState(false);
  const [showFormula, setShowFormula] = useState(false);
  const DATA_LEN = 272;
  const BASE_RENT = 128;
  const LAMPORTS_PER_BYTE_PER_EPOCH = 1;
  const MINUTES_PER_EPOCH = 90;
  const COMPRESSION_COST = 11000;
  const LAMPORTS_PER_SOL = 1_000_000_000;
  const HOURS_MAX = 36;
  const LAMPORTS_MAX = 6400;
  const numEpochs = Math.ceil(hours * 60 / MINUTES_PER_EPOCH);
  const rentPerEpoch = BASE_RENT + DATA_LEN * LAMPORTS_PER_BYTE_PER_EPOCH;
  const totalPrepaidRent = rentPerEpoch * numEpochs;
  const totalCreationCost = totalPrepaidRent + COMPRESSION_COST;
  const handleHoursChange = value => {
    const num = Math.max(3, Math.min(168, Number.parseInt(value) || 3));
    setHours(num);
  };
  const handleLamportsChange = value => {
    const num = Math.max(0, Math.min(100000, Number.parseInt(value) || 0));
    setLamportsPerWrite(num);
  };
  const hoursPresets = [24];
  const lamportsPresets = [766];
  const SliderMarkers = ({max, step}) => {
    const marks = [];
    for (let i = step; i < max; i += step) {
      const percent = i / max * 100;
      marks.push(<div key={i} className="absolute top-1/2 -translate-y-1/2 w-px h-2 bg-zinc-300 dark:bg-white/30" style={{
        left: `${percent}%`
      }} />);
    }
    return <>{marks}</>;
  };
  return <div className="p-5 rounded-3xl not-prose mt-4 dark:bg-white/5 backdrop-blur-xl border border-black/[0.04] dark:border-white/10 shadow-lg" style={{
    fontFamily: "Inter, sans-serif"
  }}>
      <div className="space-y-5">
        {}
        <div className="space-y-2 px-3">
          <div className="flex justify-between items-center">
            <span className="text-sm text-zinc-700 dark:text-white/80">
              Prepaid Epochs in Hours
            </span>
            <div className="flex items-center gap-1.5">
              {hoursPresets.map(h => <button key={h} onClick={() => {
    setHours(h);
    setShowCustomHours(false);
  }} className={`px-2.5 py-1 text-xs font-medium rounded-lg border backdrop-blur-sm transition-all ${hours === h && !showCustomHours ? "bg-blue-500/20 border-blue-500/50 text-blue-600 dark:text-blue-400" : "bg-black/[0.015] dark:bg-white/5 border-black/[0.04] dark:border-white/20 text-zinc-600 dark:text-white/70 hover:bg-black/[0.03]"}`}>
                  {h === 24 ? "Default" : `${h}h`}
                </button>)}
              {showCustomHours ? <input type="number" min="3" max="168" value={hours} onChange={e => handleHoursChange(e.target.value)} className="w-16 px-2 py-1 text-right text-xs font-mono font-medium bg-blue-500/10 dark:bg-blue-500/20 border border-blue-500/50 rounded-lg backdrop-blur-sm focus:outline-none focus:ring-2 focus:ring-blue-500/50" autoFocus /> : <button onClick={() => setShowCustomHours(true)} className="px-2.5 py-1 text-xs font-medium rounded-lg border backdrop-blur-sm transition-all bg-black/[0.015] dark:bg-white/5 border-black/[0.04] dark:border-white/20 text-zinc-600 dark:text-white/70 hover:bg-black/[0.03]">
                  Custom
                </button>}
            </div>
          </div>
          <div className="flex items-center">
            <span className="w-1/3 text-xs text-zinc-500 dark:text-white/50 whitespace-nowrap">
              ≈ {(hours * 60 / MINUTES_PER_EPOCH).toFixed(1)} epochs / {hours.toFixed(1)}h
            </span>
            <div className="w-2/3 relative">
              <SliderMarkers max={HOURS_MAX} step={2} />
              <input type="range" min="3" max={HOURS_MAX} value={Math.min(hours, HOURS_MAX)} onChange={e => {
    setHours(Number.parseInt(e.target.value));
    setShowCustomHours(false);
  }} className="relative w-full h-1.5 bg-black/[0.03] dark:bg-white/20 rounded-full appearance-none cursor-pointer backdrop-blur-sm z-10" />
            </div>
          </div>
        </div>

        {}
        <div className="space-y-2 px-3">
          <div className="flex justify-between items-center">
            <span className="text-sm text-zinc-700 dark:text-white/80">Lamports per Write</span>
            <div className="flex items-center gap-1.5">
              {lamportsPresets.map(l => <button key={l} onClick={() => {
    setLamportsPerWrite(l);
    setShowCustomLamports(false);
  }} className={`px-2.5 py-1 text-xs font-medium rounded-lg border backdrop-blur-sm transition-all ${lamportsPerWrite === l && !showCustomLamports ? "bg-blue-500/20 border-blue-500/50 text-blue-600 dark:text-blue-400" : "bg-black/[0.015] dark:bg-white/5 border-black/[0.04] dark:border-white/20 text-zinc-600 dark:text-white/70 hover:bg-black/[0.03]"}`}>
                  {l === 766 ? "Default" : l.toLocaleString()}
                </button>)}
              {showCustomLamports ? <input type="number" min="0" max="100000" value={lamportsPerWrite} onChange={e => handleLamportsChange(e.target.value)} className="w-20 px-2 py-1 text-right text-xs font-mono font-medium bg-blue-500/10 dark:bg-blue-500/20 border border-blue-500/50 rounded-lg backdrop-blur-sm focus:outline-none focus:ring-2 focus:ring-blue-500/50" autoFocus /> : <button onClick={() => setShowCustomLamports(true)} className="px-2.5 py-1 text-xs font-medium rounded-lg border backdrop-blur-sm transition-all bg-black/[0.015] dark:bg-white/5 border-black/[0.04] dark:border-white/20 text-zinc-600 dark:text-white/70 hover:bg-black/[0.03]">
                  Custom
                </button>}
            </div>
          </div>
          <div className="flex items-center">
            <span className="w-1/3 text-xs text-zinc-500 dark:text-white/50 whitespace-nowrap">
              ≈ {(lamportsPerWrite / rentPerEpoch).toFixed(1)} epochs /{" "}
              {(lamportsPerWrite / rentPerEpoch * MINUTES_PER_EPOCH / 60).toFixed(1)}h
            </span>
            <div className="w-2/3 relative">
              <SliderMarkers max={LAMPORTS_MAX} step={800} />
              <input type="range" min="0" max={LAMPORTS_MAX} step="100" value={Math.min(lamportsPerWrite, LAMPORTS_MAX)} onChange={e => {
    setLamportsPerWrite(Number.parseInt(e.target.value));
    setShowCustomLamports(false);
  }} className="relative w-full h-1.5 bg-black/[0.03] dark:bg-white/20 rounded-full appearance-none cursor-pointer backdrop-blur-sm z-10" />
            </div>
          </div>
        </div>

        {}
        <div className="grid grid-cols-2 gap-3">
          <div className="p-4 bg-black/[0.015] dark:bg-white/5 backdrop-blur-md rounded-2xl text-center border border-black/[0.04] dark:border-white/10 shadow-sm">
            <div className="text-xs text-zinc-500 dark:text-white/50 mb-1 uppercase tracking-wide">
              Total Creation Cost
            </div>
            <div className="text-xl font-mono font-semibold text-zinc-900 dark:text-white">
              {totalCreationCost.toLocaleString()}
            </div>
            <div className="text-xs text-zinc-400 dark:text-white/40">lamports</div>
            <div className="text-xs text-zinc-500 dark:text-white/50 mt-1">
              ≈ {(totalCreationCost / LAMPORTS_PER_SOL).toFixed(6)} SOL
            </div>
          </div>

          <div className="p-4 bg-black/[0.015] dark:bg-white/5 backdrop-blur-md rounded-2xl text-center border border-black/[0.04] dark:border-white/10 shadow-sm">
            <div className="text-xs text-zinc-500 dark:text-white/50 mb-1 uppercase tracking-wide">
              Top-up Amount
            </div>
            <div className="text-xl font-mono font-semibold text-zinc-900 dark:text-white">
              {lamportsPerWrite.toLocaleString()}
            </div>
            <div className="text-xs text-zinc-400 dark:text-white/40">lamports</div>
            <div className="text-xs text-zinc-500 dark:text-white/50 mt-1">
              ≈ {(lamportsPerWrite / LAMPORTS_PER_SOL).toFixed(6)} SOL
            </div>
          </div>
        </div>

        {}
        <div className="pt-3 border-t border-black/[0.04] dark:border-white/10">
          <button onClick={() => setShowFormula(!showFormula)} className="flex items-center gap-2 text-xs text-zinc-500 dark:text-white/50 hover:text-zinc-700 dark:hover:text-white/70 transition-colors">
            <span className={`transition-transform ${showFormula ? "rotate-90" : ""}`}>▶</span>
            Show formula
          </button>
          {showFormula && <div className="text-xs font-mono text-zinc-500 dark:text-white/40 mt-3">
              <div className="text-zinc-600 dark:text-white/60 mb-2">
                Total cost for {DATA_LEN}-byte light-token account:
              </div>
              total_creation_cost = prepaid_rent + compression_cost
              <br />
              <br />
              rent_per_epoch = base_rent + (data_len × lamports_per_byte_per_epoch)
              <br />
              rent_per_epoch = {BASE_RENT} + ({DATA_LEN} × {LAMPORTS_PER_BYTE_PER_EPOCH}) ={" "}
              {rentPerEpoch} lamports
              <br />
              compression_cost = {COMPRESSION_COST.toLocaleString()} lamports
            </div>}
        </div>
      </div>
    </div>;
};

***

<table>
  <tbody>
    <tr>
      <td style={{ width: "33%" }}>**light-token -> light-token Account**</td>

      <td>
        <ul>
          <li>Transfers tokens between light-token accounts</li>
        </ul>
      </td>
    </tr>

    <tr>
      <td>**SPL token -> light-token Account**</td>

      <td>
        <ul>
          <li>Transfers SPL tokens to light-token accounts</li>
          <li>SPL tokens are locked in interface PDA</li>
          <li>Tokens are minted to light-token account</li>
        </ul>
      </td>
    </tr>

    <tr>
      <td>**light-token -> SPL Account**</td>

      <td>
        <ul>
          <li>Releases SPL tokens from interface PDA to SPL account</li>
          <li>Burns tokens in source light-token account</li>
        </ul>
      </td>
    </tr>
  </tbody>
</table>

## Get Started

<Tabs>
  <Tab title="TypeScript Client">
    The `transferInterface` function transfers tokens between token accounts (SPL, Token-2022, or Light) in a single call.

    Compare to SPL:

    <CodeCompare firstCode={splTransferCode} secondCode={lightTransferCode} firstLabel="SPL" secondLabel="light-token" />

    <Info>
      Find the source code
      [here](https://github.com/Lightprotocol/light-protocol/blob/0c4e2417b2df2d564721b89e18d1aad3665120e7/js/compressed-token/src/v3/actions/transfer-interface.ts).
    </Info>

    <Steps>
      <Step>
        ### Transfer Interface

        <Accordion title="Installation">
          <Tabs>
            <Tab title="npm">
              Install packages in your working directory:

              ```bash theme={null}
              npm install @lightprotocol/stateless.js@beta \
                          @lightprotocol/compressed-token@beta
              ```

              Install the CLI globally:

              ```bash theme={null}
              npm install -g @lightprotocol/zk-compression-cli@beta
              ```
            </Tab>

            <Tab title="yarn">
              Install packages in your working directory:

              ```bash theme={null}
              yarn add @lightprotocol/stateless.js@beta \
                       @lightprotocol/compressed-token@beta
              ```

              Install the CLI globally:

              ```bash theme={null}
              yarn global add @lightprotocol/zk-compression-cli@beta
              ```
            </Tab>

            <Tab title="pnpm">
              Install packages in your working directory:

              ```bash theme={null}
              pnpm add @lightprotocol/stateless.js@beta \
                       @lightprotocol/compressed-token@beta
              ```

              Install the CLI globally:

              ```bash theme={null}
              pnpm add -g @lightprotocol/zk-compression-cli@beta
              ```
            </Tab>
          </Tabs>
        </Accordion>

        <Tabs>
          <Tab title="Localnet">
            ```bash theme={null}
            # start local test-validator in a separate terminal
            light test-validator
            ```

            <Note>
              In the code examples, use `createRpc()` without arguments for localnet.
            </Note>
          </Tab>

          <Tab title="Devnet">
            Get an API key from [Helius](https://helius.dev) and add to `.env`:

            ```bash title=".env" theme={null}
            API_KEY=<your-helius-api-key>
            ```

            <Note>
              In the code examples, use `createRpc(RPC_URL)` with the devnet URL.
            </Note>
          </Tab>
        </Tabs>

        <Tabs>
          <Tab title="Action">
            ```typescript theme={null}
            import "dotenv/config";
            import { Keypair } from "@solana/web3.js";
            import { createRpc } from "@lightprotocol/stateless.js";
            import {
                createMintInterface,
                createAtaInterface,
                mintToInterface,
                transferInterface,
                getAssociatedTokenAddressInterface,
            } from "@lightprotocol/compressed-token";
            import { homedir } from "os";
            import { readFileSync } from "fs";

            // devnet:
            const RPC_URL = `https://devnet.helius-rpc.com?api-key=${process.env.API_KEY!}`;
            const rpc = createRpc(RPC_URL);
            // localnet:
            // const rpc = createRpc();

            const payer = Keypair.fromSecretKey(
                new Uint8Array(
                    JSON.parse(readFileSync(`${homedir()}/.config/solana/id.json`, "utf8"))
                )
            );

            (async function () {
                const { mint } = await createMintInterface(rpc, payer, payer, null, 9);

                const sender = Keypair.generate();
                await createAtaInterface(rpc, payer, mint, sender.publicKey);
                const senderAta = getAssociatedTokenAddressInterface(mint, sender.publicKey);
                await mintToInterface(rpc, payer, mint, senderAta, payer, 1_000_000_000);

                const recipient = Keypair.generate();
                await createAtaInterface(rpc, payer, mint, recipient.publicKey);
                const recipientAta = getAssociatedTokenAddressInterface(mint, recipient.publicKey);

                const tx = await transferInterface(rpc, payer, senderAta, mint, recipientAta, sender, 500_000_000);

                console.log("Tx:", tx);
            })();
            ```
          </Tab>

          <Tab title="Instruction">
            ```typescript theme={null}
            import "dotenv/config";
            import {
                Keypair,
                ComputeBudgetProgram,
                Transaction,
                sendAndConfirmTransaction,
            } from "@solana/web3.js";
            import { createRpc } from "@lightprotocol/stateless.js";
            import {
                createMintInterface,
                createAtaInterface,
                mintToInterface,
                createTransferInterfaceInstruction,
                getAssociatedTokenAddressInterface,
            } from "@lightprotocol/compressed-token";
            import { homedir } from "os";
            import { readFileSync } from "fs";

            // devnet:
            const RPC_URL = `https://devnet.helius-rpc.com?api-key=${process.env.API_KEY!}`;
            const rpc = createRpc(RPC_URL);
            // localnet:
            // const rpc = createRpc();

            const payer = Keypair.fromSecretKey(
                new Uint8Array(
                    JSON.parse(readFileSync(`${homedir()}/.config/solana/id.json`, "utf8"))
                )
            );

            (async function () {
                const { mint } = await createMintInterface(rpc, payer, payer, null, 9);

                const sender = Keypair.generate();
                await createAtaInterface(rpc, payer, mint, sender.publicKey);
                const senderAta = getAssociatedTokenAddressInterface(
                    mint,
                    sender.publicKey
                );
                await mintToInterface(rpc, payer, mint, senderAta, payer, 1_000_000_000);

                const recipient = Keypair.generate();
                await createAtaInterface(rpc, payer, mint, recipient.publicKey);
                const recipientAta = getAssociatedTokenAddressInterface(
                    mint,
                    recipient.publicKey
                );

                const ix = createTransferInterfaceInstruction(
                    senderAta,
                    recipientAta,
                    sender.publicKey,
                    500_000_000
                );

                const tx = new Transaction().add(ix);
                const signature = await sendAndConfirmTransaction(rpc, tx, [payer, sender]);

                console.log("Tx:", signature);
            })();
            ```
          </Tab>
        </Tabs>
      </Step>
    </Steps>
  </Tab>

  <Tab title="Rust Client">
    The example transfers SPL token -> light-token and light-token -> light-token:

    1. Create SPL mint, SPL token accounts, and mint SPL tokens
    2. Send SPL tokens to light-token account to mint light-tokens.
    3. Transfer light-tokens to another light-token account.

    <Steps>
      <Step>
        ### Prerequisites

        <Accordion title="Dependencies">
          ```toml Cargo.toml theme={null}
          [dependencies]
          light-compressed-token-sdk = "0.1"
          light-client = "0.1"
          light-token-types = "0.1"
          solana-sdk = "2.2"
          borsh = "0.10"
          tokio = { version = "1.36", features = ["full"] }

          [dev-dependencies]
          light-program-test = "0.1"  # For in-memory tests with LiteSVM
          ```
        </Accordion>

        <Accordion title="Developer Environment">
          <Tabs>
            <Tab title="In-Memory (LightProgramTest)">
              Test with Lite-SVM (...)

              ```bash theme={null}
              # Initialize project
              cargo init my-light-project
              cd my-light-project

              # Run tests
              cargo test
              ```

              ```rust theme={null}
              use light_program_test::{LightProgramTest, ProgramTestConfig};
              use solana_sdk::signer::Signer;

              #[tokio::test]
              async fn test_example() {
                  // In-memory test environment 
                  let mut rpc = LightProgramTest::new(ProgramTestConfig::default())
                      .await
                      .unwrap();

                  let payer = rpc.get_payer().insecure_clone();
                  println!("Payer: {}", payer.pubkey());
              }
              ```
            </Tab>

            <Tab title="Localnet (LightClient)">
              Connects to a local test validator.

              <Tabs>
                <Tab title="npm">
                  ```bash theme={null}
                  npm install -g @lightprotocol/zk-compression-cli@beta
                  ```
                </Tab>

                <Tab title="yarn">
                  ```bash theme={null}
                  yarn global add @lightprotocol/zk-compression-cli@beta
                  ```
                </Tab>

                <Tab title="pnpm">
                  ```bash theme={null}
                  pnpm add -g @lightprotocol/zk-compression-cli@beta
                  ```
                </Tab>
              </Tabs>

              ```bash theme={null}
              # Initialize project
              cargo init my-light-project
              cd my-light-project

              # Start local test validator (in separate terminal)
              light test-validator
              ```

              ```rust theme={null}
              use light_client::rpc::{LightClient, LightClientConfig, Rpc};

              #[tokio::main]
              async fn main() -> Result<(), Box<dyn std::error::Error>> {
                  // Connects to http://localhost:8899
                  let rpc = LightClient::new(LightClientConfig::local()).await?;

                  let slot = rpc.get_slot().await?;
                  println!("Current slot: {}", slot);

                  Ok(())
              }
              ```
            </Tab>

            <Tab title="Devnet (LightClient)">
              Replace `<your-api-key>` with your actual API key. [Get your API key here](https://www.helius.dev/zk-compression).

              ```rust theme={null}
              use light_client::rpc::{LightClient, LightClientConfig, Rpc};

              #[tokio::main]
              async fn main() -> Result<(), Box<dyn std::error::Error>> {
                  let rpc_url = "https://devnet.helius-rpc.com?api-key=<your_api_key>";
                  let rpc = LightClient::new(
                      LightClientConfig::new(rpc_url.to_string(), None, None)
                  ).await?;

                  println!("Connected to Devnet");
                  Ok(())
              }
              ```
            </Tab>
          </Tabs>
        </Accordion>
      </Step>

      <Step>
        ### Transfer Interface
      </Step>
    </Steps>

    ```rust theme={null}
    use anchor_spl::token::{spl_token, Mint};
    use light_client::rpc::{LightClient, LightClientConfig, Rpc};
    use light_token_sdk::{
        token::{
            derive_token_ata, CreateAssociatedTokenAccount,
            Transfer, TransferFromSpl,
        },
        spl_interface::{find_spl_interface_pda_with_index, CreateSplInterfacePda},
    };
    use serde_json;
    use solana_sdk::compute_budget::ComputeBudgetInstruction;
    use solana_sdk::program_pack::Pack;
    use solana_sdk::{signature::Keypair, signer::Signer};
    use spl_token_2022::pod::PodAccount;
    use std::convert::TryFrom;
    use std::env;
    use std::fs;

    /// Test SPL → light-token → light-token
    //  with ATA creation + transfer in one transaction
    #[tokio::test(flavor = "multi_thread")]
    async fn test_spl_to_token_to_token() {
        dotenvy::dotenv().ok();

        let keypair_path = env::var("KEYPAIR_PATH")
            .unwrap_or_else(|_| format!("{}/.config/solana/id.json", env::var("HOME").unwrap()));
        let payer = load_keypair(&keypair_path).expect("Failed to load keypair");
        let api_key = env::var("api_key") // Set api_key in your .env
            .expect("api_key environment variable must be set");

        let config = LightClientConfig::devnet(
            Some("https://devnet.helius-rpc.com".to_string()),
            Some(api_key),
        );
        let mut rpc = LightClient::new_with_retry(config, None)
            .await
            .expect("Failed to initialize LightClient");

        // 2. Create SPL mint
        let mint_keypair = Keypair::new();
        let mint = mint_keypair.pubkey();
        let decimals = 2u8;

        let mint_rent = rpc
            .get_minimum_balance_for_rent_exemption(Mint::LEN)
            .await
            .unwrap();

        let create_mint_account_ix = solana_sdk::system_instruction::create_account(
            &payer.pubkey(),
            &mint,
            mint_rent,
            Mint::LEN as u64,
            &spl_token::ID,
        );

        let initialize_mint_ix = spl_token::instruction::initialize_mint(
            &spl_token::ID,
            &mint,
            &payer.pubkey(),
            None,
            decimals,
        )
        .unwrap();

        rpc.create_and_send_transaction(
            &[create_mint_account_ix, initialize_mint_ix],
            &payer.pubkey(),
            &[&payer, &mint_keypair],
        )
        .await
        .unwrap();

        // 3. Create SPL interface PDA
        let create_spl_interface_pda_ix =
            CreateSplInterfacePda::new(payer.pubkey(), mint, anchor_spl::token::ID).instruction();

        rpc.create_and_send_transaction(&[create_spl_interface_pda_ix], &payer.pubkey(), &[&payer])
            .await
            .unwrap();

        let mint_amount = 10_000u64;
        let spl_to_token_amount = 7_000u64;
        let token_transfer_amount = 3_000u64;

        // 4. Create SPL token account
        let spl_token_account_keypair = Keypair::new();
        let token_account_rent = rpc
            .get_minimum_balance_for_rent_exemption(spl_token::state::Account::LEN)
            .await
            .unwrap();
        let create_token_account_ix = solana_sdk::system_instruction::create_account(
            &payer.pubkey(),
            &spl_token_account_keypair.pubkey(),
            token_account_rent,
            spl_token::state::Account::LEN as u64,
            &spl_token::ID,
        );
        let init_token_account_ix = spl_token::instruction::initialize_account(
            &spl_token::ID,
            &spl_token_account_keypair.pubkey(),
            &mint,
            &payer.pubkey(),
        )
        .unwrap();
        rpc.create_and_send_transaction(
            &[create_token_account_ix, init_token_account_ix],
            &payer.pubkey(),
            &[&spl_token_account_keypair, &payer],
        )
        .await
        .unwrap();

        // 5. Mint SPL tokens to the SPL account
        let mint_to_ix = spl_token::instruction::mint_to(
            &spl_token::ID,
            &mint,
            &spl_token_account_keypair.pubkey(),
            &payer.pubkey(),
            &[&payer.pubkey()],
            mint_amount,
        )
        .unwrap();
        rpc.create_and_send_transaction(&[mint_to_ix], &payer.pubkey(), &[&payer])
            .await
            .unwrap();

        // Verify SPL account has tokens
        let spl_account_data = rpc
            .get_account(spl_token_account_keypair.pubkey())
            .await
            .unwrap()
            .unwrap();
        let spl_account =
            spl_pod::bytemuck::pod_from_bytes::<PodAccount>(&spl_account_data.data).unwrap();
        let initial_spl_balance: u64 = spl_account.amount.into();
        assert_eq!(initial_spl_balance, mint_amount);

        // 6. Create sender's token ATA
        let (sender_token_ata, _bump) = derive_token_ata(&payer.pubkey(), &mint);
        let create_ata_instruction =
            CreateAssociatedTokenAccount::new(payer.pubkey(), payer.pubkey(), mint)
                .instruction()
                .unwrap();

        rpc.create_and_send_transaction(&[create_ata_instruction], &payer.pubkey(), &[&payer])
            .await
            .unwrap();

        // Verify sender's token ATA was created
        let token_account_data = rpc.get_account(sender_token_ata).await.unwrap().unwrap();
        assert!(
            !token_account_data.data.is_empty(),
            "Sender token ATA should exist"
        );

        // 7. Transfer SPL tokens to sender's token account
        let (spl_interface_pda, spl_interface_pda_bump) = find_spl_interface_pda_with_index(&mint, 0);

        let spl_to_token_ix = TransferFromSpl {
            amount: spl_to_token_amount,
            spl_interface_pda_bump,
            source_spl_token_account: spl_token_account_keypair.pubkey(),
            destination_token_account: sender_token_ata,
            authority: payer.pubkey(),
            mint,
            payer: payer.pubkey(),
            spl_interface_pda,
            spl_token_program: anchor_spl::token::ID,
        }
        .instruction()
        .unwrap();

        rpc.create_and_send_transaction(&[spl_to_token_ix], &payer.pubkey(), &[&payer])
            .await
            .unwrap();

        // 8. Create recipient ATA + transfer token→token in one transaction
        let recipient = Keypair::new();
        let (recipient_token_ata, _) = derive_token_ata(&recipient.pubkey(), &mint);

        let create_recipient_ata_ix = CreateAssociatedTokenAccount::new(
            payer.pubkey(),
            recipient.pubkey(),
            mint,
        )
        .instruction()
        .unwrap();

        let token_transfer_ix = Transfer {
            source: sender_token_ata,
            destination: recipient_token_ata,
            amount: token_transfer_amount,
            authority: payer.pubkey(),
            max_top_up: None,
        }
        .instruction()
        .unwrap();

        let compute_unit_ix = ComputeBudgetInstruction::set_compute_unit_limit(10_000);
        rpc.create_and_send_transaction(
            &[compute_unit_ix, create_recipient_ata_ix, token_transfer_ix],
            &payer.pubkey(),
            &[&payer],
        )
        .await
        .unwrap();
    }

    fn load_keypair(path: &str) -> Result<Keypair, Box<dyn std::error::Error>> {
        let path = if path.starts_with("~") {
            path.replace("~", &env::var("HOME").unwrap_or_default())
        } else {
            path.to_string()
        };
        let file = fs::read_to_string(&path)?;
        let bytes: Vec<u8> = serde_json::from_str(&file)?;
        Ok(Keypair::try_from(&bytes[..])?)
    }
    ```
  </Tab>

  <Tab title="Program Guide">
    <Note>
      Find [a full code example at the end](#full-code-example).
    </Note>

    <Steps>
      <Step>
        ### Light Token Transfer Interface

        Define the number of light-tokens / SPL tokens to transfer

        * from which SPL or light-token account, and
        * to which SPL or light-token account.

        ```rust theme={null}
        use light_token_sdk::token::TransferInterfaceCpi;

        let mut transfer = TransferInterfaceCpi::new(
            data.amount,
            source_account.clone(),
            destination_account.clone(),
            authority.clone(),
            payer.clone(),
            compressed_token_program_authority.clone(),
        );
        ```

        <Accordion title="light-token Interface Accounts">
          <table>
            <colgroup>
              <col style={{ width: "25%", textAlign: "left" }} />

              <col style={{ width: "55%" }} />
            </colgroup>

            <thead>
              <tr>
                <th style={{ textAlign: "left" }} />

                <th style={{ textAlign: "left" }} />

                <th style={{ textAlign: "left" }} />
              </tr>
            </thead>

            <tbody>
              <tr>
                <td style={{ textAlign: "left" }}>
                  <strong>Source Account</strong>
                </td>

                <td>mutable</td>
                <td>The source account (SPL token account or light-token account).</td>
              </tr>

              <tr>
                <td style={{ textAlign: "left" }}>
                  <strong>Destination Account</strong>
                </td>

                <td>mutable</td>

                <td>
                  The destination account (SPL token account or light-token account).
                </td>
              </tr>

              <tr>
                <td style={{ textAlign: "left" }}>
                  <strong>Authority</strong>
                </td>

                <td>signer\*</td>

                <td>
                  * Owner of the source account.
                    <br />- \*Must be signer for `invoke()`. For `invoke_signed()`, program
                    signs via PDA seeds.
                </td>
              </tr>

              <tr>
                <td style={{ textAlign: "left" }}>
                  <strong>Payer</strong>
                </td>

                <td>signer, mutable</td>
                <td>Pays transaction fees.</td>
              </tr>

              <tr>
                <td style={{ textAlign: "left" }}>
                  <strong>Light Token Program Authority</strong>
                </td>

                <td>-</td>
                <td>The light token program authority PDA.</td>
              </tr>
            </tbody>
          </table>
        </Accordion>
      </Step>

      <Step>
        ### SPL Transfer Interface (Optional)

        The SPL transfer interface is only needed for SPL ↔ light-token transfers.

        ```rust theme={null}
        transfer = transfer.with_spl_interface(
            Some(mint.clone()),
            Some(spl_token_program.clone()),
            Some(spl_interface_pda.clone()),
            data.spl_interface_pda_bump,
        )?;
        ```

        SPL ↔ light-token transfers require a `spl_interface_pda`:

        * **SPL → light-token**: SPL tokens are locked by the light token program in the PDA, light-tokens are minted to light-token accounts
        * **light-token → SPL**: light-tokens are burned, SPL tokens transferred to SPL token accounts

        <Info>The interface PDA is derived from the `mint` pubkey and pool seed.</Info>

        <Accordion title="SPL Interface Accounts">
          <table>
            <colgroup>
              <col style={{width: '25%', textAlign: 'left'}} />

              <col style={{width: '55%'}} />
            </colgroup>

            <thead>
              <tr>
                <th style={{textAlign: 'left'}} />

                <th style={{textAlign: 'left'}} />

                <th style={{textAlign: 'left'}} />
              </tr>
            </thead>

            <tbody>
              <tr>
                <td style={{textAlign: 'left'}}><strong>Mint</strong></td>
                <td>-</td>
                <td>The SPL token mint.</td>
              </tr>

              <tr>
                <td style={{textAlign: 'left'}}><strong>SPL Token Program</strong></td>
                <td>-</td>
                <td>The SPL Token program.</td>
              </tr>

              <tr>
                <td style={{textAlign: 'left'}}><strong>Interface PDA</strong></td>
                <td>mutable</td>
                <td>Interface PDA for SPL ↔ light-token transfers.</td>
              </tr>
            </tbody>
          </table>
        </Accordion>
      </Step>

      <Step>
        ### CPI

        CPI the Light Token program to execute the transfer.
        Use `invoke()`, or `invoke_signed()` when a CPI requires a PDA signer.

        <Tabs>
          <Tab title="invoke (External signer)">
            ```rust theme={null}
            transfer.invoke()?;
            ```
          </Tab>

          <Tab title="invoke_signed (PDA is signer)">
            ```rust theme={null}
            let authority_seeds: &[&[u8]] = &[TRANSFER_INTERFACE_AUTHORITY_SEED, &[authority_bump]];
            transfer.invoke_signed(&[authority_seeds])?;
            ```
          </Tab>
        </Tabs>
      </Step>
    </Steps>

    # Full Code Example

    <Info>
      Find the source code
      [here](https://github.com/Lightprotocol/light-protocol/blob/main/sdk-tests/sdk-light-token-test/src/transfer_interface.rs).
    </Info>

    ```rust expandable theme={null}
    use borsh::{BorshDeserialize, BorshSerialize};
    use light_token_sdk::token::TransferInterfaceCpi;
    use solana_program::{account_info::AccountInfo, program_error::ProgramError, pubkey::Pubkey};

    use crate::ID;

    /// PDA seed for authority in invoke_signed variants
    pub const TRANSFER_INTERFACE_AUTHORITY_SEED: &[u8] = b"transfer_interface_authority";

    /// Instruction data for TransferInterfaceCpi
    #[derive(BorshSerialize, BorshDeserialize, Debug)]
    pub struct TransferInterfaceData {
        pub amount: u64,
        /// Required for SPL<->Token transfers, None for Token->Token
        pub token_pool_pda_bump: Option<u8>,
    }

    /// Handler for TransferInterfaceCpi (invoke)
    ///
    /// This unified interface automatically detects account types and routes to:
    /// - Token -> Token transfer
    /// - Token -> SPL transfer
    /// - SPL -> Token transfer
    ///
    /// Account order:
    /// - accounts[0]: compressed_token_program (for CPI)
    /// - accounts[1]: source_account (SPL or light-token)
    /// - accounts[2]: destination_account (SPL or light-token)
    /// - accounts[3]: authority (signer)
    /// - accounts[4]: payer (signer)
    /// - accounts[5]: compressed_token_program_authority
    ///   For SPL bridge (optional, required for SPL<->Token):
    /// - accounts[6]: mint
    /// - accounts[7]: token_pool_pda
    /// - accounts[8]: spl_token_program
    pub fn process_transfer_interface_invoke(
        accounts: &[AccountInfo],
        data: TransferInterfaceData,
    ) -> Result<(), ProgramError> {
        if accounts.len() < 6 {
            return Err(ProgramError::NotEnoughAccountKeys);
        }

        let mut transfer = TransferInterfaceCpi::new(
            data.amount,
            accounts[1].clone(), // source_account
            accounts[2].clone(), // destination_account
            accounts[3].clone(), // authority
            accounts[4].clone(), // payer
            accounts[5].clone(), // compressed_token_program_authority
        );

        // Add SPL bridge config if provided
        if accounts.len() >= 9 && data.token_pool_pda_bump.is_some() {
            transfer = transfer.with_spl_interface(
                Some(accounts[6].clone()), // mint
                Some(accounts[8].clone()), // spl_token_program
                Some(accounts[7].clone()), // token_pool_pda
                data.token_pool_pda_bump,
            )?;
        }

        transfer.invoke()?;

        Ok(())
    }

    /// Handler for TransferInterfaceCpi with PDA authority (invoke_signed)
    ///
    /// The authority is a PDA derived from TRANSFER_INTERFACE_AUTHORITY_SEED.
    ///
    /// Account order:
    /// - accounts[0]: compressed_token_program (for CPI)
    /// - accounts[1]: source_account (SPL or light-token)
    /// - accounts[2]: destination_account (SPL or light-token)
    /// - accounts[3]: authority (PDA, not signer - program signs)
    /// - accounts[4]: payer (signer)
    /// - accounts[5]: compressed_token_program_authority
    ///   For SPL bridge (optional, required for SPL<->Token):
    /// - accounts[6]: mint
    /// - accounts[7]: token_pool_pda
    /// - accounts[8]: spl_token_program
    pub fn process_transfer_interface_invoke_signed(
        accounts: &[AccountInfo],
        data: TransferInterfaceData,
    ) -> Result<(), ProgramError> {
        if accounts.len() < 6 {
            return Err(ProgramError::NotEnoughAccountKeys);
        }

        // Derive the PDA for the authority
        let (authority_pda, authority_bump) =
            Pubkey::find_program_address(&[TRANSFER_INTERFACE_AUTHORITY_SEED], &ID);

        // Verify the authority account is the PDA we expect
        if &authority_pda != accounts[3].key {
            return Err(ProgramError::InvalidSeeds);
        }

        let mut transfer = TransferInterfaceCpi::new(
            data.amount,
            accounts[1].clone(), // source_account
            accounts[2].clone(), // destination_account
            accounts[3].clone(), // authority (PDA)
            accounts[4].clone(), // payer
            accounts[5].clone(), // compressed_token_program_authority
        );

        // Add SPL bridge config if provided
        if accounts.len() >= 9 && data.token_pool_pda_bump.is_some() {
            transfer = transfer.with_spl_interface(
                Some(accounts[6].clone()), // mint
                Some(accounts[8].clone()), // spl_token_program
                Some(accounts[7].clone()), // token_pool_pda
                data.token_pool_pda_bump,
            )?;
        }

        // Invoke with PDA signing
        let authority_seeds: &[&[u8]] = &[TRANSFER_INTERFACE_AUTHORITY_SEED, &[authority_bump]];
        transfer.invoke_signed(&[authority_seeds])?;

        Ok(())
    }
    ```
  </Tab>
</Tabs>

# Next Steps

{" "}

<Card title="Learn how to close light-token accounts" icon="chevron-right" color="#0066ff" href="/light-token/cookbook/close-token-account" horizontal />
