Orpheus
Orpheus - A Next.js template for Bitcoin applications built on ZeusLayer
Section titled “Orpheus - A Next.js template for Bitcoin applications built on ZeusLayer”Orpheus is a Next.js-based template engineered for Bitcoin-centric applications, architected atop the ZeusLayer infrastructure. This documentation delivers a meticulous and comprehensive manual for developers, elucidating the methodologies for leveraging this frontend template to interface with the Zeus Program Library (ZPL) and implement cross-chain operability between Bitcoin and Solana. The ZPL underpins this integration by providing a robust framework for seamless interoperability, synergizing Bitcoin’s liquidity with Solana’s high-performance programmability. This empowers developers to construct sophisticated applications that optimally exploit the complementary technical attributes of both blockchain.
Table of Contents
Section titled “Table of Contents”- Getting Started
- Overview
- Interact with ZeusLayer via ZPL Client
- Build a Bitcoin Application in 4 Steps
Getting Started
Section titled “Getting Started”To run this example template, run the development server:
npm ci && npm run dev
Overview
Section titled “Overview”ZeusLayer is a cross-chain layer that enables interoperability between Bitcoin and programmable blockchains like Solana. The architecture is designed to provide security, scalability, and a seamless user experience across both chains.
Lifecycle of Interactions
Section titled “Lifecycle of Interactions”Check the live Interaction data on ZeusScan
Deposit Flow
Section titled “Deposit Flow”Withdrawal Flow
Section titled “Withdrawal Flow”Core Components
Section titled “Core Components”-
ZPL (Zeus Program Library): A set of Solana programs that run on-chain, including:
- BitcoinSPV: Verifies Bitcoin transactions using simplified payment verification
- TwoWayPeg: Manages the pegging and unpegging of assets between chains
- LiquidityManagement: Handles asset storage, retrieval, and liquidity pools
-
ZeusNode: Off-chain components that handle:
- Transaction Signing: Securely sign cross-chain transactions
- Relaying: Propagating transactions between Bitcoin and Solana
- Monitoring: Tracking transaction status and system health
Layer Roles
Section titled “Layer Roles”- Operator:
- Manages the system operations
- Monitors deposits and withdrawal requests
- Proposes transactions for guardian approval
- Guardian (Signing Entity):
- Provides security through multi-signature
- Validates and approves cross-chain operations
- Manages key recovery and emergency procedures
Reserves
Section titled “Reserves”There are two types of reserves to manage Bitcoin assets:
HotReserve (Bitcoin)
Section titled “HotReserve (Bitcoin)”HotReserve is a temporary storage for user deposits before they are moved to ColdReserve:
- Short-term storage for user deposits
- Provides quick access for frequent operations
- Uses Taproot addresses with user-specific script paths
ColdReserve (Bitcoin)
Section titled “ColdReserve (Bitcoin)”ColdReserve is a long-term secure storage for pooled Bitcoin:
- Long-term, secure storage for Bitcoin
- Higher security with multi-signature protection
- Uses Taproot addresses with time-locked script paths
zBTC Vault
Section titled “zBTC Vault”zBTC is a wrapped representation of Bitcoin on Solana:
- SPL Token: Follows the Solana Program Library token standard
- 1:1 Backing: Each zBTC is backed by an equivalent amount of BTC in reserves
- Programmable: Can be used in Solana DeFi applications
zBTC vault manages the issuance and redemption of zBTC tokens:
- Token Minting: Creates new zBTC when BTC is deposited
- Token Burning: Burns zBTC when BTC is withdrawn
- Balance Tracking: Ensures the system maintains proper reserves
Core Concepts
Section titled “Core Concepts”Bitcion Taproot
Section titled “Bitcion Taproot”ZeusLayer leverages Bitcoin’s Taproot technology (BIP 341), which provides enhanced privacy and flexibility:
- Key-path Spend: Uses the Guardian’s internal X-only public key for standard operations
- More efficient and private spending path
- Lower transaction fees
- Indistinguishable from regular Bitcoin transactions
- Script-path Spend: Uses the User’s X-only public key with time-locked scripts for recovery
- Enables complex spending conditions
- Provides backup recovery mechanisms
- Allows for time-locked security features
Simplified Payment Verification (SPV)
Section titled “Simplified Payment Verification (SPV)”We deploy a BitcoinSPV program on Solana that verifies Bitcoin transactions without requiring a full Bitcoin node:
- Block Headers: Validates the Bitcoin blockchain structure
- Merkle Proofs: Confirms transaction inclusion in blocks
- Transaction Parsing: Extracts relevant data from Bitcoin transactions
Interact with ZeusLayer via ZPL Client
Section titled “Interact with ZeusLayer via ZPL Client”The ZPL Client is the main entry point for interacting with the Zeus Program Library. It provides methods for account management, instruction creation, and transaction submission. The client abstracts the complexity of interacting with the Solana blockchain and the ZPL programs, making it easier for developers to build applications on top of ZPL.
Client Architecture
Section titled “Client Architecture”The ZPL Client is composed of three main modules:
- Account: Handles account derivation and data fetching
- Instruction: Creates instructions for various operations
- RpcClient: Manages transaction signing and submission
Initialization
Section titled “Initialization”import { ZplClient } from "@/zplClient";import { Connection, PublicKey } from "@solana/web3.js";
// Initialize ZPL Clientconst zplClient = new ZplClient( connection, // Solana connection walletPublicKey, // Connected wallet public key signTransaction, // Function to sign transactions twoWayPegProgramId, // Program ID for two-way peg liquidityManagementProgramId, // Program ID for liquidity management assetMint // Asset mint address);
Using ZplClient with React
Section titled “Using ZplClient with React”For React applications, you can use the provided ZplClientProvider
and useZplClient
hook to access the client throughout your application:
import { ZplClientProvider, useZplClient } from "@/contexts/ZplClientProvider";import { useConnection, useWallet } from "@solana/wallet-adapter-react";
// In your app's root componentfunction App() { return ( <ZplClientProvider> <YourComponent /> </ZplClientProvider> );}
// In your componentfunction YourComponent() { const zplClient = useZplClient();
// Now you can use zplClient to interact with ZPL // ...}
Client Capabilities
Section titled “Client Capabilities”The ZPL Client provides the following capabilities:
-
Account Management (/zplClient/account.ts):
- Deserialize on-chain hot reserve buckets, cold reserve buckets, positions, and two-way peg configuration into objects
- Derive program addresses, for example:
- TwoWayPeg/LiquidityManagement configuration, LiquidityManagementGuardianSetting, and SplTokenVaultAuthority address
- Hot/Cold reserve bucket address with/without user’s public key
- Interaction address given transaction ID and slot (we store each deposit/withdraw txns onchain to track their status)
- Position address given user’s public key
-
Instruction Creation (/zplClient/instruction.ts):
- Create/Reactivate hot reserve buckets for deposits
- Add zBTC to tBTC withdrawal requests
- Store/retrieve zBTC to/from the vault
-
Transaction Submission (/zplClient/rpcClient.ts):
- Sign and send transactions with multiple instructions
Build a Bitcoin Application in 4 Steps
Section titled “Build a Bitcoin Application in 4 Steps”This section details the primary operations that developers can perform using the ZPL. Each operation includes a description of its purpose, the required parameters, and code examples for implementation.
Step 1: Tracking Interactions
Section titled “Step 1: Tracking Interactions”The ZPL provides a comprehensive system for tracking cross-chain interactions through interactions. This section explains how to fetch and monitor transaction status.
Purpose
Section titled “Purpose”- Allows users to track the status of their cross-chain interactions
- Provides transparency into the multi-step process of deposits and withdrawals
- Enables developers to build informative UIs showing transaction progress
When to Use
Section titled “When to Use”- When displaying interaction history to users
- When monitoring the progress of ongoing interaction
- When building dashboards or analytics for cross-chain activity
Implementation
Section titled “Implementation”import { useZplClient } from "@/zplClient";import { Interaction, interactionSchema, transactionSchema } from "@/types/api";import { useFetchers } from "@/hooks/misc/useFetchers";import { useTwoWayPegConfiguration } from "@/hooks/zpl/useTwoWayPegConfiguration";import { useNetworkConfig } from "@/hooks/misc/useNetworkConfig";import useDepositInteractionsWithCache from "@/hooks/ares/useDepositInteractionsWithCache";import { PublicKey } from "@solana/web3.js";
export default function getTransactions({ solanaPubkey, bitcoinWallet }) { const zplClient = useZplClient(); const { aresFetcher, hermesFetcher } = useFetchers(); const { feeRate } = useTwoWayPegConfiguration(); const config = useNetworkConfig();
// Fetch withdrawal transactions const { data: withdrawalTransactions, hasNextPage, currentPage, itemsPerPage, handleItemsPerPage, handleNextPage, handlePrevPage, } = useInteractions( { solanaAddress: solanaPubkey?.toBase58(), destinationBitcoinAddress: bitcoinWallet ? convertP2trToTweakedXOnlyPubkey(bitcoinWallet.p2tr).toString("hex") : undefined, types: [InteractionType.Withdrawal], statuses: [ InteractionStatus.AddWithdrawalRequest, InteractionStatus.AddUnlockToUserProposal, InteractionStatus.BitcoinUnlockToUser, InteractionStatus.VerifyUnlockToUserTransaction, InteractionStatus.SolanaUnlockToUser, ], }, 10 );
// This is the schema of a interaction // amount: "TXN_SATS_AMOUNT" // app_developer: "" // from which app (Orpheus) // current_step_at: TIMESTAMP_OF_LATEST_STEP // deposit_block: BLOCK_NUMBER_OF_DEPOSIT_TO_HOTRESERVE // destination: "YOUR_SOLANA_ADDRESS" // guardian_certificate: "GUARDIAN_CERTIFICATE_ADDRESS" // guardian_setting: "GUARDIAN_SETTING_ADDRESS" // initiated_at: TIMESTAMP_OF_TRANSACTION_BE_SENT_ON_SOLANA // interaction_id: "INTERACTION_ACCOUNT_ADDRESS" // interaction_type: 0 (Deposit), 1 (Withdraw) // miner_fee: "MINER_FEE_ON_ZEUS_NODE" // service_fee: "SERVICE_FEE" (Only on Withdrawal) // source: "XONLY_PUBKEY_OF_HOTRESERVE" // status: "YOUR_TX_STATUS" (more details below) // steps: [{ // transaction: "TXN_ID", // chain: "Bitcoin" or "Solana", // action: "YOUR_STEP_STATUS", // timestamp: TIMESTAMP_OF_STEP_STATUS // }] // withdrawal_request_pda: "YOUR_WITHDRAWAL_REQUEST_PDA"
// Or you can specify the interaction id and fetch the interaction detail from our indexer API const { combinedInteractions: depositTransactions } = useDepositInteractionsWithCache({ solanaAddress: solanaPubkey?.toBase58(), bitcoinXOnlyPubkey: bitcoinWallet ? toXOnly(Buffer.from(bitcoinWallet.pubkey, "hex")).toString("hex") : undefined, });
const targetTx = depositTransactions[0]; // choose the first transaction as example const interactionSteps = await hermesFetcher( `/api/v1/raw/layer/interactions/${targetTx.interaction_id}/steps`, interactionSchema ); // The returned data is in the same format as the combinedInteractions above
// Below api returns the Bitcoin transaction detail from Bitcoin RPC const transactionDetail = await aresFetcher( `/api/v1/transaction/${targetTx.steps[0].transaction}/detail`, transactionSchema ); // This is the schema of the bitcoin transaction detail // blockhash: "TXN_BLOCK_HASH" // blocktime: TIMESTAMP_OF_TXN // confirmations: CONFIRMATIONS_OF_TXN // time: TIMESTAMP_OF_TXN // transaction: "TXN_ID"}
Understanding Interaction Types and Statuses
Section titled “Understanding Interaction Types and Statuses”The ZPL defines several interaction types and statuses to track the progress of cross-chain transactions:
Interaction Types:
InteractionType.Deposit
: Represents a deposit from Bitcoin to SolanaInteractionType.Withdrawal
: Represents a withdrawal from Solana to Bitcoin
Deposit Statuses (in order):
BitcoinDepositToHotReserve
: Initial deposit detected on Bitcoin network from user address to our hot reserve addressVerifyDepositToHotReserveTransaction
: BitcoinSPV program verifying the deposit transactionSolanaDepositToHotReserve
: Deposit confirmed and updated status on Solana by TwoWayPeg programAddLockToColdReserveProposal
: Zeus node send transaction to move BTC from hot reserve to cold reserveBitcoinLockToColdReserve
: Move BTC from hot reserve to cold reserve transaction is observed on Bitcoin networkVerifyLockToColdReserveTransaction
: BitcoinSPV program verifying cold reserve transactionSolanaLockToColdReserve
: Funds secured in cold reservePeg
: zBTC tokens minted in custody
Withdrawal Statuses (in order):
AddWithdrawalRequest
: Withdrawal request submitted on Solana, waiting for Zeus node to processAddUnlockToUserProposal
: Zeus node send transaction to move zBTC from cold reserve to user walletBitcoinUnlockToUser
: Unlocking Bitcoin from cold reserveVerifyUnlockToUserTransaction
: BitcoinSPV program verifying Bitcoin transactionSolanaUnlockToUser
: Confirming withdrawal on SolanaUnpeg
: Burning zBTC tokensDeprecateWithdrawalRequest
: Withdrawal request has been canceled (between AddWithdrawalRequest and AddUnlockToUserProposal)
Step 2: Create HotReserveBucket
Section titled “Step 2: Create HotReserveBucket”Before users can deposit Bitcoin into the system, they need to create a hot reserve bucket. This bucket serves as a temporary storage location for user deposits before they are moved to the cold reserve.
Purpose
Section titled “Purpose”- Creates a unique Bitcoin Taproot address for the user to deposit funds
- Associates the user’s Solana wallet with their Bitcoin deposit address
- Establishes the necessary on-chain accounts for tracking deposits
When to Use
Section titled “When to Use”- When a user wants to deposit Bitcoin for the first time
- When a user’s previous hot reserve bucket has expired or been deactivated
Implementation
Section titled “Implementation”This functionality is already implemented in this repo. You just need to use the following hooks:
import useHotReserveBucketActions from "@/hooks/zpl/useHotReserveBucketActions";import { useBitcoinWallet } from "@/contexts/BitcoinWalletProvider";import { CheckBucketResult } from "@/types/misc";
function YourComponent() { // Get the connected wallet const { wallet: bitcoinWallet } = useBitcoinWallet();
// Get hot reserve bucket actions const { createHotReserveBucket, reactivateHotReserveBucket, checkHotReserveBucketStatus, } = useHotReserveBucketActions(bitcoinWallet);
// Check if user has an active hot reserve bucket const checkAndPrepareHotReserveBucket = async () => { const bucketStatus = await checkHotReserveBucketStatus();
if (bucketStatus?.status === CheckBucketResult.NotFound) { // Create a new hot reserve bucket if none exists await createHotReserveBucket(); } else if ( bucketStatus?.status === CheckBucketResult.Expired || bucketStatus?.status === CheckBucketResult.Deactivated ) { // Reactivate if expired or deactivated await reactivateHotReserveBucket(); } };}
The useHotReserveBucketActions
hook handles all the complexity of interacting with the ZPL client, including:
- Getting guardian settings and cold reserve buckets
- Deriving Bitcoin addresses using Taproot
- Constructing and sending transactions
Step 3: Lock BTC to Mint zBTC
Section titled “Step 3: Lock BTC to Mint zBTC”The process of locking BTC (Bitcoin on the Bitcoin network) to mint zBTC (wrapped Bitcoin on Solana) involves several steps. This section details the complete flow from checking bucket status to monitoring deposit progress.
Purpose
Section titled “Purpose”- Allows users to convert their Bitcoin to zBTC for use in Solana applications
- Ensures secure and verifiable cross-chain transfers
- Maintains 1:1 backing of zBTC with real Bitcoin
When to Use
Section titled “When to Use”- When a user wants to bring Bitcoin into the Solana ecosystem
Implementation
Section titled “Implementation”The deposit process is already implemented through various hooks in the codebase. Here’s a simplified implementation that uses these existing hooks to implement a deposit button with input amount:
import { useState } from "react";import { useWallet } from "@solana/wallet-adapter-react";import usePersistentStore from "@/stores/persistentStore";import { useBitcoinWallet } from "@/contexts/BitcoinWalletProvider";import useBitcoinUTXOs from "@/hooks/ares/useBitcoinUTXOs";import useTwoWayPegConfiguration from "@/hooks/zpl/useTwoWayPegConfiguration";import * as bitcoin from "bitcoinjs-lib";import { useNetworkConfig } from "@/hooks/misc/useNetworkConfig";import { useZplClient } from "@/contexts/ZplClientProvider";import { constructDepositToHotReserveTx, convertBitcoinNetwork, btcToSatoshi } from "@/bitcoin";import { createAxiosInstances } from "@/utils/axios";import { getInternalXOnlyPubkeyFromUserWallet } from "@/bitcoin/wallet";import { sendTransaction } from "@/bitcoin/rpcClient";
export default function Home() { const [depositAmount, setDepositAmount] = useState(0);
const bitcoinNetwork = usePersistentStore((state) => state.bitcoinNetwork); const solanaNetwork = usePersistentStore((state) => state.solanaNetwork); const networkConfig = useNetworkConfig(); const { wallet: bitcoinWallet, signPsbt, } = useBitcoinWallet(); const { publicKey: solanaPubkey } = useWallet(); const zplClient = useZplClient();
if(!zplClient || !solanaPubkey || !bitcoinWallet) { console.log("ZPL Client not found"); } const { feeRate } = useTwoWayPegConfiguration();
const { data: bitcoinUTXOs } = useBitcoinUTXOs( bitcoinWallet?.p2tr ); const handleDeposit = async () => { const userXOnlyPublicKey = getInternalXOnlyPubkeyFromUserWallet(bitcoinWallet);
if (!userXOnlyPublicKey) throw new Error("User X Only Public Key not found");
// although we have a array of hotReserveBuckets, but the user could only bind one bitcoin address with the protocol, so we only need to get the first one const hotReserveBuckets = await zplClient.twoWayPeg.accounts.getHotReserveBucketsByBitcoinXOnlyPubkey( userXOnlyPublicKey );
if (!hotReserveBuckets || hotReserveBuckets.length === 0) { console.log("No hot reserve address found"); return; }
// NOTE: Regtest and Testnet use the same ZPL with different guardian settings, so we need to set guardian setting in env const targetHotReserveBucket = hotReserveBuckets.find( (bucket) => bucket.guardianSetting.toBase58() === networkConfig.guardianSetting ); if (!targetHotReserveBucket) throw new Error("Wrong guardian setting");
const { address: targetHotReserveAddress } = bitcoin.payments.p2tr({ pubkey: Buffer.from(targetHotReserveBucket.taprootXOnlyPublicKey), network: convertBitcoinNetwork(bitcoinNetwork), });
if (!targetHotReserveAddress) { console.log("Hot reserve address not found"); return; } let depositPsbt; try { const { psbt } = constructDepositToHotReserveTx( bitcoinUTXOs, targetHotReserveAddress, btcToSatoshi(depositAmount), userXOnlyPublicKey, feeRate, convertBitcoinNetwork(bitcoinNetwork), false ); depositPsbt = psbt; } catch (e) { if (e instanceof Error && e.message === "Insufficient UTXO") { console.log("Insufficient UTXO, please adjust the amount"); return; } else { throw e; } }
try { const signTx = await signPsbt(depositPsbt, true);
const { aresApi } = createAxiosInstances(solanaNetwork, bitcoinNetwork);
const txId = await sendTransaction(aresApi, signTx); console.log(txId);
} catch (e) { console.error(e); } }
return ( <div> {/* Deposit input and button */} <input type="number" value={depositAmount} onChange={(e) => setDepositAmount(Number(e.target.value))} placeholder="Amount to deposit" /> <button onClick={handleDeposit}>Deposit</button> </div> );}
This implementation leverages the following hooks:
Step 4-1: Redeem zBTC from Custodial
Section titled “Step 4-1: Redeem zBTC from Custodial”The ZPL provides functionality for users to store zBTC in a custodial vault and retrieve it when needed. This section explains the store and retrieve operations in detail.
Purpose
Section titled “Purpose”- Provides a secure way to store zBTC in a custodial vault
- Allows users to retrieve their zBTC when needed
When to Use
Section titled “When to Use”- Store: When a user wants to secure their zBTC without converting back to Bitcoin
- Retrieve: When a user wants to access previously stored zBTC for use in Solana applications
Redeem zBTC to your Bitcoin application
Section titled “Redeem zBTC to your Bitcoin application”By flexibly cascading sdk in Orpheus, you can set custom retrieval address to designate an alternative Escrow token account managed by your application. By implementinng this operation, redemption transactions initiated by users of your application will go to the application-controlled escrow rather than the user’s individual wallet. This functionality unlocks a range of decentralized finance (DeFi) use cases, including money markets, neutral trading strategies, liquidity provisioning, or the development of a Bitcoin-backed stablecoin.
Below is a sample implementation of creating retrieve instruction:
constructRetrieveIx( amount: BN, guardianSetting: PublicKey, receiverAta: PublicKey) {
...
const ix = new TransactionInstruction({ keys: [ { pubkey: this.walletPublicKey, isSigner: true, isWritable: true, }, { pubkey: receiverAta, isSigner: false, isWritable: true }, { pubkey: positionPda, isSigner: false, isWritable: true }, { pubkey: lmGuardianSetting, isSigner: false, isWritable: false }, { pubkey: splTokenVaultAuthority, isSigner: false, isWritable: false }, { pubkey: vaultAta, isSigner: false, isWritable: true }, { pubkey: this.assetMint, isSigner: false, isWritable: false }, { pubkey: SystemProgram.programId, isSigner: false, isWritable: false }, { pubkey: TOKEN_PROGRAM_ID, isSigner: false, isWritable: false }, { pubkey: ASSOCIATED_TOKEN_PROGRAM_ID, isSigner: false, isWritable: false, }, ], programId: this.liquidityManagementProgramId, data: instructionData, });
return ix;}
For guidance on constructing a Solana escrow, developers may consult reference implementations utilizing either the Anchor framework or native Rust programming paradigms:
Then by ingeeniously cascade a transfer instruction, you can redeem the zBTC to a custodial escrow.
Implementation
Section titled “Implementation”import { useState } from "react";import { useWallet, useConnection } from "@solana/wallet-adapter-react";import { useZplClient } from "@/contexts/ZplClientProvider";import usePositions from "@/hooks/zpl/usePositions";import { useNetworkConfig } from "@/hooks/misc/useNetworkConfig";import { PublicKey, TransactionInstruction } from "@solana/web3.js";import BigNumber from "bignumber.js";import { BN } from "bn.js";import { BTC_DECIMALS } from "@/utils/constant";import { getAssociatedTokenAddressSync, createTransferInstruction, createAssociatedTokenAccountInstruction,} from "@solana/spl-token";
export default function Home() { const [redeemAmount, setRedeemAmount] = useState(0);
const config = useNetworkConfig(); const zplClient = useZplClient(); const { publicKey: solanaPubkey } = useWallet(); const { data: positions } = usePositions(solanaPubkey); const { connection } = useConnection();
const handleRedeem = async () => { if (!redeemAmount || !zplClient) return;
try { if (!positions) return;
const sortedPositions = positions.toSorted((a, b) => b.storedAmount .sub(b.frozenAmount) .cmp(a.storedAmount.sub(a.frozenAmount)) );
const redeemAmountBN = new BN( new BigNumber(redeemAmount) .multipliedBy(new BigNumber(10).pow(BTC_DECIMALS)) .toString() );
const receiverAta = getAssociatedTokenAddressSync( zplClient.assetMint, solanaPubkey, true );
const ixs: TransactionInstruction[] = [];
let remainingAmount = redeemAmountBN.clone(); for (const position of sortedPositions) { const amountToRedeem = BN.min( position.storedAmount.sub(position.frozenAmount), remainingAmount );
const twoWayPegGuardianSetting = config.guardianSetting;
if (!twoWayPegGuardianSetting) throw new Error("Two way peg guardian setting not found");
const retrieveIx = zplClient.liquidityManagement.instructions.buildRetrieveIx( amountToRedeem, solanaPubkey, zplClient.assetMint, new PublicKey(twoWayPegGuardianSetting), receiverAta ); ixs.push(retrieveIx);
remainingAmount = remainingAmount.sub(amountToRedeem);
if (remainingAmount.eq(new BN(0))) break; }
// TODO: You can customize the retrieve address here if (process.env.NEXT_PUBLIC_DEVNET_REDEEM_ADDRESS) { const targetAddress = new PublicKey( process.env.NEXT_PUBLIC_DEVNET_REDEEM_ADDRESS ); const toATA = getAssociatedTokenAddressSync( new PublicKey(config.assetMint), targetAddress, true ); // check if the target address has an associated token account const info = await connection.getAccountInfo(toATA); if (!info) { // if not, create one const createIx = createAssociatedTokenAccountInstruction( solanaPubkey, toATA, targetAddress, new PublicKey(config.assetMint) ); ixs.push(createIx); } // add a transfer instruction to transfer the tokens to the receive_address const transferIx = createTransferInstruction( receiverAta, toATA, solanaPubkey, BigInt(redeemAmountBN.toString()) ); ixs.push(transferIx); }
const sig = await zplClient.signAndSendTransactionWithInstructions(ixs); console.log(sig); } catch (error) { console.log(error); }; } return ( <div> <input type="number" value={redeemAmount} onChange={(e) => setRedeemAmount(Number(e.target.value))} /> <button onClick={handleRedeem}>Redeem</button> </div> );}
Each position contains information about the amount stored, the guardian setting, and other metadata.
Step 4-2: Withdraw zBTC to BTC
Section titled “Step 4-2: Withdraw zBTC to BTC”The withdrawal process allows users to convert their zBTC back to native Bitcoin. This section details the complete flow from creating a withdrawal request to monitoring its progress.
Purpose
Section titled “Purpose”- Allows users to convert their zBTC back to native Bitcoin
- Ensures secure and verifiable cross-chain transfers
- Provides a way to exit the Solana ecosystem back to Bitcoin
When to Use
Section titled “When to Use”- When a user wants to move their assets from Solana back to Bitcoin
- When a user wants to realize gains or use their Bitcoin outside of Solana
- When a user needs to access their Bitcoin for other purposes
Implementation
Section titled “Implementation”import { useState } from "react";import { useWallet } from "@solana/wallet-adapter-react";import { useBitcoinWallet } from "@/contexts/BitcoinWalletProvider";import usePersistentStore from "@/stores/persistentStore";import { useZplClient } from "@/contexts/ZplClientProvider";import usePositions from "@/hooks/zpl/usePositions";import useTwoWayPegGuardianSettings from "@/hooks/hermes/useTwoWayPegGuardianSettings";import { useConnection } from "@solana/wallet-adapter-react";import useHotReserveBucketsByOwner from "@/hooks/zpl/useHotReserveBucketsByOwner";import { PublicKey, TransactionInstruction } from "@solana/web3.js";import { getAccount, getAssociatedTokenAddressSync } from "@solana/spl-token";import { convertP2trToTweakedXOnlyPubkey, xOnlyPubkeyHexToP2tr } from "@/bitcoin";import BigNumber from "bignumber.js";import { BN } from "bn.js";import { BTC_DECIMALS } from "@/utils/constant";import { formatValue } from "@/utils/format";export default function Home() { const [withdrawAmount, setWithdrawAmount] = useState(0); const assetFrom = { name: "zBTC", amount: formatValue(withdrawAmount, 6), isLocked: true, // NOTE: asset is in vault }
const bitcoinNetwork = usePersistentStore((state) => state.bitcoinNetwork);
const { publicKey: solanaPubkey } = useWallet(); const { wallet: bitcoinWallet } = useBitcoinWallet(); const { data: hotReserveBuckets } = useHotReserveBucketsByOwner(solanaPubkey); const { data: positions, } = usePositions(solanaPubkey);
const walletsInHotReserveBuckets = hotReserveBuckets.map((bucket) => xOnlyPubkeyHexToP2tr( Buffer.from(bucket.scriptPathSpendPublicKey).toString("hex"), bitcoinNetwork, "internal" ) ); const connectedWallets = bitcoinWallet?.p2tr ? Array.from(new Set([bitcoinWallet.p2tr, ...walletsInHotReserveBuckets])) : Array.from(new Set(walletsInHotReserveBuckets));
const selectedWallet = connectedWallets[0]; const zplClient = useZplClient(); const { data: twoWayPegGuardianSettings } = useTwoWayPegGuardianSettings(); const { connection } = useConnection(); const handleWithdraw = async () => { if (!zplClient) throw new Error("zplClient not found"); if (!solanaPubkey) throw new Error("Solana Pubkey not found");
const withdrawAmountBN = new BN( new BigNumber(withdrawAmount) .multipliedBy(10 ** BTC_DECIMALS) .toString() );
const twoWayPegConfiguration = await zplClient.twoWayPeg.accounts.getConfiguration();
const ixs: TransactionInstruction[] = [];
// NOTE: asset is in vault, so use the biggest position guardian first if (assetFrom.isLocked) { if (!positions) { return; }
const sortedPositions = positions.toSorted((a, b) => b.storedAmount .sub(b.frozenAmount) .cmp(a.storedAmount.sub(a.frozenAmount)) );
let remainingAmount = withdrawAmountBN.clone(); for (const position of sortedPositions) { const amountToWithdraw = BN.min( position.storedAmount.sub(position.frozenAmount), remainingAmount );
const twoWayPegGuardianSetting = twoWayPegGuardianSettings.find( (setting) => zplClient.liquidityManagement.pdas .deriveVaultSettingAddress(new PublicKey(setting.address)) .toBase58() === position.vaultSetting.toBase58() );
if (!twoWayPegGuardianSetting) return;
const vaultSettingPda = zplClient.liquidityManagement.pdas.deriveVaultSettingAddress( new PublicKey(twoWayPegGuardianSetting.address) );
const currentTimestamp = new BN(Date.now() / 1000);
const withdrawalRequestIx = zplClient.twoWayPeg.instructions.buildAddWithdrawalRequestIx( amountToWithdraw, currentTimestamp, convertP2trToTweakedXOnlyPubkey(selectedWallet), solanaPubkey, twoWayPegConfiguration.layerFeeCollector, new PublicKey(twoWayPegGuardianSetting.address), zplClient.liquidityManagementProgramId, zplClient.liquidityManagement.pdas.deriveConfigurationAddress(), vaultSettingPda, zplClient.liquidityManagement.pdas.derivePositionAddress( vaultSettingPda, solanaPubkey ) );
ixs.push(withdrawalRequestIx); remainingAmount = remainingAmount.sub(amountToWithdraw);
if (remainingAmount.eq(new BN(0))) break; } // NOTE: asset is in wallet, so need to check all guardians store quota and store to the biggest quota guardian first } else { const twoWayPegGuardiansWithQuota = await Promise.all( twoWayPegGuardianSettings.map(async (twoWayPegGuardianSetting) => { const totalSplTokenMinted = new BN( twoWayPegGuardianSetting.total_amount_pegged );
const splTokenVaultAuthority = zplClient.liquidityManagement.pdas.deriveSplTokenVaultAuthorityAddress( new PublicKey(twoWayPegGuardianSetting.address) );
const vaultAta = getAssociatedTokenAddressSync( new PublicKey(twoWayPegGuardianSetting.asset_mint), splTokenVaultAuthority, true );
let remainingStoreQuota; try { const tokenAccountData = await getAccount(connection, vaultAta); const splTokenBalance = new BN( tokenAccountData.amount.toString() ); remainingStoreQuota = totalSplTokenMinted.sub(splTokenBalance); } catch { remainingStoreQuota = new BN(0); }
return { address: twoWayPegGuardianSetting.address, remainingStoreQuota, liquidityManagementGuardianSetting: zplClient.liquidityManagement.pdas.deriveVaultSettingAddress( new PublicKey(twoWayPegGuardianSetting.address) ), }; }) );
const sortedTwoWayPegGuardiansWithQuota = twoWayPegGuardiansWithQuota.toSorted((a, b) => b.remainingStoreQuota.cmp(a.remainingStoreQuota) );
let remainingAmount = withdrawAmountBN.clone(); for (const twoWayPegGuardian of sortedTwoWayPegGuardiansWithQuota) { const amountToWithdraw = BN.min( twoWayPegGuardian.remainingStoreQuota, remainingAmount );
const storeIx = zplClient.liquidityManagement.instructions.buildStoreIx( withdrawAmountBN, solanaPubkey, zplClient.assetMint, new PublicKey(twoWayPegGuardian.address) );
const vaultSettingPda = zplClient.liquidityManagement.pdas.deriveVaultSettingAddress( new PublicKey(twoWayPegGuardian.address) );
const currentTimestamp = new BN(Date.now() / 1000);
const withdrawalRequestIx = zplClient.twoWayPeg.instructions.buildAddWithdrawalRequestIx( amountToWithdraw, currentTimestamp, convertP2trToTweakedXOnlyPubkey(selectedWallet), solanaPubkey, twoWayPegConfiguration.layerFeeCollector, new PublicKey(twoWayPegGuardian.address), zplClient.liquidityManagementProgramId, zplClient.liquidityManagement.pdas.deriveConfigurationAddress(), vaultSettingPda, zplClient.liquidityManagement.pdas.derivePositionAddress( vaultSettingPda, solanaPubkey ) );
ixs.push(storeIx); ixs.push(withdrawalRequestIx);
remainingAmount = remainingAmount.sub(amountToWithdraw);
if (remainingAmount.eq(new BN(0))) break; } }
const sig = await zplClient.signAndSendTransactionWithInstructions(ixs);
return sig; } return ( <div> <input type="number" value={withdrawAmount} onChange={(e) => setWithdrawAmount(Number(e.target.value))} /> <button onClick={handleWithdraw}>Withdraw</button> </div> );}
The withdrawal process typically takes approximately 24 hours to complete as it involves multiple steps including guardian approval, Bitcoin transaction creation, and verification.
Conclusion
Section titled “Conclusion”This documentation provides a comprehensive guide to integrating with the Zeus Program Library for cross-chain functionality between Bitcoin and Solana. By following these instructions and examples, developers can quickly get started with building their own applications using the ZPL.
For more detailed information, refer to the source code and comments in the template application.