@loyal-labs/private-transactions
v0.2.10
Published
SDK for Telegram-based private Solana deposits
Downloads
107
Maintainers
Readme
@loyal-labs/private-transactions
SDK for private SPL token deposits and transfers using MagicBlock Private Ephemeral Rollups (PER). This package wraps the telegram-private-transfer Anchor program and provides helpers for permissions, delegation, private transfers, claims, and undelegation.
Installation
bun add @loyal-labs/private-transactions
# or
npm install @loyal-labs/private-transactionsPeer Dependencies
bun add @coral-xyz/anchor @solana/web3.js @solana/spl-token @magicblock-labs/ephemeral-rollups-sdkQuick Start
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 tokenMint = new PublicKey("<mint>");
const client = await LoyalPrivateTransactionsClient.fromConfig({
signer,
baseRpcEndpoint: "https://api.devnet.solana.com",
// Mainnet: https://mainnet-tee.magicblock.app
// Devnet: https://devnet-tee.magicblock.app
ephemeralRpcEndpoint: "https://mainnet-tee.magicblock.app",
ephemeralWsEndpoint: "wss://mainnet-tee.magicblock.app",
commitment: "confirmed",
});
// Shield: move tokens into private deposit in one base transaction
await client.shieldTokens({
tokenMint,
user: signer.publicKey,
amount: 1_000_000,
validator: ER_VALIDATOR,
});
// Private transfer (on PER) — destination username deposit must already exist and be delegated
await client.transferToUsernameDeposit({
tokenMint,
username: "alice_user",
amount: 100_000,
user: signer.publicKey,
payer: signer.publicKey,
});
// Unshield: withdraw from private deposit in one base transaction
await client.unshieldTokens({
tokenMint,
user: signer.publicKey,
amount: 1_000_000,
magicProgram: MAGIC_PROGRAM_ID,
magicContext: MAGIC_CONTEXT_ID,
});PER Authentication
For hosted PER endpoints (devnet-tee.magicblock.app, mainnet-tee.magicblock.app), the SDK acquires auth tokens automatically during fromConfig.
If you need explicit control, fetch the token externally and pass it through authToken:
import { getAuthToken } from "@magicblock-labs/ephemeral-rollups-sdk";
const authToken = await getAuthToken(
"https://mainnet-tee.magicblock.app",
wallet.publicKey,
wallet.signMessage
);
const client = await LoyalPrivateTransactionsClient.fromConfig({
signer: wallet,
baseRpcEndpoint: "https://api.mainnet-beta.solana.com",
ephemeralRpcEndpoint: "https://mainnet-tee.magicblock.app",
ephemeralWsEndpoint: "wss://mainnet-tee.magicblock.app",
authToken,
});API Overview
Factory Method
fromConfig({ signer, baseRpcEndpoint, ephemeralRpcEndpoint, ... })
Shield / Unshield
shieldTokens— one-transaction base shield flow, with optional pre-undelegate when the deposit is already delegatedunshieldTokens— one-transaction base unshield flow, with optional pre-undelegate and automatic re-delegate when balance remainsbuildShieldFlowTransactionPlan— create the plannedshieldorunshieldtransactions and instruction metadata oncebuildShieldTokensTransactionPlan/buildUnshieldTokensTransactionPlan— explicit shield/unshield plan buildersestimateShieldFlowFee— estimate transaction-level network fees plus instruction-attributed rent from an existing planestimateShieldTokensFee/estimateUnshieldTokensFee— explicit shield/unshield estimators for an existing planexecuteShieldFlowTransactionPlan— send the exact transactions from an existing plan in orderexecuteShieldTokensTransactionPlan/executeUnshieldTokensTransactionPlan— explicit shield/unshield executors for an existing planinitializeDeposit— create deposit account (no-op if exists)modifyBalance— deposit (increase: true) or withdraw (increase: false) real tokenscreatePermission— set up PER access control (idempotent)delegateDeposit— delegate to TEE validator
Fee estimates use Solana's getFeeForMessage on the planned transaction
messages. Instruction rows report net rentLamports: positive values are rent
locked for newly created accounts, negative values are rent reclaimed by close
or undelegate cleanup. Delegation rent credits are net of MagicBlock's
undelegate session fee, so undelegation is not a full refund of every lamport
held by delegation accounts. For native-SOL flows, instruction rows also report
nativeLamports for the shielded/unshielded SOL principal; this is separate
from protocol fees and rent, but it does affect the payer's SOL balance.
feeAndRentLamports excludes native token principal, while totalLamports
is a cost-style net SOL impact for the common payer=user flow: positive values
are debits/costs and negative values are credits/gains. If payer differs from
user, nativeLamports belongs to the token owner while fees/rent may belong to
the payer. Network fees are not attributed per
instruction because Solana charges them at the transaction/message level. Build
the plan once and pass the same plan into the estimator so the estimate is tied
to the exact instructions your app is about to inspect or send. To execute that
exact plan, pass it to the matching execute*TransactionPlan method; it will
send any pre-undelegate transaction first, wait for the required owner transition
when the plan includes one, then send the base transaction.
const shieldPlan = await client.buildShieldTokensTransactionPlan({
user: signer.publicKey,
tokenMint,
amount: 1_000_000,
});
const shieldEstimate = await client.estimateShieldTokensFee({
plan: shieldPlan,
});
const unshieldPlan = await client.buildUnshieldTokensTransactionPlan({
user: signer.publicKey,
tokenMint,
amount: 1_000_000,
});
const unshieldEstimate = await client.estimateUnshieldTokensFee({
plan: unshieldPlan,
});
console.log(shieldEstimate.feeAndRentLamports);
console.log(shieldEstimate.totalLamports);
console.log(unshieldEstimate.feeAndRentLamports);
console.log(unshieldEstimate.totalLamports);
const shieldResult = await client.executeShieldTokensTransactionPlan({
plan: shieldPlan,
});
const unshieldResult = await client.executeUnshieldTokensTransactionPlan({
plan: unshieldPlan,
});
console.log(shieldResult.signatures);
console.log(unshieldResult.signatures);Private Transfers (on PER)
transferDeposit— transfer between user depositstransferToUsernameDeposit— transfer to username depositclaimUsernameDepositToDeposit— claim from username deposit with verified Telegram session
Username Deposits
initializeUsernameDeposit— create username deposit accountcreateUsernamePermission— PER access control for username depositdelegateUsernameDeposit— delegate username deposit to PERundelegateUsernameDeposit— commit and undelegate username deposit
Commit / Undelegate
undelegateDeposit— commit PER state, return deposit to base layerundelegateUsernameDeposit
Queries
getBaseDeposit/getEphemeralDepositgetBaseUsernameDeposit/getEphemeralUsernameDeposit
Accessors
publicKeygetBaseProgram()getEphemeralProgram()getProgramId()
PDA Helpers
findDepositPdafindUsernameDepositPdafindVaultPdafindPermissionPdafindDelegationRecordPdafindDelegationMetadataPdafindBufferPda
Development
bun install
bun run typecheck
bun test --timeout 60000