Boring Vault UI SDK

Solana Vault Integration

Complete SDK for interacting with Boring Vault on Solana blockchain

Introduction

The Boring Vault Solana SDK provides comprehensive tools for interacting with Boring Vault on the Solana blockchain. This SDK supports both SPL token deposits and native SOL deposits, along with queued withdrawals and comprehensive vault management.

Features

  • SPL Token Deposits - Deposit any supported SPL token into vaults
  • Native SOL Deposits - Direct SOL deposits without wrapping
  • Queued Withdrawals - Request withdrawals through the Boring Queue system
  • Withdraw Status Tracking - Check status of pending withdraw requests
  • Vault Information - Get share values, supply, and total assets (TVL)
  • Input Validation - Automatic validation of deposit amounts and parameters
  • Wallet Support - Compatible with both wallet adapters and keypairs
  • Error Handling - Comprehensive error handling with detailed messages
  • Transaction Monitoring - Built-in transaction status polling and explorer links

Installation & Dependency Management

Installation

npm install boring-vault-ui

Handling Dependency Conflicts

Due to version conflicts in the Solana ecosystem, you may encounter module resolution errors. If you see errors like:

Error: Package subpath './dist/lib/client' is not defined by "exports" in .../rpc-websockets/package.json

Add the following overrides to your package.json:

{
  "overrides": {
    "@coral-xyz/anchor": {
      "@solana/web3.js": "^1.98.0"
    },
    "@pythnetwork/pyth-solana-receiver": {
      "@solana/web3.js": "^1.77.0"
    }
  }
}

After adding the overrides, run:

rm -rf node_modules package-lock.json
npm install

VaultSDK

The VaultSDK class is the main interface for interacting with Boring Vault on Solana.

Setup

import { VaultSDK } from 'boring-vault-ui/solana';

// Initialize the SDK with an RPC URL
const vaultSDK = new VaultSDK('https://api.mainnet-beta.solana.com');
// You can also use shortcuts like 'mainnet', 'devnet', 'testnet'
const vaultSDK = new VaultSDK('mainnet');

Getting Vault Information

Get User Share Balance

async function getUserShares() {
  const userAddress = 'user_wallet_address_here';
  const vaultId = 1;
  
  try {
    const userShares = await vaultSDK.fetchUserShares(userAddress, vaultId);
    
    console.log(`User owns ${userShares} shares in vault ${vaultId}`);
    return userShares;
  } catch (error) {
    console.error('Error fetching user shares:', error);
    return 0;
  }
}

Get Complete Vault Data

import { web3 } from '@coral-xyz/anchor';

async function getVaultInfo() {
  const vaultAddress = new web3.PublicKey('your_vault_address_here');
  
  try {
    const vaultData = await vaultSDK.getVaultData(vaultAddress);
    
    console.log('Vault ID:', vaultData.vaultState.vaultId.toString());
    console.log('Authority:', vaultData.vaultState.authority.toString());
    console.log('Share Mint:', vaultData.vaultState.shareMint.toString());
    console.log('Paused:', vaultData.vaultState.paused);
    console.log('Deposit Sub-Account:', vaultData.vaultState.depositSubAccount);
    console.log('Withdraw Sub-Account:', vaultData.vaultState.withdrawSubAccount);
    
    // Teller state information
    if (vaultData.tellerState) {
      console.log('Base Asset:', vaultData.tellerState.baseAsset.toString());
      console.log('Exchange Rate:', vaultData.tellerState.exchangeRate.toString());
      console.log('Platform Fee:', vaultData.tellerState.platformFeeBps / 100, '%');
      console.log('Performance Fee:', vaultData.tellerState.performanceFeeBps / 100, '%');
    }
    
    return vaultData;
  } catch (error) {
    console.error('Error fetching vault data:', error);
    return null;
  }
}

Get Vault Balance

async function checkVaultBalance() {
  const vaultAddress = new web3.PublicKey('your_vault_address_here');
  
  try {
    const balance = await vaultSDK.getVaultBalance(vaultAddress);
    console.log(`Vault balance: ${balance} lamports`);
    return balance;
  } catch (error) {
    console.error('Error fetching vault balance:', error);
    return '0';
  }
}

Get Share Value

Get the value of 1 share in terms of the underlying base asset.

