Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- 'use client';
- import { Program } from '@coral-xyz/anchor';
- import { useConnectorClient, useTransactionSigner } from '@solana/connector';
- import {
- ASSOCIATED_TOKEN_PROGRAM_ID,
- createAssociatedTokenAccountInstruction,
- createCloseAccountInstruction,
- getAssociatedTokenAddressSync,
- NATIVE_MINT,
- TOKEN_PROGRAM_ID,
- } from '@solana/spl-token';
- import { Connection, PublicKey, Transaction, type TransactionInstruction } from '@solana/web3.js';
- import { useMutation } from '@tanstack/react-query';
- import BN from 'bn.js';
- import { useCallback } from 'react';
- import {
- ORACLE_VAULT_DATA_SEED,
- SOLANA_PRICE_DENOMINATOR,
- VAULT_AUTHORITY_SEED,
- VAULT_DATA_SEED,
- YO_ORACLE_PROGRAM_ID,
- YOSOL_FEE_RECIPIENT,
- } from '@/lib/config/vaults/solana/config';
- import type { YoVault } from '@/lib/config/vaults/solana/yo-vault-sol';
- import yoVaultSolIdl from '@/lib/config/vaults/solana/yo-vault-sol.json';
- type SolanaVaultAddresses = {
- tokenMint: string; // vault.asset.address
- shareMint: string; // vault.shareAsset.address
- };
- type RedeemResult = {
- signature: string;
- confirmed: boolean;
- };
- const DENOMINATOR_BN = new BN(SOLANA_PRICE_DENOMINATOR.toString());
- /**
- * Preview redeem to determine if the redemption can be fulfilled immediately.
- * Matches solana-dapp's previewRedeem logic.
- */
- async function previewRedeem(
- program: Program<YoVault>,
- connection: Connection,
- shares: BN,
- tokenMint: PublicKey
- ): Promise<{ immediate: boolean; assets: BN; fee: BN }> {
- const vaultProgramId = program.programId;
- const [oraclePda] = PublicKey.findProgramAddressSync(
- [Buffer.from(ORACLE_VAULT_DATA_SEED), vaultProgramId.toBuffer()],
- YO_ORACLE_PROGRAM_ID
- );
- const [vaultDataPda] = PublicKey.findProgramAddressSync(
- [Buffer.from(VAULT_DATA_SEED)],
- vaultProgramId
- );
- const [vaultAuthorityPda] = PublicKey.findProgramAddressSync(
- [Buffer.from(VAULT_AUTHORITY_SEED)],
- vaultProgramId
- );
- const vaultTokenAccount = getAssociatedTokenAddressSync(tokenMint, vaultAuthorityPda, true);
- const [oracleAccount, vaultData, vaultBalanceInfo] = await Promise.all([
- connection.getAccountInfo(oraclePda),
- program.account.vaultData.fetch(vaultDataPda),
- connection.getTokenAccountBalance(vaultTokenAccount),
- ]);
- if (!oracleAccount?.data) {
- throw new Error('Could not fetch oracle vault data');
- }
- const oracleData = program.coder.accounts.decode('oracleVaultData', oracleAccount.data);
- const lastPrice: BN = oracleData.lastPrice;
- const pctRedemptionFee: number = vaultData.pctRedemptionFee;
- const totalPendingRedeems: BN = vaultData.totalPendingRedeems;
- const vaultBalance = new BN(vaultBalanceInfo.value.amount);
- // Compute assets and fee
- const assets = shares.mul(lastPrice).div(DENOMINATOR_BN);
- const fee = assets.mul(new BN(pctRedemptionFee)).div(DENOMINATOR_BN);
- const assetsAfterFee = assets.sub(fee);
- const available = vaultBalance.sub(totalPendingRedeems);
- return {
- immediate: available.gte(assetsAfterFee),
- assets: assetsAfterFee,
- fee,
- };
- }
- /**
- * Hook for redeeming shares from a Solana vault.
- *
- * Flow (matches solana-dapp):
- * 1. Check if tokenMint is NATIVE_MINT - if so, create WSOL ATA if needed
- * 2. Call vault requestRedeem instruction via Anchor
- * 3. Only unwrap WSOL if redemption is immediately fulfilled AND tokenMint is NATIVE_MINT
- *
- * @param addresses - Vault addresses from backend vault data
- * @param enabled - Whether the hook should be active (use false for EVM vaults)
- */
- export function useSolanaRedeem(addresses: SolanaVaultAddresses | null, enabled = true) {
- const { signer, ready, address: walletAddress } = useTransactionSigner();
- const client = useConnectorClient();
- const mutation = useMutation({
- mutationFn: async (sharesLamports: bigint): Promise<RedeemResult> => {
- if (!signer || !walletAddress) {
- throw new Error('Wallet not connected');
- }
- if (!addresses) {
- throw new Error('Vault addresses not available');
- }
- // Get RPC URL from connector client
- const rpcUrl = client?.getRpcUrl();
- if (!rpcUrl) {
- throw new Error('No RPC endpoint configured');
- }
- const connection = new Connection(rpcUrl, 'confirmed');
- const walletPubkey = new PublicKey(walletAddress);
- const sharesBN = new BN(sharesLamports.toString());
- const tokenMint = new PublicKey(addresses.tokenMint);
- const shareMint = new PublicKey(addresses.shareMint);
- const isNativeMint = tokenMint.equals(NATIVE_MINT);
- const program: Program<YoVault> = new Program(yoVaultSolIdl as YoVault, { connection });
- // Preview redeem to determine if immediate
- const { immediate } = await previewRedeem(program, connection, sharesBN, tokenMint);
- const ixs: TransactionInstruction[] = [];
- // Only handle WSOL ATA for native SOL vault
- if (isNativeMint) {
- const userWsolAccount = getAssociatedTokenAddressSync(NATIVE_MINT, walletPubkey);
- const ataInfo = await connection.getAccountInfo(userWsolAccount);
- if (!ataInfo) {
- ixs.push(
- createAssociatedTokenAccountInstruction(
- walletPubkey,
- userWsolAccount,
- walletPubkey,
- NATIVE_MINT,
- TOKEN_PROGRAM_ID,
- ASSOCIATED_TOKEN_PROGRAM_ID
- )
- );
- }
- }
- // Build redeem instruction via Anchor
- const redeemIx = await program.methods
- .requestRedeem(sharesBN)
- .accounts({
- user: walletPubkey,
- tokenMint,
- shareMint,
- feeRecipient: YOSOL_FEE_RECIPIENT,
- program: program.programId,
- })
- .instruction();
- ixs.push(redeemIx);
- // Only unwrap WSOL if immediate fulfillment AND native SOL vault
- if (isNativeMint && immediate) {
- const userWsolAccount = getAssociatedTokenAddressSync(NATIVE_MINT, walletPubkey);
- ixs.push(
- createCloseAccountInstruction(
- userWsolAccount,
- walletPubkey,
- walletPubkey,
- [],
- TOKEN_PROGRAM_ID
- )
- );
- }
- // Build and send transaction
- const tx = new Transaction().add(...ixs);
- const { blockhash, lastValidBlockHeight } = await connection.getLatestBlockhash();
- tx.recentBlockhash = blockhash;
- tx.feePayer = walletPubkey;
- tx.lastValidBlockHeight = lastValidBlockHeight;
- // Sign and send via ConnectorKit
- const signature = await signer.signAndSendTransaction(tx);
- return { signature, confirmed: true };
- },
- });
- const reset = useCallback(() => {
- mutation.reset();
- }, [mutation]);
- return {
- executeRedeem: mutation.mutate,
- executeRedeemAsync: mutation.mutateAsync,
- isPending: mutation.isPending,
- isConfirming: false,
- isConfirmed: !!mutation.data?.signature,
- error: mutation.error,
- txSignature: mutation.data?.signature,
- isReady: enabled && ready && !!signer,
- reset,
- };
- }
- export default useSolanaRedeem;
Advertisement