diff --git a/hooks/useGovernanceAssets.ts b/hooks/useGovernanceAssets.ts index 08bf8acdcb..33afc9cf20 100644 --- a/hooks/useGovernanceAssets.ts +++ b/hooks/useGovernanceAssets.ts @@ -206,6 +206,11 @@ export default function useGovernanceAssets() { name: 'Friktion: Deposit into Volt', isVisible: canUseAnyInstruction, }, + { + id: Instructions.WithdrawFromVolt, + name: 'Friktion: Withdraw from Volt', + isVisible: canUseAnyInstruction, + }, { id: Instructions.CreateSolendObligationAccount, name: 'Solend: Create Obligation Account', diff --git a/package.json b/package.json index e5d78dd825..5f0cd5c5b7 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,7 @@ "@cardinal/namespaces-components": "^2.5.0", "@emotion/react": "^11.1.5", "@emotion/styled": "^11.3.0", - "@friktion-labs/friktion-sdk": "^1.1.13", + "@friktion-labs/friktion-sdk": "^1.1.25", "@headlessui/react": "^1.4.2", "@heroicons/react": "^1.0.1", "@marinade.finance/marinade-ts-sdk": "^2.0.9", diff --git a/pages/dao/[symbol]/proposal/components/instructions/Friktion/FriktionDeposit.tsx b/pages/dao/[symbol]/proposal/components/instructions/Friktion/FriktionDeposit.tsx index 0759162e93..68f144a17e 100644 --- a/pages/dao/[symbol]/proposal/components/instructions/Friktion/FriktionDeposit.tsx +++ b/pages/dao/[symbol]/proposal/components/instructions/Friktion/FriktionDeposit.tsx @@ -16,7 +16,7 @@ import useGovernanceAssets from '@hooks/useGovernanceAssets' import { Governance } from '@solana/spl-governance' import { ProgramAccount } from '@solana/spl-governance' import GovernedAccountSelect from '../../GovernedAccountSelect' -import { getFriktionDepositInstruction } from '@utils/instructionTools' +import { getFriktionDepositInstruction } from '@utils/instructions/Friktion' import Select from '@components/inputs/Select' import { FriktionSnapshot, VoltSnapshot } from '@friktion-labs/friktion-sdk' diff --git a/pages/dao/[symbol]/proposal/components/instructions/Friktion/FriktionWithdraw.tsx b/pages/dao/[symbol]/proposal/components/instructions/Friktion/FriktionWithdraw.tsx new file mode 100644 index 0000000000..56948f5279 --- /dev/null +++ b/pages/dao/[symbol]/proposal/components/instructions/Friktion/FriktionWithdraw.tsx @@ -0,0 +1,193 @@ +import React, { useContext, useEffect, useState } from 'react' +import Input from '@components/inputs/Input' +import useRealm from '@hooks/useRealm' +import { getMintMinAmountAsDecimal } from '@tools/sdk/units' +import { PublicKey } from '@solana/web3.js' +import { precision } from '@utils/formatting' +import useWalletStore from 'stores/useWalletStore' +import { GovernedMultiTypeAccount } from '@utils/tokens' +import { + FriktionWithdrawForm, + UiInstruction, +} from '@utils/uiTypes/proposalCreationTypes' +import { NewProposalContext } from '../../../new' +import { getFriktionWithdrawSchema } from '@utils/validations' +import useGovernanceAssets from '@hooks/useGovernanceAssets' +import { Governance } from '@solana/spl-governance' +import { ProgramAccount } from '@solana/spl-governance' +import GovernedAccountSelect from '../../GovernedAccountSelect' +import { getFriktionWithdrawInstruction } from '@utils/instructions/Friktion' +import Select from '@components/inputs/Select' +import { FriktionSnapshot, VoltSnapshot } from '@friktion-labs/friktion-sdk' + +const FriktionWithdraw = ({ + index, + governance, +}: { + index: number + governance: ProgramAccount | null +}) => { + const connection = useWalletStore((s) => s.connection) + const wallet = useWalletStore((s) => s.current) + const { realmInfo } = useRealm() + const { governedTokenAccountsWithoutNfts } = useGovernanceAssets() + const shouldBeGoverned = index !== 0 && governance + const programId: PublicKey | undefined = realmInfo?.programId + const [form, setForm] = useState({ + amount: undefined, + governedTokenAccount: undefined, + voltVaultId: '', + depositTokenMint: undefined, + programId: programId?.toString(), + mintInfo: undefined, + }) + // eslint-disable-next-line @typescript-eslint/ban-types + const [friktionVolts, setFriktionVolts] = useState( + null + ) + const [governedAccount, setGovernedAccount] = useState< + ProgramAccount | undefined + >(undefined) + const [formErrors, setFormErrors] = useState({}) + const mintMinAmount = form.mintInfo + ? getMintMinAmountAsDecimal(form.mintInfo) + : 1 + const currentPrecision = precision(mintMinAmount) + const { handleSetInstructions } = useContext(NewProposalContext) + const handleSetForm = ({ propertyName, value }) => { + setFormErrors({}) + setForm({ ...form, [propertyName]: value }) + } + const setMintInfo = (value) => { + setForm({ ...form, mintInfo: value }) + } + const setAmount = (event) => { + const value = event.target.value + handleSetForm({ + value: value, + propertyName: 'amount', + }) + } + const validateAmountOnBlur = () => { + const value = form.amount + + handleSetForm({ + value: parseFloat( + Math.max( + Number(mintMinAmount), + Math.min(Number(Number.MAX_SAFE_INTEGER), Number(value)) + ).toFixed(currentPrecision) + ), + propertyName: 'amount', + }) + } + + async function getInstruction(): Promise { + return getFriktionWithdrawInstruction({ + schema, + form, + amount: form.amount ?? 0, + programId, + connection, + wallet, + setFormErrors, + }) + } + useEffect(() => { + // call for the mainnet friktion volts + const callfriktionRequest = async () => { + const response = await fetch( + 'https://friktion-labs.github.io/mainnet-tvl-snapshots/friktionSnapshot.json' + ) + const parsedResponse = (await response.json()) as FriktionSnapshot + setFriktionVolts(parsedResponse.allMainnetVolts as VoltSnapshot[]) + } + + callfriktionRequest() + }, []) + + useEffect(() => { + handleSetForm({ + propertyName: 'programId', + value: programId?.toString(), + }) + }, [realmInfo?.programId]) + useEffect(() => { + handleSetInstructions( + { governedAccount: governedAccount, getInstruction }, + index + ) + }, [form]) + useEffect(() => { + setGovernedAccount(form.governedTokenAccount?.governance) + setMintInfo(form.governedTokenAccount?.mint?.account) + }, [form.governedTokenAccount]) + const schema = getFriktionWithdrawSchema() + + return ( + <> + { + handleSetForm({ value, propertyName: 'governedTokenAccount' }) + }} + value={form.governedTokenAccount} + error={formErrors['governedTokenAccount']} + shouldBeGoverned={shouldBeGoverned} + governance={governance} + > + + + + ) +} + +export default FriktionWithdraw diff --git a/pages/dao/[symbol]/proposal/new.tsx b/pages/dao/[symbol]/proposal/new.tsx index ee0094df3a..4b6a658605 100644 --- a/pages/dao/[symbol]/proposal/new.tsx +++ b/pages/dao/[symbol]/proposal/new.tsx @@ -51,6 +51,7 @@ import WithdrawObligationCollateralAndRedeemReserveLiquidity from './components/ import SplTokenTransfer from './components/instructions/SplTokenTransfer' import VoteBySwitch from './components/VoteBySwitch' import FriktionDeposit from './components/instructions/Friktion/FriktionDeposit' +import FriktionWithdraw from './components/instructions/Friktion/FriktionWithdraw' import MakeChangePerpMarket from './components/instructions/Mango/MakeChangePerpMarket' import MakeAddOracle from './components/instructions/Mango/MakeAddOracle' import MakeAddSpotMarket from './components/instructions/Mango/MakeAddSpotMarket' @@ -277,6 +278,8 @@ const New = () => { ) case Instructions.DepositIntoVolt: return + case Instructions.WithdrawFromVolt: + return case Instructions.CreateSolendObligationAccount: return case Instructions.InitSolendObligationAccount: diff --git a/utils/instructions/Friktion/index.ts b/utils/instructions/Friktion/index.ts new file mode 100644 index 0000000000..98c503ff4c --- /dev/null +++ b/utils/instructions/Friktion/index.ts @@ -0,0 +1,413 @@ +import { + ConnectedVoltSDK, + FriktionSDK, + PendingDepositWithKey, + VoltSDK, +} from '@friktion-labs/friktion-sdk' +import { AnchorWallet } from '@friktion-labs/friktion-sdk/dist/cjs/src/miscUtils' +import { WSOL_MINT } from '@components/instructions/tools' +import Decimal from 'decimal.js' +import { serializeInstructionToBase64 } from '@solana/spl-governance' +import { + ASSOCIATED_TOKEN_PROGRAM_ID, + Token, + TOKEN_PROGRAM_ID, +} from '@solana/spl-token' +import { WalletAdapter } from '@solana/wallet-adapter-base' +import { + Account, + Keypair, + PublicKey, + TransactionInstruction, +} from '@solana/web3.js' + +import type { ConnectionContext } from 'utils/connection' +import { getATA } from '../../ataTools' +import { GovernedTokenAccount } from '../../tokens' +import { UiInstruction } from '../../uiTypes/proposalCreationTypes' +import { validateInstruction } from '@utils/instructionTools' +import BN from 'bn.js' + +export async function getFriktionDepositInstruction({ + schema, + form, + amount, + connection, + wallet, + setFormErrors, +}: { + schema: any + form: any + amount: number + programId: PublicKey | undefined + connection: ConnectionContext + wallet: WalletAdapter | undefined + setFormErrors: any +}): Promise { + const isValid = await validateInstruction({ schema, form, setFormErrors }) + let serializedInstruction = '' + const prerequisiteInstructions: TransactionInstruction[] = [] + const governedTokenAccount = form.governedTokenAccount as GovernedTokenAccount + const voltVaultId = new PublicKey(form.voltVaultId as string) + + const signers: Keypair[] = [] + if ( + isValid && + amount && + governedTokenAccount?.token?.publicKey && + governedTokenAccount?.token && + governedTokenAccount?.mint?.account && + governedTokenAccount?.governance && + wallet + ) { + const sdk = new FriktionSDK({ + provider: { + connection: connection.current, + wallet: (wallet as unknown) as AnchorWallet, + }, + }) + const cVoltSDK = new ConnectedVoltSDK( + connection.current, + wallet.publicKey as PublicKey, + await sdk.loadVoltByKey(voltVaultId), + undefined, + governedTokenAccount.governance.pubkey + ) + + const voltVault = cVoltSDK.voltVault + const vaultMint = cVoltSDK.voltVault.vaultMint + + //we find true receiver address if its wallet and we need to create ATA the ata address will be the receiver + const { currentAddress: receiverAddress, needToCreateAta } = await getATA({ + connection: connection, + receiverAddress: governedTokenAccount.governance.pubkey, + mintPK: vaultMint, + wallet, + }) + //we push this createATA instruction to transactions to create right before creating proposal + //we don't want to create ata only when instruction is serialized + if (needToCreateAta) { + prerequisiteInstructions.push( + Token.createAssociatedTokenAccountInstruction( + ASSOCIATED_TOKEN_PROGRAM_ID, // always ASSOCIATED_TOKEN_PROGRAM_ID + TOKEN_PROGRAM_ID, // always TOKEN_PROGRAM_ID + vaultMint, // mint + receiverAddress, // ata + governedTokenAccount.governance.pubkey, // owner of token account + wallet.publicKey! // fee payer + ) + ) + } + + let pendingDepositInfo + try { + const key = ( + await VoltSDK.findPendingDepositInfoAddress( + voltVaultId, + governedTokenAccount.governance.pubkey, + cVoltSDK.sdk.programs.Volt.programId + ) + )[0] + const acct = await cVoltSDK.sdk.programs.Volt.account.pendingDeposit.fetch( + key + ) + pendingDepositInfo = { + ...acct, + key: key, + } as PendingDepositWithKey + } catch (err) { + pendingDepositInfo = null + } + + if ( + pendingDepositInfo && + pendingDepositInfo.roundNumber.lt(voltVault.roundNumber) && + pendingDepositInfo?.numUnderlyingDeposited?.gtn(0) + ) { + prerequisiteInstructions.push( + await cVoltSDK.claimPending(receiverAddress) + ) + } + + let depositTokenAccountKey: PublicKey | null + + if (governedTokenAccount.isSol) { + const { currentAddress: receiverAddress, needToCreateAta } = await getATA( + { + connection: connection, + receiverAddress: governedTokenAccount.governance.pubkey, + mintPK: new PublicKey(WSOL_MINT), + wallet, + } + ) + if (needToCreateAta) { + prerequisiteInstructions.push( + Token.createAssociatedTokenAccountInstruction( + ASSOCIATED_TOKEN_PROGRAM_ID, // always ASSOCIATED_TOKEN_PROGRAM_ID + TOKEN_PROGRAM_ID, // always TOKEN_PROGRAM_ID + new PublicKey(WSOL_MINT), // mint + receiverAddress, // ata + governedTokenAccount.governance.pubkey, // owner of token account + wallet.publicKey! // fee payer + ) + ) + } + depositTokenAccountKey = receiverAddress + } else { + depositTokenAccountKey = governedTokenAccount.transferAddress! + } + + try { + let decimals = 9 + + if (!governedTokenAccount.isSol) { + const underlyingAssetMintInfo = await new Token( + connection.current, + governedTokenAccount.mint.publicKey, + TOKEN_PROGRAM_ID, + (null as unknown) as Account + ).getMintInfo() + decimals = underlyingAssetMintInfo.decimals + } + + const depositIx = governedTokenAccount.isSol + ? await cVoltSDK.depositWithTransfer( + new Decimal(amount), + depositTokenAccountKey, + receiverAddress, + governedTokenAccount.transferAddress!, + governedTokenAccount.governance.pubkey, + decimals + ) + : await cVoltSDK.deposit( + new Decimal(amount), + depositTokenAccountKey, + receiverAddress, + governedTokenAccount.governance.pubkey, + decimals + ) + + const governedAccountIndex = depositIx.keys.findIndex( + (k) => + k.pubkey.toString() === + governedTokenAccount.governance?.pubkey.toString() + ) + depositIx.keys[governedAccountIndex].isSigner = true + + serializedInstruction = serializeInstructionToBase64(depositIx) + } catch (e) { + if (e instanceof Error) { + throw new Error('Error: ' + e.message) + } + throw e + } + } + const obj: UiInstruction = { + serializedInstruction, + isValid, + governance: governedTokenAccount?.governance, + prerequisiteInstructions: prerequisiteInstructions, + signers, + shouldSplitIntoSeparateTxs: true, + } + return obj +} + +export async function getFriktionWithdrawInstruction({ + schema, + form, + amount, + connection, + wallet, + setFormErrors, +}: { + schema: any + form: any + amount: number + programId: PublicKey | undefined + connection: ConnectionContext + wallet: WalletAdapter | undefined + setFormErrors: any +}): Promise { + const isValid = await validateInstruction({ schema, form, setFormErrors }) + let serializedInstruction = '' + const prerequisiteInstructions: TransactionInstruction[] = [] + const governedTokenAccount = form.governedTokenAccount as GovernedTokenAccount + const voltVaultId = new PublicKey(form.voltVaultId as string) + const depositTokenMint = new PublicKey(form.depositTokenMint as string) + const signers: Keypair[] = [] + if ( + isValid && + amount && + governedTokenAccount?.token?.publicKey && + governedTokenAccount?.token && + governedTokenAccount?.mint?.account && + governedTokenAccount?.governance && + wallet + ) { + const sdk = new FriktionSDK({ + provider: { + connection: connection.current, + wallet: (wallet as unknown) as AnchorWallet, + }, + }) + const cVoltSDK = new ConnectedVoltSDK( + connection.current, + wallet.publicKey as PublicKey, + await sdk.loadVoltByKey(voltVaultId), + undefined, + governedTokenAccount.governance.pubkey + ) + + const voltVault = cVoltSDK.voltVault + const vaultMint = cVoltSDK.voltVault.vaultMint + + try { + let depositTokenDest: PublicKey | null + + if (governedTokenAccount.isSol) { + const { + currentAddress: receiverAddress, + needToCreateAta, + } = await getATA({ + connection: connection, + receiverAddress: governedTokenAccount.governance.pubkey, + mintPK: new PublicKey(WSOL_MINT), + wallet, + }) + if (needToCreateAta) { + prerequisiteInstructions.push( + Token.createAssociatedTokenAccountInstruction( + ASSOCIATED_TOKEN_PROGRAM_ID, // always ASSOCIATED_TOKEN_PROGRAM_ID + TOKEN_PROGRAM_ID, // always TOKEN_PROGRAM_ID + new PublicKey(WSOL_MINT), // mint + receiverAddress, // ata + governedTokenAccount.governance.pubkey, // owner of token account + wallet.publicKey! // fee payer + ) + ) + } + depositTokenDest = receiverAddress + } else { + depositTokenDest = governedTokenAccount.transferAddress! + } + + //we find true receiver address if its wallet and we need to create ATA the ata address will be the receiver + const { + currentAddress: vaultTokenAccount, + needToCreateAta, + } = await getATA({ + connection: connection, + receiverAddress: governedTokenAccount.governance.pubkey, + mintPK: vaultMint, + wallet, + }) + //we push this createATA instruction to transactions to create right before creating proposal + //we don't want to create ata only when instruction is serialized + if (needToCreateAta) { + prerequisiteInstructions.push( + Token.createAssociatedTokenAccountInstruction( + ASSOCIATED_TOKEN_PROGRAM_ID, // always ASSOCIATED_TOKEN_PROGRAM_ID + TOKEN_PROGRAM_ID, // always TOKEN_PROGRAM_ID + vaultMint, // mint + vaultTokenAccount, // ata + governedTokenAccount.governance.pubkey, // owner of token account + wallet.publicKey! // fee payer + ) + ) + } + + let pendingDepositInfo + try { + const key = ( + await VoltSDK.findPendingDepositInfoAddress( + voltVaultId, + governedTokenAccount.governance.pubkey, + cVoltSDK.sdk.programs.Volt.programId + ) + )[0] + const acct = await cVoltSDK.sdk.programs.Volt.account.pendingDeposit.fetch( + key + ) + pendingDepositInfo = { + ...acct, + key: key, + } as PendingDepositWithKey + } catch (err) { + pendingDepositInfo = null + } + + if ( + pendingDepositInfo && + pendingDepositInfo.roundNumber.lt(voltVault.roundNumber) && + pendingDepositInfo?.numUnderlyingDeposited?.gtn(0) + ) { + prerequisiteInstructions.push( + await cVoltSDK.claimPending(vaultTokenAccount) + ) + } + + let pendingWithdrawalInfo + + try { + const key = ( + await VoltSDK.findPendingWithdrawalInfoAddress( + voltVaultId, + governedTokenAccount.governance.pubkey, + cVoltSDK.sdk.programs.Volt.programId + ) + )[0] + const acct = await this.sdk.programs.Volt.account.pendingWithdrawal.fetch( + key + ) + pendingWithdrawalInfo = { + ...acct, + key: key, + } + } catch (err) { + pendingWithdrawalInfo = null + } + if ( + pendingWithdrawalInfo && + pendingWithdrawalInfo.roundNumber.lt(voltVault.roundNumber) && + pendingWithdrawalInfo?.numVoltRedeemed?.gtn(0) + ) { + prerequisiteInstructions.push( + await cVoltSDK.claimPendingWithdrawal(depositTokenDest) + ) + } + + const withdrawIx = await cVoltSDK.withdrawHumanAmount( + new BN(amount), + depositTokenMint, + vaultTokenAccount, + null, + depositTokenDest, + governedTokenAccount.governance.pubkey + ) + + const governedAccountIndex = withdrawIx.keys.findIndex( + (k) => + k.pubkey.toString() === + governedTokenAccount.governance?.pubkey.toString() + ) + withdrawIx.keys[governedAccountIndex].isSigner = true + + serializedInstruction = serializeInstructionToBase64(withdrawIx) + } catch (e) { + if (e instanceof Error) { + throw new Error('Error: ' + e.message) + } + throw e + } + } + const obj: UiInstruction = { + serializedInstruction, + isValid, + governance: governedTokenAccount?.governance, + prerequisiteInstructions: prerequisiteInstructions, + signers, + shouldSplitIntoSeparateTxs: true, + } + return obj +} diff --git a/utils/uiTypes/proposalCreationTypes.ts b/utils/uiTypes/proposalCreationTypes.ts index 25131f4a64..25a0b3f648 100644 --- a/utils/uiTypes/proposalCreationTypes.ts +++ b/utils/uiTypes/proposalCreationTypes.ts @@ -41,6 +41,15 @@ export interface FriktionDepositForm { mintInfo: MintInfo | undefined } +export interface FriktionWithdrawForm { + amount: number | undefined + governedTokenAccount: GovernedTokenAccount | undefined + voltVaultId: string + depositTokenMint: string | undefined + programId: string | undefined + mintInfo: MintInfo | undefined +} + export interface GrantForm { destinationAccount: string amount: number | undefined @@ -239,6 +248,7 @@ export enum Instructions { Clawback, CreateAssociatedTokenAccount, DepositIntoVolt, + WithdrawFromVolt, CreateSolendObligationAccount, InitSolendObligationAccount, DepositReserveLiquidityAndObligationCollateral, diff --git a/utils/validations.tsx b/utils/validations.tsx index 910ce867ff..c8a8f1495f 100644 --- a/utils/validations.tsx +++ b/utils/validations.tsx @@ -199,10 +199,6 @@ export const getFriktionDepositSchema = ({ form }) => { 'amount', 'Transfer amount must be less than the source account available amount', async function (val: number) { - const isNft = governedTokenAccount?.isNft - if (isNft) { - return true - } if (val && !form.governedTokenAccount) { return this.createError({ message: `Please select source account to validate the amount`, @@ -228,6 +224,13 @@ export const getFriktionDepositSchema = ({ form }) => { }) } +export const getFriktionWithdrawSchema = () => { + return yup.object().shape({ + governedTokenAccount: yup.object().required('Source account is required'), + amount: yup.number().typeError('Amount is required'), + }) +} + export const getTokenTransferSchema = ({ form, connection }) => { const governedTokenAccount = form.governedTokenAccount as GovernedTokenAccount return yup.object().shape({ diff --git a/yarn.lock b/yarn.lock index 22b72431b7..969f706a15 100644 --- a/yarn.lock +++ b/yarn.lock @@ -863,10 +863,10 @@ dotenv-expand "^6.0.1" yargs "^17.0.1" -"@friktion-labs/friktion-sdk@^1.1.13": - version "1.1.13" - resolved "https://registry.yarnpkg.com/@friktion-labs/friktion-sdk/-/friktion-sdk-1.1.13.tgz#59ba9c6ee268568f39964b5ae1e33ba76c0024fe" - integrity sha512-xmLTCqUPwdCrI3bFXCkcSgLWe/faPG5CefOx1XzSbndw3qrE8GvMD9MU/PwJ4va9s4bOClvPKR10szM6DKp54A== +"@friktion-labs/friktion-sdk@^1.1.25": + version "1.1.25" + resolved "https://registry.yarnpkg.com/@friktion-labs/friktion-sdk/-/friktion-sdk-1.1.25.tgz#9233d3252cf38f04dc8cf8e5afef5047507c9291" + integrity sha512-RMnXNngfizYipSLBE+1fpSoptL940NYZTDv9vlDP+YdDVOyvAWc06pa2KLdKFyXQg/3pvcRRkaM12/+FgJtCWg== dependencies: "@friktion-labs/entropy-client" "^1.0.0" "@oclif/command" "^1.8.16"