async function getShareValue() {
  const vaultId = 1;
  
  try {
    const shareValue = await vaultSDK.fetchShareValue(vaultId);
    
    console.log(`1 share = ${shareValue} base asset units`);
    console.log(`Current exchange rate: ${shareValue}`);
    
    return shareValue;
  } catch (error) {
    console.error('Error fetching share value:', error);
    return 0;
  }
}

Get Total Assets (TVL)

Get the total value locked (TVL) in a vault, calculated as total shares × share value.

async function getVaultTVL() {
  const vaultId = 1;
  
  try {
    const totalAssets = await vaultSDK.fetchTotalAssets(vaultId);
    
    console.log(`Vault TVL: ${totalAssets} base asset units`);
    console.log(`Total value locked: ${totalAssets}`);
    
    // You can also verify the calculation manually
    const shareSupply = await vaultSDK.fetchShareMintSupply(vaultId);
    const shareValue = await vaultSDK.fetchShareValue(vaultId);
    const calculatedTVL = shareSupply * shareValue;
    
    console.log(`Manual calculation: ${shareSupply} × ${shareValue} = ${calculatedTVL}`);
    console.log(`Direct fetch: ${totalAssets}`);
    
    return totalAssets;
  } catch (error) {
    console.error('Error fetching total assets:', error);
    return 0;
  }
}

Depositing Assets

SPL Token Deposits

import { web3 } from '@coral-xyz/anchor';

async function depositTokens() {
  // Your wallet (can be wallet adapter or keypair)
  const wallet = {
    publicKey: new web3.PublicKey('user_wallet_address'),
    signTransaction: async (tx: web3.Transaction) => {
      // Wallet adapter signing logic
      return signedTransaction;
    }
  };
  
  // Or use a keypair directly
  // const wallet = web3.Keypair.fromSecretKey(secretKey);
  
  const vaultId = 1;
  const tokenMint = 'J1toso1uCk3RLmjorhTtrVwY9HJ7X8V9yYac6Y7kGCPn'; // jitoSOL
  const depositAmount = BigInt('1000000000'); // 1 token (with 9 decimals)
  const minMintAmount = BigInt('950000000');  // 5% slippage tolerance
  
  try {
    const signature = await vaultSDK.deposit(
      wallet,
      vaultId,
      tokenMint,
      depositAmount,
      minMintAmount,
      {
        skipPreflight: false,
        maxRetries: 30
      }
    );
    
    console.log(`Deposit successful! Transaction: ${signature}`);
    console.log(`View on explorer: https://solscan.io/tx/${signature}`);
    
    return signature;
  } catch (error) {
    console.error('Deposit failed:', error);
    throw error;
  }
}

Native SOL Deposits

async function depositSOL() {
  const wallet = /* your wallet */;
  const vaultId = 1;
  const depositAmount = BigInt('1000000000'); // 1 SOL in lamports
  const minMintAmount = BigInt('950000000');  // 5% slippage tolerance
  
  try {
    const signature = await vaultSDK.depositSol(
      wallet,
      vaultId,
      depositAmount,
      minMintAmount,
      {
        skipPreflight: false,
        maxRetries: 30
      }
    );
    
    console.log(`SOL deposit successful! Transaction: ${signature}`);
    console.log(`View on explorer: https://solscan.io/tx/${signature}`);
    
    return signature;
  } catch (error) {
    console.error('SOL deposit failed:', error);
    throw error;
  }
}

Queued Withdrawals

The SDK supports queued withdrawals through the Boring Queue system, allowing users to request withdrawals that can be fulfilled by external solvers at a discount.

async function queueWithdrawal() {
  const wallet = /* your wallet */;
  const vaultId = 1;
  const tokenOut = 'J1toso1uCk3RLmjorhTtrVwY9HJ7X8V9yYac6Y7kGCPn'; // jitoSOL
  const shareAmount = 0.5; // Human-readable amount (0.5 shares)
  const discountPercent = 2.5; // 2.5% discount for solvers
  const secondsToDeadline = 86400 * 7; // 7 days
  
  try {
    const signature = await vaultSDK.queueBoringWithdraw(
      wallet,
      vaultId,
      tokenOut,
      shareAmount,
      discountPercent,
      secondsToDeadline,
      {
        skipPreflight: false,
        maxRetries: 30
      }
    );
    
    console.log(`Withdrawal queued! Transaction: ${signature}`);
    console.log(`View on explorer: https://solscan.io/tx/${signature}`);
    
    return signature;
  } catch (error) {
    console.error('Queue withdrawal failed:', error);
    throw error;
  }
}

