diff --git a/pages/dao/[symbol]/proposal/components/instructions/Meteora/CreatePosition.tsx b/pages/dao/[symbol]/proposal/components/instructions/Meteora/CreatePosition.tsx index 964b84e60..e60183558 100644 --- a/pages/dao/[symbol]/proposal/components/instructions/Meteora/CreatePosition.tsx +++ b/pages/dao/[symbol]/proposal/components/instructions/Meteora/CreatePosition.tsx @@ -1,54 +1,74 @@ -import React, { useContext, useEffect, useState } from 'react'; -import * as yup from 'yup'; -import BN from 'bn.js'; -import { ProgramAccount, serializeInstructionToBase64, Governance } from '@solana/spl-governance'; -import { validateInstruction } from '@utils/instructionTools'; -import { UiInstruction } from '@utils/uiTypes/proposalCreationTypes'; -import { PublicKey, Keypair, Connection } from '@solana/web3.js'; -import { NewProposalContext } from '../../../new'; -import InstructionForm, { InstructionInput } from '../FormCreator'; -import { InstructionInputType } from '../inputInstructionType'; -import useWalletOnePointOh from '@hooks/useWalletOnePointOh'; -import useGovernanceAssets from '@hooks/useGovernanceAssets'; -import DLMM from '@meteora-ag/dlmm'; -import { toStrategyParameters } from '@meteora-ag/dlmm'; -import { useConnection } from '@solana/wallet-adapter-react'; -import { MeteoraCreatePositionForm } from '@utils/uiTypes/proposalCreationTypes'; -import { StrategyParameters } from '@meteora-ag/dlmm'; +import React, { useContext, useEffect, useState } from 'react' +import * as yup from 'yup' +import BN from 'bn.js' +import { + ProgramAccount, + serializeInstructionToBase64, + Governance, +} from '@solana/spl-governance' +import { validateInstruction } from '@utils/instructionTools' +import { UiInstruction } from '@utils/uiTypes/proposalCreationTypes' +import { + PublicKey, + Keypair, + Connection, + ComputeBudgetProgram, +} from '@solana/web3.js' +import { NewProposalContext } from '../../../new' +import InstructionForm, { InstructionInput } from '../FormCreator' +import { InstructionInputType } from '../inputInstructionType' +import useWalletOnePointOh from '@hooks/useWalletOnePointOh' +import useGovernanceAssets from '@hooks/useGovernanceAssets' +import DLMM from '@meteora-ag/dlmm' +import { toStrategyParameters } from '@meteora-ag/dlmm' +import { useConnection } from '@solana/wallet-adapter-react' +import { MeteoraCreatePositionForm } from '@utils/uiTypes/proposalCreationTypes' +import { StrategyParameters } from '@meteora-ag/dlmm' const schema = yup.object().shape({ governedAccount: yup.object().required('Governed account is required'), dlmmPoolAddress: yup.string().required('DLMM pool address is required'), baseToken: yup.string().required('Base token is required'), - baseTokenAmount: yup.number().required('Base token amount is required').min(0), + baseTokenAmount: yup + .number() + .required('Base token amount is required') + .min(0), quoteToken: yup.string().required('Quote token is required'), - quoteTokenAmount: yup.number().required('Quote token amount is required').min(0), - strategy: yup.number().required('Strategy is required'), + quoteTokenAmount: yup + .number() + .required('Quote token amount is required') + .min(0), + strategy: yup.object().required('Strategy is required'), minPrice: yup.number().required('Min price is required').min(0), maxPrice: yup.number().required('Max price is required').min(0), - numBins: yup.number().required('Number of bins is required').min(1) -}); + numBins: yup.number().required('Number of bins is required').min(1), +}) const strategyOptions = [ - { name: 'Spot', value: 0, description: 'Provides a uniform distribution that is versatile and risk adjusted, suitable for any type of market and conditions. This is similar to setting a CLMM price range.' }, - { name: 'Curve', value: 1 }, - { name: 'Bid Ask', value: 2 }, -]; + { + name: 'Spot', + value: 6, + description: + 'Provides a uniform distribution that is versatile and risk adjusted, suitable for any type of market and conditions. This is similar to setting a CLMM price range.', + }, + { name: 'Curve', value: 7 }, + { name: 'Bid Ask', value: 8 }, +] const DLMMCreatePosition = ({ index, governance, }: { - index: number; - governance: ProgramAccount | null; + index: number + governance: ProgramAccount | null }) => { - const { assetAccounts } = useGovernanceAssets(); - const wallet = useWalletOnePointOh(); - const connected = !!wallet?.connected; - const { connection } = useConnection(); - const [formErrors, setFormErrors] = useState>({}); - const { handleSetInstructions } = useContext(NewProposalContext); - const shouldBeGoverned = !!(index !== 0 && governance); + const { assetAccounts } = useGovernanceAssets() + const wallet = useWalletOnePointOh() + const connected = !!wallet?.connected + const { connection } = useConnection() + const [formErrors, setFormErrors] = useState>({}) + const { handleSetInstructions } = useContext(NewProposalContext) + const shouldBeGoverned = !!(index !== 0 && governance) const [form, setForm] = useState({ governedAccount: undefined, dlmmPoolAddress: '', @@ -56,125 +76,158 @@ const DLMMCreatePosition = ({ baseTokenAmount: 0, quoteToken: '', quoteTokenAmount: 0, - strategy: 0, + strategy: { + name: 'Spot', + value: 0, + description: + 'Provides a uniform distribution that is versatile and risk adjusted, suitable for any type of market and conditions. This is similar to setting a CLMM price range.', + }, minPrice: 0, maxPrice: 0, numBins: 69, autoFill: false, - positionPubkey: '', + // positionPubkey: '', description: '', - binStep: 0 - }); + binStep: 0, + }) - const getInstruction = async (): Promise => { - const isValid = await validateInstruction({ schema, form, setFormErrors }); - if (!isValid || !form?.governedAccount?.governance?.account || !wallet?.publicKey || !connected) { - return { serializedInstruction: '', isValid: false, governance: form?.governedAccount?.governance }; + const getInstruction = async (): Promise => { + const isValid = await validateInstruction({ schema, form, setFormErrors }) + if ( + !isValid || + !form?.governedAccount?.governance?.account || + !wallet?.publicKey || + !connected + ) { + return { + serializedInstruction: '', + isValid: false, + governance: form?.governedAccount?.governance, + } } try { - const dlmmPoolPk = new PublicKey(form.dlmmPoolAddress); - const dlmmPool = await DLMM.create(connection, dlmmPoolPk); - await dlmmPool.refetchStates(); + const dlmmPoolPk = new PublicKey(form.dlmmPoolAddress) + const dlmmPool = await DLMM.create(connection, dlmmPoolPk) + // await dlmmPool.refetchStates() // Get active bin and calculate range - const activeBin = await dlmmPool.getActiveBin(); - console.log('Active bin:', activeBin); + const activeBin = await dlmmPool.getActiveBin() // Calculate bin IDs based on prices - let minBinId: number, maxBinId: number; - + let minBinId: number, maxBinId: number + // Use binStep from form state instead of dlmmPool.state - const binStep = form.binStep; - + const binStep = dlmmPool?.lbPair?.binStep + if (!binStep) { - throw new Error('Bin step not available'); + throw new Error('Bin step not available') } if (form.autoFill) { - const TOTAL_RANGE_INTERVAL = 10; - minBinId = activeBin.binId - TOTAL_RANGE_INTERVAL; - maxBinId = activeBin.binId + TOTAL_RANGE_INTERVAL; + const TOTAL_RANGE_INTERVAL = 25 + minBinId = activeBin.binId - TOTAL_RANGE_INTERVAL + maxBinId = activeBin.binId + TOTAL_RANGE_INTERVAL } else { - minBinId = Math.floor(Math.log(form.minPrice) / Math.log(1 + binStep/10000)); - maxBinId = Math.ceil(Math.log(form.maxPrice) / Math.log(1 + binStep/10000)); - + minBinId = Math.floor( + Math.log(form.minPrice) / Math.log(1 + binStep / 10000), + ) + maxBinId = Math.ceil( + Math.log(form.maxPrice) / Math.log(1 + binStep / 10000), + ) + // Calculate and update number of bins based on price range - const calculatedNumBins = maxBinId - minBinId + 1; - + const calculatedNumBins = maxBinId - minBinId + 1 + // Update form with calculated number of bins - setForm(prev => ({ + setForm((prev) => ({ ...prev, - numBins: calculatedNumBins - })); - + numBins: calculatedNumBins, + })) + // Validate bin range (can keep this as a safety check) - if (calculatedNumBins > 69) { // Max bins allowed - throw new Error('Price range too large - exceeds maximum allowed bins (69)'); + if (calculatedNumBins > 69) { + // Max bins allowed + throw new Error( + 'Price range too large - exceeds maximum allowed bins (69)', + ) } } // Get actual prices from bin IDs for validation using the correct bin step - const minBinPrice = (1 + binStep/10000) ** minBinId; - const maxBinPrice = (1 + binStep/10000) ** maxBinId; - console.log(`Price Range: ${minBinPrice} - ${maxBinPrice}`); + const minBinPrice = (1 + binStep / 10000) ** minBinId + const maxBinPrice = (1 + binStep / 10000) ** maxBinId + console.log(`Price Range: ${minBinPrice} - ${maxBinPrice}`) // Convert amounts to BN directly - const totalXAmount = new BN(form.baseTokenAmount); - const totalYAmount = new BN(form.quoteTokenAmount); + const totalXAmount = new BN(form.baseTokenAmount) + const totalYAmount = new BN(form.quoteTokenAmount) // Generate position keypair - const positionKeypair = Keypair.generate(); + const positionKeypair = Keypair.generate() // Create the position transaction - const createPositionTx = await dlmmPool.initializePositionAndAddLiquidityByStrategy({ - positionPubKey: positionKeypair.publicKey, - user: form.governedAccount?.governance.pubkey, - totalXAmount, - totalYAmount, - strategy: { - maxBinId, - minBinId, - strategyType: form.strategy, - }, - }); - - const txArray = Array.isArray(createPositionTx) ? createPositionTx : [createPositionTx]; + const createPositionTx = + await dlmmPool.initializePositionAndAddLiquidityByStrategy({ + positionPubKey: positionKeypair.publicKey, + user: wallet?.publicKey, + totalXAmount, + slippage: 5, + totalYAmount, + strategy: { + maxBinId, + minBinId, + strategyType: form.strategy.value, + }, + }) + + // Filter out compute budget program instructions, they get added later in the dryRun. + const filteredInstructions = createPositionTx.instructions.filter( + (ix) => !ix.programId.equals(ComputeBudgetProgram.programId), + ) + + createPositionTx.instructions = filteredInstructions + + const txArray = Array.isArray(createPositionTx) + ? createPositionTx + : [createPositionTx] if (txArray.length === 0) { - throw new Error('No transactions returned by create position.'); + throw new Error('No transactions returned by create position.') } - const primaryInstructions = txArray[0].instructions; + const primaryInstructions = txArray[0].instructions if (primaryInstructions.length === 0) { - throw new Error('No instructions in the create position transaction.'); + throw new Error('No instructions in the create position transaction.') } // Set the primary instruction as the first one - const serializedInstruction = serializeInstructionToBase64(primaryInstructions[0]); - + // const serializedInstruction = '' + // Add any remaining instructions as additional instructions - const additionalSerializedInstructions = primaryInstructions.slice(1).map( - instruction => serializeInstructionToBase64(instruction) - ); + const additionalSerializedInstructions = primaryInstructions.map( + (instruction) => serializeInstructionToBase64(instruction), + ) return { - serializedInstruction, + serializedInstruction: '', additionalSerializedInstructions, isValid: true, governance: form?.governedAccount?.governance, signers: [positionKeypair], - }; - + } } catch (err) { - console.error('Error building create position instruction:', err); + console.error('Error building create position instruction:', err) setFormErrors((prev) => ({ ...prev, general: 'Error building create position instruction: ' + err.message, - })); - return { serializedInstruction: '', isValid: false, governance: form?.governedAccount?.governance }; + })) + return { + serializedInstruction: '', + isValid: false, + governance: form?.governedAccount?.governance, + } } - - }; + } const inputs: InstructionInput[] = [ { @@ -255,45 +308,52 @@ const DLMMCreatePosition = ({ initialValue: form.autoFill, name: 'autoFill', type: InstructionInputType.SWITCH, - } - ]; + }, + ] useEffect(() => { - handleSetInstructions({ governedAccount: form.governedAccount?.governance, getInstruction }, index); - }, [form, handleSetInstructions, index]); + handleSetInstructions( + { governedAccount: form.governedAccount?.governance, getInstruction }, + index, + ) + }, [form, handleSetInstructions, index]) useEffect(() => { const fetchPoolData = async () => { - if (!form.dlmmPoolAddress) return; + if (!form.dlmmPoolAddress) return try { - const uri = `https://dlmm-api.meteora.ag/pair/${form.dlmmPoolAddress}`; - const response = await fetch(uri); - if (!response.ok) throw new Error('Failed to fetch pool data'); + const uri = `https://dlmm-api.meteora.ag/pair/${form.dlmmPoolAddress}` + const response = await fetch(uri) + if (!response.ok) throw new Error('Failed to fetch pool data') - const data = await response.json(); + const data = await response.json() const parsePairs = (pairs: string) => { - const [quoteToken, baseToken] = pairs.split('-').map((pair: string) => pair.trim()); - return { quoteToken, baseToken }; - }; - const { quoteToken, baseToken } = parsePairs(data.name); - const binStep = data.binStep; // Get binStep from API response + const [quoteToken, baseToken] = pairs + .split('-') + .map((pair: string) => pair.trim()) + return { quoteToken, baseToken } + } + const { quoteToken, baseToken } = parsePairs(data.name) + const binStep = data.binStep // Get binStep from API response setForm((prevForm) => ({ ...prevForm, baseToken, quoteToken, binStep, // Store binStep in form state - })); + })) - console.log(`Updated pool data - baseToken: ${baseToken}, quoteToken: ${quoteToken}, binStep: ${binStep}`); + console.log( + `Updated pool data - baseToken: ${baseToken}, quoteToken: ${quoteToken}, binStep: ${binStep}`, + ) } catch (error) { - console.error('Error fetching pool data:', error); + console.error('Error fetching pool data:', error) } - }; + } - fetchPoolData(); - }, [form.dlmmPoolAddress]); + fetchPoolData() + }, [form.dlmmPoolAddress]) return ( - ); -}; + ) +} -export default DLMMCreatePosition; +export default DLMMCreatePosition diff --git a/utils/uiTypes/proposalCreationTypes.ts b/utils/uiTypes/proposalCreationTypes.ts index 965a5cbfa..0856146a0 100644 --- a/utils/uiTypes/proposalCreationTypes.ts +++ b/utils/uiTypes/proposalCreationTypes.ts @@ -122,7 +122,8 @@ export interface ClawbackForm { holdupTime: number } -export interface SendTokenCompactViewForm extends Omit { +export interface SendTokenCompactViewForm + extends Omit { destinationAccount: string[] amount: (number | undefined)[] txDollarAmount: (string | undefined)[] @@ -569,112 +570,114 @@ export interface DualFinanceVoteDepositForm { } export interface SymmetryCreateBasketForm { - governedAccount?: AssetAccount, - basketType: number, - basketName: string, - basketSymbol: string, - basketMetadataUrl: string, + governedAccount?: AssetAccount + basketType: number + basketName: string + basketSymbol: string + basketMetadataUrl: string basketComposition: { - name: string, - symbol: string, - token: PublicKey; - weight: number; - }[], - rebalanceThreshold: number, - rebalanceSlippageTolerance: number, - depositFee: number, - feeCollectorAddress:string, - liquidityProvision: boolean, - liquidityProvisionRange: number, + name: string + symbol: string + token: PublicKey + weight: number + }[] + rebalanceThreshold: number + rebalanceSlippageTolerance: number + depositFee: number + feeCollectorAddress: string + liquidityProvision: boolean + liquidityProvisionRange: number } - export interface SymmetryEditBasketForm { - governedAccount?: AssetAccount, - basketAddress?: PublicKey, - basketType: number, - basketName: string, - basketSymbol: string, - basketMetadataUrl: string, + governedAccount?: AssetAccount + basketAddress?: PublicKey + basketType: number + basketName: string + basketSymbol: string + basketMetadataUrl: string basketComposition: { - name: string, - symbol: string, - token: PublicKey; - weight: number; - }[], - rebalanceThreshold: number, - rebalanceSlippageTolerance: number, - depositFee: number, - feeCollectorAddress:string, - liquidityProvision: boolean, - liquidityProvisionRange: number, + name: string + symbol: string + token: PublicKey + weight: number + }[] + rebalanceThreshold: number + rebalanceSlippageTolerance: number + depositFee: number + feeCollectorAddress: string + liquidityProvision: boolean + liquidityProvisionRange: number } export interface SymmetryDepositForm { - governedAccount?: AssetAccount, - basketAddress?: PublicKey, - depositToken?: PublicKey, - depositAmount: number, + governedAccount?: AssetAccount + basketAddress?: PublicKey + depositToken?: PublicKey + depositAmount: number } export interface SymmetryWithdrawForm { - governedAccount?: AssetAccount, - basketAddress?: PublicKey, - withdrawAmount: number, + governedAccount?: AssetAccount + basketAddress?: PublicKey + withdrawAmount: number withdrawType: number } export interface CreateMeteoraPoolForm { - poolType: number; - baseTokenMint: string; - quoteTokenMint: string; - fee: number; - governedAccount?: string; + poolType: number + baseTokenMint: string + quoteTokenMint: string + fee: number + governedAccount?: string governedTokenAccount?: { governance: { - account: string; - }; - }; + account: string + } + } } - +export interface MeteoraStrategy { + name: string + value: number + description: string +} export interface MeteoraAddLiquidityForm { - governedAccount: AssetAccount | undefined; - dlmmPoolAddress: string; - positionPubkey: AssetAccount | undefined; - quoteToken: string; - baseToken: string; - strategy: number; + governedAccount: AssetAccount | undefined + dlmmPoolAddress: string + positionPubkey: AssetAccount | undefined + quoteToken: string + baseToken: string + strategy: number } export interface MeteoraRemoveLiquidityForm { - governedAccount: AssetAccount | undefined; - dlmmPoolAddress: string; - positionPubkey: string; - removeAll: boolean; + governedAccount: AssetAccount | undefined + dlmmPoolAddress: string + positionPubkey: string + removeAll: boolean } export interface MeteoraClaimRewardsForm { - governedAccount: AssetAccount | undefined; - dlmmPoolAddress: string; - rewards: string; + governedAccount: AssetAccount | undefined + dlmmPoolAddress: string + rewards: string } export interface MeteoraCreatePositionForm { - governedAccount: AssetAccount | undefined; - dlmmPoolAddress: string; - positionPubkey: string; - baseTokenAmount: number; - quoteTokenAmount: number; - quoteToken: string; - baseToken: string; - strategy: number; - minPrice: number; - maxPrice: number; - numBins: number; - autoFill: boolean; - description: string; - binStep: number; + governedAccount: AssetAccount | undefined + dlmmPoolAddress: string + // positionPubkey: string; + baseTokenAmount: number + quoteTokenAmount: number + quoteToken: string + baseToken: string + strategy: MeteoraStrategy + minPrice: number + maxPrice: number + numBins: number + autoFill: boolean + description: string + binStep: number } -