Skip to main content

Installation

bun add @loyal-labs/private-transactions
# or
npm install @loyal-labs/private-transactions

Peer Dependencies

bun add @coral-xyz/anchor @solana/web3.js @solana/spl-token @magicblock-labs/ephemeral-rollups-sdk

Create a Client

fromConfig(...) creates a client with connections to both base Solana and the MagicBlock PER endpoint. It handles TEE integrity verification and PER auth token acquisition automatically.
import { Keypair, PublicKey } from "@solana/web3.js";
import {
  ER_VALIDATOR,
  LoyalPrivateTransactionsClient,
  MAGIC_CONTEXT_ID,
  MAGIC_PROGRAM_ID,
} from "@loyal-labs/private-transactions";

const signer = Keypair.fromSecretKey(Uint8Array.from([...secretBytes]));

const client = await LoyalPrivateTransactionsClient.fromConfig({
  signer,
  baseRpcEndpoint: "https://api.devnet.solana.com",
  // Mainnet: https://mainnet-tee.magicblock.app
  // Devnet: https://tee.magicblock.app
  ephemeralRpcEndpoint: "https://mainnet-tee.magicblock.app",
  ephemeralWsEndpoint: "wss://mainnet-tee.magicblock.app",
  commitment: "confirmed",
});

const tokenMint = new PublicKey("<token-mint>");
const user = signer.publicKey;

Shield: Move Tokens into Private Deposit

Shield moves tokens from your wallet into the private layer (PER).

1. Initialize and fund the deposit (base layer)

// Create deposit account (no-op if already exists)
await client.initializeDeposit({ tokenMint, user, payer: user });

// Transfer tokens from your ATA into the program vault
await client.modifyBalance({
  tokenMint,
  user,
  payer: user,
  userTokenAccount: new PublicKey("<sender-ata>"),
  amount: 1_000_000,
  increase: true,
});

2. Create permission and delegate to PER

// Set up PER access control (idempotent — returns null if already exists)
await client.createPermission({ tokenMint, user, payer: user });

// Delegate deposit to TEE validator — account moves into PER
await client.delegateDeposit({
  tokenMint,
  user,
  payer: user,
  validator: ER_VALIDATOR,
});
Your deposit is now private — only you can see it inside PER.

Private Transfers

Transfer to another user by Telegram username. The destination username deposit must already exist and be delegated.
await client.transferToUsernameDeposit({
  tokenMint,
  username: "alice_user",
  amount: 100_000,
  user,
  payer: user,
  sessionToken: null,
});
Or transfer directly to another wallet address:
await client.transferDeposit({
  tokenMint,
  user,
  destinationUser: new PublicKey("<recipient-wallet>"),
  amount: 100_000,
  payer: user,
  sessionToken: null,
});

Claim and Unshield

Claim from username deposit

The recipient proves Telegram identity via a verified session PDA and claims to their own deposit:
await client.claimUsernameDepositToDeposit({
  username: "alice_user",
  tokenMint,
  amount: 100_000,
  recipient: new PublicKey("<recipient-wallet>"),
  session: new PublicKey("<telegram-session-pda>"),
});

Unshield: move tokens back to wallet

// Commit PER state and return deposit to base layer
await client.undelegateDeposit({
  tokenMint,
  user,
  payer: user,
  sessionToken: null,
  magicProgram: MAGIC_PROGRAM_ID,
  magicContext: MAGIC_CONTEXT_ID,
});

// Withdraw tokens from vault back to your ATA
await client.modifyBalance({
  tokenMint,
  user,
  payer: user,
  userTokenAccount: new PublicKey("<sender-ata>"),
  amount: 1_000_000,
  increase: false,
});

Next Steps

  • Read How It Works for the full base-layer + PER lifecycle and smart contract details.
  • See API Reference for all method signatures and types.
  • Use CLI to execute the same flow via bun scripts/telegram-private-transfer.ts.