BoringVaultSolana (Low-Level API)

For advanced use cases, you can access the low-level BoringVaultSolana class directly.

Creating a BoringVaultSolana Instance

import { VaultSDK } from 'boring-vault-ui/solana';

// Access the low-level BoringVaultSolana instance
const vaultSDK = new VaultSDK('mainnet');
const boringVault = vaultSDK.getBoringVault();

// Or create directly with custom configuration
import { BoringVaultSolana } from 'boring-vault-ui/solana';
import { createSolanaClient } from 'gill';

const boringVault = new BoringVaultSolana({
  solanaClient: createSolanaClient({ urlOrMoniker: 'mainnet' }),
  programId: '5ZRnXG4GsUMLaN7w2DtJV1cgLgcXHmuHCmJ2MxoorWCE'
});

User Share Balances

async function getUserShares() {
  const walletAddress = 'user_wallet_address_here';
  const vaultId = 1;
  
  try {
    const userShares = await vaultSDK.fetchUserShares(walletAddress, vaultId);
    
    console.log(`User owns ${userShares} shares in vault ${vaultId}`);
    
    return userShares;
  } catch (error) {
    console.error('Error fetching user shares:', error);
    return 0;
  }
}

Building Transactions

SPL Token Deposit Transaction

async function buildDepositTx() {
  const payerPublicKey = new web3.PublicKey('payer_address');
  const vaultId = 1;
  const depositMint = new web3.PublicKey('J1toso1uCk3RLmjorhTtrVwY9HJ7X8V9yYac6Y7kGCPn');
  const depositAmount = BigInt('1000000000');
  const minMintAmount = BigInt('950000000');
  
  try {
    const transaction = await boringVault.buildDepositTransaction(
      payerPublicKey,
      vaultId,
      depositMint,
      depositAmount,
      minMintAmount
    );
    
    console.log('Transaction built successfully');
    return transaction;
  } catch (error) {
    console.error('Error building transaction:', error);
    throw error;
  }
}

SOL Deposit Transaction

async function buildSOLDepositTx() {
  const payerPublicKey = new web3.PublicKey('payer_address');
  const vaultId = 1;
  const depositAmount = BigInt('1000000000'); // 1 SOL
  const minMintAmount = BigInt('950000000');
  
  try {
    const transaction = await boringVault.buildDepositSolTransaction(
      payerPublicKey,
      vaultId,
      depositAmount,
      minMintAmount
    );
    
    console.log('SOL deposit transaction built successfully');
    return transaction;
  } catch (error) {
    console.error('Error building SOL deposit transaction:', error);
    throw error;
  }
}

PDA Derivations

// Get vault state PDA
const vaultStatePDA = await boringVault.getVaultStatePDA(vaultId);

// Get vault PDA for specific sub-account
const vaultPDA = await boringVault.getVaultPDA(vaultId, subAccount);

// Get share token mint PDA
const shareMintPDA = await boringVault.getShareTokenPDA(vaultStatePDA);

// Get asset data PDA
const assetDataPDA = await boringVault.getAssetDataPDA(vaultStatePDA, assetMint);

// Queue-related PDAs
const queueStatePDA = await boringVault.getQueueStatePDA(vaultId);
const queuePDA = await boringVault.getQueuePDA(vaultId);
const userWithdrawStatePDA = await boringVault.getUserWithdrawStatePDA(userPublicKey);

Working with Keypairs

For testing or administrative operations, you may need to load keypairs from Solana CLI format:

import { createKeyPairSignerFromPrivateKeyBytes } from 'gill';
import * as fs from 'fs';

async function loadKeypair(keypairPath: string) {
  try {
    const keyData = JSON.parse(fs.readFileSync(keypairPath, 'utf-8'));
    const secretKey = new Uint8Array(keyData);
    
    // Extract private key bytes (first 32 bytes)
    const privateKeyBytes = secretKey.slice(0, 32);
    
    // Create keypair signer
    const keypairSigner = await createKeyPairSignerFromPrivateKeyBytes(privateKeyBytes);
    
    console.log(`Loaded keypair: ${keypairSigner.address}`);
    return keypairSigner;
  } catch (error) {
    console.error('Failed to load keypair:', error);
    throw error;
  }
}

