Build a Bitcoin Application in 4 Steps
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.