Input Validation

The SDK automatically validates input parameters:

  • Deposit amounts must be positive non-zero values
  • Minimum mint amounts must be positive non-zero values
  • Discount percentages must be between 0% and 5%
  • Share amounts must be greater than 0
  • Deadline seconds must be at least 1 hour
// These will throw validation errors:
await vaultSDK.deposit(wallet, vaultId, mint, BigInt(0), minAmount); // ❌ Zero deposit
await vaultSDK.depositSol(wallet, vaultId, BigInt(-1), minAmount); // ❌ Negative amount
await vaultSDK.queueBoringWithdraw(wallet, vaultId, tokenOut, 0, 10); // ❌ 10% discount too high

Error Handling

The SDK provides comprehensive error handling with detailed messages:

try {
  await vaultSDK.deposit(/* ... */);
} catch (error) {
  if (error.message.includes('Invalid depositAmount')) {
    console.error('Deposit amount validation failed');
  } else if (error.message.includes('Asset not allowed')) {
    console.error('This asset is not supported for deposits');
  } else if (error.message.includes('Vault paused')) {
    console.error('Vault is currently paused');
  } else {
    console.error('Unexpected error:', error);
  }
}

Complete Example

Here's a comprehensive example showing common vault operations:

import { VaultSDK } from 'boring-vault-ui/solana';
import { web3 } from '@coral-xyz/anchor';

async function vaultOperationsExample() {
  // Initialize SDK
  const vaultSDK = new VaultSDK('mainnet');
  const vaultAddress = new web3.PublicKey('your_vault_address_here');
  const vaultId = 1;
  
  // Your wallet setup
  const wallet = /* your wallet */;
  
  try {
    // 1. Get vault information
    console.log('=== Fetching Vault Data ===');
    const vaultData = await vaultSDK.getVaultData(vaultAddress);
    console.log(`Vault ID: ${vaultData.vaultState.vaultId}`);
    console.log(`Authority: ${vaultData.vaultState.authority}`);
    console.log(`Paused: ${vaultData.vaultState.paused}`);
    
    // 2. Check vault balance
    console.log('\n=== Checking Vault Balance ===');
    const vaultBalance = await vaultSDK.getVaultBalance(vaultAddress);
    console.log(`Vault Balance: ${vaultBalance} lamports`);
    
    // 3. Check user share balance
    console.log('\n=== Checking User Shares ===');
    const userShares = await vaultSDK.fetchUserShares(
      wallet.publicKey.toString(),
      vaultId
    );
    console.log(`User Shares: ${userShares}`);
    
    // 4. Get share value and total assets
    console.log('\n=== Checking Share Value & TVL ===');
    const shareValue = await vaultSDK.fetchShareValue(vaultId);
    const totalAssets = await vaultSDK.fetchTotalAssets(vaultId);
    console.log(`Share Value: ${shareValue} base asset units`);
    console.log(`Total Assets (TVL): ${totalAssets} base asset units`);
    
    // 5. Deposit SOL
    console.log('\n=== Depositing SOL ===');
    const depositAmount = BigInt('100000000'); // 0.1 SOL
    const minMintAmount = BigInt('95000000');  // 5% slippage
    
    const depositSignature = await vaultSDK.depositSol(
      wallet,
      vaultId,
      depositAmount,
      minMintAmount
    );
    console.log(`SOL Deposit Success: ${depositSignature}`);
    
    // 6. Queue withdrawal
    console.log('\n=== Queuing Withdrawal ===');
    const withdrawSignature = await vaultSDK.queueBoringWithdraw(
      wallet,
      vaultId,
      'J1toso1uCk3RLmjorhTtrVwY9HJ7X8V9yYac6Y7kGCPn', // jitoSOL
      0.05, // 0.05 shares
      1.0,  // 1% discount
      86400 * 7 // 7 days deadline
    );
    console.log(`Withdrawal Queued: ${withdrawSignature}`);
    
  } catch (error) {
    console.error('Operation failed:', error);
  }
}

Constants

The SDK includes useful constants for Solana operations:

import {
  BORING_VAULT_PROGRAM_ID,
  BORING_QUEUE_PROGRAM_ID,
  JITO_SOL_MINT_ADDRESS,
  TOKEN_2022_PROGRAM_ID,
  DEFAULT_DECIMALS
} from 'boring-vault-ui/solana';

console.log('Boring Vault Program:', BORING_VAULT_PROGRAM_ID);
console.log('Boring Queue Program:', BORING_QUEUE_PROGRAM_ID);
console.log('JitoSOL Mint:', JITO_SOL_MINT_ADDRESS);
console.log('Default Decimals:', DEFAULT_DECIMALS);

Transaction Options

All transaction methods support the following options:

interface TransactionOptions {
  skipPreflight?: boolean;    // Skip transaction simulation (default: false)
  maxRetries?: number;        // Maximum retry attempts (default: 30)
  skipStatusCheck?: boolean;  // Skip status polling (default: false)
}

Best Practices

  1. Always validate inputs - The SDK validates automatically, but check business logic too
  2. Handle errors gracefully - Network issues and program errors can occur
  3. Use appropriate slippage - 5% is usually safe for most operations
  4. Monitor transactions - Use the provided explorer links to track status
  5. Test on devnet first - Always test new integrations on devnet
  6. Reserve SOL for fees - Keep ~0.01 SOL for transaction fees when depositing SOL
  7. Check vault status - Ensure vaults aren't paused before attempting operations

Updating price feeds

To guarantee the freshest JitoSOL/SOL price — crank the Pyth oracle just before sending your state-changing transaction:

import { buildPythOracleCrankTransactions, JITOSOL_SOL_PYTH_FEED } from 'boring-vault-ui/solana';

const { transactions, signers } = await buildPythOracleCrankTransactions(
  connection,
  payer.publicKey,
  [JITOSOL_SOL_PYTH_FEED]
);

for (let i = 0; i < transactions.length; i++) {
  transactions[i].sign(...signers[i], keypair);
  await connection.sendRawTransaction(transactions[i].serialize());
}

User Share Management

fetchUserShares

This function provides the decimal adjusted (human readable) numerical value of vault shares that a user owns in their account.

Inputs

  • userAddress: The address of the user in the vault you'd like to get the shares for
  • vaultId: The vault ID to check shares for

Outputs

A promise that returns the decimal adjusted (human readable) total numerical value of all shares of a vault a user owns as a number.

Example

import { VaultSDK } from 'boring-vault-ui/solana';

const vaultSDK = new VaultSDK('https://api.mainnet-beta.solana.com');

async function checkUserShares() {
  const userAddress = 'YourWalletAddressHere';
  const vaultId = 1;
  
  try {
    const userShares = await vaultSDK.fetchUserShares(userAddress, vaultId);
    
    console.log(`User owns ${userShares} shares in vault ${vaultId}`);
    
    if (userShares > 0) {
      console.log('User has shares in this vault');
    } else {
      console.log('User has no shares in this vault');
    }
    
    return userShares;
  } catch (error) {
    console.error('Error fetching user shares:', error);
    return 0;
  }
}

fetchShareValue

This function retrieves the value for 1 share of the vault in terms of the underlying baseAsset.

Inputs

  • vaultId: The vault ID to get the share value for

Outputs

A promise that returns the decimal adjusted (human readable) numerical value for 1 share in terms of the underlying baseAsset as a number.

Example

async function checkShareValue() {
  const vaultId = 1;
  
  try {
    const shareValue = await vaultSDK.fetchShareValue(vaultId);
    
    console.log(`1 share = ${shareValue} base asset units`);
    console.log(`Current exchange rate: ${shareValue}`);
    
    return shareValue;
  } catch (error) {
    console.error('Error fetching share value:', error);
    return 0;
  }
}

fetchTotalAssets

This function retrieves a vault's TVL in terms of the baseAsset of the vault. Calculated as total shares × share value.

Inputs

  • vaultId: The vault ID to get the total assets for

Outputs

A promise that returns the decimal adjusted (human readable) total asset numerical value of the vault (aka TVL) in terms of the baseAsset as a number.

Example

async function checkVaultTVL() {
  const vaultId = 1;
  
  try {
    const totalAssets = await vaultSDK.fetchTotalAssets(vaultId);
    
    console.log(`Vault TVL: ${totalAssets} base asset units`);
    
    return totalAssets;
  } catch (error) {
    console.error('Error fetching total assets:', error);
    return 0;
  }
}

Withdraw Status Tracking

boringQueueStatuses

This function retrieves a list of all NON-EXPIRED withdraw intents for a user. It's designed to be compatible with the EVM version of the Boring Vault API.

Inputs

  • userAddress: The user's wallet address (string or PublicKey)
  • vaultId: (Optional) vault ID to filter by - only returns requests for this specific vault

Outputs

A promise that returns a list of BoringQueueStatus objects with the following properties:

PropertyTypeDescription
noncenumberThe nonce/ID of the withdraw request
userstringThe user's wallet address
tokenOutTokenMetadataThe output token information
tokenOut.addressstringToken mint address
tokenOut.decimalsnumberDecimal precision of the token
sharesWithdrawingnumberHuman-readable number of shares being withdrawn
assetsWithdrawingnumberHuman-readable number of tokens being withdrawn
creationTimenumberUnix seconds when the request was made
secondsToMaturitynumberSeconds until the request can be fulfilled
secondsToDeadlinenumberSeconds after maturity until the request expires
errorCodenumberError code (always 0 on Solana - valid requests only)
transactionHashOpenedstringTransaction hash (empty string on Solana)

Features

  • Performance Optimized: Fetches only the latest 7 requests by default
  • Automatic Filtering: Only returns non-expired requests
  • Token Metadata: Automatically fetches token decimals and addresses
  • Human-Readable: Converts raw amounts to properly formatted numbers
  • Vault Filtering: Optional vault-specific filtering

Example

import { VaultSDK, type BoringQueueStatus } from 'boring-vault-ui/solana';

const vaultSDK = new VaultSDK('https://api.mainnet-beta.solana.com');

async function checkWithdrawStatus() {
  const userAddress = 'YourWalletAddressHere';
  
  try {
    // Get all non-expired withdraw requests for a user
    const statuses: BoringQueueStatus[] = await vaultSDK.boringQueueStatuses(userAddress);
    
    console.log(`Found ${statuses.length} pending withdraw requests:`);
    
    statuses.forEach((status, index) => {
      console.log(`\nRequest ${index + 1}:`);
      console.log(`  - Nonce: ${status.nonce}`);
      console.log(`  - Token: ${status.tokenOut.address}`);
      console.log(`  - Shares: ${status.sharesWithdrawing}`);
      console.log(`  - Assets: ${status.assetsWithdrawing}`);
      console.log(`  - Created: ${new Date(status.creationTime * 1000).toISOString()}`);
      console.log(`  - Maturity: ${status.secondsToMaturity} seconds`);
      console.log(`  - Deadline: ${status.secondsToDeadline} seconds`);
      
      // Check status
      if (status.secondsToMaturity === 0) {
        console.log(`  - Status: ✅ Ready to claim`);
      } else {
        console.log(`  - Status: ⏳ Maturing in ${Math.ceil(status.secondsToMaturity / 60)} minutes`);
      }
    });

    // Get withdraw requests for a specific vault only
    const vault1Statuses = await vaultSDK.boringQueueStatuses(userAddress, 1);
    console.log(`\nFound ${vault1Statuses.length} requests for vault 1`);
    
    return statuses;
  } catch (error) {
    console.error('Error fetching withdraw statuses:', error);
    return [];
  }
}

// Usage in React component
function WithdrawStatusComponent() {
  const [statuses, setStatuses] = useState<BoringQueueStatus[]>([]);
  const [loading, setLoading] = useState(true);
  
  useEffect(() => {
    async function fetchStatuses() {
      try {
        const vaultSDK = new VaultSDK('mainnet');
        const results = await vaultSDK.boringQueueStatuses(userWalletAddress);
        setStatuses(results);
      } catch (error) {
        console.error('Failed to fetch withdraw statuses:', error);
      } finally {
        setLoading(false);
      }
    }
    
    fetchStatuses();
  }, [userWalletAddress]);
  
  if (loading) return <div>Loading withdraw requests...</div>;
  
  return (
    <div>
      <h3>Pending Withdrawals ({statuses.length})</h3>
      {statuses.map((status) => (
        <div key={status.nonce}>
          <p>Withdrawing {status.sharesWithdrawing} shares</p>
          <p>Token: {status.tokenOut.address}</p>
          <p>Status: {status.secondsToMaturity === 0 ? 'Ready' : 'Maturing'}</p>
        </div>
      ))}
    </div>
  );
}