diff --git a/.browserslistrc b/.browserslistrc new file mode 100644 index 0000000000..92bf34cff3 --- /dev/null +++ b/.browserslistrc @@ -0,0 +1,11 @@ +[production] +last 2 major versions and >0.2% +Firefox ESR +not dead +not ie 11 +not op_mini all + +[development] +last 1 chrome version +last 1 firefox version +last 1 safari version diff --git a/.eslintrc.json b/.eslintrc.json index bb842cb7a7..31e2dbeca4 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -24,16 +24,9 @@ "react/react-in-jsx-scope": 0, "react/display-name": 0, "react/prop-types": 0, - "@typescript-eslint/explicit-function-return-type": 0, - "@typescript-eslint/explicit-module-boundary-types": 0, - "@typescript-eslint/explicit-member-accessibility": 0, - "@typescript-eslint/indent": 0, - "@typescript-eslint/member-delimiter-style": 0, - "@typescript-eslint/ban-ts-comment": 0, "@typescript-eslint/no-non-null-assertion": 0, - "@typescript-eslint/no-explicit-any": 0, "@typescript-eslint/no-var-requires": 0, - "@typescript-eslint/no-use-before-define": 0, + "@typescript-eslint/ban-ts-comment": 0, "@typescript-eslint/no-unused-vars": [ 2, { diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml deleted file mode 100644 index ab46c7107f..0000000000 --- a/.github/workflows/pull-request.yml +++ /dev/null @@ -1,16 +0,0 @@ -on: - pull_request: - types: [opened, synchronize] - -jobs: - test: - name: yarn test-all - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - uses: actions/setup-node@v2 - with: - node-version: lts/* - cache: yarn - - run: yarn install - - run: yarn test-all diff --git a/.gitignore b/.gitignore index 6f921c7f24..f4cc61e3f6 100644 --- a/.gitignore +++ b/.gitignore @@ -33,5 +33,10 @@ yarn-error.log* # vercel .vercel +# visual studio +.vscode + +.npmrc +.env # TypeScript cache *.tsbuildinfo diff --git a/.prettierrc b/.prettierrc index b2095be81e..9e4c08afed 100644 --- a/.prettierrc +++ b/.prettierrc @@ -1,4 +1,5 @@ { - "semi": false, - "singleQuote": true + "semi": true, + "singleQuote": true, + "trailingComma": "all" } diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index bf6b9c08ba..0000000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "typescript.tsdk": "node_modules/typescript/lib", - "cSpell.words": ["Addin", "blockworks", "solana"] -} diff --git a/@types/buffer-layout.d.ts b/@types/buffer-layout.d.ts index a9dd43d306..31445e0cfc 100644 --- a/@types/buffer-layout.d.ts +++ b/@types/buffer-layout.d.ts @@ -1,90 +1,90 @@ declare module 'buffer-layout' { export class Layout { - span: number - property?: string + span: number; + property?: string; - constructor(span: number, property?: string) + constructor(span: number, property?: string); - decode(b: Buffer, offset?: number): T - encode(src: T, b: Buffer, offset?: number): number - getSpan(b: Buffer, offset?: number): number - replicate(name: string): this + decode(b: Buffer, offset?: number): T; + encode(src: T, b: Buffer, offset?: number): number; + getSpan(b: Buffer, offset?: number): number; + replicate(name: string): this; } export interface EnumLayout extends Layout { - registry: Record> + registry: Record>; } export class Structure extends Layout { - span: any + span: any; } export function greedy( elementSpan?: number, - property?: string - ): Layout + property?: string, + ): Layout; export function offset( layout: Layout, offset?: number, - property?: string - ): Layout - export function u8(property?: string): Layout - export function u16(property?: string): Layout - export function u24(property?: string): Layout - export function u32(property?: string): Layout - export function u40(property?: string): Layout - export function u48(property?: string): Layout - export function nu64(property?: string): Layout - export function u16be(property?: string): Layout - export function u24be(property?: string): Layout - export function u32be(property?: string): Layout - export function u40be(property?: string): Layout - export function u48be(property?: string): Layout - export function nu64be(property?: string): Layout - export function s8(property?: string): Layout - export function s16(property?: string): Layout - export function s24(property?: string): Layout - export function s32(property?: string): Layout - export function s40(property?: string): Layout - export function s48(property?: string): Layout - export function ns64(property?: string): Layout - export function s16be(property?: string): Layout - export function s24be(property?: string): Layout - export function s32be(property?: string): Layout - export function s40be(property?: string): Layout - export function s48be(property?: string): Layout - export function ns64be(property?: string): Layout - export function f32(property?: string): Layout - export function f32be(property?: string): Layout - export function f64(property?: string): Layout - export function f64be(property?: string): Layout + property?: string, + ): Layout; + export function u8(property?: string): Layout; + export function u16(property?: string): Layout; + export function u24(property?: string): Layout; + export function u32(property?: string): Layout; + export function u40(property?: string): Layout; + export function u48(property?: string): Layout; + export function nu64(property?: string): Layout; + export function u16be(property?: string): Layout; + export function u24be(property?: string): Layout; + export function u32be(property?: string): Layout; + export function u40be(property?: string): Layout; + export function u48be(property?: string): Layout; + export function nu64be(property?: string): Layout; + export function s8(property?: string): Layout; + export function s16(property?: string): Layout; + export function s24(property?: string): Layout; + export function s32(property?: string): Layout; + export function s40(property?: string): Layout; + export function s48(property?: string): Layout; + export function ns64(property?: string): Layout; + export function s16be(property?: string): Layout; + export function s24be(property?: string): Layout; + export function s32be(property?: string): Layout; + export function s40be(property?: string): Layout; + export function s48be(property?: string): Layout; + export function ns64be(property?: string): Layout; + export function f32(property?: string): Layout; + export function f32be(property?: string): Layout; + export function f64(property?: string): Layout; + export function f64be(property?: string): Layout; export function struct( fields: Layout[], property?: string, - decodePrefixes?: boolean - ): Layout + decodePrefixes?: boolean, + ): Layout; export function bits( word: Layout, msb?: boolean, - property?: string - ): any + property?: string, + ): any; export function seq( elementLayout: Layout, count: number | Layout, - property?: string - ): Layout + property?: string, + ): Layout; export function union( discr: Layout, defaultLayout?: any, - property?: string - ): any + property?: string, + ): any; export function unionLayoutDiscriminator( layout: Layout, - property?: string - ): any + property?: string, + ): any; export function blob( length: number | Layout, - property?: string - ): Layout - export function cstr(property?: string): Layout - export function utf8(maxSpan: number, property?: string): Layout + property?: string, + ): Layout; + export function cstr(property?: string): Layout; + export function utf8(maxSpan: number, property?: string): Layout; } diff --git a/@types/index.d.ts b/@types/index.d.ts index 9b9471da0d..091d25e210 100644 --- a/@types/index.d.ts +++ b/@types/index.d.ts @@ -1,4 +1,4 @@ declare module '*.svg' { - const content: any - export default content + const content: any; + export default content; } diff --git a/@types/types.ts b/@types/types.ts index a9c06fd276..34444e3080 100644 --- a/@types/types.ts +++ b/@types/types.ts @@ -1,12 +1,12 @@ -import type { AccountInfo, PublicKey } from '@solana/web3.js' +import type { AccountInfo, PublicKey } from '@solana/web3.js'; export interface EndpointInfo { - name: string - url: string + name: string; + url: string; } export interface TokenAccount { - pubkey: PublicKey - account: AccountInfo | null - effectiveMint: PublicKey + pubkey: PublicKey; + account: AccountInfo | null; + effectiveMint: PublicKey; } diff --git a/Strategies/components/CreateRefLink.tsx b/Strategies/components/CreateRefLink.tsx index 90539bacb5..982ec2770f 100644 --- a/Strategies/components/CreateRefLink.tsx +++ b/Strategies/components/CreateRefLink.tsx @@ -4,74 +4,74 @@ import { makeRegisterReferrerIdInstruction, MangoGroup, INFO_LEN, -} from '@blockworks-foundation/mango-client' -import Button, { LinkButton } from '@components/Button' -import Input from '@components/inputs/Input' -import Tooltip from '@components/Tooltip' -import { DuplicateIcon } from '@heroicons/react/outline' -import useGovernanceAssets from '@hooks/useGovernanceAssets' -import { PublicKey, Transaction } from '@solana/web3.js' -import { notify } from '@utils/notifications' -import { sendTransaction } from '@utils/send' -import { useState, useEffect } from 'react' -import useWalletStore from 'stores/useWalletStore' -import { tryGetMangoAccount } from 'Strategies/protocols/mango/tools' -import { MarketStore } from 'Strategies/store/marketStore' -const minMngoToCreateLink = 10000 +} from '@blockworks-foundation/mango-client'; +import Button, { LinkButton } from '@components/Button'; +import Input from '@components/inputs/Input'; +import Tooltip from '@components/Tooltip'; +import { DuplicateIcon } from '@heroicons/react/outline'; +import useGovernanceAssets from '@hooks/useGovernanceAssets'; +import { PublicKey, Transaction } from '@solana/web3.js'; +import { notify } from '@utils/notifications'; +import { sendTransaction } from '@utils/send'; +import { useState, useEffect } from 'react'; +import useWalletStore from 'stores/useWalletStore'; +import { tryGetMangoAccount } from 'Strategies/protocols/mango/tools'; +import { MarketStore } from 'Strategies/store/marketStore'; +const minMngoToCreateLink = 10000; const CreateRefForm = ({ selectedMangoAccount, market, mint, }: { - selectedMangoAccount: MangoAccount - market: MarketStore - mint: PublicKey + selectedMangoAccount: MangoAccount; + market: MarketStore; + mint: PublicKey; }) => { - const connection = useWalletStore((s) => s.connection) + const connection = useWalletStore((s) => s.connection); const link = connection.cluster === 'devnet' ? `http://devnet.mango.markets/?ref=` - : `https://trade.mango.markets/?ref` + : `https://trade.mango.markets/?ref`; const depositIndex = market.group!.tokens.findIndex( - (x) => x.mint.toBase58() === mint.toBase58() - ) - const { canUseTransferInstruction } = useGovernanceAssets() + (x) => x.mint.toBase58() === mint.toBase58(), + ); + const { canUseTransferInstruction } = useGovernanceAssets(); - const wallet = useWalletStore((s) => s.current) + const wallet = useWalletStore((s) => s.current); const currentPosition = selectedMangoAccount .getUiDeposit( market.cache!.rootBankCache[depositIndex], market.group!, - depositIndex + depositIndex, ) - .toNumber() - const [existingLinks, setExistingLinks] = useState([]) - const [linkGenerated, setLinkGenerated] = useState(false) - const [linkName, setLinkName] = useState('') - const connected = useWalletStore((s) => s.connected) + .toNumber(); + const [existingLinks, setExistingLinks] = useState([]); + const [linkGenerated, setLinkGenerated] = useState(false); + const [linkName, setLinkName] = useState(''); + const connected = useWalletStore((s) => s.connected); const handleCreateLink = async () => { - setLinkGenerated(false) + setLinkGenerated(false); try { - const signers = [] - const programId = market.client!.programId - const mangoGroup = market.group + const signers = []; + const programId = market.client!.programId; + const mangoGroup = market.group; const { referrerPda, encodedReferrerId } = await getReferrerPda( mangoGroup!, linkName, - programId - ) + programId, + ); const instruction = makeRegisterReferrerIdInstruction( programId, mangoGroup!.publicKey, selectedMangoAccount!.publicKey, referrerPda, wallet!.publicKey!, - encodedReferrerId - ) + encodedReferrerId, + ); - const transaction = new Transaction() - transaction.add(instruction) + const transaction = new Transaction(); + transaction.add(instruction); await sendTransaction({ transaction, wallet, @@ -79,29 +79,29 @@ const CreateRefForm = ({ signers, sendingMessage: 'Creating ref link', successMessage: 'Ref link created', - }) - setLinkGenerated(true) + }); + setLinkGenerated(true); } catch (e) { - setLinkGenerated(false) - notify({ type: 'error', message: "Can't generate link" }) + setLinkGenerated(false); + notify({ type: 'error', message: "Can't generate link" }); } - } + }; const getReferrerPda = async ( mangoGroup: MangoGroup, referrerId: string, - programId: PublicKey + programId: PublicKey, ): Promise<{ referrerPda: PublicKey; encodedReferrerId: Buffer }> => { - const encoded = Buffer.from(referrerId, 'utf8') + const encoded = Buffer.from(referrerId, 'utf8'); if (encoded.length > INFO_LEN) { throw new Error( - `info string too long. Must be less than or equal to ${INFO_LEN} bytes` - ) + `info string too long. Must be less than or equal to ${INFO_LEN} bytes`, + ); } const encodedReferrerId = Buffer.concat([ encoded, Buffer.alloc(INFO_LEN - encoded.length, 0), - ]) + ]); // Generate the PDA pubkey const [referrerIdRecordPk] = await PublicKey.findProgramAddress( @@ -110,30 +110,32 @@ const CreateRefForm = ({ new Buffer('ReferrerIdRecord', 'utf-8'), encodedReferrerId, ], - programId - ) + programId, + ); - return { referrerPda: referrerIdRecordPk, encodedReferrerId } - } + return { referrerPda: referrerIdRecordPk, encodedReferrerId }; + }; useEffect(() => { const getRefLinks = async () => { - const client = market.client - const mangoAccountPk = selectedMangoAccount!.publicKey - const account = await tryGetMangoAccount(market, mangoAccountPk) + const client = market.client; + const mangoAccountPk = selectedMangoAccount!.publicKey; + const account = await tryGetMangoAccount(market, mangoAccountPk); if (account) { - const referrerIds = await client?.getReferrerIdsForMangoAccount(account) + const referrerIds = await client?.getReferrerIdsForMangoAccount( + account, + ); if (referrerIds) { - setExistingLinks(referrerIds) + setExistingLinks(referrerIds); } } - } + }; if (selectedMangoAccount) { - getRefLinks() + getRefLinks(); } else { - setExistingLinks([]) + setExistingLinks([]); } - setLinkName('') - }, [selectedMangoAccount]) + setLinkName(''); + }, [selectedMangoAccount]); return (
{ - navigator.clipboard.writeText(`${link}${linkName}`) + navigator.clipboard.writeText(`${link}${linkName}`); }} > @@ -203,7 +205,7 @@ const CreateRefForm = ({ { - navigator.clipboard.writeText(`${link}${x.referrerId}`) + navigator.clipboard.writeText(`${link}${x.referrerId}`); }} > @@ -214,7 +216,7 @@ const CreateRefForm = ({
) : null} - ) -} + ); +}; -export default CreateRefForm +export default CreateRefForm; diff --git a/Strategies/components/Delegate.tsx b/Strategies/components/Delegate.tsx index 62843358f5..967cdd034d 100644 --- a/Strategies/components/Delegate.tsx +++ b/Strategies/components/Delegate.tsx @@ -1,101 +1,101 @@ import { MangoAccount, makeSetDelegateInstruction, -} from '@blockworks-foundation/mango-client' -import AdditionalProposalOptions from '@components/AdditionalProposalOptions' -import Button from '@components/Button' -import Input from '@components/inputs/Input' -import Loading from '@components/Loading' -import Tooltip from '@components/Tooltip' -import useCreateProposal from '@hooks/useCreateProposal' -import useGovernanceAssets from '@hooks/useGovernanceAssets' -import useQueryContext from '@hooks/useQueryContext' -import useRealm from '@hooks/useRealm' +} from '@blockworks-foundation/mango-client'; +import AdditionalProposalOptions from '@components/AdditionalProposalOptions'; +import Button from '@components/Button'; +import Input from '@components/inputs/Input'; +import Loading from '@components/Loading'; +import Tooltip from '@components/Tooltip'; +import useCreateProposal from '@hooks/useCreateProposal'; +import useGovernanceAssets from '@hooks/useGovernanceAssets'; +import useQueryContext from '@hooks/useQueryContext'; +import useRealm from '@hooks/useRealm'; import { ProgramAccount, Governance, getInstructionDataFromBase64, serializeInstructionToBase64, -} from '@solana/spl-governance' -import { PublicKey } from '@solana/web3.js' -import { abbreviateAddress } from '@utils/formatting' -import { validateInstruction } from '@utils/instructionTools' -import { notify } from '@utils/notifications' -import { getValidatedPublickKey } from '@utils/validations' -import { InstructionDataWithHoldUpTime } from 'actions/createProposal' -import { useRouter } from 'next/router' -import { useState } from 'react' -import { MarketStore } from 'Strategies/store/marketStore' -import * as yup from 'yup' +} from '@solana/spl-governance'; +import { PublicKey } from '@solana/web3.js'; +import { abbreviateAddress } from '@utils/formatting'; +import { validateInstruction } from '@utils/instructionTools'; +import { notify } from '@utils/notifications'; +import { getValidatedPublickKey } from '@utils/validations'; +import { InstructionDataWithHoldUpTime } from 'actions/createProposal'; +import { useRouter } from 'next/router'; +import { useState } from 'react'; +import { MarketStore } from 'Strategies/store/marketStore'; +import * as yup from 'yup'; const DelegateForm = ({ selectedMangoAccount, governance, market, }: { - selectedMangoAccount: MangoAccount - governance: ProgramAccount - market: MarketStore + selectedMangoAccount: MangoAccount; + governance: ProgramAccount; + market: MarketStore; }) => { - const router = useRouter() - const { symbol } = useRealm() - const { fmtUrlWithCluster } = useQueryContext() - const { handleCreateProposal } = useCreateProposal() - const { canUseTransferInstruction } = useGovernanceAssets() - const groupConfig = market.groupConfig! - const [voteByCouncil, setVoteByCouncil] = useState(false) - const [isLoading, setIsLoading] = useState(false) + const router = useRouter(); + const { symbol } = useRealm(); + const { fmtUrlWithCluster } = useQueryContext(); + const { handleCreateProposal } = useCreateProposal(); + const { canUseTransferInstruction } = useGovernanceAssets(); + const groupConfig = market.groupConfig!; + const [voteByCouncil, setVoteByCouncil] = useState(false); + const [isLoading, setIsLoading] = useState(false); const [form, setForm] = useState({ title: '', description: '', delegateAddress: '', - }) - const [formErrors, setFormErrors] = useState({}) + }); + const [formErrors, setFormErrors] = useState({}); const proposalTitle = `Set delegate for MNGO account: ${abbreviateAddress( - selectedMangoAccount?.publicKey - )}` + selectedMangoAccount?.publicKey, + )}`; const handleSetForm = ({ propertyName, value }) => { - setFormErrors({}) - setForm({ ...form, [propertyName]: value }) - } + setFormErrors({}); + setForm({ ...form, [propertyName]: value }); + }; const handleProposeDelegate = async () => { - const isValid = await validateInstruction({ schema, form, setFormErrors }) + const isValid = await validateInstruction({ schema, form, setFormErrors }); if (!isValid) { - return + return; } - setIsLoading(true) + setIsLoading(true); const delegateMangoAccount = makeSetDelegateInstruction( groupConfig.mangoProgramId, groupConfig.publicKey, selectedMangoAccount!.publicKey, governance!.pubkey, - new PublicKey(form.delegateAddress) - ) + new PublicKey(form.delegateAddress), + ); try { const instructionData: InstructionDataWithHoldUpTime = { data: getInstructionDataFromBase64( - serializeInstructionToBase64(delegateMangoAccount) + serializeInstructionToBase64(delegateMangoAccount), ), holdUpTime: governance!.account!.config.minInstructionHoldUpTime, prerequisiteInstructions: [], - } + }; const proposalAddress = await handleCreateProposal({ title: form.title || proposalTitle, description: form.description, instructionsData: [instructionData], governance: governance!, voteByCouncil, - }) + }); const url = fmtUrlWithCluster( - `/dao/${symbol}/proposal/${proposalAddress}` - ) - router.push(url) + `/dao/${symbol}/proposal/${proposalAddress}`, + ); + router.push(url); } catch (e) { - console.log(e) - notify({ type: 'error', message: "Can't create proposal" }) + console.error(e); + notify({ type: 'error', message: "Can't create proposal" }); } - } + }; const schema = yup.object().shape({ delegateAddress: yup .string() @@ -105,21 +105,21 @@ const DelegateForm = ({ function (val: string) { if (val) { try { - return !!getValidatedPublickKey(val) + return !!getValidatedPublickKey(val); } catch (e) { - console.log(e) + console.error(e); return this.createError({ message: `${e}`, - }) + }); } } else { return this.createError({ message: `Delegate address is required`, - }) + }); } - } + }, ), - }) + }); return (
- ) -} + ); +}; -export default DelegateForm +export default DelegateForm; diff --git a/Strategies/components/DepositModal.tsx b/Strategies/components/DepositModal.tsx index 7d3943adfc..fa6008a4b8 100644 --- a/Strategies/components/DepositModal.tsx +++ b/Strategies/components/DepositModal.tsx @@ -1,7 +1,7 @@ -import Modal from '@components/Modal' -import ModalHeader from './ModalHeader' -import MangoDeposit from './MangoDepositComponent' -import BigNumber from 'bignumber.js' +import Modal from '@components/Modal'; +import ModalHeader from './ModalHeader'; +import MangoDeposit from './MangoDepositComponent'; +import BigNumber from 'bignumber.js'; const DepositModal = ({ onClose, @@ -18,8 +18,8 @@ const DepositModal = ({ governedTokenAccount, }) => { const currentPositionFtm = new BigNumber( - currentPosition.toFixed(0) - ).toFormat() + currentPosition.toFixed(0), + ).toFormat(); return ( ) : null} - ) -} + ); +}; -export default DepositModal +export default DepositModal; diff --git a/Strategies/components/MangoDepositComponent.tsx b/Strategies/components/MangoDepositComponent.tsx index 4f4307c8cc..36701a1278 100644 --- a/Strategies/components/MangoDepositComponent.tsx +++ b/Strategies/components/MangoDepositComponent.tsx @@ -2,51 +2,51 @@ import { MangoAccount, MangoAccountLayout, PublicKey, -} from '@blockworks-foundation/mango-client' -import Button, { LinkButton } from '@components/Button' -import Input from '@components/inputs/Input' -import Loading from '@components/Loading' -import Tooltip from '@components/Tooltip' -import useGovernanceAssets from '@hooks/useGovernanceAssets' -import useQueryContext from '@hooks/useQueryContext' -import useRealm from '@hooks/useRealm' -import { getProgramVersionForRealm } from '@models/registry/api' -import { BN } from '@project-serum/anchor' +} from '@blockworks-foundation/mango-client'; +import Button, { LinkButton } from '@components/Button'; +import Input from '@components/inputs/Input'; +import Loading from '@components/Loading'; +import Tooltip from '@components/Tooltip'; +import useGovernanceAssets from '@hooks/useGovernanceAssets'; +import useQueryContext from '@hooks/useQueryContext'; +import useRealm from '@hooks/useRealm'; +import { getProgramVersionForRealm } from '@models/registry/api'; import { getNativeTreasuryAddress, RpcContext, withCreateNativeTreasury, -} from '@solana/spl-governance' -import { SystemProgram, TransactionInstruction } from '@solana/web3.js' +} from '@solana/spl-governance'; +import { SystemProgram, TransactionInstruction } from '@solana/web3.js'; import { fmtMintAmount, getMintDecimalAmount, getMintMinAmountAsDecimal, parseMintNaturalAmountFromDecimal, -} from '@tools/sdk/units' -import { abbreviateAddress, precision } from '@utils/formatting' -import tokenService from '@utils/services/token' -import { GovernedTokenAccount } from '@utils/tokens' -import BigNumber from 'bignumber.js' -import { useRouter } from 'next/router' -import { useEffect, useState } from 'react' -import useWalletStore from 'stores/useWalletStore' -import useMarketStore, { MarketStore } from 'Strategies/store/marketStore' -import { HandleCreateProposalWithStrategy } from 'Strategies/types/types' -import useVoteStakeRegistryClientStore from 'VoteStakeRegistry/stores/voteStakeRegistryClientStore' -import ButtonGroup from '@components/ButtonGroup' -import Switch from '@components/Switch' -import Select from '@components/inputs/Select' -import CreateRefForm from './CreateRefLink' -import DelegateForm from './Delegate' -import AdditionalProposalOptions from '@components/AdditionalProposalOptions' -import { validateInstruction } from '@utils/instructionTools' -import * as yup from 'yup' -import { getValidatedPublickKey } from '@utils/validations' +} from '@tools/sdk/units'; +import { abbreviateAddress, precision } from '@utils/formatting'; +import tokenService from '@utils/services/token'; +import { GovernedTokenAccount } from '@utils/tokens'; +import BigNumber from 'bignumber.js'; +import { useRouter } from 'next/router'; +import { useEffect, useState } from 'react'; +import useWalletStore from 'stores/useWalletStore'; +import useMarketStore, { MarketStore } from 'Strategies/store/marketStore'; +import { HandleCreateProposalWithStrategy } from 'Strategies/types/types'; +import useVoteStakeRegistryClientStore from 'VoteStakeRegistry/stores/voteStakeRegistryClientStore'; +import ButtonGroup from '@components/ButtonGroup'; +import Switch from '@components/Switch'; +import Select from '@components/inputs/Select'; +import CreateRefForm from './CreateRefLink'; +import DelegateForm from './Delegate'; +import AdditionalProposalOptions from '@components/AdditionalProposalOptions'; +import { validateInstruction } from '@utils/instructionTools'; +import * as yup from 'yup'; +import { getValidatedPublickKey } from '@utils/validations'; +import { BN_ZERO } from '@utils/helpers'; -const DEPOSIT = 'Deposit' -const CREATE_REF_LINK = 'Create Referral Link' -const DELEGATE_ACCOUNT = 'Delegate' +const DEPOSIT = 'Deposit'; +const CREATE_REF_LINK = 'Create Referral Link'; +const DELEGATE_ACCOUNT = 'Delegate'; const MangoDepositComponent = ({ handledMint, @@ -55,14 +55,14 @@ const MangoDepositComponent = ({ mangoAccounts, governedTokenAccount, }: { - handledMint: string - currentPositionFtm: string - createProposalFcn: HandleCreateProposalWithStrategy - mangoAccounts: MangoAccount[] - governedTokenAccount: GovernedTokenAccount + handledMint: string; + currentPositionFtm: string; + createProposalFcn: HandleCreateProposalWithStrategy; + mangoAccounts: MangoAccount[]; + governedTokenAccount: GovernedTokenAccount; }) => { - const router = useRouter() - const { fmtUrlWithCluster } = useQueryContext() + const router = useRouter(); + const { fmtUrlWithCluster } = useQueryContext(); const { proposals, realmInfo, @@ -71,139 +71,139 @@ const MangoDepositComponent = ({ mint, councilMint, symbol, - } = useRealm() - const [isDepositing, setIsDepositing] = useState(false) + } = useRealm(); + const [isDepositing, setIsDepositing] = useState(false); const [ selectedMangoAccount, setSelectedMangoAccount, ] = useState( - mangoAccounts.length ? mangoAccounts[0] : null - ) - const [voteByCouncil, setVoteByCouncil] = useState(false) - const client = useVoteStakeRegistryClientStore((s) => s.state.client) - const market = useMarketStore((s) => s) - const connection = useWalletStore((s) => s.connection) - const wallet = useWalletStore((s) => s.current) - const tokenInfo = tokenService.getTokenInfo(handledMint) - const { canUseTransferInstruction } = useGovernanceAssets() + mangoAccounts.length ? mangoAccounts[0] : null, + ); + const [voteByCouncil, setVoteByCouncil] = useState(false); + const client = useVoteStakeRegistryClientStore((s) => s.state.client); + const market = useMarketStore((s) => s); + const connection = useWalletStore((s) => s.connection); + const wallet = useWalletStore((s) => s.current); + const tokenInfo = tokenService.getTokenInfo(handledMint); + const { canUseTransferInstruction } = useGovernanceAssets(); const treasuryAmount = governedTokenAccount?.token ? governedTokenAccount.token.account.amount - : new BN(0) - const mintInfo = governedTokenAccount?.mint?.account + : BN_ZERO; + const mintInfo = governedTokenAccount?.mint?.account; const [form, setForm] = useState({ title: '', description: '', delegateAddress: '', delegateDeposit: false, amount: '', - }) - const [formErrors, setFormErrors] = useState({}) + }); + const [formErrors, setFormErrors] = useState({}); const proposalTitle = `Deposit ${form.amount} ${ tokenService.getTokenInfo(governedTokenAccount.mint!.publicKey.toBase58()) ?.symbol || 'tokens' - } to Mango account` + } to Mango account`; const handleSetForm = ({ propertyName, value }) => { - setFormErrors({}) - setForm({ ...form, [propertyName]: value }) - } - const [proposalType, setProposalType] = useState('Deposit') - const mintMinAmount = mintInfo ? getMintMinAmountAsDecimal(mintInfo) : 1 + setFormErrors({}); + setForm({ ...form, [propertyName]: value }); + }; + const [proposalType, setProposalType] = useState('Deposit'); + const mintMinAmount = mintInfo ? getMintMinAmountAsDecimal(mintInfo) : 1; const maxAmount = mintInfo ? getMintDecimalAmount(mintInfo, treasuryAmount) - : new BigNumber(0) - const maxAmountFtm = fmtMintAmount(mintInfo, treasuryAmount) - const currentPrecision = precision(mintMinAmount) - const group = market!.group! + : new BigNumber(0); + const maxAmountFtm = fmtMintAmount(mintInfo, treasuryAmount); + const currentPrecision = precision(mintMinAmount); + const group = market!.group!; const depositIndex = group.tokens.findIndex( - (x) => x.mint.toBase58() === handledMint - ) + (x) => x.mint.toBase58() === handledMint, + ); const tabs = [ { val: DEPOSIT, isVisible: true }, { val: CREATE_REF_LINK, isVisible: selectedMangoAccount !== null }, { val: DELEGATE_ACCOUNT, isVisible: selectedMangoAccount !== null }, ] .filter((x) => x.isVisible) - .map((x) => x.val) + .map((x) => x.val); const validateAmountOnBlur = () => { handleSetForm({ propertyName: 'amount', value: parseFloat( Math.max( Number(mintMinAmount), - Math.min(Number(Number.MAX_SAFE_INTEGER), Number(form.amount)) - ).toFixed(currentPrecision) + Math.min(Number(Number.MAX_SAFE_INTEGER), Number(form.amount)), + ).toFixed(currentPrecision), ), - }) - } + }); + }; useEffect(() => { if (selectedMangoAccount === null) { - setProposalType(DEPOSIT) + setProposalType(DEPOSIT); } - }, [selectedMangoAccount]) + }, [selectedMangoAccount]); const handleSolPayment = async () => { - const instructions: TransactionInstruction[] = [] + const instructions: TransactionInstruction[] = []; const toAddress = await getNativeTreasuryAddress( realm!.owner, - governedTokenAccount!.governance!.pubkey - ) + governedTokenAccount!.governance!.pubkey, + ); const hasSolAccount = await connection.current.getParsedAccountInfo( - toAddress - ) + toAddress, + ); if (!hasSolAccount.value) { await withCreateNativeTreasury( instructions, realm!.owner, governedTokenAccount!.governance!.pubkey, - wallet!.publicKey! - ) + wallet!.publicKey!, + ); } const minRentAmount = await connection.current.getMinimumBalanceForRentExemption( - MangoAccountLayout.span - ) + MangoAccountLayout.span, + ); const transferIx = SystemProgram.transfer({ fromPubkey: wallet!.publicKey!, toPubkey: toAddress, lamports: minRentAmount, - }) - instructions.push(transferIx) - return instructions - } + }); + instructions.push(transferIx); + return instructions; + }; const handleDeposit = async () => { - const isValid = await validateInstruction({ schema, form, setFormErrors }) + const isValid = await validateInstruction({ schema, form, setFormErrors }); if (!isValid) { - return + return; } try { - setIsDepositing(true) - const prerequisiteInstructions: TransactionInstruction[] = [] - const mangoAccountPk = selectedMangoAccount?.publicKey || null + setIsDepositing(true); + const prerequisiteInstructions: TransactionInstruction[] = []; + const mangoAccountPk = selectedMangoAccount?.publicKey || null; if (!mangoAccountPk) { - const solAccountInstruction = await handleSolPayment() - prerequisiteInstructions.push(...solAccountInstruction) + const solAccountInstruction = await handleSolPayment(); + prerequisiteInstructions.push(...solAccountInstruction); } const rpcContext = new RpcContext( new PublicKey(realm!.owner.toString()), getProgramVersionForRealm(realmInfo!), wallet!, connection.current, - connection.endpoint - ) + connection.endpoint, + ); const mintAmount = parseMintNaturalAmountFromDecimal( form.amount!, - governedTokenAccount!.mint!.account.decimals - ) + governedTokenAccount!.mint!.account.decimals, + ); const ownTokenRecord = ownVoterWeight.getTokenRecordToCreateProposal( - governedTokenAccount!.governance!.account.config - ) + governedTokenAccount!.governance!.account.config, + ); const defaultProposalMint = voteByCouncil ? realm?.account.config.councilMint : !mint?.supply.isZero() ? realm!.account.communityMint : !councilMint?.supply.isZero() ? realm!.account.config.councilMint - : undefined + : undefined; const proposalAddress = await createProposalFcn( rpcContext, handledMint, @@ -222,17 +222,17 @@ const MangoDepositComponent = ({ prerequisiteInstructions, false, market, - client - ) + client, + ); const url = fmtUrlWithCluster( - `/dao/${symbol}/proposal/${proposalAddress}` - ) - router.push(url) + `/dao/${symbol}/proposal/${proposalAddress}`, + ); + router.push(url); } catch (e) { - console.log(e) + console.error(e); } - setIsDepositing(false) - } + setIsDepositing(false); + }; const schema = yup.object().shape({ delegateAddress: yup .string() @@ -241,26 +241,26 @@ const MangoDepositComponent = ({ 'Delegate address validation error', function (val: string) { if (!form.delegateDeposit) { - return true + return true; } if (val) { try { - return !!getValidatedPublickKey(val) + return !!getValidatedPublickKey(val); } catch (e) { - console.log(e) + console.error(e); return this.createError({ message: `${e}`, - }) + }); } } else { return this.createError({ message: `Delegate address is required`, - }) + }); } - } + }, ), amount: yup.number().required('Amount is required').min(mintMinAmount), - }) + }); return (
{ - handleSetForm({ value, propertyName: 'voter' }) + handleSetForm({ value, propertyName: 'voter' }); }} placeholder="Please select..." value={form.voter?.voterAuthority.toBase58()} @@ -221,13 +221,13 @@ const Clawback = ({ {x.voterAuthority.toBase58()} - ) + ); })} - x.mint?.publicKey.toBase58() === - form.deposit?.mint.publicKey.toBase58() - ) as GovernedMultiTypeAccount[] - } + governedAccounts={governedTokenAccountsWithoutNfts.filter( + (x) => + x.mint?.publicKey.toBase58() === + form.deposit?.mint.publicKey.toBase58(), + )} onChange={(value) => { - handleSetForm({ value, propertyName: 'governedTokenAccount' }) + handleSetForm({ value, propertyName: 'governedTokenAccount' }); }} value={form.governedTokenAccount} error={formErrors['governedTokenAccount']} - shouldBeGoverned={shouldBeGoverned} + shouldBeGoverned={!!shouldBeGoverned} governance={governance} - > + /> - ) -} + ); +}; -export default Clawback +export default Clawback; diff --git a/VoteStakeRegistry/components/instructions/Grant.tsx b/VoteStakeRegistry/components/instructions/Grant.tsx index c6f840b053..82caa4e2bb 100644 --- a/VoteStakeRegistry/components/instructions/Grant.tsx +++ b/VoteStakeRegistry/components/instructions/Grant.tsx @@ -1,62 +1,61 @@ -import React, { useContext, useEffect, useState } from 'react' -import Input from '@components/inputs/Input' -import useRealm from '@hooks/useRealm' -import { AccountInfo } from '@solana/spl-token' +import React, { useContext, useEffect, useState } from 'react'; +import Input from '@components/inputs/Input'; +import useRealm from '@hooks/useRealm'; +import { AccountInfo } from '@solana/spl-token'; import { getMintMinAmountAsDecimal, parseMintNaturalAmountFromDecimal, -} from '@tools/sdk/units' -import { PublicKey, TransactionInstruction } from '@solana/web3.js' -import { precision } from '@utils/formatting' -import { tryParseKey } from '@tools/validators/pubkey' -import useWalletStore from 'stores/useWalletStore' +} from '@tools/sdk/units'; +import { PublicKey, TransactionInstruction } from '@solana/web3.js'; +import { precision } from '@utils/formatting'; +import { tryParseKey } from '@tools/validators/pubkey'; +import useWalletStore from 'stores/useWalletStore'; +import { TokenProgramAccount, tryGetTokenAccount } from '@utils/tokens'; import { - GovernedMultiTypeAccount, - TokenProgramAccount, - tryGetTokenAccount, -} from '@utils/tokens' -import { GrantForm, UiInstruction } from '@utils/uiTypes/proposalCreationTypes' -import { getAccountName } from '@components/instructions/tools' -import { debounce } from '@utils/debounce' -import { getTokenTransferSchema } from '@utils/validations' -import useGovernanceAssets from '@hooks/useGovernanceAssets' + GrantForm, + FormInstructionData, +} from '@utils/uiTypes/proposalCreationTypes'; +import { getAccountName } from '@components/instructions/tools'; +import { debounce } from '@utils/debounce'; +import { getTokenTransferSchema } from '@utils/validations'; +import useGovernanceAssets from '@hooks/useGovernanceAssets'; import { Governance, serializeInstructionToBase64, withCreateTokenOwnerRecord, -} from '@solana/spl-governance' -import { ProgramAccount } from '@solana/spl-governance' -import { validateInstruction } from '@utils/instructionTools' -import { NewProposalContext } from 'pages/dao/[symbol]/proposal/new' -import GovernedAccountSelect from 'pages/dao/[symbol]/proposal/components/GovernedAccountSelect' -import { lockupTypes } from 'VoteStakeRegistry/tools/types' -import Select from '@components/inputs/Select' -import Switch from '@components/Switch' -import { getFormattedStringFromDays } from 'VoteStakeRegistry/tools/dateTools' -import * as yup from 'yup' -import { getGrantInstruction } from 'VoteStakeRegistry/actions/getGrantInstruction' -import { getRegistrarPDA } from 'VoteStakeRegistry/sdk/accounts' -import { tryGetRegistrar } from 'VoteStakeRegistry/sdk/api' -import useVoteStakeRegistryClientStore from 'VoteStakeRegistry/stores/voteStakeRegistryClientStore' -import dayjs from 'dayjs' +} from '@solana/spl-governance'; +import { ProgramAccount } from '@solana/spl-governance'; +import { validateInstruction } from '@utils/instructionTools'; +import { NewProposalContext } from 'pages/dao/[symbol]/proposal/new'; +import GovernedAccountSelect from 'pages/dao/[symbol]/proposal/components/GovernedAccountSelect'; +import { lockupTypes } from 'VoteStakeRegistry/tools/types'; +import Select from '@components/inputs/Select'; +import Switch from '@components/Switch'; +import { getFormattedStringFromDays } from 'VoteStakeRegistry/tools/dateTools'; +import * as yup from 'yup'; +import { getGrantInstruction } from 'VoteStakeRegistry/actions/getGrantInstruction'; +import { getRegistrarPDA } from 'VoteStakeRegistry/sdk/accounts'; +import { tryGetRegistrar } from 'VoteStakeRegistry/sdk/api'; +import useVoteStakeRegistryClientStore from 'VoteStakeRegistry/stores/voteStakeRegistryClientStore'; +import dayjs from 'dayjs'; const Grant = ({ index, governance, }: { - index: number - governance: ProgramAccount | null + index: number; + governance: ProgramAccount | null; }) => { - const client = useVoteStakeRegistryClientStore((s) => s.state.client) - const dateNow = dayjs().unix() - const connection = useWalletStore((s) => s.connection) - const wallet = useWalletStore((s) => s.current) - const { realm, tokenRecords } = useRealm() - const { governedTokenAccountsWithoutNfts } = useGovernanceAssets() - const shouldBeGoverned = index !== 0 && governance - const [startDate, setStartDate] = useState(dayjs().format('DD-MM-YYYY')) - const [endDate, setEndDate] = useState('') - const [useableGrantMints, setUseableGrantMints] = useState([]) + const client = useVoteStakeRegistryClientStore((s) => s.state.client); + const dateNow = dayjs().unix(); + const connection = useWalletStore((s) => s.connection); + const wallet = useWalletStore((s) => s.current); + const { realm, tokenRecords } = useRealm(); + const { governedTokenAccountsWithoutNfts } = useGovernanceAssets(); + const shouldBeGoverned = index !== 0 && governance; + const [startDate, setStartDate] = useState(dayjs().format('DD-MM-YYYY')); + const [endDate, setEndDate] = useState(''); + const [useableGrantMints, setUseableGrantMints] = useState([]); const [form, setForm] = useState({ destinationAccount: '', // No default transfer amount @@ -67,64 +66,64 @@ const Grant = ({ periods: 0, allowClawback: true, lockupKind: lockupTypes[0], - }) + }); const [governedAccount, setGovernedAccount] = useState< ProgramAccount | undefined - >(undefined) + >(undefined); const [ destinationAccount, setDestinationAccount, - ] = useState | null>(null) - const [formErrors, setFormErrors] = useState({}) + ] = useState | null>(null); + const [formErrors, setFormErrors] = useState({}); const mintMinAmount = form.mintInfo ? getMintMinAmountAsDecimal(form.mintInfo) - : 1 - const currentPrecision = precision(mintMinAmount) - const { handleSetInstructions } = useContext(NewProposalContext) + : 1; + const currentPrecision = precision(mintMinAmount); + const { handleSetInstruction } = useContext(NewProposalContext); const handleSetForm = ({ propertyName, value }) => { - setFormErrors({}) - setForm({ ...form, [propertyName]: value }) - } + setFormErrors({}); + setForm({ ...form, [propertyName]: value }); + }; const setMintInfo = (value) => { - setForm({ ...form, mintInfo: value }) - } + setForm({ ...form, mintInfo: value }); + }; const setAmount = (event) => { - const value = event.target.value + const value = event.target.value; handleSetForm({ value: value, propertyName: 'amount', - }) - } + }); + }; const validateAmountOnBlur = () => { - const value = form.amount + const value = form.amount; handleSetForm({ value: parseFloat( Math.max( Number(mintMinAmount), - Math.min(Number(Number.MAX_SAFE_INTEGER), Number(value)) - ).toFixed(currentPrecision) + Math.min(Number(Number.MAX_SAFE_INTEGER), Number(value)), + ).toFixed(currentPrecision), ), propertyName: 'amount', - }) - } - async function getInstruction(): Promise { - const isValid = await validateInstruction({ schema, form, setFormErrors }) - let serializedInstruction = '' - const prerequisiteInstructions: TransactionInstruction[] = [] + }); + }; + async function getInstruction(): Promise { + const isValid = await validateInstruction({ schema, form, setFormErrors }); + let serializedInstruction = ''; + const prerequisiteInstructions: TransactionInstruction[] = []; if ( isValid && form.governedTokenAccount?.token?.publicKey && form.governedTokenAccount?.token && form.governedTokenAccount?.mint?.account ) { - const sourceAccount = form.governedTokenAccount.token?.account.address - const destinationAccount = new PublicKey(form.destinationAccount) + const sourceAccount = form.governedTokenAccount.token?.account.address; + const destinationAccount = new PublicKey(form.destinationAccount); const mintAmount = parseMintNaturalAmountFromDecimal( form.amount!, - form.governedTokenAccount.mint.account.decimals - ) - const currentTokenOwnerRecord = tokenRecords[form.destinationAccount] + form.governedTokenAccount.mint.account.decimals, + ); + const currentTokenOwnerRecord = tokenRecords[form.destinationAccount]; if (!currentTokenOwnerRecord) { await withCreateTokenOwnerRecord( prerequisiteInstructions, @@ -132,8 +131,8 @@ const Grant = ({ realm!.pubkey, destinationAccount, realm!.account.communityMint, - wallet!.publicKey! - ) + wallet!.publicKey!, + ); } const grantIx = await getGrantInstruction({ fromPk: sourceAccount, @@ -149,32 +148,32 @@ const Grant = ({ lockupKind: form.lockupKind.value, allowClawback: form.allowClawback, client: client!, - }) - serializedInstruction = serializeInstructionToBase64(grantIx!) + }); + serializedInstruction = serializeInstructionToBase64(grantIx!); } - const obj: UiInstruction = { + const obj: FormInstructionData = { serializedInstruction, isValid, governance: form.governedTokenAccount?.governance, prerequisiteInstructions: prerequisiteInstructions, chunkSplitByDefault: true, - } - return obj + }; + return obj; } const handleChangeStartDate = (e) => { - const value = e.target.value - setStartDate(value) - const unixDate = dayjs(value).unix() + const value = e.target.value; + setStartDate(value); + const unixDate = dayjs(value).unix(); handleSetForm({ value: !isNaN(unixDate) ? unixDate : 0, propertyName: 'startDateUnixSeconds', - }) - } + }); + }; const handleChangeEndDate = (e) => { - const value = e.target.value - setEndDate(value) - } + const value = e.target.value; + setEndDate(value); + }; useEffect(() => { if ( startDate && @@ -182,45 +181,45 @@ const Grant = ({ dayjs(startDate).isValid() && dayjs(endDate).isValid() ) { - const daysDifference = dayjs(endDate).diff(dayjs(startDate), 'days') - const monthsDifference = dayjs(endDate).diff(dayjs(startDate), 'months') + const daysDifference = dayjs(endDate).diff(dayjs(startDate), 'days'); + const monthsDifference = dayjs(endDate).diff(dayjs(startDate), 'months'); const periods = - form.lockupKind.value !== 'monthly' ? daysDifference : monthsDifference + form.lockupKind.value !== 'monthly' ? daysDifference : monthsDifference; handleSetForm({ value: periods > 0 ? periods : 0, propertyName: 'periods', - }) + }); } - }, [startDate, endDate, form.lockupKind.value]) + }, [startDate, endDate, form.lockupKind.value]); useEffect(() => { if (form.destinationAccount) { debounce.debounceFcn(async () => { - const pubKey = tryParseKey(form.destinationAccount) + const pubKey = tryParseKey(form.destinationAccount); if (pubKey) { - const account = await tryGetTokenAccount(connection.current, pubKey) - setDestinationAccount(account ? account : null) + const account = await tryGetTokenAccount(connection.current, pubKey); + setDestinationAccount(account ? account : null); } else { - setDestinationAccount(null) + setDestinationAccount(null); } - }) + }); } else { - setDestinationAccount(null) + setDestinationAccount(null); } - }, [form.destinationAccount]) + }, [form.destinationAccount]); useEffect(() => { - handleSetInstructions( + handleSetInstruction( { governedAccount: governedAccount, getInstruction }, - index - ) - }, [form]) + index, + ); + }, [form]); useEffect(() => { - setGovernedAccount(form.governedTokenAccount?.governance) - setMintInfo(form.governedTokenAccount?.mint?.account) - }, [form.governedTokenAccount]) + setGovernedAccount(form.governedTokenAccount?.governance); + setMintInfo(form.governedTokenAccount?.mint?.account); + }, [form.governedTokenAccount]); const destinationAccountName = destinationAccount?.publicKey && - getAccountName(destinationAccount?.account.address) + getAccountName(destinationAccount?.account.address); const schema = getTokenTransferSchema({ form, connection }).concat( yup.object().shape({ startDateUnixSeconds: yup @@ -231,33 +230,33 @@ const Grant = ({ .number() .required('End date required') .min(1, 'End date cannot be prior to start date'), - }) - ) + }), + ); useEffect(() => { const getGrantMints = async () => { - const clientProgramId = client!.program.programId + const clientProgramId = client!.program.programId; const { registrar } = await getRegistrarPDA( realm!.pubkey, realm!.account.communityMint, - clientProgramId - ) - const existingRegistrar = await tryGetRegistrar(registrar, client!) + clientProgramId, + ); + const existingRegistrar = await tryGetRegistrar(registrar, client!); if (existingRegistrar) { setUseableGrantMints( - existingRegistrar.votingMints.map((x) => x.mint.toBase58()) - ) + existingRegistrar.votingMints.map((x) => x.mint.toBase58()), + ); } - } + }; if (client) { - getGrantMints() + getGrantMints(); } - }, [client]) + }, [client]); return ( <>
{form.lockupKind.info}
- x.mint && useableGrantMints.includes(x.mint.publicKey.toBase58()) - ) as GovernedMultiTypeAccount[] - } + governedAccounts={governedTokenAccountsWithoutNfts.filter( + (x) => + x.mint && useableGrantMints.includes(x.mint.publicKey.toBase58()), + )} onChange={(value) => { - handleSetForm({ value, propertyName: 'governedTokenAccount' }) + handleSetForm({ value, propertyName: 'governedTokenAccount' }); }} value={form.governedTokenAccount} error={formErrors['governedTokenAccount']} - shouldBeGoverned={shouldBeGoverned} + shouldBeGoverned={!!shouldBeGoverned} governance={governance} - > + />
Allow dao to clawback
@@ -326,7 +323,7 @@ const Grant = ({ handleSetForm({ value: e.target.value, propertyName: 'periods', - }) + }); }} error={formErrors['periods']} > @@ -381,7 +378,7 @@ const Grant = ({
Vesting rate: {(form.amount / form.periods).toFixed(2)} p/m
)} - ) -} + ); +}; -export default Grant +export default Grant; diff --git a/VoteStakeRegistry/hooks/useVoteRegistry.ts b/VoteStakeRegistry/hooks/useVoteRegistry.ts index e125ca4fb5..35c44da277 100644 --- a/VoteStakeRegistry/hooks/useVoteRegistry.ts +++ b/VoteStakeRegistry/hooks/useVoteRegistry.ts @@ -1,27 +1,27 @@ -import { useEffect } from 'react' -import useWalletStore from 'stores/useWalletStore' -import useRealm from '@hooks/useRealm' -import useVoteStakeRegistryClientStore from 'VoteStakeRegistry/stores/voteStakeRegistryClientStore' +import { useEffect } from 'react'; +import useWalletStore from 'stores/useWalletStore'; +import useRealm from '@hooks/useRealm'; +import useVoteStakeRegistryClientStore from 'VoteStakeRegistry/stores/voteStakeRegistryClientStore'; export function useVoteRegistry() { - const { realm } = useRealm() + const { realm } = useRealm(); const { handleSetRegistrar, handleSetClient, - } = useVoteStakeRegistryClientStore() - const wallet = useWalletStore((s) => s.current) - const connection = useWalletStore((s) => s.connection) - const client = useVoteStakeRegistryClientStore((s) => s.state.client) + } = useVoteStakeRegistryClientStore(); + const wallet = useWalletStore((s) => s.current); + const connection = useWalletStore((s) => s.connection); + const client = useVoteStakeRegistryClientStore((s) => s.state.client); useEffect(() => { if (wallet?.connected) { - handleSetClient(wallet, connection) + handleSetClient(wallet, connection); } - }, [connection.endpoint, wallet?.connected, realm?.pubkey]) + }, [connection.endpoint, wallet?.connected, realm?.pubkey]); useEffect(() => { if (realm && client) { - handleSetRegistrar(client, realm) + handleSetRegistrar(client, realm); } - }, [realm?.pubkey, client]) + }, [realm?.pubkey, client]); } diff --git a/VoteStakeRegistry/sdk/accounts.tsx b/VoteStakeRegistry/sdk/accounts.tsx index 37f6716e11..fa74795a3c 100644 --- a/VoteStakeRegistry/sdk/accounts.tsx +++ b/VoteStakeRegistry/sdk/accounts.tsx @@ -1,101 +1,101 @@ -import { BN } from '@project-serum/anchor' -import { MintInfo } from '@solana/spl-token' -import { PublicKey } from '@solana/web3.js' -import { TokenProgramAccount } from '@utils/tokens' +import { BN } from '@project-serum/anchor'; +import { MintInfo } from '@solana/spl-token'; +import { PublicKey } from '@solana/web3.js'; +import { TokenProgramAccount } from '@utils/tokens'; export interface Voter { - deposits: Deposit[] - voterAuthority: PublicKey - registrar: PublicKey + deposits: Deposit[]; + voterAuthority: PublicKey; + registrar: PublicKey; //there are more fields but no use for them on ui yet } export interface votingMint { - baselineVoteWeightScaledFactor: BN - digitShift: number - grantAuthority: PublicKey - lockupSaturationSecs: BN - maxExtraLockupVoteWeightScaledFactor: BN - mint: PublicKey + baselineVoteWeightScaledFactor: BN; + digitShift: number; + grantAuthority: PublicKey; + lockupSaturationSecs: BN; + maxExtraLockupVoteWeightScaledFactor: BN; + mint: PublicKey; } -export type LockupType = 'none' | 'monthly' | 'cliff' | 'constant' | 'daily' //there is also daily type but not used on ui yet +export type LockupType = 'none' | 'monthly' | 'cliff' | 'constant' | 'daily'; //there is also daily type but not used on ui yet export interface Registrar { - governanceProgramId: PublicKey - realm: PublicKey - realmAuthority: PublicKey - realmGoverningTokenMint: PublicKey - votingMints: votingMint[] + governanceProgramId: PublicKey; + realm: PublicKey; + realmAuthority: PublicKey; + realmGoverningTokenMint: PublicKey; + votingMints: votingMint[]; //there are more fields but no use for them on ui yet } interface LockupKind { - none: object - daily: object - monthly: object - cliff: object - constant: object + none: object; + daily: object; + monthly: object; + cliff: object; + constant: object; } interface Lockup { - endTs: BN - kind: LockupKind - startTs: BN + endTs: BN; + kind: LockupKind; + startTs: BN; } export interface Deposit { - allowClawback: boolean - amountDepositedNative: BN - amountInitiallyLockedNative: BN - isUsed: boolean - lockup: Lockup - votingMintConfigIdx: number + allowClawback: boolean; + amountDepositedNative: BN; + amountInitiallyLockedNative: BN; + isUsed: boolean; + lockup: Lockup; + votingMintConfigIdx: number; } export interface DepositWithMintAccount extends Deposit { - mint: TokenProgramAccount - index: number - available: BN - vestingRate: BN | null - currentlyLocked: BN - nextVestingTimestamp: BN | null - votingPower: BN - votingPowerBaseline: BN + mint: TokenProgramAccount; + index: number; + available: BN; + vestingRate: BN | null; + currentlyLocked: BN; + nextVestingTimestamp: BN | null; + votingPower: BN; + votingPowerBaseline: BN; } -export const unusedMintPk = '11111111111111111111111111111111' +export const unusedMintPk = '11111111111111111111111111111111'; export const getRegistrarPDA = async ( realmPk: PublicKey, mint: PublicKey, - clientProgramId: PublicKey + clientProgramId: PublicKey, ) => { const [registrar, registrarBump] = await PublicKey.findProgramAddress( [realmPk.toBuffer(), Buffer.from('registrar'), mint.toBuffer()], - clientProgramId - ) + clientProgramId, + ); return { registrar, registrarBump, - } -} + }; +}; export const getVoterPDA = async ( registrar: PublicKey, walletPk: PublicKey, - clientProgramId: PublicKey + clientProgramId: PublicKey, ) => { const [voter, voterBump] = await PublicKey.findProgramAddress( [registrar.toBuffer(), Buffer.from('voter'), walletPk.toBuffer()], - clientProgramId - ) + clientProgramId, + ); return { voter, voterBump, - } -} + }; +}; export const getVoterWeightPDA = async ( registrar: PublicKey, walletPk: PublicKey, - clientProgramId: PublicKey + clientProgramId: PublicKey, ) => { const [voterWeightPk, voterWeightBump] = await PublicKey.findProgramAddress( [ @@ -103,11 +103,11 @@ export const getVoterWeightPDA = async ( Buffer.from('voter-weight-record'), walletPk.toBuffer(), ], - clientProgramId - ) + clientProgramId, + ); return { voterWeightPk, voterWeightBump, - } -} + }; +}; diff --git a/VoteStakeRegistry/sdk/api.ts b/VoteStakeRegistry/sdk/api.ts index 36ae0f89e4..3cab16be05 100644 --- a/VoteStakeRegistry/sdk/api.ts +++ b/VoteStakeRegistry/sdk/api.ts @@ -1,40 +1,40 @@ -import { VsrClient } from '@blockworks-foundation/voter-stake-registry-client' -import { PublicKey } from '@solana/web3.js' -import { Registrar, Voter } from './accounts' +import { VsrClient } from '@blockworks-foundation/voter-stake-registry-client'; +import { PublicKey } from '@solana/web3.js'; +import { Registrar, Voter } from './accounts'; export const tryGetVoter = async (voterPk: PublicKey, client: VsrClient) => { try { - const voter = await client?.program.account.voter.fetch(voterPk) - return voter as Voter + const voter = await client?.program.account.voter.fetch(voterPk); + return voter as Voter; } catch (e) { - return null + return null; } -} +}; export const tryGetRegistrar = async ( registrarPk: PublicKey, - client: VsrClient + client: VsrClient, ) => { try { const existingRegistrar = await client.program.account.registrar.fetch( - registrarPk - ) - return existingRegistrar as Registrar + registrarPk, + ); + return existingRegistrar as Registrar; } catch (e) { - return null + return null; } -} +}; export const getMintCfgIdx = async ( registrarPk: PublicKey, mintPK: PublicKey, - client: VsrClient + client: VsrClient, ) => { - const existingRegistrar = await tryGetRegistrar(registrarPk, client) + const existingRegistrar = await tryGetRegistrar(registrarPk, client); const mintCfgIdx = existingRegistrar?.votingMints.findIndex( - (x) => x.mint.toBase58() === mintPK.toBase58() - ) + (x) => x.mint.toBase58() === mintPK.toBase58(), + ); if (mintCfgIdx === null || mintCfgIdx === -1) { - throw 'mint not configured to use' + throw 'mint not configured to use'; } - return mintCfgIdx -} + return mintCfgIdx; +}; diff --git a/VoteStakeRegistry/sdk/withCreateNewDeposit.ts b/VoteStakeRegistry/sdk/withCreateNewDeposit.ts index e056cc6c86..b26388fd43 100644 --- a/VoteStakeRegistry/sdk/withCreateNewDeposit.ts +++ b/VoteStakeRegistry/sdk/withCreateNewDeposit.ts @@ -4,23 +4,23 @@ import { SYSVAR_INSTRUCTIONS_PUBKEY, SYSVAR_RENT_PUBKEY, TransactionInstruction, -} from '@solana/web3.js' -import { withCreateTokenOwnerRecord } from '@solana/spl-governance' +} from '@solana/web3.js'; +import { withCreateTokenOwnerRecord } from '@solana/spl-governance'; import { ASSOCIATED_TOKEN_PROGRAM_ID, Token, TOKEN_PROGRAM_ID, -} from '@solana/spl-token' -import { BN } from '@project-serum/anchor' +} from '@solana/spl-token'; +import { BN } from '@project-serum/anchor'; import { getRegistrarPDA, getVoterPDA, getVoterWeightPDA, LockupType, -} from 'VoteStakeRegistry/sdk/accounts' -import { VsrClient } from '@blockworks-foundation/voter-stake-registry-client' -import { getMintCfgIdx, tryGetVoter } from './api' -import { getPeriod } from 'VoteStakeRegistry/tools/deposits' +} from 'VoteStakeRegistry/sdk/accounts'; +import { VsrClient } from '@blockworks-foundation/voter-stake-registry-client'; +import { getMintCfgIdx, tryGetVoter } from './api'; +import { getPeriod } from 'VoteStakeRegistry/tools/deposits'; export const withCreateNewDeposit = async ({ instructions, @@ -34,47 +34,47 @@ export const withCreateNewDeposit = async ({ lockupKind, client, }: { - instructions: TransactionInstruction[] - walletPk: PublicKey - mintPk: PublicKey - communityMintPk: PublicKey - realmPk: PublicKey - programId: PublicKey - tokenOwnerRecordPk: PublicKey | null - lockUpPeriodInDays: number - lockupKind: LockupType - client?: VsrClient + instructions: TransactionInstruction[]; + walletPk: PublicKey; + mintPk: PublicKey; + communityMintPk: PublicKey; + realmPk: PublicKey; + programId: PublicKey; + tokenOwnerRecordPk: PublicKey | null; + lockUpPeriodInDays: number; + lockupKind: LockupType; + client?: VsrClient; }) => { if (!client) { - throw 'no vote registry plugin' + throw 'no vote registry plugin'; } - const systemProgram = SystemProgram.programId - const clientProgramId = client!.program.programId - let tokenOwnerRecordPubKey = tokenOwnerRecordPk + const systemProgram = SystemProgram.programId; + const clientProgramId = client!.program.programId; + let tokenOwnerRecordPubKey = tokenOwnerRecordPk; const { registrar } = await getRegistrarPDA( realmPk, communityMintPk, - clientProgramId - ) + clientProgramId, + ); const { voter, voterBump } = await getVoterPDA( registrar, walletPk, - clientProgramId - ) + clientProgramId, + ); const { voterWeightPk, voterWeightBump } = await getVoterWeightPDA( registrar, walletPk, - clientProgramId - ) - const existingVoter = await tryGetVoter(voter, client) + clientProgramId, + ); + const existingVoter = await tryGetVoter(voter, client); const voterATAPk = await Token.getAssociatedTokenAddress( ASSOCIATED_TOKEN_PROGRAM_ID, TOKEN_PROGRAM_ID, mintPk, - voter - ) + voter, + ); //spl governance tokenownerrecord pubkey if (!tokenOwnerRecordPubKey) { @@ -84,8 +84,8 @@ export const withCreateNewDeposit = async ({ realmPk, walletPk, mintPk, - walletPk - ) + walletPk, + ); } if (!existingVoter) { instructions.push( @@ -100,10 +100,10 @@ export const withCreateNewDeposit = async ({ rent: SYSVAR_RENT_PUBKEY, instructions: SYSVAR_INSTRUCTIONS_PUBKEY, }, - }) - ) + }), + ); } - const mintCfgIdx = await getMintCfgIdx(registrar, mintPk, client) + const mintCfgIdx = await getMintCfgIdx(registrar, mintPk, client); //none type deposits are used only to store tokens that will be withdrawable immediately so there is no need to create new every time and there should be one per mint //for other kinds of deposits we always want to create new deposit @@ -113,25 +113,26 @@ export const withCreateNewDeposit = async ({ (x) => x.isUsed && typeof x.lockup.kind[lockupKind] !== 'undefined' && - x.votingMintConfigIdx === mintCfgIdx + x.votingMintConfigIdx === mintCfgIdx, ) - : -1 + : -1; const createNewDeposit = typeof indexOfNoneTypeDeposit === 'undefined' || - indexOfNoneTypeDeposit === -1 + indexOfNoneTypeDeposit === -1; - const firstFreeIdx = existingVoter?.deposits?.findIndex((x) => !x.isUsed) || 0 + const firstFreeIdx = + existingVoter?.deposits?.findIndex((x) => !x.isUsed) || 0; if (firstFreeIdx === -1 && createNewDeposit) { - throw 'User has to much active deposits' + throw 'User has to much active deposits'; } if (createNewDeposit) { //in case we do monthly close up we pass months not days. - const period = getPeriod(lockUpPeriodInDays, lockupKind) - const allowClawback = false - const startTime = new BN(new Date().getTime() / 1000) + const period = getPeriod(lockUpPeriodInDays, lockupKind); + const allowClawback = false; + const startTime = new BN(new Date().getTime() / 1000); const createDepositEntryInstruction = client?.program.instruction.createDepositEntry( firstFreeIdx, { [lockupKind]: {} }, @@ -151,12 +152,12 @@ export const withCreateNewDeposit = async ({ associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID, vault: voterATAPk, }, - } - ) - instructions.push(createDepositEntryInstruction) + }, + ); + instructions.push(createDepositEntryInstruction); } - const depositIdx = !createNewDeposit ? indexOfNoneTypeDeposit! : firstFreeIdx + const depositIdx = !createNewDeposit ? indexOfNoneTypeDeposit! : firstFreeIdx; return { depositIdx, registrar, @@ -164,5 +165,5 @@ export const withCreateNewDeposit = async ({ voter, tokenOwnerRecordPubKey, voterWeightPk, - } -} + }; +}; diff --git a/VoteStakeRegistry/sdk/withUpdateVoterWeightRecord.ts b/VoteStakeRegistry/sdk/withUpdateVoterWeightRecord.ts index e1a5e5500c..751a83d1ae 100644 --- a/VoteStakeRegistry/sdk/withUpdateVoterWeightRecord.ts +++ b/VoteStakeRegistry/sdk/withUpdateVoterWeightRecord.ts @@ -1,43 +1,43 @@ -import { VsrClient } from '@blockworks-foundation/voter-stake-registry-client' +import { VsrClient } from '@blockworks-foundation/voter-stake-registry-client'; import { ProgramAccount, Realm, SYSTEM_PROGRAM_ID, -} from '@solana/spl-governance' -import { PublicKey, TransactionInstruction } from '@solana/web3.js' +} from '@solana/spl-governance'; +import { PublicKey, TransactionInstruction } from '@solana/web3.js'; import { getRegistrarPDA, getVoterPDA, getVoterWeightPDA, -} from 'VoteStakeRegistry/sdk/accounts' +} from 'VoteStakeRegistry/sdk/accounts'; export const withUpdateVoterWeightRecord = async ( instructions: TransactionInstruction[], walletPk: PublicKey, realm: ProgramAccount, - client?: VsrClient + client?: VsrClient, ) => { //if no plugin then we dont do anything if (!realm.account.config.useCommunityVoterWeightAddin) { - return + return; } if (!client) { - throw 'no vote registry plugin' + throw 'no vote registry plugin'; } - const clientProgramId = client!.program.programId + const clientProgramId = client!.program.programId; //TODO support both mints for now only community is supported const { registrar } = await getRegistrarPDA( realm.pubkey, realm.account.communityMint, - client!.program.programId - ) - const { voter } = await getVoterPDA(registrar, walletPk, clientProgramId) + client!.program.programId, + ); + const { voter } = await getVoterPDA(registrar, walletPk, clientProgramId); const { voterWeightPk } = await getVoterWeightPDA( registrar, walletPk, - clientProgramId - ) + clientProgramId, + ); instructions.push( client.program.instruction.updateVoterWeightRecord({ @@ -47,7 +47,7 @@ export const withUpdateVoterWeightRecord = async ( voterWeightRecord: voterWeightPk, systemProgram: SYSTEM_PROGRAM_ID, }, - }) - ) - return voterWeightPk -} + }), + ); + return voterWeightPk; +}; diff --git a/VoteStakeRegistry/sdk/withVoteRegistryDeposit.ts b/VoteStakeRegistry/sdk/withVoteRegistryDeposit.ts index ee06657f50..7c75db6b8f 100644 --- a/VoteStakeRegistry/sdk/withVoteRegistryDeposit.ts +++ b/VoteStakeRegistry/sdk/withVoteRegistryDeposit.ts @@ -1,9 +1,9 @@ -import { PublicKey, TransactionInstruction } from '@solana/web3.js' -import { TOKEN_PROGRAM_ID } from '@solana/spl-token' -import { BN } from '@project-serum/anchor' -import { LockupType } from 'VoteStakeRegistry/sdk/accounts' -import { VsrClient } from '@blockworks-foundation/voter-stake-registry-client' -import { withCreateNewDeposit } from './withCreateNewDeposit' +import { PublicKey, TransactionInstruction } from '@solana/web3.js'; +import { TOKEN_PROGRAM_ID } from '@solana/spl-token'; +import { BN } from '@project-serum/anchor'; +import { LockupType } from 'VoteStakeRegistry/sdk/accounts'; +import { VsrClient } from '@blockworks-foundation/voter-stake-registry-client'; +import { withCreateNewDeposit } from './withCreateNewDeposit'; export const withVoteRegistryDeposit = async ({ instructions, @@ -19,22 +19,22 @@ export const withVoteRegistryDeposit = async ({ communityMintPk, client, }: { - instructions: TransactionInstruction[] - walletPk: PublicKey + instructions: TransactionInstruction[]; + walletPk: PublicKey; //from where we deposit our founds - fromPk: PublicKey - mintPk: PublicKey - realmPk: PublicKey - programId: PublicKey - amount: BN - communityMintPk: PublicKey - tokenOwnerRecordPk: PublicKey | null - lockUpPeriodInDays: number - lockupKind: LockupType - client?: VsrClient + fromPk: PublicKey; + mintPk: PublicKey; + realmPk: PublicKey; + programId: PublicKey; + amount: BN; + communityMintPk: PublicKey; + tokenOwnerRecordPk: PublicKey | null; + lockUpPeriodInDays: number; + lockupKind: LockupType; + client?: VsrClient; }) => { if (!client) { - throw 'no vote registry plugin' + throw 'no vote registry plugin'; } const { @@ -53,7 +53,7 @@ export const withVoteRegistryDeposit = async ({ lockupKind, communityMintPk, client, - }) + }); const depositInstruction = client?.program.instruction.deposit( depositIdx, amount, @@ -66,7 +66,7 @@ export const withVoteRegistryDeposit = async ({ depositAuthority: walletPk, tokenProgram: TOKEN_PROGRAM_ID, }, - } - ) - instructions.push(depositInstruction) -} + }, + ); + instructions.push(depositInstruction); +}; diff --git a/VoteStakeRegistry/sdk/withVoteRegistryWithdraw.ts b/VoteStakeRegistry/sdk/withVoteRegistryWithdraw.ts index a79f8656ac..ac3a4bce65 100644 --- a/VoteStakeRegistry/sdk/withVoteRegistryWithdraw.ts +++ b/VoteStakeRegistry/sdk/withVoteRegistryWithdraw.ts @@ -1,17 +1,17 @@ -import { Connection, PublicKey, TransactionInstruction } from '@solana/web3.js' +import { Connection, PublicKey, TransactionInstruction } from '@solana/web3.js'; import { ASSOCIATED_TOKEN_PROGRAM_ID, Token, TOKEN_PROGRAM_ID, -} from '@solana/spl-token' -import { BN } from '@project-serum/anchor' +} from '@solana/spl-token'; +import { BN } from '@project-serum/anchor'; import { getRegistrarPDA, getVoterPDA, getVoterWeightPDA, -} from 'VoteStakeRegistry/sdk/accounts' -import { VsrClient } from '@blockworks-foundation/voter-stake-registry-client' -import { tryGetTokenAccount } from '@utils/tokens' +} from 'VoteStakeRegistry/sdk/accounts'; +import { VsrClient } from '@blockworks-foundation/voter-stake-registry-client'; +import { tryGetTokenAccount } from '@utils/tokens'; export const withVoteRegistryWithdraw = async ({ instructions, @@ -26,50 +26,50 @@ export const withVoteRegistryWithdraw = async ({ client, connection, }: { - instructions: TransactionInstruction[] - walletPk: PublicKey - mintPk: PublicKey - realmPk: PublicKey - communityMintPk: PublicKey - amount: BN - tokenOwnerRecordPubKey: PublicKey - depositIndex: number - connection: Connection + instructions: TransactionInstruction[]; + walletPk: PublicKey; + mintPk: PublicKey; + realmPk: PublicKey; + communityMintPk: PublicKey; + amount: BN; + tokenOwnerRecordPubKey: PublicKey; + depositIndex: number; + connection: Connection; //if we want to close deposit after doing operation we need to fill this because we can close only deposits that have 0 tokens inside - closeDepositAfterOperation?: boolean - client?: VsrClient + closeDepositAfterOperation?: boolean; + client?: VsrClient; }) => { if (!client) { - throw 'no vote registry plugin' + throw 'no vote registry plugin'; } - const clientProgramId = client!.program.programId + const clientProgramId = client!.program.programId; const { registrar } = await getRegistrarPDA( realmPk, communityMintPk, - client!.program.programId - ) - const { voter } = await getVoterPDA(registrar, walletPk, clientProgramId) + client!.program.programId, + ); + const { voter } = await getVoterPDA(registrar, walletPk, clientProgramId); const { voterWeightPk } = await getVoterWeightPDA( registrar, walletPk, - clientProgramId - ) + clientProgramId, + ); const voterATAPk = await Token.getAssociatedTokenAddress( ASSOCIATED_TOKEN_PROGRAM_ID, TOKEN_PROGRAM_ID, mintPk, - voter - ) + voter, + ); const ataPk = await Token.getAssociatedTokenAddress( ASSOCIATED_TOKEN_PROGRAM_ID, // always ASSOCIATED_TOKEN_PROGRAM_ID TOKEN_PROGRAM_ID, // always TOKEN_PROGRAM_ID mintPk, // mint - walletPk // owner - ) - const isExistingAta = await tryGetTokenAccount(connection, ataPk) + walletPk, // owner + ); + const isExistingAta = await tryGetTokenAccount(connection, ataPk); if (!isExistingAta) { instructions.push( Token.createAssociatedTokenAccountInstruction( @@ -78,9 +78,9 @@ export const withVoteRegistryWithdraw = async ({ mintPk, // mint ataPk, // ata walletPk, // owner of token account - walletPk // fee payer - ) - ) + walletPk, // fee payer + ), + ); } instructions.push( @@ -95,8 +95,8 @@ export const withVoteRegistryWithdraw = async ({ destination: ataPk, tokenProgram: TOKEN_PROGRAM_ID, }, - }) - ) + }), + ); if (closeDepositAfterOperation) { const close = client.program.instruction.closeDepositEntry(depositIndex, { @@ -104,7 +104,7 @@ export const withVoteRegistryWithdraw = async ({ voter: voter, voterAuthority: walletPk, }, - }) - instructions.push(close) + }); + instructions.push(close); } -} +}; diff --git a/VoteStakeRegistry/stores/useDepositStore.tsx b/VoteStakeRegistry/stores/useDepositStore.tsx index 35182b2697..c2957846cb 100644 --- a/VoteStakeRegistry/stores/useDepositStore.tsx +++ b/VoteStakeRegistry/stores/useDepositStore.tsx @@ -1,17 +1,18 @@ -import create, { State } from 'zustand' -import { DepositWithMintAccount } from 'VoteStakeRegistry/sdk/accounts' -import { Connection, PublicKey } from '@solana/web3.js' -import { VsrClient } from '@blockworks-foundation/voter-stake-registry-client' -import { BN } from '@blockworks-foundation/voter-stake-registry-client/node_modules/@project-serum/anchor' -import { getDeposits } from 'VoteStakeRegistry/tools/deposits' +import create, { State } from 'zustand'; +import { DepositWithMintAccount } from 'VoteStakeRegistry/sdk/accounts'; +import { Connection, PublicKey } from '@solana/web3.js'; +import { VsrClient } from '@blockworks-foundation/voter-stake-registry-client'; +import { BN } from '@blockworks-foundation/voter-stake-registry-client/node_modules/@project-serum/anchor'; +import { getDeposits } from 'VoteStakeRegistry/tools/deposits'; +import { BN_ZERO } from '@utils/helpers'; interface DepositStore extends State { state: { - deposits: DepositWithMintAccount[] - votingPower: BN - votingPowerFromDeposits: BN - } - resetDepositState: () => void + deposits: DepositWithMintAccount[]; + votingPower: BN; + votingPowerFromDeposits: BN; + }; + resetDepositState: () => void; getOwnedDeposits: ({ isUsed, realmPk, @@ -20,20 +21,20 @@ interface DepositStore extends State { client, connection, }: { - isUsed?: boolean | undefined - realmPk: PublicKey - walletPk: PublicKey - communityMintPk: PublicKey - client: VsrClient - connection: Connection - }) => Promise + isUsed?: boolean | undefined; + realmPk: PublicKey; + walletPk: PublicKey; + communityMintPk: PublicKey; + client: VsrClient; + connection: Connection; + }) => Promise; } const defaultState = { deposits: [], - votingPower: new BN(0), - votingPowerFromDeposits: new BN(0), -} + votingPower: BN_ZERO, + votingPowerFromDeposits: BN_ZERO, +}; const useDepositStore = create((set, _get) => ({ state: { @@ -41,8 +42,8 @@ const useDepositStore = create((set, _get) => ({ }, resetDepositState: () => { set((s) => { - s.state = { ...defaultState } - }) + s.state = { ...defaultState }; + }); }, getOwnedDeposits: async ({ isUsed = true, @@ -63,14 +64,14 @@ const useDepositStore = create((set, _get) => ({ communityMintPk, client, connection, - }) + }); set((s) => { - s.state.votingPower = votingPower - s.state.deposits = deposits - s.state.votingPowerFromDeposits = votingPowerFromDeposits - }) + s.state.votingPower = votingPower; + s.state.deposits = deposits; + s.state.votingPowerFromDeposits = votingPowerFromDeposits; + }); }, -})) +})); -export default useDepositStore +export default useDepositStore; diff --git a/VoteStakeRegistry/stores/voteStakeRegistryClientStore.tsx b/VoteStakeRegistry/stores/voteStakeRegistryClientStore.tsx index 2aa2795c5a..5725afa1c6 100644 --- a/VoteStakeRegistry/stores/voteStakeRegistryClientStore.tsx +++ b/VoteStakeRegistry/stores/voteStakeRegistryClientStore.tsx @@ -1,32 +1,32 @@ -import create, { State } from 'zustand' -import { VsrClient } from '@blockworks-foundation/voter-stake-registry-client' -import { getRegistrarPDA, Registrar } from 'VoteStakeRegistry/sdk/accounts' -import { Provider } from '@project-serum/anchor' -import { Wallet } from '@project-serum/sol-wallet-adapter' -import { tryGetRegistrar } from 'VoteStakeRegistry/sdk/api' -import { SignerWalletAdapter } from '@solana/wallet-adapter-base' -import { ConnectionContext } from '@utils/connection' -import { ProgramAccount, Realm } from '@solana/spl-governance' +import create, { State } from 'zustand'; +import { VsrClient } from '@blockworks-foundation/voter-stake-registry-client'; +import { getRegistrarPDA, Registrar } from 'VoteStakeRegistry/sdk/accounts'; +import { Provider } from '@project-serum/anchor'; +import { Wallet } from '@project-serum/sol-wallet-adapter'; +import { tryGetRegistrar } from 'VoteStakeRegistry/sdk/api'; +import { SignerWalletAdapter } from '@solana/wallet-adapter-base'; +import { ConnectionContext } from '@utils/connection'; +import { ProgramAccount, Realm } from '@solana/spl-governance'; interface useVoteStakeRegistryClientStore extends State { state: { - client: VsrClient | undefined - communityMintRegistrar: Registrar | null - } + client: VsrClient | undefined; + communityMintRegistrar: Registrar | null; + }; handleSetClient: ( wallet: SignerWalletAdapter | undefined, - connection: ConnectionContext - ) => void + connection: ConnectionContext, + ) => void; handleSetRegistrar: ( client: VsrClient, - realm: ProgramAccount | undefined - ) => void + realm: ProgramAccount | undefined, + ) => void; } const defaultState = { client: undefined, communityMintRegistrar: null, -} +}; const useVoteStakeRegistryClientStore = create( (set, _get) => ({ @@ -34,33 +34,33 @@ const useVoteStakeRegistryClientStore = create( ...defaultState, }, handleSetClient: async (wallet, connection) => { - const options = Provider.defaultOptions() + const options = Provider.defaultOptions(); const provider = new Provider( connection.current, (wallet as unknown) as Wallet, - options - ) + options, + ); const vsrClient = await VsrClient.connect( provider, - connection.cluster === 'devnet' - ) + connection.cluster === 'devnet', + ); set((s) => { - s.state.client = vsrClient - }) + s.state.client = vsrClient; + }); }, handleSetRegistrar: async (client, realm) => { - const clientProgramId = client!.program.programId + const clientProgramId = client!.program.programId; const { registrar } = await getRegistrarPDA( realm!.pubkey, realm!.account.communityMint, - clientProgramId - ) - const existingRegistrar = await tryGetRegistrar(registrar, client!) + clientProgramId, + ); + const existingRegistrar = await tryGetRegistrar(registrar, client!); set((s) => { - s.state.communityMintRegistrar = existingRegistrar - }) + s.state.communityMintRegistrar = existingRegistrar; + }); }, - }) -) + }), +); -export default useVoteStakeRegistryClientStore +export default useVoteStakeRegistryClientStore; diff --git a/VoteStakeRegistry/tools/dateTools.ts b/VoteStakeRegistry/tools/dateTools.ts index f20e6b1777..c1c61d5d9e 100644 --- a/VoteStakeRegistry/tools/dateTools.ts +++ b/VoteStakeRegistry/tools/dateTools.ts @@ -1,70 +1,70 @@ -import { BN } from '@project-serum/anchor' -import { DepositWithMintAccount } from 'VoteStakeRegistry/sdk/accounts' +import { BN } from '@project-serum/anchor'; +import { DepositWithMintAccount } from 'VoteStakeRegistry/sdk/accounts'; -export const DAYS_PER_YEAR = 365 -export const SECS_PER_DAY = 86400 -export const DAYS_PER_MONTH = DAYS_PER_YEAR / 12 -export const SECS_PER_MONTH = DAYS_PER_MONTH * SECS_PER_DAY -export const HOURS_PER_DAY = 24 -export const MINS_PER_HOUR = 60 +export const DAYS_PER_YEAR = 365; +export const SECS_PER_DAY = 86400; +export const DAYS_PER_MONTH = DAYS_PER_YEAR / 12; +export const SECS_PER_MONTH = DAYS_PER_MONTH * SECS_PER_DAY; +export const HOURS_PER_DAY = 24; +export const MINS_PER_HOUR = 60; export function getFormattedStringFromDays( numberOfDays: number, - fullFormat = false + fullFormat = false, ) { - const years = Math.floor(numberOfDays / DAYS_PER_YEAR) - const months = Math.floor((numberOfDays % DAYS_PER_YEAR) / DAYS_PER_MONTH) - const days = Math.floor((numberOfDays % DAYS_PER_YEAR) % DAYS_PER_MONTH) - const hours = (numberOfDays - Math.floor(numberOfDays)) * HOURS_PER_DAY - const hoursInt = Math.floor(hours) - const minutes = Math.floor((hours - hoursInt) * MINS_PER_HOUR) - const yearSuffix = years > 1 ? 'years' : 'year' - const monthSuffix = months > 1 ? 'months' : 'month' - const daysSuffix = days > 1 ? 'days' : 'day' + const years = Math.floor(numberOfDays / DAYS_PER_YEAR); + const months = Math.floor((numberOfDays % DAYS_PER_YEAR) / DAYS_PER_MONTH); + const days = Math.floor((numberOfDays % DAYS_PER_YEAR) % DAYS_PER_MONTH); + const hours = (numberOfDays - Math.floor(numberOfDays)) * HOURS_PER_DAY; + const hoursInt = Math.floor(hours); + const minutes = Math.floor((hours - hoursInt) * MINS_PER_HOUR); + const yearSuffix = years > 1 ? 'years' : 'year'; + const monthSuffix = months > 1 ? 'months' : 'month'; + const daysSuffix = days > 1 ? 'days' : 'day'; const yearsDisplay = - years > 0 ? years + ` ${fullFormat ? yearSuffix : 'y'} ` : '' + years > 0 ? years + ` ${fullFormat ? yearSuffix : 'y'} ` : ''; const monthsDisplay = - months > 0 ? months + ` ${fullFormat ? monthSuffix : 'm'} ` : '' + months > 0 ? months + ` ${fullFormat ? monthSuffix : 'm'} ` : ''; const daysDisplay = - days > 0 ? days + ` ${fullFormat ? daysSuffix : 'd'} ` : '' - const hoursDisplay = hours > 0 ? ` ${hoursInt} h ${minutes} min` : '' + days > 0 ? days + ` ${fullFormat ? daysSuffix : 'd'} ` : ''; + const hoursDisplay = hours > 0 ? ` ${hoursInt} h ${minutes} min` : ''; const text = !years && !months && days <= 1 ? daysDisplay + hoursDisplay - : yearsDisplay + monthsDisplay + daysDisplay - return text ? text : 0 + : yearsDisplay + monthsDisplay + daysDisplay; + return text ? text : 0; } export const yearsToDays = (years: number) => { - return DAYS_PER_YEAR * years -} + return DAYS_PER_YEAR * years; +}; export const daysToYear = (days: number) => { - return days / DAYS_PER_YEAR -} + return days / DAYS_PER_YEAR; +}; export const yearsToSecs = (years: number) => { - return DAYS_PER_YEAR * years * SECS_PER_DAY -} + return DAYS_PER_YEAR * years * SECS_PER_DAY; +}; export const secsToDays = (secs: number) => { - return secs / SECS_PER_DAY -} + return secs / SECS_PER_DAY; +}; export const daysToMonths = (days: number) => { - return days / DAYS_PER_MONTH -} + return days / DAYS_PER_MONTH; +}; export const getMinDurationFmt = (deposit: DepositWithMintAccount) => { - return getFormattedStringFromDays(getMinDurationInDays(deposit)) -} + return getFormattedStringFromDays(getMinDurationInDays(deposit)); +}; export const getTimeLeftFromNowFmt = (deposit: DepositWithMintAccount) => { - const dateNowSecTimeStampBN = new BN(new Date().getTime() / 1000) + const dateNowSecTimeStampBN = new BN(new Date().getTime() / 1000); return getFormattedStringFromDays( - deposit.lockup.endTs.sub(dateNowSecTimeStampBN).toNumber() / SECS_PER_DAY - ) -} + deposit.lockup.endTs.sub(dateNowSecTimeStampBN).toNumber() / SECS_PER_DAY, + ); +}; export const getMinDurationInDays = (deposit: DepositWithMintAccount) => { return ( deposit.lockup.endTs.sub(deposit.lockup.startTs).toNumber() / SECS_PER_DAY - ) -} + ); +}; diff --git a/VoteStakeRegistry/tools/deposits.ts b/VoteStakeRegistry/tools/deposits.ts index db3e1a5594..6794c082b3 100644 --- a/VoteStakeRegistry/tools/deposits.ts +++ b/VoteStakeRegistry/tools/deposits.ts @@ -1,15 +1,13 @@ -import { VsrClient } from '@blockworks-foundation/voter-stake-registry-client' -import { - BN, - EventParser, -} from '@blockworks-foundation/voter-stake-registry-client/node_modules/@project-serum/anchor' +import { VsrClient } from '@blockworks-foundation/voter-stake-registry-client'; +import { EventParser } from '@blockworks-foundation/voter-stake-registry-client/node_modules/@project-serum/anchor'; import { ProgramAccount, Realm, simulateTransaction, -} from '@solana/spl-governance' -import { PublicKey, Transaction, Connection } from '@solana/web3.js' -import { tryGetMint } from '@utils/tokens' +} from '@solana/spl-governance'; +import { PublicKey, Transaction, Connection } from '@solana/web3.js'; +import { BN_ZERO } from '@utils/helpers'; +import { tryGetMint } from '@utils/tokens'; import { getRegistrarPDA, getVoterPDA, @@ -17,10 +15,10 @@ import { DepositWithMintAccount, LockupType, Registrar, -} from 'VoteStakeRegistry/sdk/accounts' -import { tryGetVoter, tryGetRegistrar } from 'VoteStakeRegistry/sdk/api' -import { DAYS_PER_MONTH } from './dateTools' -import { MONTHLY } from './types' +} from 'VoteStakeRegistry/sdk/accounts'; +import { tryGetVoter, tryGetRegistrar } from 'VoteStakeRegistry/sdk/api'; +import { DAYS_PER_MONTH } from './dateTools'; +import { MONTHLY } from './types'; export const getDeposits = async ({ isUsed = true, @@ -30,31 +28,31 @@ export const getDeposits = async ({ client, connection, }: { - isUsed?: boolean | undefined - realmPk: PublicKey - walletPk: PublicKey - communityMintPk: PublicKey - client: VsrClient - connection: Connection + isUsed?: boolean | undefined; + realmPk: PublicKey; + walletPk: PublicKey; + communityMintPk: PublicKey; + client: VsrClient; + connection: Connection; }) => { - const clientProgramId = client.program.programId + const clientProgramId = client.program.programId; const { registrar } = await getRegistrarPDA( realmPk, communityMintPk, - clientProgramId - ) - const { voter } = await getVoterPDA(registrar, walletPk, clientProgramId) - const existingVoter = await tryGetVoter(voter, client) - const existingRegistrar = await tryGetRegistrar(registrar, client) - const mintCfgs = existingRegistrar?.votingMints || [] - const mints = {} - let votingPower = new BN(0) - let votingPowerFromDeposits = new BN(0) - let deposits: DepositWithMintAccount[] = [] + clientProgramId, + ); + const { voter } = await getVoterPDA(registrar, walletPk, clientProgramId); + const existingVoter = await tryGetVoter(voter, client); + const existingRegistrar = await tryGetRegistrar(registrar, client); + const mintCfgs = existingRegistrar?.votingMints || []; + const mints = {}; + let votingPower = BN_ZERO; + let votingPowerFromDeposits = BN_ZERO; + let deposits: DepositWithMintAccount[] = []; for (const i of mintCfgs) { if (i.mint.toBase58() !== unusedMintPk) { - const mint = await tryGetMint(connection, i.mint) - mints[i.mint.toBase58()] = mint + const mint = await tryGetMint(connection, i.mint); + mints[i.mint.toBase58()] = mint; } } if (existingVoter) { @@ -65,11 +63,11 @@ export const getDeposits = async ({ ...x, mint: mints[mintCfgs![x.votingMintConfigIdx].mint.toBase58()], index: idx, - } as DepositWithMintAccount) + } as DepositWithMintAccount), ) - .filter((x) => typeof isUsed === 'undefined' || x.isUsed === isUsed) - const usedDeposits = deposits.filter((x) => x.isUsed) - const areThereAnyUsedDeposits = usedDeposits.length + .filter((x) => typeof isUsed === 'undefined' || x.isUsed === isUsed); + const usedDeposits = deposits.filter((x) => x.isUsed); + const areThereAnyUsedDeposits = usedDeposits.length; if (areThereAnyUsedDeposits) { const events = await getDepositsAdditionalInfoEvents( @@ -77,43 +75,43 @@ export const getDeposits = async ({ usedDeposits, connection, registrar, - voter - ) - const DEPOSIT_EVENT_NAME = 'DepositEntryInfo' - const VOTER_INFO_EVENT_NAME = 'VoterInfo' - const depositsInfo = events.filter((x) => x.name === DEPOSIT_EVENT_NAME) + voter, + ); + const DEPOSIT_EVENT_NAME = 'DepositEntryInfo'; + const VOTER_INFO_EVENT_NAME = 'VoterInfo'; + const depositsInfo = events.filter((x) => x.name === DEPOSIT_EVENT_NAME); const votingPowerEntry = events.find( - (x) => x.name === VOTER_INFO_EVENT_NAME - ) + (x) => x.name === VOTER_INFO_EVENT_NAME, + ); deposits = deposits.map((x) => { const additionalInfoData = depositsInfo.find( - (info) => info.data.depositEntryIndex === x.index - ).data + (info) => info.data.depositEntryIndex === x.index, + ).data; - x.currentlyLocked = additionalInfoData.locking?.amount || new BN(0) - x.available = additionalInfoData.unlocked || new BN(0) - x.vestingRate = additionalInfoData.locking?.vesting?.rate || new BN(0) + x.currentlyLocked = additionalInfoData.locking?.amount || BN_ZERO; + x.available = additionalInfoData.unlocked || BN_ZERO; + x.vestingRate = additionalInfoData.locking?.vesting?.rate || BN_ZERO; x.nextVestingTimestamp = - additionalInfoData.locking?.vesting?.nextTimestamp || null - x.votingPower = additionalInfoData.votingPower || new BN(0) + additionalInfoData.locking?.vesting?.nextTimestamp || null; + x.votingPower = additionalInfoData.votingPower || BN_ZERO; x.votingPowerBaseline = - additionalInfoData.votingPowerBaseline || new BN(0) - return x - }) + additionalInfoData.votingPowerBaseline || BN_ZERO; + return x; + }); if ( votingPowerEntry && !votingPowerEntry.data.votingPowerBaseline.isZero() ) { - votingPowerFromDeposits = votingPowerEntry.data.votingPowerBaseline + votingPowerFromDeposits = votingPowerEntry.data.votingPowerBaseline; } if (votingPowerEntry && !votingPowerEntry.data.votingPower.isZero()) { - votingPower = votingPowerEntry.data.votingPower + votingPower = votingPowerEntry.data.votingPower; } - return { votingPower, deposits, votingPowerFromDeposits } + return { votingPower, deposits, votingPowerFromDeposits }; } } - return { votingPower, deposits, votingPowerFromDeposits } -} + return { votingPower, deposits, votingPowerFromDeposits }; +}; export const calcMultiplier = ({ depositScaledFactor, @@ -121,110 +119,115 @@ export const calcMultiplier = ({ lockupSecs, lockupSaturationSecs, }: { - depositScaledFactor: number - maxExtraLockupVoteWeightScaledFactor: number - lockupSecs: number - lockupSaturationSecs: number + depositScaledFactor: number; + maxExtraLockupVoteWeightScaledFactor: number; + lockupSecs: number; + lockupSaturationSecs: number; }) => { const calc = (depositScaledFactor + (maxExtraLockupVoteWeightScaledFactor * Math.min(lockupSecs, lockupSaturationSecs)) / lockupSaturationSecs) / - depositScaledFactor - return depositScaledFactor !== 0 ? calc : 0 -} + depositScaledFactor; + return depositScaledFactor !== 0 ? calc : 0; +}; export const getPeriod = ( lockUpPeriodInDays: number, - lockupKind: LockupType + lockupKind: LockupType, ) => { //in case we do monthly close up we pass months not days. const period = lockupKind !== MONTHLY ? lockUpPeriodInDays - : lockUpPeriodInDays / DAYS_PER_MONTH - const maxMonthsNumber = 72 - const daysLimit = 2190 + : lockUpPeriodInDays / DAYS_PER_MONTH; + const maxMonthsNumber = 72; + const daysLimit = 2190; //additional prevention of lockup being to high in case of monthly lockup 72 months as 6 years //in case of other types 2190 days as 6 years if (lockupKind === MONTHLY && period > maxMonthsNumber) { - throw 'lockup period is to hight' + throw 'lockup period is to hight'; } if (lockupKind !== MONTHLY && period > daysLimit) { - throw 'lockup period is to hight' + throw 'lockup period is to hight'; } - return period -} + return period; +}; export const calcMintMultiplier = ( lockupSecs: number, registrar: Registrar | null, - realm: ProgramAccount | undefined + realm: ProgramAccount | undefined, ) => { - const mintCfgs = registrar?.votingMints + const mintCfgs = registrar?.votingMints; const mintCfg = mintCfgs?.find( - (x) => x.mint.toBase58() === realm?.account.communityMint.toBase58() - ) + (x) => x.mint.toBase58() === realm?.account.communityMint.toBase58(), + ); if (mintCfg) { const { lockupSaturationSecs, baselineVoteWeightScaledFactor, maxExtraLockupVoteWeightScaledFactor, - } = mintCfg - const depositScaledFactorNum = baselineVoteWeightScaledFactor.toNumber() - const maxExtraLockupVoteWeightScaledFactorNum = maxExtraLockupVoteWeightScaledFactor.toNumber() - const lockupSaturationSecsNum = lockupSaturationSecs.toNumber() + } = mintCfg; + const depositScaledFactorNum = baselineVoteWeightScaledFactor.toNumber(); + const maxExtraLockupVoteWeightScaledFactorNum = maxExtraLockupVoteWeightScaledFactor.toNumber(); + const lockupSaturationSecsNum = lockupSaturationSecs.toNumber(); //(deposit_scaled_factor + max_extra_lockup_vote_weight_scaled_factor * min(lockup_secs, lockup_saturation_secs) / lockup_saturation_secs) / deposit_scaled_factor const calced = calcMultiplier({ depositScaledFactor: depositScaledFactorNum, maxExtraLockupVoteWeightScaledFactor: maxExtraLockupVoteWeightScaledFactorNum, lockupSaturationSecs: lockupSaturationSecsNum, lockupSecs, - }) + }); - return parseFloat(calced.toFixed(2)) + return parseFloat(calced.toFixed(2)); } - return 0 -} + return 0; +}; const getDepositsAdditionalInfoEvents = async ( client: VsrClient, usedDeposits: DepositWithMintAccount[], connection: Connection, registrar: PublicKey, - voter: PublicKey + voter: PublicKey, ) => { // The wallet can be any existing account for the simulation // Note: when running a local validator ensure the account is copied from devnet: --clone ENmcpFCpxN1CqyUjuog9yyUVfdXBKF3LVCwLr7grJZpk -ud - const walletPk = new PublicKey('ENmcpFCpxN1CqyUjuog9yyUVfdXBKF3LVCwLr7grJZpk') + const walletPk = new PublicKey( + 'ENmcpFCpxN1CqyUjuog9yyUVfdXBKF3LVCwLr7grJZpk', + ); //because we switch wallet in here we can't use rpc from npm module //anchor dont allow to switch wallets inside existing client //parse events response as anchor do - const events: any[] = [] - const parser = new EventParser(client.program.programId, client.program.coder) - const maxRange = 8 - const itemsCount = usedDeposits.length - const numberOfSimulations = Math.ceil(itemsCount / maxRange) + const events: any[] = []; + const parser = new EventParser( + client.program.programId, + client.program.coder, + ); + const maxRange = 8; + const itemsCount = usedDeposits.length; + const numberOfSimulations = Math.ceil(itemsCount / maxRange); for (let i = 0; i < numberOfSimulations; i++) { - const take = maxRange - const transaction = new Transaction({ feePayer: walletPk }) + const take = maxRange; + const transaction = new Transaction({ feePayer: walletPk }); transaction.add( client.program.instruction.logVoterInfo(maxRange * i, take, { accounts: { registrar, voter, }, - }) - ) + }), + ); const batchOfDeposits = await simulateTransaction( connection, transaction, - 'recent' - ) + 'recent', + ); parser.parseLogs(batchOfDeposits.value.logs!, (event) => { - events.push(event) - }) + events.push(event); + }); } - return events -} + return events; +}; diff --git a/VoteStakeRegistry/tools/types.ts b/VoteStakeRegistry/tools/types.ts index 464bf9529b..4a4fb15d68 100644 --- a/VoteStakeRegistry/tools/types.ts +++ b/VoteStakeRegistry/tools/types.ts @@ -1,21 +1,21 @@ -import { LockupType } from 'VoteStakeRegistry/sdk/accounts' +import { LockupType } from 'VoteStakeRegistry/sdk/accounts'; export interface Period { - defaultValue: number - display: string + defaultValue: number; + display: string; } export interface LockupKind { - value: LockupType - info: string[] - displayName: string + value: LockupType; + info: string[]; + displayName: string; } export interface VestingPeriod { - value: number - display: string - info: string + value: number; + display: string; + info: string; } -export const MONTHLY = 'monthly' -export const CONSTANT = 'constant' +export const MONTHLY = 'monthly'; +export const CONSTANT = 'constant'; export const lockupTypes: LockupKind[] = [ { value: 'cliff', @@ -44,7 +44,7 @@ export const lockupTypes: LockupKind[] = [ 'Example: You lock 12.000 tokens for one year with monthly vesting. Every month 1.000 tokens unlock. After the year, all tokens have unlocked.', ], }, -] +]; export const vestingPeriods: VestingPeriod[] = [ { @@ -52,4 +52,4 @@ export const vestingPeriods: VestingPeriod[] = [ display: 'Monthly', info: 'per month', }, -] +]; diff --git a/actions/cancelProposal.ts b/actions/cancelProposal.ts index c3a345107a..d1e69abb9f 100644 --- a/actions/cancelProposal.ts +++ b/actions/cancelProposal.ts @@ -3,29 +3,32 @@ import { PublicKey, Transaction, TransactionInstruction, -} from '@solana/web3.js' +} from '@solana/web3.js'; -import { getGovernanceProgramVersion, RpcContext } from '@solana/spl-governance' -import { Proposal } from '@solana/spl-governance' -import { ProgramAccount } from '@solana/spl-governance' -import { sendTransaction } from 'utils/send' -import { withCancelProposal } from '@solana/spl-governance' +import { + getGovernanceProgramVersion, + RpcContext, +} from '@solana/spl-governance'; +import { Proposal } from '@solana/spl-governance'; +import { ProgramAccount } from '@solana/spl-governance'; +import { sendTransaction } from 'utils/send'; +import { withCancelProposal } from '@solana/spl-governance'; export const cancelProposal = async ( { connection, wallet, programId, walletPubkey }: RpcContext, realmPk: PublicKey, - proposal: ProgramAccount | undefined + proposal: ProgramAccount | undefined, ) => { - const instructions: TransactionInstruction[] = [] - const signers: Keypair[] = [] - const governanceAuthority = walletPubkey + const instructions: TransactionInstruction[] = []; + const signers: Keypair[] = []; + const governanceAuthority = walletPubkey; // Explicitly request the version before making RPC calls to work around race conditions in resolving // the version for RealmInfo const programVersion = await getGovernanceProgramVersion( connection, - programId - ) + programId, + ); withCancelProposal( instructions, @@ -35,12 +38,12 @@ export const cancelProposal = async ( proposal!.account.governance, proposal!.pubkey, proposal!.account.tokenOwnerRecord, - governanceAuthority - ) + governanceAuthority, + ); - const transaction = new Transaction({ feePayer: walletPubkey }) + const transaction = new Transaction({ feePayer: walletPubkey }); - transaction.add(...instructions) + transaction.add(...instructions); await sendTransaction({ transaction, @@ -49,5 +52,5 @@ export const cancelProposal = async ( signers, sendingMessage: 'Cancelling proposal', successMessage: 'Proposal cancelled', - }) -} + }); +}; diff --git a/actions/castVote.ts b/actions/castVote.ts deleted file mode 100644 index 784be04db4..0000000000 --- a/actions/castVote.ts +++ /dev/null @@ -1,94 +0,0 @@ -import { - Keypair, - PublicKey, - Transaction, - TransactionInstruction, -} from '@solana/web3.js' -import { - ChatMessageBody, - getGovernanceProgramVersion, - GOVERNANCE_CHAT_PROGRAM_ID, - Proposal, - Realm, - withPostChatMessage, - YesNoVote, -} from '@solana/spl-governance' -import { ProgramAccount } from '@solana/spl-governance' -import { RpcContext } from '@solana/spl-governance' - -import { Vote } from '@solana/spl-governance' - -import { withCastVote } from '@solana/spl-governance' -import { sendTransaction } from '../utils/send' -import { withUpdateVoterWeightRecord } from 'VoteStakeRegistry/sdk/withUpdateVoterWeightRecord' -import { VsrClient } from '@blockworks-foundation/voter-stake-registry-client' - -export async function castVote( - { connection, wallet, programId, walletPubkey }: RpcContext, - realm: ProgramAccount, - proposal: ProgramAccount, - tokeOwnerRecord: PublicKey, - yesNoVote: YesNoVote, - message?: ChatMessageBody | undefined, - client?: VsrClient -) { - const signers: Keypair[] = [] - const instructions: TransactionInstruction[] = [] - - const governanceAuthority = walletPubkey - const payer = walletPubkey - - // Explicitly request the version before making RPC calls to work around race conditions in resolving - // the version for RealmInfo - const programVersion = await getGovernanceProgramVersion( - connection, - programId - ) - - //will run only if plugin is connected with realm - const voterWeight = await withUpdateVoterWeightRecord( - instructions, - wallet.publicKey!, - realm, - client - ) - - await withCastVote( - instructions, - programId, - programVersion, - realm.pubkey, - proposal.account.governance, - proposal.pubkey, - proposal.account.tokenOwnerRecord, - tokeOwnerRecord, - governanceAuthority, - proposal.account.governingTokenMint, - Vote.fromYesNoVote(yesNoVote), - payer, - voterWeight - ) - - if (message) { - await withPostChatMessage( - instructions, - signers, - GOVERNANCE_CHAT_PROGRAM_ID, - programId, - realm.pubkey, - proposal.account.governance, - proposal.pubkey, - tokeOwnerRecord, - governanceAuthority, - payer, - undefined, - message, - voterWeight - ) - } - - const transaction = new Transaction() - transaction.add(...instructions) - - await sendTransaction({ transaction, wallet, connection, signers }) -} diff --git a/actions/castVotes.ts b/actions/castVotes.ts new file mode 100644 index 0000000000..05a17c1dbd --- /dev/null +++ b/actions/castVotes.ts @@ -0,0 +1,123 @@ +import { VsrClient } from '@blockworks-foundation/voter-stake-registry-client'; +import { Keypair, Transaction, TransactionInstruction } from '@solana/web3.js'; +import { withUpdateVoterWeightRecord } from 'VoteStakeRegistry/sdk/withUpdateVoterWeightRecord'; +import { + ChatMessageBody, + getGovernanceProgramVersion, + GOVERNANCE_CHAT_PROGRAM_ID, + Proposal, + Realm, + TokenOwnerRecord, + withPostChatMessage, + YesNoVote, +} from '@solana/spl-governance'; +import { ProgramAccount } from '@solana/spl-governance'; +import { RpcContext } from '@solana/spl-governance'; +import { Vote } from '@solana/spl-governance'; +import { withCastVote } from '@solana/spl-governance'; +import { sendTransaction } from '../utils/send'; +import { notify } from '@utils/notifications'; + +export async function castVotes({ + rpcContext: { connection, wallet, programId, walletPubkey }, + vote, + realm, + proposal, + tokenOwnerRecordsToVoteWith, + message, + client, +}: { + rpcContext: RpcContext; + realm: ProgramAccount; + proposal: ProgramAccount; + tokenOwnerRecordsToVoteWith: ProgramAccount[]; + vote: YesNoVote; + message?: ChatMessageBody; + client?: VsrClient; +}) { + const signers: Keypair[] = []; + let instructions: TransactionInstruction[] = []; + + const governanceAuthority = walletPubkey; + const payer = walletPubkey; + + // Explicitly request the version before making RPC calls to work around race conditions in resolving + // the version for RealmInfo + const programVersion = await getGovernanceProgramVersion( + connection, + programId, + ); + + //will run only if plugin is connected with realm + const voterWeight = await withUpdateVoterWeightRecord( + instructions, + wallet.publicKey!, + realm, + client, + ); + + // Post the message in the first transaction + if (message) { + await withPostChatMessage( + instructions, + signers, + GOVERNANCE_CHAT_PROGRAM_ID, + programId, + realm.pubkey, + proposal.account.governance, + proposal.pubkey, + + // Use the first Token Owner Record in a completely arbitrary way + tokenOwnerRecordsToVoteWith[0].pubkey, + + governanceAuthority, + payer, + undefined, + message, + voterWeight, + ); + } + + let i = 0; + + for (const tokenOwnerRecord of tokenOwnerRecordsToVoteWith) { + await withCastVote( + instructions, + programId, + programVersion, + realm.pubkey, + proposal.account.governance, + proposal.pubkey, + proposal.account.tokenOwnerRecord, + tokenOwnerRecord.pubkey, + governanceAuthority, + proposal.account.governingTokenMint, + Vote.fromYesNoVote(vote), + payer, + voterWeight, + ); + + const transaction = new Transaction(); + transaction.add(...instructions); + + if (tokenOwnerRecordsToVoteWith.length > 1) { + notify({ + type: 'info', + message: `Voting ${i + 1}/${tokenOwnerRecordsToVoteWith.length}`, + }); + } + + await sendTransaction({ transaction, wallet, connection, signers }); + + instructions = []; + i++; + } + + // If there is no vote to cast but there is a message, send the transaction + if (!tokenOwnerRecordsToVoteWith.length && message) { + const transaction = new Transaction(); + transaction.add(...instructions); + + await sendTransaction({ transaction, wallet, connection, signers }); + } +} diff --git a/actions/chat/postMessage.ts b/actions/chat/postMessage.ts index bd21cdf096..8da7ae7230 100644 --- a/actions/chat/postMessage.ts +++ b/actions/chat/postMessage.ts @@ -3,19 +3,19 @@ import { Keypair, Transaction, TransactionInstruction, -} from '@solana/web3.js' +} from '@solana/web3.js'; import { GOVERNANCE_CHAT_PROGRAM_ID, Proposal, Realm, -} from '@solana/spl-governance' -import { ChatMessageBody } from '@solana/spl-governance' -import { withPostChatMessage } from '@solana/spl-governance' -import { ProgramAccount } from '@solana/spl-governance' -import { RpcContext } from '@solana/spl-governance' -import { sendTransaction } from '../../utils/send' -import { VsrClient } from '@blockworks-foundation/voter-stake-registry-client' -import { withUpdateVoterWeightRecord } from 'VoteStakeRegistry/sdk/withUpdateVoterWeightRecord' +} from '@solana/spl-governance'; +import { ChatMessageBody } from '@solana/spl-governance'; +import { withPostChatMessage } from '@solana/spl-governance'; +import { ProgramAccount } from '@solana/spl-governance'; +import { RpcContext } from '@solana/spl-governance'; +import { sendTransaction } from '../../utils/send'; +import { VsrClient } from '@blockworks-foundation/voter-stake-registry-client'; +import { withUpdateVoterWeightRecord } from 'VoteStakeRegistry/sdk/withUpdateVoterWeightRecord'; export async function postChatMessage( { connection, wallet, programId, walletPubkey }: RpcContext, @@ -24,21 +24,21 @@ export async function postChatMessage( tokeOwnerRecord: PublicKey, body: ChatMessageBody, replyTo?: PublicKey, - client?: VsrClient + client?: VsrClient, ) { - const signers: Keypair[] = [] - const instructions: TransactionInstruction[] = [] + const signers: Keypair[] = []; + const instructions: TransactionInstruction[] = []; - const governanceAuthority = walletPubkey - const payer = walletPubkey + const governanceAuthority = walletPubkey; + const payer = walletPubkey; //will run only if plugin is connected with realm const voterWeight = await withUpdateVoterWeightRecord( instructions, wallet.publicKey!, realm, - client - ) + client, + ); await withPostChatMessage( instructions, @@ -53,11 +53,11 @@ export async function postChatMessage( payer, replyTo, body, - voterWeight - ) + voterWeight, + ); - const transaction = new Transaction() - transaction.add(...instructions) + const transaction = new Transaction(); + transaction.add(...instructions); - await sendTransaction({ transaction, wallet, connection, signers }) + await sendTransaction({ transaction, wallet, connection, signers }); } diff --git a/actions/createMultisigRealm.ts b/actions/createMultisigRealm.ts deleted file mode 100644 index e6175bac87..0000000000 --- a/actions/createMultisigRealm.ts +++ /dev/null @@ -1,248 +0,0 @@ -import { - getTokenOwnerRecordAddress, - GovernanceConfig, - MintMaxVoteWeightSource, - SetRealmAuthorityAction, - VoteThresholdPercentage, - VoteTipping, -} from '@solana/spl-governance' - -import { withCreateMintGovernance } from '@solana/spl-governance' -import { withCreateRealm } from '@solana/spl-governance' -import { withDepositGoverningTokens } from '@solana/spl-governance' -import { withSetRealmAuthority } from '@solana/spl-governance' -import { BN } from '@project-serum/anchor' -import { - Connection, - Keypair, - PublicKey, - TransactionInstruction, -} from '@solana/web3.js' - -import { withCreateAssociatedTokenAccount } from '@tools/sdk/splToken/withCreateAssociatedTokenAccount' -import { withCreateMint } from '@tools/sdk/splToken/withCreateMint' -import { withMintTo } from '@tools/sdk/splToken/withMintTo' -import { - getMintNaturalAmountFromDecimalAsBN, - getTimestampFromDays, -} from '@tools/sdk/units' -import { - getWalletPublicKey, - sendTransactions, - SequenceType, - WalletSigner, -} from 'utils/sendTransactions' -import { chunks } from '@utils/helpers' -import { MIN_COMMUNITY_TOKENS_TO_CREATE_W_0_SUPPLY } from '@tools/constants' - -/// Creates multisig realm with community mint with 0 supply -/// and council mint used as multisig token -export const createMultisigRealm = async ( - connection: Connection, - programId: PublicKey, - programVersion: number, - - name: string, - yesVoteThreshold: number, - councilWalletPks: PublicKey[], - - wallet: WalletSigner -) => { - const walletPk = getWalletPublicKey(wallet) - - const mintsSetupInstructions: TransactionInstruction[] = [] - const councilMembersInstructions: TransactionInstruction[] = [] - - const mintsSetupSigners: Keypair[] = [] - - // Default to 100% supply - const communityMintMaxVoteWeightSource = - MintMaxVoteWeightSource.FULL_SUPPLY_FRACTION - - // The community mint is going to have 0 supply and we arbitrarily set it to 1m - const minCommunityTokensToCreate = MIN_COMMUNITY_TOKENS_TO_CREATE_W_0_SUPPLY - - // Community mint decimals - const communityMintDecimals = 6 - - // Create community mint - const communityMintPk = await withCreateMint( - connection, - mintsSetupInstructions, - mintsSetupSigners, - walletPk, - null, - communityMintDecimals, - walletPk - ) - - // Create council mint - const councilMintPk = await withCreateMint( - connection, - mintsSetupInstructions, - mintsSetupSigners, - walletPk, - null, - 0, - walletPk - ) - - let walletAtaPk: PublicKey | undefined - const tokenAmount = 1 - - for (const teamWalletPk of councilWalletPks) { - const ataPk = await withCreateAssociatedTokenAccount( - councilMembersInstructions, - councilMintPk, - teamWalletPk, - walletPk - ) - - // Mint 1 council token to each team member - await withMintTo( - councilMembersInstructions, - councilMintPk, - ataPk, - walletPk, - tokenAmount - ) - - if (teamWalletPk.equals(walletPk)) { - walletAtaPk = ataPk - } - } - - // Create realm - const realmInstructions: TransactionInstruction[] = [] - const realmSigners: Keypair[] = [] - - // Convert to mint natural amount - const minCommunityTokensToCreateAsMintValue = getMintNaturalAmountFromDecimalAsBN( - minCommunityTokensToCreate, - communityMintDecimals - ) - - const realmPk = await withCreateRealm( - realmInstructions, - programId, - programVersion, - name, - walletPk, - communityMintPk, - walletPk, - councilMintPk, - communityMintMaxVoteWeightSource, - minCommunityTokensToCreateAsMintValue, - undefined - ) - - let tokenOwnerRecordPk: PublicKey - - // If the current wallet is in the team then deposit the council token - if (walletAtaPk) { - await withDepositGoverningTokens( - realmInstructions, - programId, - programVersion, - realmPk, - walletAtaPk, - councilMintPk, - walletPk, - walletPk, - walletPk, - new BN(tokenAmount) - ) - - // TODO: return from withDepositGoverningTokens in the SDK - tokenOwnerRecordPk = await getTokenOwnerRecordAddress( - programId, - realmPk, - councilMintPk, - walletPk - ) - } else { - // Let's throw for now if the current wallet isn't in the team - // TODO: To fix it we would have to make it temp. as part of the team and then remove after the realm is created - throw new Error('Current wallet must be in the team') - } - - // Put community and council mints under the realm governance with default config - const config = new GovernanceConfig({ - voteThresholdPercentage: new VoteThresholdPercentage({ - value: yesVoteThreshold, - }), - minCommunityTokensToCreateProposal: minCommunityTokensToCreateAsMintValue, - // Do not use instruction hold up time - minInstructionHoldUpTime: 0, - // max voting time 3 days - maxVotingTime: getTimestampFromDays(3), - voteTipping: VoteTipping.Strict, - proposalCoolOffTime: 0, - minCouncilTokensToCreateProposal: new BN(1), - }) - - const communityMintGovPk = await withCreateMintGovernance( - realmInstructions, - programId, - programVersion, - realmPk, - communityMintPk, - config, - walletPk, - walletPk, - tokenOwnerRecordPk, - walletPk, - walletPk - ) - - await withCreateMintGovernance( - realmInstructions, - programId, - programVersion, - realmPk, - councilMintPk, - config, - walletPk, - walletPk, - tokenOwnerRecordPk, - walletPk, - walletPk - ) - - // Set the community governance as the realm authority - withSetRealmAuthority( - realmInstructions, - programId, - programVersion, - realmPk, - walletPk, - communityMintGovPk, - SetRealmAuthorityAction.SetChecked - ) - - try { - const councilMembersChunks = chunks(councilMembersInstructions, 10) - // only walletPk needs to sign the minting instructions and it's a signer by default and we don't have to include any more signers - const councilMembersSignersChunks = Array(councilMembersChunks.length).fill( - [] - ) - - const tx = await sendTransactions( - connection, - wallet, - [mintsSetupInstructions, ...councilMembersChunks, realmInstructions], - [mintsSetupSigners, ...councilMembersSignersChunks, realmSigners], - SequenceType.Sequential - ) - - return { - tx, - realmPk, - communityMintPk, - councilMintPk, - } - } catch (ex) { - console.error(ex) - throw ex - } -} diff --git a/actions/createProposal.ts b/actions/createProposal.ts index 543dd01754..b9b3e06002 100644 --- a/actions/createProposal.ts +++ b/actions/createProposal.ts @@ -3,7 +3,7 @@ import { PublicKey, Transaction, TransactionInstruction, -} from '@solana/web3.js' +} from '@solana/web3.js'; import { getGovernanceProgramVersion, @@ -14,26 +14,26 @@ import { Realm, VoteType, withCreateProposal, -} from '@solana/spl-governance' -import { withAddSignatory } from '@solana/spl-governance' -import { RpcContext } from '@solana/spl-governance' -import { withInsertTransaction } from '@solana/spl-governance' -import { InstructionData } from '@solana/spl-governance' -import { sendTransaction } from 'utils/send' -import { withSignOffProposal } from '@solana/spl-governance' -import { withUpdateVoterWeightRecord } from 'VoteStakeRegistry/sdk/withUpdateVoterWeightRecord' -import { VsrClient } from '@blockworks-foundation/voter-stake-registry-client' -import { sendTransactions, SequenceType } from '@utils/sendTransactions' -import { chunks } from '@utils/helpers' -import { UiInstruction } from '@utils/uiTypes/proposalCreationTypes' +} from '@solana/spl-governance'; +import { withAddSignatory } from '@solana/spl-governance'; +import { RpcContext } from '@solana/spl-governance'; +import { withInsertTransaction } from '@solana/spl-governance'; +import { InstructionData } from '@solana/spl-governance'; +import { sendTransaction } from 'utils/send'; +import { withSignOffProposal } from '@solana/spl-governance'; +import { withUpdateVoterWeightRecord } from 'VoteStakeRegistry/sdk/withUpdateVoterWeightRecord'; +import { VsrClient } from '@blockworks-foundation/voter-stake-registry-client'; +import { sendTransactions, SequenceType } from '@utils/sendTransactions'; +import { chunks } from '@utils/helpers'; +import { FormInstructionData } from '@utils/uiTypes/proposalCreationTypes'; export interface InstructionDataWithHoldUpTime { - data: InstructionData | null - holdUpTime: number | undefined - prerequisiteInstructions: TransactionInstruction[] - chunkSplitByDefault?: boolean - signers?: Keypair[] - shouldSplitIntoSeparateTxs?: boolean | undefined + data: InstructionData | null; + holdUpTime: number | undefined; + prerequisiteInstructions: TransactionInstruction[]; + chunkSplitByDefault?: boolean; + signers?: Keypair[]; + shouldSplitIntoSeparateTxs?: boolean | undefined; } export class InstructionDataWithHoldUpTime { @@ -41,19 +41,19 @@ export class InstructionDataWithHoldUpTime { instruction, governance, }: { - instruction: UiInstruction - governance?: ProgramAccount + instruction: FormInstructionData; + governance?: ProgramAccount; }) { this.data = instruction.serializedInstruction ? getInstructionDataFromBase64(instruction.serializedInstruction) - : null + : null; this.holdUpTime = typeof instruction.customHoldUpTime !== undefined ? instruction.customHoldUpTime - : governance?.account?.config.minInstructionHoldUpTime + : governance?.account?.config.minInstructionHoldUpTime; - this.prerequisiteInstructions = instruction.prerequisiteInstructions || [] - this.chunkSplitByDefault = instruction.chunkSplitByDefault || false + this.prerequisiteInstructions = instruction.prerequisiteInstructions || []; + this.chunkSplitByDefault = instruction.chunkSplitByDefault || false; } } @@ -68,41 +68,41 @@ export const createProposal = async ( proposalIndex: number, instructionsData: InstructionDataWithHoldUpTime[], isDraft: boolean, - client?: VsrClient + client?: VsrClient, ): Promise => { - const instructions: TransactionInstruction[] = [] + const instructions: TransactionInstruction[] = []; - const governanceAuthority = walletPubkey - const signatory = walletPubkey - const payer = walletPubkey - const notificationTitle = isDraft ? 'proposal draft' : 'proposal' - const prerequisiteInstructions: TransactionInstruction[] = [] + const governanceAuthority = walletPubkey; + const signatory = walletPubkey; + const payer = walletPubkey; + const notificationTitle = isDraft ? 'proposal draft' : 'proposal'; + const prerequisiteInstructions: TransactionInstruction[] = []; // sum up signers - const signers: Keypair[] = instructionsData.flatMap((x) => x.signers ?? []) + const signers: Keypair[] = instructionsData.flatMap((x) => x.signers ?? []); const shouldSplitIntoSeparateTxs: boolean = instructionsData .flatMap((x) => x.shouldSplitIntoSeparateTxs) - .some((x) => x) + .some((x) => x); // Explicitly request the version before making RPC calls to work around race conditions in resolving // the version for RealmInfo const programVersion = await getGovernanceProgramVersion( connection, - programId - ) + programId, + ); // V2 Approve/Deny configuration - const voteType = VoteType.SINGLE_CHOICE - const options = ['Approve'] - const useDenyOption = true + const voteType = VoteType.SINGLE_CHOICE; + const options = ['Approve']; + const useDenyOption = true; //will run only if plugin is connected with realm const voterWeight = await withUpdateVoterWeightRecord( instructions, wallet.publicKey!, realm, - client - ) + client, + ); const proposalAddress = await withCreateProposal( instructions, @@ -120,8 +120,8 @@ export const createProposal = async ( options, useDenyOption, payer, - voterWeight - ) + voterWeight, + ); await withAddSignatory( instructions, @@ -131,27 +131,27 @@ export const createProposal = async ( tokenOwnerRecord, governanceAuthority, signatory, - payer - ) + payer, + ); // TODO: Return signatoryRecordAddress from the SDK call const signatoryRecordAddress = await getSignatoryRecordAddress( programId, proposalAddress, - signatory - ) + signatory, + ); - const insertInstructions: TransactionInstruction[] = [] + const insertInstructions: TransactionInstruction[] = []; const splitToChunkByDefault = instructionsData.filter( - (x) => x.chunkSplitByDefault - ).length + (x) => x.chunkSplitByDefault, + ).length; for (const [index, instruction] of instructionsData .filter((x) => x.data) .entries()) { if (instruction.data) { if (instruction.prerequisiteInstructions) { - prerequisiteInstructions.push(...instruction.prerequisiteInstructions) + prerequisiteInstructions.push(...instruction.prerequisiteInstructions); } await withInsertTransaction( insertInstructions, @@ -165,12 +165,12 @@ export const createProposal = async ( 0, instruction.holdUpTime || 0, [instruction.data], - payer - ) + payer, + ); } } - const insertInstructionCount = insertInstructions.length + const insertInstructionCount = insertInstructions.length; if (!isDraft) { withSignOffProposal( @@ -182,16 +182,16 @@ export const createProposal = async ( proposalAddress, signatory, signatoryRecordAddress, - undefined - ) + undefined, + ); } if (shouldSplitIntoSeparateTxs) { - const transaction1 = new Transaction() - const transaction2 = new Transaction() + const transaction1 = new Transaction(); + const transaction2 = new Transaction(); - transaction1.add(...prerequisiteInstructions, ...instructions) - transaction2.add(...insertInstructions) + transaction1.add(...prerequisiteInstructions, ...instructions); + transaction2.add(...insertInstructions); await sendTransaction({ transaction: transaction1, @@ -200,7 +200,7 @@ export const createProposal = async ( signers, sendingMessage: `creating ${notificationTitle}`, successMessage: `${notificationTitle} created`, - }) + }); await sendTransaction({ transaction: transaction2, @@ -209,20 +209,20 @@ export const createProposal = async ( signers: undefined, sendingMessage: `inserting into ${notificationTitle}`, successMessage: `inserted into ${notificationTitle}`, - }) + }); } else if (insertInstructionCount <= 2 && !splitToChunkByDefault) { // This is an arbitrary threshold and we assume that up to 2 instructions can be inserted as a single Tx // This is conservative setting and we might need to revise it if we have more empirical examples or // reliable way to determine Tx size - const transaction = new Transaction() + const transaction = new Transaction(); // We merge instructions with prerequisiteInstructions // Prerequisite instructions can came from instructions as something we need to do before instruction can be executed // For example we create ATAs if they don't exist as part of the proposal creation flow transaction.add( ...prerequisiteInstructions, ...instructions, - ...insertInstructions - ) + ...insertInstructions, + ); await sendTransaction({ transaction, @@ -231,21 +231,21 @@ export const createProposal = async ( signers, sendingMessage: `creating ${notificationTitle}`, successMessage: `${notificationTitle} created`, - }) + }); } else { - const insertChunks = chunks(insertInstructions, 2) - const signerChunks = Array(insertChunks.length).fill([]) + const insertChunks = chunks(insertInstructions, 2); + const signerChunks = Array(insertChunks.length).fill([]); - console.log(`Creating proposal using ${insertChunks.length} chunks`) + console.info(`Creating proposal using ${insertChunks.length} chunks`); await sendTransactions( connection, wallet, [prerequisiteInstructions, instructions, ...insertChunks], [[], [], ...signerChunks], - SequenceType.Sequential - ) + SequenceType.Sequential, + ); } - return proposalAddress -} + return proposalAddress; +}; diff --git a/actions/createTreasuryAccount.ts b/actions/createTreasuryAccount.ts deleted file mode 100644 index 80fdec8f6d..0000000000 --- a/actions/createTreasuryAccount.ts +++ /dev/null @@ -1,97 +0,0 @@ -import { - Keypair, - PublicKey, - Transaction, - TransactionInstruction, -} from '@solana/web3.js' - -import { - getGovernanceProgramVersion, - GovernanceConfig, - ProgramAccount, - Realm, - withCreateNativeTreasury, -} from '@solana/spl-governance' - -import { withCreateTokenGovernance } from '@solana/spl-governance' -import { RpcContext } from '@solana/spl-governance' -import { sendTransaction } from '@utils/send' -import { withCreateSplTokenAccount } from '@models/withCreateSplTokenAccount' -import { withUpdateVoterWeightRecord } from 'VoteStakeRegistry/sdk/withUpdateVoterWeightRecord' -import { VsrClient } from '@blockworks-foundation/voter-stake-registry-client' -import { DEFAULT_NATIVE_SOL_MINT } from '@components/instructions/tools' - -export const createTreasuryAccount = async ( - { connection, wallet, programId, walletPubkey }: RpcContext, - realm: ProgramAccount, - mint: PublicKey, - config: GovernanceConfig, - tokenOwnerRecord: PublicKey, - client?: VsrClient -): Promise => { - const instructions: TransactionInstruction[] = [] - const signers: Keypair[] = [] - - // Explicitly request the version before making RPC calls to work around race conditions in resolving - // the version for RealmInfo - const programVersion = await getGovernanceProgramVersion( - connection, - programId - ) - - //will run only if plugin is connected with realm - const voterWeight = await withUpdateVoterWeightRecord( - instructions, - wallet.publicKey!, - realm, - client - ) - - const tokenAccount = await withCreateSplTokenAccount( - connection, - wallet!, - instructions, - signers, - mint - ) - - const governanceAuthority = walletPubkey - - const governanceAddress = await withCreateTokenGovernance( - instructions, - programId, - programVersion, - realm.pubkey, - tokenAccount.tokenAccountAddress, - config, - true, - walletPubkey, - tokenOwnerRecord, - walletPubkey, - governanceAuthority, - voterWeight - ) - - if (mint.toBase58() === DEFAULT_NATIVE_SOL_MINT) { - await withCreateNativeTreasury( - instructions, - programId, - governanceAddress, - walletPubkey - ) - } - - const transaction = new Transaction() - transaction.add(...instructions) - - await sendTransaction({ - transaction, - wallet, - connection, - signers, - sendingMessage: 'Creating treasury account', - successMessage: 'Treasury account has been created', - }) - - return governanceAddress -} diff --git a/actions/dryRunInstruction.ts b/actions/dryRunInstruction.ts index ec35c835ba..617473b9f2 100644 --- a/actions/dryRunInstruction.ts +++ b/actions/dryRunInstruction.ts @@ -1,30 +1,30 @@ -import { InstructionData } from '@solana/spl-governance' +import { InstructionData } from '@solana/spl-governance'; import { Connection, Transaction, TransactionInstruction, -} from '@solana/web3.js' -import { simulateTransaction } from '../utils/send' -import { WalletAdapter } from '@solana/wallet-adapter-base' +} from '@solana/web3.js'; +import { simulateTransaction } from '../utils/send'; +import { WalletAdapter } from '@solana/wallet-adapter-base'; export async function dryRunInstruction( connection: Connection, wallet: WalletAdapter, instructionData: InstructionData, - prerequisiteInstructionsToRun?: TransactionInstruction[] | undefined + prerequisiteInstructionsToRun?: TransactionInstruction[] | undefined, ) { - const transaction = new Transaction({ feePayer: wallet.publicKey }) + const transaction = new Transaction({ feePayer: wallet.publicKey }); if (prerequisiteInstructionsToRun) { - prerequisiteInstructionsToRun.map((x) => transaction.add(x)) + prerequisiteInstructionsToRun.map((x) => transaction.add(x)); } transaction.add({ keys: instructionData.accounts, programId: instructionData.programId, data: Buffer.from(instructionData.data), - }) + }); - const result = await simulateTransaction(connection, transaction, 'single') + const result = await simulateTransaction(connection, transaction, 'single'); - return { response: result.value, transaction } + return { response: result.value, transaction }; } diff --git a/actions/executeInstructions.ts b/actions/executeInstructions.ts index f2cb59ba4e..1d608963df 100644 --- a/actions/executeInstructions.ts +++ b/actions/executeInstructions.ts @@ -4,17 +4,17 @@ import { ProposalTransaction, RpcContext, withExecuteTransaction, -} from '@solana/spl-governance' -import { Transaction, TransactionInstruction } from '@solana/web3.js' -import { sendSignedTransaction, signTransaction } from '@utils/send' +} from '@solana/spl-governance'; +import { Transaction, TransactionInstruction } from '@solana/web3.js'; +import { sendSignedTransaction, signTransaction } from '@utils/send'; // Merge instructions within one Transaction, sign it and execute it export const executeInstructions = async ( { connection, wallet, programId, programVersion }: RpcContext, proposal: ProgramAccount, - proposalInstructions: ProgramAccount[] + proposalInstructions: ProgramAccount[], ) => { - const instructions: TransactionInstruction[] = [] + const instructions: TransactionInstruction[] = []; await Promise.all( proposalInstructions.map((instruction) => @@ -26,26 +26,26 @@ export const executeInstructions = async ( proposal.account.governance, proposal.pubkey, instruction.pubkey, - [instruction.account.getSingleInstruction()] - ) - ) - ) + [instruction.account.getSingleInstruction()], + ), + ), + ); - const transaction = new Transaction() + const transaction = new Transaction(); - transaction.add(...instructions) + transaction.add(...instructions); const signedTransaction = await signTransaction({ transaction, wallet, connection, signers: [], - }) + }); await sendSignedTransaction({ signedTransaction, connection, sendingMessage: 'Executing instruction', successMessage: 'Execution finalized', - }) -} + }); +}; diff --git a/actions/executeTransaction.ts b/actions/executeTransaction.ts index f774452533..e80e3eec64 100644 --- a/actions/executeTransaction.ts +++ b/actions/executeTransaction.ts @@ -1,30 +1,31 @@ -import { Keypair, Transaction, TransactionInstruction } from '@solana/web3.js' +import { Keypair, Transaction, TransactionInstruction } from '@solana/web3.js'; import { getGovernanceProgramVersion, Proposal, ProposalTransaction, -} from '@solana/spl-governance' +} from '@solana/spl-governance'; -import { withExecuteTransaction } from '@solana/spl-governance' -import { RpcContext } from '@solana/spl-governance' -import { ProgramAccount } from '@solana/spl-governance' -import { sendTransaction } from '@utils/send' +import { withExecuteTransaction } from '@solana/spl-governance'; +import { RpcContext } from '@solana/spl-governance'; +import { ProgramAccount } from '@solana/spl-governance'; +import { sendTransaction } from '@utils/send'; export const executeTransaction = async ( { connection, wallet, programId }: RpcContext, proposal: ProgramAccount, - instruction: ProgramAccount + instruction: ProgramAccount, + additionalSigner?: Keypair, ) => { - const signers: Keypair[] = [] - const instructions: TransactionInstruction[] = [] + const signers: Keypair[] = []; + const instructions: TransactionInstruction[] = []; // Explicitly request the version before making RPC calls to work around race conditions in resolving // the version for RealmInfo const programVersion = await getGovernanceProgramVersion( connection, - programId - ) + programId, + ); await withExecuteTransaction( instructions, @@ -33,12 +34,16 @@ export const executeTransaction = async ( proposal.account.governance, proposal.pubkey, instruction.pubkey, - [instruction.account.getSingleInstruction()] - ) + [instruction.account.getSingleInstruction()], + ); + + const transaction = new Transaction(); - const transaction = new Transaction() + transaction.add(...instructions); - transaction.add(...instructions) + if (additionalSigner) { + signers.push(additionalSigner); + } await sendTransaction({ transaction, @@ -47,5 +52,5 @@ export const executeTransaction = async ( signers, sendingMessage: 'Executing instruction', successMessage: 'Execution finalized', - }) -} + }); +}; diff --git a/actions/finalizeVotes.ts b/actions/finalizeVotes.ts index ad85773a40..d8e8b69173 100644 --- a/actions/finalizeVotes.ts +++ b/actions/finalizeVotes.ts @@ -1,32 +1,32 @@ import { getGovernanceProgramVersion, ProgramAccount, -} from '@solana/spl-governance' -import { RpcContext } from '@solana/spl-governance' +} from '@solana/spl-governance'; +import { RpcContext } from '@solana/spl-governance'; import { Keypair, PublicKey, Transaction, TransactionInstruction, -} from '@solana/web3.js' -import { sendTransaction } from '@utils/send' -import { Proposal } from '@solana/spl-governance' -import { withFinalizeVote } from '@solana/spl-governance' +} from '@solana/web3.js'; +import { sendTransaction } from '@utils/send'; +import { Proposal } from '@solana/spl-governance'; +import { withFinalizeVote } from '@solana/spl-governance'; export const finalizeVote = async ( { connection, wallet, programId }: RpcContext, realm: PublicKey, - proposal: ProgramAccount + proposal: ProgramAccount, ) => { - const signers: Keypair[] = [] - const instructions: TransactionInstruction[] = [] + const signers: Keypair[] = []; + const instructions: TransactionInstruction[] = []; // Explicitly request the version before making RPC calls to work around race conditions in resolving // the version for RealmInfo const programVersion = await getGovernanceProgramVersion( connection, - programId - ) + programId, + ); await withFinalizeVote( instructions, @@ -36,12 +36,12 @@ export const finalizeVote = async ( proposal.account.governance, proposal.pubkey, proposal.account.tokenOwnerRecord, - proposal.account.governingTokenMint - ) + proposal.account.governingTokenMint, + ); - const transaction = new Transaction() + const transaction = new Transaction(); - transaction.add(...instructions) + transaction.add(...instructions); await sendTransaction({ transaction, @@ -50,5 +50,5 @@ export const finalizeVote = async ( signers, sendingMessage: 'Finalizing votes', successMessage: 'Votes finalized', - }) -} + }); +}; diff --git a/actions/flagInstructionError.ts b/actions/flagInstructionError.ts index 665810f4e4..bbf7a7e5a9 100644 --- a/actions/flagInstructionError.ts +++ b/actions/flagInstructionError.ts @@ -3,31 +3,31 @@ import { PublicKey, Transaction, TransactionInstruction, -} from '@solana/web3.js' +} from '@solana/web3.js'; -import { getGovernanceProgramVersion, Proposal } from '@solana/spl-governance' +import { getGovernanceProgramVersion, Proposal } from '@solana/spl-governance'; -import { withFlagTransactionError } from '@solana/spl-governance' -import { RpcContext } from '@solana/spl-governance' -import { ProgramAccount } from '@solana/spl-governance' -import { sendTransaction } from '@utils/send' +import { withFlagTransactionError } from '@solana/spl-governance'; +import { RpcContext } from '@solana/spl-governance'; +import { ProgramAccount } from '@solana/spl-governance'; +import { sendTransaction } from '@utils/send'; export const flagInstructionError = async ( { connection, wallet, programId, walletPubkey }: RpcContext, proposal: ProgramAccount, - proposalInstruction: PublicKey + proposalInstruction: PublicKey, ) => { - const governanceAuthority = walletPubkey + const governanceAuthority = walletPubkey; - const signers: Keypair[] = [] - const instructions: TransactionInstruction[] = [] + const signers: Keypair[] = []; + const instructions: TransactionInstruction[] = []; // Explicitly request the version before making RPC calls to work around race conditions in resolving // the version for RealmInfo const programVersion = await getGovernanceProgramVersion( connection, - programId - ) + programId, + ); withFlagTransactionError( instructions, @@ -36,12 +36,12 @@ export const flagInstructionError = async ( proposal.pubkey, proposal.account.tokenOwnerRecord, governanceAuthority, - proposalInstruction - ) + proposalInstruction, + ); - const transaction = new Transaction({ feePayer: walletPubkey }) + const transaction = new Transaction({ feePayer: walletPubkey }); - transaction.add(...instructions) + transaction.add(...instructions); await sendTransaction({ transaction, @@ -50,5 +50,5 @@ export const flagInstructionError = async ( signers, sendingMessage: 'Flagging instruction as broken', successMessage: 'Instruction flagged as broken', - }) -} + }); +}; diff --git a/actions/registerProgramGovernance.ts b/actions/registerProgramGovernance.ts index f6341aef15..d2d505c6e3 100644 --- a/actions/registerProgramGovernance.ts +++ b/actions/registerProgramGovernance.ts @@ -3,19 +3,19 @@ import { PublicKey, Transaction, TransactionInstruction, -} from '@solana/web3.js' +} from '@solana/web3.js'; import { getGovernanceProgramVersion, GovernanceType, ProgramAccount, Realm, -} from '@solana/spl-governance' -import { GovernanceConfig } from '@solana/spl-governance' -import { withCreateProgramGovernance } from '@solana/spl-governance' -import { RpcContext } from '@solana/spl-governance' -import { sendTransaction } from '@utils/send' -import { withUpdateVoterWeightRecord } from 'VoteStakeRegistry/sdk/withUpdateVoterWeightRecord' -import { VsrClient } from '@blockworks-foundation/voter-stake-registry-client' +} from '@solana/spl-governance'; +import { GovernanceConfig } from '@solana/spl-governance'; +import { withCreateProgramGovernance } from '@solana/spl-governance'; +import { RpcContext } from '@solana/spl-governance'; +import { sendTransaction } from '@utils/send'; +import { withUpdateVoterWeightRecord } from 'VoteStakeRegistry/sdk/withUpdateVoterWeightRecord'; +import { VsrClient } from '@blockworks-foundation/voter-stake-registry-client'; export const registerProgramGovernance = async ( { connection, wallet, programId, walletPubkey }: RpcContext, @@ -25,29 +25,29 @@ export const registerProgramGovernance = async ( config: GovernanceConfig, transferAuthority: boolean, tokenOwnerRecord: PublicKey, - client?: VsrClient + client?: VsrClient, ): Promise => { - const instructions: TransactionInstruction[] = [] - const signers: Keypair[] = [] - let governanceAddress - const governanceAuthority = walletPubkey + const instructions: TransactionInstruction[] = []; + const signers: Keypair[] = []; + let governanceAddress; + const governanceAuthority = walletPubkey; // Explicitly request the version before making RPC calls to work around race conditions in resolving // the version for RealmInfo const programVersion = await getGovernanceProgramVersion( connection, - programId - ) + programId, + ); //will run only if plugin is connected with realm const voterWeight = await withUpdateVoterWeightRecord( instructions, wallet.publicKey!, realm, - client - ) + client, + ); - console.log('VERSION', programVersion) + console.info('Governance Program version:', programVersion); switch (governanceType) { case GovernanceType.Program: { @@ -63,17 +63,19 @@ export const registerProgramGovernance = async ( tokenOwnerRecord, walletPubkey, governanceAuthority, - voterWeight - ) - break + voterWeight, + ); + break; } default: { - throw new Error(`Governance type ${governanceType} is not supported yet.`) + throw new Error( + `Governance type ${governanceType} is not supported yet.`, + ); } } - const transaction = new Transaction() - transaction.add(...instructions) + const transaction = new Transaction(); + transaction.add(...instructions); await sendTransaction({ transaction, wallet, @@ -81,7 +83,7 @@ export const registerProgramGovernance = async ( signers, sendingMessage: 'Creating treasury account', successMessage: 'Treasury account has been created', - }) + }); - return governanceAddress -} + return governanceAddress; +}; diff --git a/actions/registerRealm.ts b/actions/registerRealm.ts index 1ae3954f58..0dc86bf79b 100644 --- a/actions/registerRealm.ts +++ b/actions/registerRealm.ts @@ -4,8 +4,8 @@ import { PublicKey, Transaction, TransactionInstruction, -} from '@solana/web3.js' -import BN from 'bn.js' +} from '@solana/web3.js'; +import BN from 'bn.js'; import { getTokenOwnerRecordAddress, GovernanceConfig, @@ -13,47 +13,47 @@ import { SetRealmAuthorityAction, VoteThresholdPercentage, VoteTipping, -} from '@solana/spl-governance' -import { withCreateRealm } from '@solana/spl-governance' -import { sendTransaction } from '../utils/send' +} from '@solana/spl-governance'; +import { withCreateRealm } from '@solana/spl-governance'; +import { sendTransaction } from '../utils/send'; import { sendTransactions, SequenceType, WalletSigner, -} from 'utils/sendTransactions' -import { withCreateMint } from '@tools/sdk/splToken/withCreateMint' -import { withCreateAssociatedTokenAccount } from '@tools/sdk/splToken/withCreateAssociatedTokenAccount' -import { withMintTo } from '@tools/sdk/splToken/withMintTo' -import { chunks } from '@utils/helpers' +} from 'utils/sendTransactions'; +import { withCreateMint } from '@tools/sdk/splToken/withCreateMint'; +import { withCreateAssociatedTokenAccount } from '@tools/sdk/splToken/withCreateAssociatedTokenAccount'; +import { withMintTo } from '@tools/sdk/splToken/withMintTo'; +import { chunks, BN_ZERO } from '@utils/helpers'; import { SignerWalletAdapter, WalletConnectionError, -} from '@solana/wallet-adapter-base' -import { withDepositGoverningTokens } from '@solana/spl-governance' +} from '@solana/wallet-adapter-base'; +import { withDepositGoverningTokens } from '@solana/spl-governance'; import { getMintNaturalAmountFromDecimalAsBN, getTimestampFromDays, -} from '@tools/sdk/units' -import { withCreateMintGovernance } from '@solana/spl-governance' -import { withSetRealmAuthority } from '@solana/spl-governance' -import { AccountInfo, u64 } from '@solana/spl-token' -import { ProgramAccount } from '@project-serum/common' -import { tryGetAta } from '@utils/validations' -import { ConnectionContext } from '@utils/connection' -import { MIN_COMMUNITY_TOKENS_TO_CREATE_W_0_SUPPLY } from '@tools/constants' -import BigNumber from 'bignumber.js' +} from '@tools/sdk/units'; +import { withCreateMintGovernance } from '@solana/spl-governance'; +import { withSetRealmAuthority } from '@solana/spl-governance'; +import { AccountInfo, u64 } from '@solana/spl-token'; +import { tryGetAta } from '@utils/validations'; +import { ConnectionContext } from '@utils/connection'; +import { MIN_COMMUNITY_TOKENS_TO_CREATE_W_0_SUPPLY } from '@tools/constants'; +import BigNumber from 'bignumber.js'; +import { TokenProgramAccount } from '@utils/tokens'; interface RegisterRealmRpc { - connection: ConnectionContext - wallet: SignerWalletAdapter - walletPubkey: PublicKey + connection: ConnectionContext; + wallet: SignerWalletAdapter; + walletPubkey: PublicKey; } /** * The default amount of decimals for the community token */ -export const COMMUNITY_MINT_DECIMALS = 6 +export const COMMUNITY_MINT_DECIMALS = 6; /** * Prepares the mint instructions @@ -74,18 +74,18 @@ async function prepareMintInstructions( tokenDecimals = 0, council = false, mintPk?: PublicKey, - otherOwners?: PublicKey[] + otherOwners?: PublicKey[], ) { - console.debug('preparing mint instructions') + console.debug('preparing mint instructions'); - let _mintPk: PublicKey | undefined = undefined - let walletAtaPk: PublicKey | undefined - const mintInstructions: TransactionInstruction[] = [] - const mintSigners: Keypair[] = [] + let _mintPk: PublicKey | undefined = undefined; + let walletAtaPk: PublicKey | undefined; + const mintInstructions: TransactionInstruction[] = []; + const mintSigners: Keypair[] = []; const councilTokenAmount = new u64( - new BigNumber(1).shiftedBy(tokenDecimals).toString() - ) + new BigNumber(1).shiftedBy(tokenDecimals).toString(), + ); if (!council || (council && otherOwners?.length)) { // If mintPk is undefined, then @@ -99,19 +99,17 @@ async function prepareMintInstructions( walletPubkey, null, tokenDecimals, - walletPubkey - )) + walletPubkey, + )); // If the array of other owners is not empty // then should create mints to them if (otherOwners?.length) { for (const ownerPk of otherOwners) { - const ata: ProgramAccount | undefined = await tryGetAta( - connection.current, - ownerPk, - _mintPk - ) - const shouldMint = !ata?.account.amount.gt(new BN(0)) + const ata: + | TokenProgramAccount + | undefined = await tryGetAta(connection.current, ownerPk, _mintPk); + const shouldMint = !ata?.account.amount.gt(BN_ZERO); const ataPk = ata?.publicKey ?? @@ -119,31 +117,31 @@ async function prepareMintInstructions( mintInstructions, _mintPk, ownerPk, - walletPubkey - )) + walletPubkey, + )); // Mint 1 token to each owner if (shouldMint && ataPk) { - console.debug('will mint to ', { ataPk }) + console.debug('will mint to ', { ataPk }); await withMintTo( mintInstructions, _mintPk, ataPk, walletPubkey, - councilTokenAmount - ) + councilTokenAmount, + ); } if (ownerPk.equals(walletPubkey)) { - walletAtaPk = ataPk + walletAtaPk = ataPk; } } } } - const instructionChunks = chunks(mintInstructions, 10) - const signersChunks = Array(instructionChunks.length).fill([]) - signersChunks[0] = mintSigners + const instructionChunks = chunks(mintInstructions, 10); + const signersChunks = Array(instructionChunks.length).fill([]); + signersChunks[0] = mintSigners; return { mintPk: _mintPk, walletAtaPk, @@ -167,7 +165,7 @@ async function prepareMintInstructions( * Amount of tokens minted to the council members */ councilTokenAmount, - } + }; } /** @@ -178,17 +176,17 @@ async function prepareMintInstructions( function createGovernanceConfig( yesVoteThreshold = 60, tokenDecimals?: number, - minCommunityTokensToCreateGovernance?: string + minCommunityTokensToCreateGovernance?: string, ): GovernanceConfig { - console.debug('mounting governance config') + console.debug('mounting governance config'); const minCommunityTokensToCreateAsMintValue = getMintNaturalAmountFromDecimalAsBN( minCommunityTokensToCreateGovernance && +minCommunityTokensToCreateGovernance > 0 ? +minCommunityTokensToCreateGovernance : MIN_COMMUNITY_TOKENS_TO_CREATE_W_0_SUPPLY, - tokenDecimals ?? COMMUNITY_MINT_DECIMALS - ) + tokenDecimals ?? COMMUNITY_MINT_DECIMALS, + ); // Put community and council mints under the realm governance with default config return new GovernanceConfig({ @@ -203,7 +201,7 @@ function createGovernanceConfig( voteTipping: VoteTipping.Strict, proposalCoolOffTime: 0, minCouncilTokensToCreateProposal: new BN(1), - }) + }); } /** @@ -229,18 +227,18 @@ async function prepareGovernanceInstructions( realmPk: PublicKey, tokenOwnerRecordPk: PublicKey, realmInstructions: TransactionInstruction[], - transferAuthority?: boolean + transferAuthority?: boolean, ) { - console.debug('Preparing governance instructions') + console.debug('Preparing governance instructions'); const config = createGovernanceConfig( yesVoteThreshold, communityTokenDecimals, - minCommunityTokensToCreateGovernance - ) + minCommunityTokensToCreateGovernance, + ); if (transferAuthority) { - console.debug('transfer community mint authority') + console.debug('transfer community mint authority'); const communityMintGovPk = await withCreateMintGovernance( realmInstructions, programId, @@ -252,8 +250,8 @@ async function prepareGovernanceInstructions( walletPubkey, tokenOwnerRecordPk, walletPubkey, - walletPubkey - ) + walletPubkey, + ); // Set the community governance as the realm authority withSetRealmAuthority( @@ -263,8 +261,8 @@ async function prepareGovernanceInstructions( realmPk, walletPubkey, communityMintGovPk, - SetRealmAuthorityAction.SetChecked - ) + SetRealmAuthorityAction.SetChecked, + ); } if (councilMintPk) @@ -280,8 +278,8 @@ async function prepareGovernanceInstructions( walletPubkey, tokenOwnerRecordPk, walletPubkey, - walletPubkey - ) + walletPubkey, + ); } /** @@ -302,21 +300,21 @@ function sendTransactionFactory( councilSignersChunks: Keypair[][], realmInstructions: TransactionInstruction[], communityMintInstructions?: TransactionInstruction[], - communityMintSigners?: Keypair[] + communityMintSigners?: Keypair[], ) { - console.debug('factoring sendtransaction') + console.debug('factoring sendtransaction'); - const instructions: TransactionInstruction[][] = [realmInstructions] - const signerSets: Keypair[][] = [[]] + const instructions: TransactionInstruction[][] = [realmInstructions]; + const signerSets: Keypair[][] = [[]]; if (councilMembersChunks.length) { - instructions.unshift(...councilMembersChunks) - signerSets.unshift(...councilSignersChunks) + instructions.unshift(...councilMembersChunks); + signerSets.unshift(...councilSignersChunks); } if (communityMintInstructions && communityMintSigners) { - instructions.unshift(communityMintInstructions) - signerSets.unshift(communityMintSigners) + instructions.unshift(communityMintInstructions); + signerSets.unshift(communityMintSigners); } if (instructions.length > 1) { @@ -325,12 +323,12 @@ function sendTransactionFactory( wallet, instructions, signerSets, - SequenceType.Sequential - ) + SequenceType.Sequential, + ); } else { - const transaction = new Transaction() - transaction.add(...realmInstructions) - return sendTransaction({ transaction, wallet, connection }) + const transaction = new Transaction(); + transaction.add(...realmInstructions); + return sendTransaction({ transaction, wallet, connection }); } } @@ -370,12 +368,12 @@ export async function registerRealm( transferAuthority = true, communityMintTokenDecimals?: number, councilMintTokenDecimals?: number, - councilWalletPks?: PublicKey[] + councilWalletPks?: PublicKey[], ): Promise { - if (!wallet) throw WalletConnectionError - console.debug('starting register realm') + if (!wallet) throw WalletConnectionError; + console.debug('starting register realm'); - const realmInstructions: TransactionInstruction[] = [] + const realmInstructions: TransactionInstruction[] = []; const { mintPk: councilMintPk, @@ -389,36 +387,36 @@ export async function registerRealm( councilMintTokenDecimals, true, councilMint, - councilWalletPks - ) + councilWalletPks, + ); let communityMintInstructions: | TransactionInstruction[] - | undefined = undefined - let communityMintPk: PublicKey | undefined = communityMint - let communityMintSigners: Keypair[] | undefined = undefined + | undefined = undefined; + let communityMintPk: PublicKey | undefined = communityMint; + let communityMintSigners: Keypair[] | undefined = undefined; // If user doens't provides a community mint, we'll generate it if (!communityMint) { const communityDetails = await prepareMintInstructions( connection, walletPubkey, - COMMUNITY_MINT_DECIMALS - ) - communityMintInstructions = communityDetails.mintInstructions - communityMintPk = communityDetails.mintPk - communityMintSigners = communityDetails.mintSigners + COMMUNITY_MINT_DECIMALS, + ); + communityMintInstructions = communityDetails.mintInstructions; + communityMintPk = communityDetails.mintPk; + communityMintSigners = communityDetails.mintSigners; } - if (!communityMintPk) throw new Error('Invalid community mint public key.') + if (!communityMintPk) throw new Error('Invalid community mint public key.'); const _minCommunityTokensToCreateGovernance = getMintNaturalAmountFromDecimalAsBN( minCommunityTokensToCreateGovernance && +minCommunityTokensToCreateGovernance > 0 ? +minCommunityTokensToCreateGovernance : MIN_COMMUNITY_TOKENS_TO_CREATE_W_0_SUPPLY, - communityMintTokenDecimals ?? COMMUNITY_MINT_DECIMALS - ) + communityMintTokenDecimals ?? COMMUNITY_MINT_DECIMALS, + ); const realmAddress = await withCreateRealm( realmInstructions, @@ -431,10 +429,10 @@ export async function registerRealm( councilMintPk, communityMintMaxVoteWeightSource, _minCommunityTokensToCreateGovernance, - undefined - ) + undefined, + ); - let tokenOwnerRecordPk: PublicKey | undefined = undefined + let tokenOwnerRecordPk: PublicKey | undefined = undefined; // If the current wallet is in the team then deposit the council token if (councilMintPk) { @@ -444,8 +442,8 @@ export async function registerRealm( programId, realmAddress, councilMintPk, - walletPubkey - ) + walletPubkey, + ); await withDepositGoverningTokens( realmInstructions, @@ -457,12 +455,12 @@ export async function registerRealm( walletPubkey, walletPubkey, walletPubkey, - councilTokenAmount - ) + councilTokenAmount, + ); } else { // Let's throw for now if the current wallet isn't in the team // TODO: To fix it we would have to make it temp. as part of the team and then remove after the realm is created - throw new Error('Current wallet must be in the team') + throw new Error('Current wallet must be in the team'); } } @@ -481,8 +479,8 @@ export async function registerRealm( realmAddress, tokenOwnerRecordPk, realmInstructions, - transferAuthority - ) + transferAuthority, + ); } const txnToSend = sendTransactionFactory( @@ -492,14 +490,14 @@ export async function registerRealm( councilSignersChunks, realmInstructions, communityMintInstructions, - communityMintSigners - ) - console.debug('sending transaction') - await txnToSend - console.debug('transaction sent') + communityMintSigners, + ); + console.debug('sending transaction'); + await txnToSend; + console.debug('transaction sent'); console.debug({ communityMintPk, councilMintPk, - }) - return realmAddress + }); + return realmAddress; } diff --git a/actions/relinquishVote.ts b/actions/relinquishVote.ts deleted file mode 100644 index e20cf73472..0000000000 --- a/actions/relinquishVote.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { - Keypair, - PublicKey, - Transaction, - TransactionInstruction, -} from '@solana/web3.js' - -import { Proposal } from '@solana/spl-governance' -import { RpcContext } from '@solana/spl-governance' -import { ProgramAccount } from '@solana/spl-governance' -import { sendTransaction } from '../utils/send' -import { withRelinquishVote } from '@solana/spl-governance' - -export const relinquishVote = async ( - { connection, wallet, programId, walletPubkey }: RpcContext, - proposal: ProgramAccount, - tokenOwnerRecord: PublicKey, - voteRecord: PublicKey, - instructions: TransactionInstruction[] = [] -) => { - const signers: Keypair[] = [] - - const governanceAuthority = walletPubkey - const beneficiary = walletPubkey - - withRelinquishVote( - instructions, - programId, - proposal.account.governance, - proposal.pubkey, - tokenOwnerRecord, - proposal.account.governingTokenMint, - voteRecord, - governanceAuthority, - beneficiary - ) - - const transaction = new Transaction() - transaction.add(...instructions) - - await sendTransaction({ transaction, wallet, connection, signers }) -} diff --git a/actions/relinquishVotes.ts b/actions/relinquishVotes.ts new file mode 100644 index 0000000000..da1eb30bf4 --- /dev/null +++ b/actions/relinquishVotes.ts @@ -0,0 +1,53 @@ +import { + Keypair, + PublicKey, + Transaction, + TransactionInstruction, +} from '@solana/web3.js'; + +import { Proposal } from '@solana/spl-governance'; +import { RpcContext } from '@solana/spl-governance'; +import { ProgramAccount } from '@solana/spl-governance'; +import { sendTransaction } from '../utils/send'; +import { withRelinquishVote } from '@solana/spl-governance'; + +export default async ({ + rpcContext: { connection, wallet, programId, walletPubkey }, + proposal, + records, + instructions = [], +}: { + rpcContext: RpcContext; + proposal: ProgramAccount; + + records: { + tokenOwnerRecord: PublicKey; + voteRecord: PublicKey; + }[]; + + instructions: TransactionInstruction[]; +}) => { + const signers: Keypair[] = []; + + const governanceAuthority = walletPubkey; + const beneficiary = walletPubkey; + + for (const record of records) { + withRelinquishVote( + instructions, + programId, + proposal.account.governance, + proposal.pubkey, + record.tokenOwnerRecord, + proposal.account.governingTokenMint, + record.voteRecord, + governanceAuthority, + beneficiary, + ); + } + + const transaction = new Transaction(); + transaction.add(...instructions); + + await sendTransaction({ transaction, wallet, connection, signers }); +}; diff --git a/actions/signOffProposal.ts b/actions/signOffProposal.ts index 551bd23270..0bed98cadf 100644 --- a/actions/signOffProposal.ts +++ b/actions/signOffProposal.ts @@ -3,33 +3,33 @@ import { PublicKey, Transaction, TransactionInstruction, -} from '@solana/web3.js' +} from '@solana/web3.js'; import { getGovernanceProgramVersion, Proposal, RpcContext, -} from '@solana/spl-governance' -import { SignatoryRecord } from '@solana/spl-governance' -import { ProgramAccount } from '@solana/spl-governance' -import { sendTransaction } from 'utils/send' -import { withSignOffProposal } from '@solana/spl-governance' +} from '@solana/spl-governance'; +import { SignatoryRecord } from '@solana/spl-governance'; +import { ProgramAccount } from '@solana/spl-governance'; +import { sendTransaction } from 'utils/send'; +import { withSignOffProposal } from '@solana/spl-governance'; export const signOffProposal = async ( { connection, wallet, programId }: RpcContext, realmPk: PublicKey, proposal: ProgramAccount, - signatoryRecord: ProgramAccount + signatoryRecord: ProgramAccount, ) => { - const instructions: TransactionInstruction[] = [] - const signers: Keypair[] = [] + const instructions: TransactionInstruction[] = []; + const signers: Keypair[] = []; // Explicitly request the version before making RPC calls to work around race conditions in resolving // the version for RealmInfo const programVersion = await getGovernanceProgramVersion( connection, - programId - ) + programId, + ); withSignOffProposal( instructions, @@ -40,12 +40,12 @@ export const signOffProposal = async ( proposal.pubkey, signatoryRecord.account.signatory, signatoryRecord?.pubkey, - undefined - ) + undefined, + ); - const transaction = new Transaction() + const transaction = new Transaction(); - transaction.add(...instructions) + transaction.add(...instructions); await sendTransaction({ transaction, @@ -54,5 +54,5 @@ export const signOffProposal = async ( signers, sendingMessage: 'Signing off proposal', successMessage: 'Proposal signed off', - }) -} + }); +}; diff --git a/components/AddMemberIcon.tsx b/components/AddMemberIcon.tsx index d29a72a6e7..ac4b9b589a 100644 --- a/components/AddMemberIcon.tsx +++ b/components/AddMemberIcon.tsx @@ -37,7 +37,7 @@ const AddMemberIcon = ({ className }) => { strokeLinejoin="round" /> - ) -} + ); +}; -export default AddMemberIcon +export default AddMemberIcon; diff --git a/components/AdditionalProposalOptions.tsx b/components/AdditionalProposalOptions.tsx index 00c02ccef6..9b827517a6 100644 --- a/components/AdditionalProposalOptions.tsx +++ b/components/AdditionalProposalOptions.tsx @@ -1,10 +1,10 @@ -import { ChevronDownIcon } from '@heroicons/react/outline' -import useRealm from '@hooks/useRealm' -import VoteBySwitch from 'pages/dao/[symbol]/proposal/components/VoteBySwitch' -import { useState } from 'react' -import { LinkButton } from './Button' -import Input from './inputs/Input' -import Textarea from './inputs/Textarea' +import { ChevronDownIcon } from '@heroicons/react/outline'; +import useRealm from '@hooks/useRealm'; +import VoteBySwitch from 'pages/dao/[symbol]/proposal/components/VoteBySwitch'; +import { useState } from 'react'; +import { LinkButton } from './Button'; +import Input from './inputs/Input'; +import Textarea from './inputs/Textarea'; const AdditionalProposalOptions = ({ title, @@ -15,16 +15,16 @@ const AdditionalProposalOptions = ({ voteByCouncil, setVoteByCouncil, }: { - title: string - description: string - setTitle: (evt) => void - setDescription: (evt) => void - defaultTitle: string - voteByCouncil: boolean - setVoteByCouncil: (val) => void + title: string; + description: string; + setTitle: (evt) => void; + setDescription: (evt) => void; + defaultTitle: string; + voteByCouncil: boolean; + setVoteByCouncil: (val) => void; }) => { - const [showOptions, setShowOptions] = useState(false) - const { canChooseWhoVote } = useRealm() + const [showOptions, setShowOptions] = useState(false); + const { canChooseWhoVote } = useRealm(); return ( <> { - setVoteByCouncil(!voteByCouncil) + setVoteByCouncil(!voteByCouncil); }} /> )}
)} - ) -} + ); +}; -export default AdditionalProposalOptions +export default AdditionalProposalOptions; diff --git a/components/ApprovalQuorum.tsx b/components/ApprovalQuorum.tsx index 4335bd5b30..f88dc118ee 100644 --- a/components/ApprovalQuorum.tsx +++ b/components/ApprovalQuorum.tsx @@ -1,14 +1,14 @@ -import Tooltip from './Tooltip' +import Tooltip from './Tooltip'; import { CheckCircleIcon, InformationCircleIcon, -} from '@heroicons/react/outline' +} from '@heroicons/react/outline'; type ApprovalProgressProps = { - progress: number - showBg?: boolean - yesVotesRequired: number -} + progress: number; + showBg?: boolean; + yesVotesRequired: number; +}; const ApprovalProgress = ({ progress, @@ -31,7 +31,7 @@ const ApprovalProgress = ({ undefined, { maximumFractionDigits: 0, - } + }, )} ${progress > 0 ? 'more' : ''} Yes vote${ yesVotesRequired > 1 ? 's' : '' } required`}

@@ -58,7 +58,7 @@ const ApprovalProgress = ({
{/* ) : null} */}
- ) -} + ); +}; -export default ApprovalProgress +export default ApprovalProgress; diff --git a/components/AssetsList/AssetItem.tsx b/components/AssetsList/AssetItem.tsx index 375ed362bf..cfcc30dfe1 100644 --- a/components/AssetsList/AssetItem.tsx +++ b/components/AssetsList/AssetItem.tsx @@ -1,52 +1,52 @@ -import { useEffect, useState } from 'react' -import { ExternalLinkIcon, TerminalIcon } from '@heroicons/react/outline' -import { ProgramAccount } from '@solana/spl-governance' -import { Governance } from '@solana/spl-governance' -import { getProgramName } from '@components/instructions/programs/names' -import { abbreviateAddress } from '@utils/formatting' -import { PublicKey } from '@solana/web3.js' -import Button, { SecondaryButton } from '@components/Button' -import Tooltip from '@components/Tooltip' -import useWalletStore from 'stores/useWalletStore' -import { getProgramSlot } from '@tools/sdk/bpfUpgradeableLoader/accounts' -import useGovernanceAssets from '@hooks/useGovernanceAssets' -import Modal from '@components/Modal' -import UpgradeProgram from './UpgradeProgram' -import CloseBuffers from './CloseBuffers' -import { getExplorerUrl } from '@components/explorer/tools' +import { useEffect, useState } from 'react'; +import { ExternalLinkIcon, TerminalIcon } from '@heroicons/react/outline'; +import { ProgramAccount } from '@solana/spl-governance'; +import { Governance } from '@solana/spl-governance'; +import { getProgramName } from '@components/instructions/programs/names'; +import { abbreviateAddress } from '@utils/formatting'; +import { PublicKey } from '@solana/web3.js'; +import Button, { SecondaryButton } from '@components/Button'; +import Tooltip from '@components/Tooltip'; +import useWalletStore from 'stores/useWalletStore'; +import { getProgramSlot } from '@tools/sdk/bpfUpgradeableLoader/accounts'; +import useGovernanceAssets from '@hooks/useGovernanceAssets'; +import Modal from '@components/Modal'; +import UpgradeProgram from './UpgradeProgram'; +import CloseBuffers from './CloseBuffers'; +import { getExplorerUrl } from '@components/explorer/tools'; const AssetItem = ({ item, panelView, }: { - item: ProgramAccount - panelView?: boolean + item: ProgramAccount; + panelView?: boolean; }) => { - const { canUseProgramUpgradeInstruction } = useGovernanceAssets() - const [slot, setSlot] = useState(0) - const [openUpgradeModal, setOpenUpgradeModal] = useState(false) - const [openCloseBuffersModal, setOpenCloseBuffersModal] = useState(false) - const [loadSlot, setLoadSlot] = useState(false) - const connection = useWalletStore((s) => s.connection) - const name = item ? getProgramName(item.account.governedAccount) : '' + const { canUseProgramUpgradeInstruction } = useGovernanceAssets(); + const [slot, setSlot] = useState(0); + const [openUpgradeModal, setOpenUpgradeModal] = useState(false); + const [openCloseBuffersModal, setOpenCloseBuffersModal] = useState(false); + const [loadSlot, setLoadSlot] = useState(false); + const connection = useWalletStore((s) => s.connection); + const name = item ? getProgramName(item.account.governedAccount) : ''; const governedAccount = item ? abbreviateAddress(item?.account.governedAccount as PublicKey) - : '' - const programId = item!.account.governedAccount.toBase58() + : ''; + const programId = item!.account.governedAccount.toBase58(); useEffect(() => { const handleSetProgramVersion = async () => { try { - setLoadSlot(true) - const slot = await getProgramSlot(connection.current, programId) - setSlot(slot) + setLoadSlot(true); + const slot = await getProgramSlot(connection.current, programId); + setSlot(slot); } catch (e) { - console.log(e) + console.error(e); } - setLoadSlot(false) - } - handleSetProgramVersion() - }, [JSON.stringify(item)]) + setLoadSlot(false); + }; + handleSetProgramVersion(); + }, [JSON.stringify(item)]); return (
@@ -62,7 +62,7 @@ const AssetItem = ({ className="default-transition flex items-center mt-0.5 text-fgd-3 hover:text-fgd-2 text-xs" href={getExplorerUrl( connection.endpoint, - item?.account.governedAccount + item?.account.governedAccount, )} target="_blank" rel="noopener noreferrer" @@ -122,7 +122,7 @@ const AssetItem = ({ {openUpgradeModal && ( { - setOpenUpgradeModal(false) + setOpenUpgradeModal(false); }} isOpen={openUpgradeModal} > @@ -132,7 +132,7 @@ const AssetItem = ({ {openCloseBuffersModal && ( { - setOpenCloseBuffersModal(false) + setOpenCloseBuffersModal(false); }} isOpen={openCloseBuffersModal} > @@ -140,7 +140,7 @@ const AssetItem = ({ )}
- ) -} + ); +}; -export default AssetItem +export default AssetItem; diff --git a/components/AssetsList/AssetsCompactWrapper.tsx b/components/AssetsList/AssetsCompactWrapper.tsx index 384038f291..064e2b463d 100644 --- a/components/AssetsList/AssetsCompactWrapper.tsx +++ b/components/AssetsList/AssetsCompactWrapper.tsx @@ -1,15 +1,15 @@ -import React from 'react' -import AssetsList from './AssetsList' -import { ChevronRightIcon } from '@heroicons/react/solid' -import useRealm from '@hooks/useRealm' -import useQueryContext from '@hooks/useQueryContext' -import { useRouter } from 'next/router' -import { LinkButton } from '@components/Button' +import React from 'react'; +import AssetsList from './AssetsList'; +import { ChevronRightIcon } from '@heroicons/react/solid'; +import useRealm from '@hooks/useRealm'; +import useQueryContext from '@hooks/useQueryContext'; +import { useRouter } from 'next/router'; +import { LinkButton } from '@components/Button'; const AssetsCompactWrapper = () => { - const router = useRouter() - const { symbol } = useRealm() - const { fmtUrlWithCluster } = useQueryContext() + const router = useRouter(); + const { symbol } = useRealm(); + const { fmtUrlWithCluster } = useQueryContext(); return (
@@ -18,8 +18,8 @@ const AssetsCompactWrapper = () => { { - const url = fmtUrlWithCluster(`/dao/${symbol}/assets`) - router.push(url) + const url = fmtUrlWithCluster(`/dao/${symbol}/assets`); + router.push(url); }} > View @@ -30,7 +30,6 @@ const AssetsCompactWrapper = () => {
- ) -} - -export default AssetsCompactWrapper + ); +}; +export default AssetsCompactWrapper; diff --git a/components/AssetsList/AssetsList.tsx b/components/AssetsList/AssetsList.tsx index 5bc3ca6297..60ba0cfe33 100644 --- a/components/AssetsList/AssetsList.tsx +++ b/components/AssetsList/AssetsList.tsx @@ -1,17 +1,17 @@ -import AssetItem from './AssetItem' -import useGovernanceAssets from '@hooks/useGovernanceAssets' -import { GovernanceAccountType } from '@solana/spl-governance' +import AssetItem from './AssetItem'; +import useGovernanceAssets from '@hooks/useGovernanceAssets'; +import { GovernanceAccountType } from '@solana/spl-governance'; interface AssetsListProps { - panelView?: boolean + panelView?: boolean; } const AssetsList = ({ panelView }: AssetsListProps) => { - const { getGovernancesByAccountTypes } = useGovernanceAssets() + const { getGovernancesByAccountTypes } = useGovernanceAssets(); const programGovernances = getGovernancesByAccountTypes([ GovernanceAccountType.ProgramGovernanceV1, GovernanceAccountType.ProgramGovernanceV2, - ]) + ]); return !panelView ? (
{programGovernances.map((x) => ( @@ -24,6 +24,6 @@ const AssetsList = ({ panelView }: AssetsListProps) => { ))}
- ) -} -export default AssetsList + ); +}; +export default AssetsList; diff --git a/components/AssetsList/BaseGovernanceForm.tsx b/components/AssetsList/BaseGovernanceForm.tsx index 96d5025417..192f17fbc9 100644 --- a/components/AssetsList/BaseGovernanceForm.tsx +++ b/components/AssetsList/BaseGovernanceForm.tsx @@ -1,6 +1,6 @@ -import Input from '@components/inputs/Input' -import AmountSlider from '@components/Slider' -import useRealm from '@hooks/useRealm' +import Input from '@components/inputs/Input'; +import AmountSlider from '@components/Slider'; +import useRealm from '@hooks/useRealm'; import { fmtPercentage, getMintMinAmountAsDecimal, @@ -8,86 +8,86 @@ import { getMintSupplyFractionAsDecimalPercentage, getMintSupplyPercentageAsDecimal, parseMintNaturalAmountFromDecimal, -} from '@tools/sdk/units' -import BigNumber from 'bignumber.js' -import React, { useEffect, useState } from 'react' +} from '@tools/sdk/units'; +import BigNumber from 'bignumber.js'; +import React, { useEffect, useState } from 'react'; export interface BaseGovernanceFormFields { - minCommunityTokensToCreateProposal: number - minInstructionHoldUpTime: number - maxVotingTime: number - voteThreshold: number + minCommunityTokensToCreateProposal: number; + minInstructionHoldUpTime: number; + maxVotingTime: number; + voteThreshold: number; } const BaseGovernanceForm = ({ formErrors, form, setForm, setFormErrors }) => { - const { realmInfo, mint: realmMint } = useRealm() + const { realmInfo, mint: realmMint } = useRealm(); const [minTokensPercentage, setMinTokensPercentage] = useState< number | undefined - >() + >(); const handleSetForm = ({ propertyName, value }) => { - setFormErrors({}) - setForm({ ...form, [propertyName]: value }) - } + setFormErrors({}); + setForm({ ...form, [propertyName]: value }); + }; const validateMinMax = (e) => { - const fieldName = e.target.name - const min = e.target.min || 0 - const max = e.target.max || Number.MAX_SAFE_INTEGER - const value = form[fieldName] - const currentPrecision = new BigNumber(min).decimalPlaces() + const fieldName = e.target.name; + const min = e.target.min || 0; + const max = e.target.max || Number.MAX_SAFE_INTEGER; + const value = form[fieldName]; + const currentPrecision = new BigNumber(min).decimalPlaces(); handleSetForm({ value: parseFloat( Math.max(Number(min), Math.min(Number(max), Number(value))).toFixed( - currentPrecision - ) + currentPrecision, + ), ), propertyName: fieldName, - }) - } + }); + }; function parseMinTokensToCreateProposal( value: string | number, - mintDecimals: number + mintDecimals: number, ) { return typeof value === 'string' ? parseMintNaturalAmountFromDecimal(value, mintDecimals) - : getMintNaturalAmountFromDecimal(value, mintDecimals) + : getMintNaturalAmountFromDecimal(value, mintDecimals); } const onMinTokensChange = (minTokensToCreateProposal: number | string) => { const minTokens = realmMint ? parseMinTokensToCreateProposal( minTokensToCreateProposal, - realmMint.decimals + realmMint.decimals, ) - : 0 - setMinTokensPercentage(getMinTokensPercentage(minTokens)) - } + : 0; + setMinTokensPercentage(getMinTokensPercentage(minTokens)); + }; const getMinTokensPercentage = (amount: number) => - realmMint ? getMintSupplyFractionAsDecimalPercentage(realmMint, amount) : 0 + realmMint ? getMintSupplyFractionAsDecimalPercentage(realmMint, amount) : 0; // Use 1% of mint supply as the default value for minTokensToCreateProposal and the default increment step in the input editor const mintSupply1Percent = realmMint ? getMintSupplyPercentageAsDecimal(realmMint, 1) - : 100 + : 100; const minTokenAmount = realmMint ? getMintMinAmountAsDecimal(realmMint) - : 0.0001 + : 0.0001; // If the supply is small and 1% is below the minimum mint amount then coerce to the minimum value - const minTokenStep = Math.max(mintSupply1Percent, minTokenAmount) + const minTokenStep = Math.max(mintSupply1Percent, minTokenAmount); const getSupplyPercent = () => { const hasMinTokensPercentage = - !!minTokensPercentage && !isNaN(minTokensPercentage) + !!minTokensPercentage && !isNaN(minTokensPercentage); const percent = hasMinTokensPercentage && minTokensPercentage ? fmtPercentage(minTokensPercentage) - : '' - return hasMinTokensPercentage &&
{`${percent} of token supply`}
- } + : ''; + return hasMinTokensPercentage &&
{`${percent} of token supply`}
; + }; useEffect(() => { - onMinTokensChange(form.minCommunityTokensToCreateProposal) - }, [form.minCommunityTokensToCreateProposal, realmInfo?.symbol]) + onMinTokensChange(form.minCommunityTokensToCreateProposal); + }, [form.minCommunityTokensToCreateProposal, realmInfo?.symbol]); return ( <> @@ -163,12 +163,12 @@ const BaseGovernanceForm = ({ formErrors, form, setForm, setFormErrors }) => { handleSetForm({ value: $e, propertyName: 'voteThreshold', - }) + }); }} /> - ) -} + ); +}; -export default BaseGovernanceForm +export default BaseGovernanceForm; diff --git a/components/AssetsList/CloseBuffers.tsx b/components/AssetsList/CloseBuffers.tsx index 2e5b08fcf5..6be5174720 100644 --- a/components/AssetsList/CloseBuffers.tsx +++ b/components/AssetsList/CloseBuffers.tsx @@ -1,65 +1,65 @@ -import { ExternalLinkIcon } from '@heroicons/react/outline' -import { ChevronDownIcon } from '@heroicons/react/solid' -import { AccountInfo, ParsedAccountData, PublicKey } from '@solana/web3.js' -import useRealm from 'hooks/useRealm' -import Input from 'components/inputs/Input' -import Button, { LinkButton } from '@components/Button' -import Textarea from 'components/inputs/Textarea' -import VoteBySwitch from 'pages/dao/[symbol]/proposal/components/VoteBySwitch' -import useWalletStore from 'stores/useWalletStore' -import { getValidatedPublickKey } from 'utils/validations' -import { useEffect, useState } from 'react' -import { UiInstruction } from 'utils/uiTypes/proposalCreationTypes' -import { serializeInstructionToBase64 } from '@solana/spl-governance' -import { useRouter } from 'next/router' -import { notify } from 'utils/notifications' -import useQueryContext from 'hooks/useQueryContext' -import { validateInstruction } from 'utils/instructionTools' -import * as yup from 'yup' -import { BPF_UPGRADE_LOADER_ID, GovernedProgramAccount } from '@utils/tokens' -import Loading from '@components/Loading' -import useCreateProposal from '@hooks/useCreateProposal' -import { getExplorerUrl } from '@components/explorer/tools' -import { InstructionDataWithHoldUpTime } from 'actions/createProposal' -import { createCloseBuffer } from '@tools/sdk/bpfUpgradeableLoader/createCloseBuffer' -import { abbreviateAddress } from '@utils/formatting' -import useGovernanceAssets from '@hooks/useGovernanceAssets' -import { Governance, ProgramAccount } from '@solana/spl-governance' +import { ExternalLinkIcon } from '@heroicons/react/outline'; +import { ChevronDownIcon } from '@heroicons/react/solid'; +import { AccountInfo, ParsedAccountData, PublicKey } from '@solana/web3.js'; +import useRealm from 'hooks/useRealm'; +import Input from 'components/inputs/Input'; +import Button, { LinkButton } from '@components/Button'; +import Textarea from 'components/inputs/Textarea'; +import VoteBySwitch from 'pages/dao/[symbol]/proposal/components/VoteBySwitch'; +import useWalletStore from 'stores/useWalletStore'; +import { getValidatedPublickKey } from 'utils/validations'; +import { useEffect, useState } from 'react'; +import { FormInstructionData } from 'utils/uiTypes/proposalCreationTypes'; +import { serializeInstructionToBase64 } from '@solana/spl-governance'; +import { useRouter } from 'next/router'; +import { notify } from 'utils/notifications'; +import useQueryContext from 'hooks/useQueryContext'; +import { validateInstruction } from 'utils/instructionTools'; +import * as yup from 'yup'; +import { BPF_UPGRADE_LOADER_ID, GovernedProgramAccount } from '@utils/tokens'; +import Loading from '@components/Loading'; +import useCreateProposal from '@hooks/useCreateProposal'; +import { getExplorerUrl } from '@components/explorer/tools'; +import { InstructionDataWithHoldUpTime } from 'actions/createProposal'; +import { createCloseBuffer } from '@tools/sdk/bpfUpgradeableLoader/createCloseBuffer'; +import { abbreviateAddress } from '@utils/formatting'; +import useGovernanceAssets from '@hooks/useGovernanceAssets'; +import { Governance, ProgramAccount } from '@solana/spl-governance'; interface CloseBuffersForm { - governedAccount: GovernedProgramAccount | undefined - programId: string | undefined - solReceiverAddress: string - description: string - title: string + governedAccount: GovernedProgramAccount | undefined; + programId: string | undefined; + solReceiverAddress: string; + description: string; + title: string; } const CloseBuffers = ({ program }: { program: ProgramAccount }) => { - const { handleCreateProposal } = useCreateProposal() - const { governedTokenAccountsWithoutNfts } = useGovernanceAssets() - const router = useRouter() - const connection = useWalletStore((s) => s.connection) - const wallet = useWalletStore((s) => s.current) + const { handleCreateProposal } = useCreateProposal(); + const { governedTokenAccountsWithoutNfts } = useGovernanceAssets(); + const router = useRouter(); + const connection = useWalletStore((s) => s.connection); + const wallet = useWalletStore((s) => s.current); const governedAccount = { governance: program!, - } - const { fmtUrlWithCluster } = useQueryContext() - const { symbol } = router.query - const { realmInfo, canChooseWhoVote, realm } = useRealm() - const [isBuffersLoading, setIsBuffersLoading] = useState(false) - const programId: PublicKey | undefined = realmInfo?.programId + }; + const { fmtUrlWithCluster } = useQueryContext(); + const { symbol } = router.query; + const { realmInfo, canChooseWhoVote, realm } = useRealm(); + const [isBuffersLoading, setIsBuffersLoading] = useState(false); + const programId: PublicKey | undefined = realmInfo?.programId; const [buffers, setBuffers] = useState< { - pubkey: PublicKey - account: AccountInfo + pubkey: PublicKey; + account: AccountInfo; }[] - >([]) + >([]); const highestLampartsAmountInGovernedTokenAccounts = Math.max( ...governedTokenAccountsWithoutNfts .filter((x) => x.isSol) - .map((x) => x.solAccount!.lamports) - ) - const solAccounts = governedTokenAccountsWithoutNfts.filter((x) => x.isSol) + .map((x) => x.solAccount!.lamports), + ); + const solAccounts = governedTokenAccountsWithoutNfts.filter((x) => x.isSol); const [form, setForm] = useState({ governedAccount: governedAccount, programId: programId?.toString(), @@ -68,7 +68,7 @@ const CloseBuffers = ({ program }: { program: ProgramAccount }) => { .find( (x) => x.solAccount?.lamports === - highestLampartsAmountInGovernedTokenAccounts + highestLampartsAmountInGovernedTokenAccounts, )! .transferAddress!.toBase58() : wallet?.publicKey?.toBase58() @@ -76,52 +76,52 @@ const CloseBuffers = ({ program }: { program: ProgramAccount }) => { : '', description: '', title: '', - }) - const [voteByCouncil, setVoteByCouncil] = useState(false) - const [showOptions, setShowOptions] = useState(false) - const [isLoading, setIsLoading] = useState(false) - const [formErrors, setFormErrors] = useState({}) + }); + const [voteByCouncil, setVoteByCouncil] = useState(false); + const [showOptions, setShowOptions] = useState(false); + const [isLoading, setIsLoading] = useState(false); + const [formErrors, setFormErrors] = useState({}); const proposalTitle = `Close buffers for program ${ form.governedAccount?.governance?.account.governedAccount ? abbreviateAddress( - form.governedAccount?.governance?.account.governedAccount + form.governedAccount?.governance?.account.governedAccount, ) : '' - }` + }`; const handleSetForm = ({ propertyName, value }) => { - setFormErrors({}) - setForm({ ...form, [propertyName]: value }) - } + setFormErrors({}); + setForm({ ...form, [propertyName]: value }); + }; const schema = yup.object().shape({ solReceiverAddress: yup .string() .test('accountTests', 'Account validation error', function (val: string) { if (val) { try { - return !!getValidatedPublickKey(val) + return !!getValidatedPublickKey(val); } catch (e) { - console.log(e) + console.error(e); return this.createError({ message: `${e}`, - }) + }); } } else { return this.createError({ message: `Retrieved SOL receiver address is required`, - }) + }); } }), governedAccount: yup .object() .nullable() .required('Program governed account is required'), - }) - async function getInstructions(): Promise { - const isValid = await validateInstruction({ schema, form, setFormErrors }) - const instructions: UiInstruction[] = [] + }); + async function getInstructions(): Promise { + const isValid = await validateInstruction({ schema, form, setFormErrors }); + const instructions: FormInstructionData[] = []; for (let i = 0; i < buffers.length; i++) { - let serializedInstruction = '' + let serializedInstruction = ''; if ( isValid && programId && @@ -131,27 +131,27 @@ const CloseBuffers = ({ program }: { program: ProgramAccount }) => { const closeIx = await createCloseBuffer( buffers[i].pubkey, new PublicKey(form.solReceiverAddress), - form.governedAccount.governance.pubkey - ) - serializedInstruction = serializeInstructionToBase64(closeIx) + form.governedAccount.governance.pubkey, + ); + serializedInstruction = serializeInstructionToBase64(closeIx); } - const obj: UiInstruction = { + const obj: FormInstructionData = { serializedInstruction: serializedInstruction, isValid, governance: form.governedAccount?.governance, - } - instructions.push(obj) + }; + instructions.push(obj); } - return instructions + return instructions; } const handlePropose = async () => { - setIsLoading(true) - const instructions: UiInstruction[] = await getInstructions() + setIsLoading(true); + const instructions: FormInstructionData[] = await getInstructions(); if (instructions.length && instructions[0].isValid) { - const governance = form.governedAccount?.governance + const governance = form.governedAccount?.governance; if (!realm) { - setIsLoading(false) - throw 'No realm selected' + setIsLoading(false); + throw 'No realm selected'; } const instructionsData = instructions.map( @@ -159,8 +159,8 @@ const CloseBuffers = ({ program }: { program: ProgramAccount }) => { new InstructionDataWithHoldUpTime({ instruction: x, governance, - }) - ) + }), + ); try { const proposalAddress = await handleCreateProposal({ title: form.title ? form.title : proposalTitle, @@ -168,30 +168,30 @@ const CloseBuffers = ({ program }: { program: ProgramAccount }) => { voteByCouncil, instructionsData: instructionsData, governance: governance!, - }) + }); const url = fmtUrlWithCluster( - `/dao/${symbol}/proposal/${proposalAddress}` - ) - router.push(url) + `/dao/${symbol}/proposal/${proposalAddress}`, + ); + router.push(url); } catch (ex) { - notify({ type: 'error', message: `${ex}` }) + notify({ type: 'error', message: `${ex}` }); } } - setIsLoading(false) - } + setIsLoading(false); + }; useEffect(() => { handleSetForm({ propertyName: 'programId', value: programId?.toString(), - }) - }, [realmInfo?.programId]) + }); + }, [realmInfo?.programId]); useEffect(() => { const getBuffers = async () => { try { - setBuffers([]) - setIsBuffersLoading(true) + setBuffers([]); + setIsBuffersLoading(true); const buffers = await connection.current.getParsedProgramAccounts( BPF_UPGRADE_LOADER_ID, { @@ -203,18 +203,18 @@ const CloseBuffers = ({ program }: { program: ProgramAccount }) => { }, }, ], - } - ) - setBuffers(buffers) + }, + ); + setBuffers(buffers); } catch (e) { - notify({ type: 'error', message: "Can't fetch buffers" }) + notify({ type: 'error', message: "Can't fetch buffers" }); } - setIsBuffersLoading(false) - } + setIsBuffersLoading(false); + }; if (form.governedAccount?.governance?.pubkey.toBase58()) { - getBuffers() + getBuffers(); } - }, [form.governedAccount?.governance?.pubkey.toBase58()]) + }, [form.governedAccount?.governance?.pubkey.toBase58()]); return ( <>

@@ -303,7 +303,7 @@ const CloseBuffers = ({ program }: { program: ProgramAccount }) => { { - setVoteByCouncil(!voteByCouncil) + setVoteByCouncil(!voteByCouncil); }} /> )} @@ -319,7 +319,7 @@ const CloseBuffers = ({ program }: { program: ProgramAccount }) => {
Propose Close {buffers.length > 1 ? 'Buffers' : 'Buffer'}
- ) -} + ); +}; -export default CloseBuffers +export default CloseBuffers; diff --git a/components/AssetsList/NewProgramForm.tsx b/components/AssetsList/NewProgramForm.tsx index 73aee8ab47..492d6dd52c 100644 --- a/components/AssetsList/NewProgramForm.tsx +++ b/components/AssetsList/NewProgramForm.tsx @@ -1,32 +1,32 @@ -import Button from 'components/Button' -import Input from 'components/inputs/Input' -import PreviousRouteBtn from 'components/PreviousRouteBtn' -import Tooltip from 'components/Tooltip' -import useQueryContext from 'hooks/useQueryContext' -import useRealm from 'hooks/useRealm' -import { RpcContext } from '@solana/spl-governance' -import { PublicKey } from '@solana/web3.js' -import { tryParseKey } from 'tools/validators/pubkey' -import { isFormValid } from 'utils/formValidation' -import { getGovernanceConfig } from 'utils/GovernanceTools' -import { notify } from 'utils/notifications' -import { useRouter } from 'next/router' -import React, { useEffect, useState } from 'react' -import useWalletStore from 'stores/useWalletStore' -import * as yup from 'yup' +import Button from 'components/Button'; +import Input from 'components/inputs/Input'; +import PreviousRouteBtn from 'components/PreviousRouteBtn'; +import Tooltip from 'components/Tooltip'; +import useQueryContext from 'hooks/useQueryContext'; +import useRealm from 'hooks/useRealm'; +import { RpcContext } from '@solana/spl-governance'; +import { PublicKey } from '@solana/web3.js'; +import { tryParseKey } from 'tools/validators/pubkey'; +import { isFormValid } from 'utils/formValidation'; +import { getGovernanceConfig } from 'utils/GovernanceTools'; +import { notify } from 'utils/notifications'; +import { useRouter } from 'next/router'; +import React, { useEffect, useState } from 'react'; +import useWalletStore from 'stores/useWalletStore'; +import * as yup from 'yup'; import BaseGovernanceForm, { BaseGovernanceFormFields, -} from './BaseGovernanceForm' -import { registerProgramGovernance } from 'actions/registerProgramGovernance' -import { GovernanceType } from '@solana/spl-governance' -import Switch from 'components/Switch' -import { debounce } from '@utils/debounce' -import { MIN_COMMUNITY_TOKENS_TO_CREATE_W_0_SUPPLY } from '@tools/constants' -import { getProgramVersionForRealm } from '@models/registry/api' -import useVoteStakeRegistryClientStore from 'VoteStakeRegistry/stores/voteStakeRegistryClientStore' +} from './BaseGovernanceForm'; +import { registerProgramGovernance } from 'actions/registerProgramGovernance'; +import { GovernanceType } from '@solana/spl-governance'; +import Switch from 'components/Switch'; +import { debounce } from '@utils/debounce'; +import { MIN_COMMUNITY_TOKENS_TO_CREATE_W_0_SUPPLY } from '@tools/constants'; +import { getProgramVersionForRealm } from '@models/registry/api'; +import useVoteStakeRegistryClientStore from 'VoteStakeRegistry/stores/voteStakeRegistryClientStore'; interface NewProgramForm extends BaseGovernanceFormFields { - programId: string - transferAuthority: boolean + programId: string; + transferAuthority: boolean; } const defaultFormValues = { @@ -39,60 +39,60 @@ const defaultFormValues = { maxVotingTime: 3, voteThreshold: 60, transferAuthority: true, -} +}; const NewProgramForm = () => { - const router = useRouter() - const { fmtUrlWithCluster } = useQueryContext() - const client = useVoteStakeRegistryClientStore((s) => s.state.client) + const router = useRouter(); + const { fmtUrlWithCluster } = useQueryContext(); + const client = useVoteStakeRegistryClientStore((s) => s.state.client); const { realmInfo, realm, mint: realmMint, symbol, ownVoterWeight, - } = useRealm() - const wallet = useWalletStore((s) => s.current) - const connection = useWalletStore((s) => s.connection) - const connected = useWalletStore((s) => s.connected) - const { fetchRealm } = useWalletStore((s) => s.actions) + } = useRealm(); + const wallet = useWalletStore((s) => s.current); + const connection = useWalletStore((s) => s.connection); + const connected = useWalletStore((s) => s.connected); + const { fetchRealm } = useWalletStore((s) => s.actions); const [form, setForm] = useState({ ...defaultFormValues, - }) - const [isLoading, setIsLoading] = useState(false) - const [formErrors, setFormErrors] = useState({}) + }); + const [isLoading, setIsLoading] = useState(false); + const [formErrors, setFormErrors] = useState({}); const tokenOwnerRecord = ownVoterWeight.canCreateGovernanceUsingCouncilTokens() ? ownVoterWeight.councilTokenRecord : realm && ownVoterWeight.canCreateGovernanceUsingCommunityTokens(realm) ? ownVoterWeight.communityTokenRecord - : undefined + : undefined; const handleSetForm = ({ propertyName, value }) => { - setFormErrors({}) - setForm({ ...form, [propertyName]: value }) - } + setFormErrors({}); + setForm({ ...form, [propertyName]: value }); + }; const handleCreate = async () => { try { if (!realm) { - throw 'No realm selected' + throw 'No realm selected'; } if (!connected) { - throw 'Please connect your wallet' + throw 'Please connect your wallet'; } if (!tokenOwnerRecord) { - throw "You don't have enough governance power to create a new program governance" + throw "You don't have enough governance power to create a new program governance"; } - const { isValid, validationErrors } = await isFormValid(schema, form) - setFormErrors(validationErrors) + const { isValid, validationErrors } = await isFormValid(schema, form); + setFormErrors(validationErrors); if (isValid && realmMint) { - setIsLoading(true) + setIsLoading(true); const rpcContext = new RpcContext( new PublicKey(realm.owner.toString()), getProgramVersionForRealm(realmInfo!), wallet!, connection.current, - connection.endpoint - ) + connection.endpoint, + ); const governanceConfigValues = { minTokensToCreateProposal: form.minCommunityTokensToCreateProposal, @@ -100,8 +100,8 @@ const NewProgramForm = () => { maxVotingTime: form.maxVotingTime, voteThresholdPercentage: form.voteThreshold, mintDecimals: realmMint.decimals, - } - const governanceConfig = getGovernanceConfig(governanceConfigValues) + }; + const governanceConfig = getGovernanceConfig(governanceConfigValues); await registerProgramGovernance( rpcContext, GovernanceType.Program, @@ -110,11 +110,11 @@ const NewProgramForm = () => { governanceConfig, form.transferAuthority, tokenOwnerRecord!.pubkey, - client - ) - setIsLoading(false) - fetchRealm(realmInfo!.programId, realmInfo!.realmId) - router.push(fmtUrlWithCluster(`/dao/${symbol}/`)) + client, + ); + setIsLoading(false); + fetchRealm(realmInfo!.programId, realmInfo!.realmId); + router.push(fmtUrlWithCluster(`/dao/${symbol}/`)); } } catch (e) { //TODO how do we present errors maybe something more generic ? @@ -122,10 +122,10 @@ const NewProgramForm = () => { type: 'error', message: `Can't create governance`, description: `Transaction error ${e}`, - }) - setIsLoading(false) + }); + setIsLoading(false); } - } + }; //if you altering this look at useEffect for form.programId const schema = yup.object().shape({ programId: yup @@ -136,44 +136,44 @@ const NewProgramForm = () => { async function (val: string) { if (val) { try { - const pubKey = tryParseKey(val) + const pubKey = tryParseKey(val); if (!pubKey) { return this.createError({ message: `Invalid account address`, - }) + }); } const accountData = await connection.current.getParsedAccountInfo( - pubKey - ) + pubKey, + ); if (!accountData || !accountData.value) { return this.createError({ message: `Account not found`, - }) + }); } - return true + return true; } catch (e) { return this.createError({ message: `Invalid account address`, - }) + }); } } else { return this.createError({ message: `Program id is required`, - }) + }); } - } + }, ), - }) + }); useEffect(() => { if (form.programId) { //now validation contains only programId if more fields come it would be good to reconsider this method. debounce.debounceFcn(async () => { - const { validationErrors } = await isFormValid(schema, form) - setFormErrors(validationErrors) - }) + const { validationErrors } = await isFormValid(schema, form); + setFormErrors(validationErrors); + }); } - }, [form.programId]) + }, [form.programId]); return (
@@ -226,7 +226,7 @@ const NewProgramForm = () => {
- ) -} + ); +}; -export default NewProgramForm +export default NewProgramForm; diff --git a/components/AssetsList/UpgradeProgram.tsx b/components/AssetsList/UpgradeProgram.tsx index 447bcda8f3..7d75bc0bc9 100644 --- a/components/AssetsList/UpgradeProgram.tsx +++ b/components/AssetsList/UpgradeProgram.tsx @@ -1,57 +1,57 @@ -import { ChevronDownIcon } from '@heroicons/react/solid' -import { PublicKey } from '@solana/web3.js' -import useRealm from 'hooks/useRealm' -import Input from 'components/inputs/Input' -import Button, { LinkButton } from '@components/Button' -import Textarea from 'components/inputs/Textarea' -import VoteBySwitch from 'pages/dao/[symbol]/proposal/components/VoteBySwitch' -import useWalletStore from 'stores/useWalletStore' -import { validateBuffer } from 'utils/validations' -import { useEffect, useState } from 'react' +import { ChevronDownIcon } from '@heroicons/react/solid'; +import { PublicKey } from '@solana/web3.js'; +import useRealm from 'hooks/useRealm'; +import Input from 'components/inputs/Input'; +import Button, { LinkButton } from '@components/Button'; +import Textarea from 'components/inputs/Textarea'; +import VoteBySwitch from 'pages/dao/[symbol]/proposal/components/VoteBySwitch'; +import useWalletStore from 'stores/useWalletStore'; +import { validateBuffer } from 'utils/validations'; +import { useEffect, useState } from 'react'; import { ProgramUpgradeForm, - UiInstruction, -} from 'utils/uiTypes/proposalCreationTypes' + FormInstructionData, +} from 'utils/uiTypes/proposalCreationTypes'; import { getInstructionDataFromBase64, serializeInstructionToBase64, -} from '@solana/spl-governance' -import { RpcContext } from '@solana/spl-governance' -import { Governance, ProgramAccount } from '@solana/spl-governance' -import { useRouter } from 'next/router' -import { createProposal } from 'actions/createProposal' -import { notify } from 'utils/notifications' -import useQueryContext from 'hooks/useQueryContext' -import { validateInstruction } from 'utils/instructionTools' -import * as yup from 'yup' -import { createUpgradeInstruction } from '@tools/sdk/bpfUpgradeableLoader/createUpgradeInstruction' -import { debounce } from '@utils/debounce' -import { isFormValid } from '@utils/formValidation' -import { getProgramVersionForRealm } from '@models/registry/api' -import ProgramUpgradeInfo from 'pages/dao/[symbol]/proposal/components/instructions/bpfUpgradeableLoader/ProgramUpgradeInfo' -import useVoteStakeRegistryClientStore from 'VoteStakeRegistry/stores/voteStakeRegistryClientStore' -import { getProgramName } from '@components/instructions/programs/names' +} from '@solana/spl-governance'; +import { RpcContext } from '@solana/spl-governance'; +import { Governance, ProgramAccount } from '@solana/spl-governance'; +import { useRouter } from 'next/router'; +import { createProposal } from 'actions/createProposal'; +import { notify } from 'utils/notifications'; +import useQueryContext from 'hooks/useQueryContext'; +import { validateInstruction } from 'utils/instructionTools'; +import * as yup from 'yup'; +import { createUpgradeInstruction } from '@tools/sdk/bpfUpgradeableLoader/createUpgradeInstruction'; +import { debounce } from '@utils/debounce'; +import { isFormValid } from '@utils/formValidation'; +import { getProgramVersionForRealm } from '@models/registry/api'; +import ProgramUpgradeInfo from 'pages/dao/[symbol]/proposal/components/instructions/bpfUpgradeableLoader/ProgramUpgradeInfo'; +import useVoteStakeRegistryClientStore from 'VoteStakeRegistry/stores/voteStakeRegistryClientStore'; +import { getProgramName } from '@components/instructions/programs/names'; interface UpgradeProgramCompactForm extends ProgramUpgradeForm { - description: string - title: string + description: string; + title: string; } const UpgradeProgram = ({ program, }: { - program: ProgramAccount + program: ProgramAccount; }) => { - const router = useRouter() - const client = useVoteStakeRegistryClientStore((s) => s.state.client) - const connection = useWalletStore((s) => s.connection) - const wallet = useWalletStore((s) => s.current) + const router = useRouter(); + const client = useVoteStakeRegistryClientStore((s) => s.state.client); + const connection = useWalletStore((s) => s.connection); + const wallet = useWalletStore((s) => s.current); const governedAccount = { governance: program!, - } - const { fmtUrlWithCluster } = useQueryContext() - const { fetchRealmGovernance } = useWalletStore((s) => s.actions) - const { symbol } = router.query + }; + const { fmtUrlWithCluster } = useQueryContext(); + const { fetchRealmGovernance } = useWalletStore((s) => s.actions); + const { symbol } = router.query; const { realmInfo, canChooseWhoVote, @@ -59,26 +59,25 @@ const UpgradeProgram = ({ realm, ownVoterWeight, mint, - } = useRealm() - const programId: PublicKey | undefined = realmInfo?.programId + } = useRealm(); + const programId: PublicKey | undefined = realmInfo?.programId; const [form, setForm] = useState({ governedAccount: governedAccount, - programId: programId?.toString(), bufferAddress: '', description: '', title: '', - }) - const [voteByCouncil, setVoteByCouncil] = useState(false) - const [showOptions, setShowOptions] = useState(false) - const [isLoading, setIsLoading] = useState(false) - const [formErrors, setFormErrors] = useState({}) - const proposalTitle = `Upgrade ${form.governedAccount?.governance?.account.governedAccount.toBase58()}` - const name = program ? getProgramName(program.account.governedAccount) : '' + }); + const [voteByCouncil, setVoteByCouncil] = useState(false); + const [showOptions, setShowOptions] = useState(false); + const [isLoading, setIsLoading] = useState(false); + const [formErrors, setFormErrors] = useState({}); + const proposalTitle = `Upgrade ${form.governedAccount?.governance?.account.governedAccount.toBase58()}`; + const name = program ? getProgramName(program.account.governedAccount) : ''; const handleSetForm = ({ propertyName, value }) => { - setFormErrors({}) - setForm({ ...form, [propertyName]: value }) - } + setFormErrors({}); + setForm({ ...form, [propertyName]: value }); + }; const schema = yup.object().shape({ bufferAddress: yup .string() @@ -88,58 +87,59 @@ const UpgradeProgram = ({ await validateBuffer( connection, val, - form.governedAccount?.governance?.pubkey - ) - return true + form.governedAccount?.governance?.pubkey, + ); + return true; } catch (e) { return this.createError({ message: `${e}`, - }) + }); } } else { return this.createError({ message: `Buffer address is required`, - }) + }); } }), governedAccount: yup .object() .nullable() .required('Program governed account is required'), - }) - async function getInstruction(): Promise { - const isValid = await validateInstruction({ schema, form, setFormErrors }) - let serializedInstruction = '' + }); + async function getInstruction(): Promise { + const isValid = await validateInstruction({ schema, form, setFormErrors }); + let serializedInstruction = ''; if ( isValid && programId && form.governedAccount?.governance?.account && - wallet?.publicKey + wallet?.publicKey && + form.bufferAddress ) { const upgradeIx = await createUpgradeInstruction( form.governedAccount.governance.account.governedAccount, new PublicKey(form.bufferAddress), form.governedAccount.governance.pubkey, - wallet!.publicKey - ) - serializedInstruction = serializeInstructionToBase64(upgradeIx) + wallet!.publicKey, + ); + serializedInstruction = serializeInstructionToBase64(upgradeIx); } - const obj: UiInstruction = { + const obj: FormInstructionData = { serializedInstruction: serializedInstruction, isValid, governance: form.governedAccount?.governance, - } - return obj + }; + return obj; } const handlePropose = async () => { - setIsLoading(true) - const instruction: UiInstruction = await getInstruction() + setIsLoading(true); + const instruction: FormInstructionData = await getInstruction(); if (instruction.isValid) { - const governance = form.governedAccount?.governance - let proposalAddress: PublicKey | null = null + const governance = form.governedAccount?.governance; + let proposalAddress: PublicKey | null = null; if (!realm) { - setIsLoading(false) - throw 'No realm selected' + setIsLoading(false); + throw 'No realm selected'; } const rpcContext = new RpcContext( @@ -147,40 +147,40 @@ const UpgradeProgram = ({ getProgramVersionForRealm(realmInfo!), wallet!, connection.current, - connection.endpoint - ) + connection.endpoint, + ); const instructionData = { data: instruction.serializedInstruction ? getInstructionDataFromBase64(instruction.serializedInstruction) : null, holdUpTime: governance?.account?.config.minInstructionHoldUpTime, prerequisiteInstructions: instruction.prerequisiteInstructions || [], - } + }; try { // Fetch governance to get up to date proposalCount const selectedGovernance = (await fetchRealmGovernance( - governance?.pubkey - )) as ProgramAccount + governance?.pubkey, + )) as ProgramAccount; const ownTokenRecord = ownVoterWeight.getTokenRecordToCreateProposal( - governance!.account.config - ) + governance!.account.config, + ); const defaultProposalMint = !mint?.supply.isZero() ? realm.account.communityMint : !councilMint?.supply.isZero() ? realm.account.config.councilMint - : undefined + : undefined; const proposalMint = canChooseWhoVote && voteByCouncil ? realm.account.config.councilMint - : defaultProposalMint + : defaultProposalMint; if (!proposalMint) { throw new Error( - 'There is no suitable governing token for the proposal' - ) + 'There is no suitable governing token for the proposal', + ); } //Description same as title proposalAddress = await createProposal( @@ -194,34 +194,34 @@ const UpgradeProgram = ({ selectedGovernance?.account?.proposalCount, [instructionData], false, - client - ) + client, + ); const url = fmtUrlWithCluster( - `/dao/${symbol}/proposal/${proposalAddress}` - ) - router.push(url) + `/dao/${symbol}/proposal/${proposalAddress}`, + ); + router.push(url); } catch (ex) { - notify({ type: 'error', message: `${ex}` }) + notify({ type: 'error', message: `${ex}` }); } } - setIsLoading(false) - } + setIsLoading(false); + }; useEffect(() => { handleSetForm({ propertyName: 'programId', value: programId?.toString(), - }) - }, [realmInfo?.programId]) + }); + }, [realmInfo?.programId]); useEffect(() => { if (form.bufferAddress) { debounce.debounceFcn(async () => { - const { validationErrors } = await isFormValid(schema, form) - setFormErrors(validationErrors) - }) + const { validationErrors } = await isFormValid(schema, form); + setFormErrors(validationErrors); + }); } - }, [form.bufferAddress]) + }, [form.bufferAddress]); return ( <>

Upgrade {name}

@@ -287,7 +287,7 @@ const UpgradeProgram = ({ { - setVoteByCouncil(!voteByCouncil) + setVoteByCouncil(!voteByCouncil); }} /> )} @@ -298,7 +298,7 @@ const UpgradeProgram = ({
Propose Upgrade
- ) -} + ); +}; -export default UpgradeProgram +export default UpgradeProgram; diff --git a/components/Button.tsx b/components/Button.tsx index 8f9574a0b6..5e067bc04f 100644 --- a/components/Button.tsx +++ b/components/Button.tsx @@ -1,15 +1,15 @@ -import { FunctionComponent } from 'react' -import Loading from './Loading' -import Tooltip from './Tooltip' +import { FunctionComponent } from 'react'; +import Loading from './Loading'; +import Tooltip from './Tooltip'; interface ButtonProps { - className?: string - isLoading?: boolean - onClick?: () => void - disabled?: boolean - small?: boolean - tooltipMessage?: string - style?: any + className?: string; + isLoading?: boolean; + onClick?: () => void; + disabled?: boolean; + small?: boolean; + tooltipMessage?: string; + style?: any; } const Button: FunctionComponent = ({ @@ -39,10 +39,10 @@ const Button: FunctionComponent = ({
{isLoading ? : children}
- ) -} + ); +}; -export default Button +export default Button; export const SecondaryButton: FunctionComponent = ({ children, @@ -67,8 +67,8 @@ export const SecondaryButton: FunctionComponent = ({
{isLoading ? : children}
- ) -} + ); +}; export const LinkButton: FunctionComponent = ({ children, @@ -86,5 +86,5 @@ export const LinkButton: FunctionComponent = ({ > {children} - ) -} + ); +}; diff --git a/components/ButtonGroup.tsx b/components/ButtonGroup.tsx index 40a9cf301a..32ea2dcbe2 100644 --- a/components/ButtonGroup.tsx +++ b/components/ButtonGroup.tsx @@ -1,12 +1,12 @@ -import { FunctionComponent } from 'react' +import { FunctionComponent } from 'react'; interface ButtonGroupProps { - activeValue: string - className?: string - onChange: (x) => void - unit?: string - values: Array - names?: Array + activeValue: string; + className?: string; + onChange: (x) => void; + unit?: string; + values: Array; + names?: Array; } const ButtonGroup: FunctionComponent = ({ @@ -51,7 +51,7 @@ const ButtonGroup: FunctionComponent = ({ ))} - ) -} + ); +}; -export default ButtonGroup +export default ButtonGroup; diff --git a/components/ConnectWalletButton.tsx b/components/ConnectWalletButton.tsx index 082a448b07..46fd2bdca5 100644 --- a/components/ConnectWalletButton.tsx +++ b/components/ConnectWalletButton.tsx @@ -1,130 +1,63 @@ -import { Menu } from '@headlessui/react' -import { useMemo, useState } from 'react' -import { CheckCircleIcon, ChevronDownIcon } from '@heroicons/react/solid' -import styled from '@emotion/styled' -import useWalletStore from '../stores/useWalletStore' +import { Menu } from '@headlessui/react'; +import { useMemo } from 'react'; +import { CheckCircleIcon, ChevronDownIcon } from '@heroicons/react/solid'; +import styled from '@emotion/styled'; +import useWalletStore from '../stores/useWalletStore'; import { getWalletProviderByUrl, WALLET_PROVIDERS, -} from '../utils/wallet-adapters' -import { - AddressImage, - DisplayAddress, - useAddressName, - useWalletIdentity, -} from '@cardinal/namespaces-components' -import { BackspaceIcon } from '@heroicons/react/solid' -import { UserCircleIcon } from '@heroicons/react/outline' -import { abbreviateAddress } from '@utils/formatting' -import { useRouter } from 'next/router' -import TwitterIcon from './TwitterIcon' -import Switch from './Switch' +} from '../utils/wallet-adapters'; const StyledWalletProviderLabel = styled.p` font-size: 0.65rem; line-height: 1.5; -` +`; const ConnectWalletButton = (props) => { const { connected, current, providerUrl, - connection, set: setWalletStore, - } = useWalletStore((s) => s) + } = useWalletStore((s) => s); const provider = useMemo(() => getWalletProviderByUrl(providerUrl), [ providerUrl, - ]) - - const [useDevnet, setUseDevnet] = useState(false) - const router = useRouter() - const handleToggleDevnet = () => { - setUseDevnet(!useDevnet) - if (useDevnet) { - router.push(`${window.location.pathname}`) - } else { - router.push(`${window.location.href}?cluster=devnet`) - } - } + ]); const handleConnectDisconnect = async () => { try { if (connected) { - await current?.disconnect() + await current?.disconnect(); } else { - await current?.connect() + await current?.connect(); } } catch (e) { - console.warn('handleConnectDisconnect', e) + console.warn('handleConnectDisconnect', e); } - } - - const { show } = useWalletIdentity() - - const { displayName } = useAddressName( - connection.current, - current?.publicKey || undefined - ) + }; - const walletAddressFormatted = current?.publicKey - ? abbreviateAddress(current?.publicKey) - : '' + if (!provider) { + return null; + } return (
@@ -133,9 +66,7 @@ const ConnectWalletButton = (props) => { {({ open }) => ( <> - + { /> - <> - {WALLET_PROVIDERS.map(({ name, url, icon }) => ( - - - - ))} - - {current && current.publicKey && ( - <> -
- - show( - // @ts-ignore - current, - connection.current, - connection.cluster - ) - } - > - - - - - - { - handleToggleDevnet() - }} - > - - - - )} - + {WALLET_PROVIDERS.map(({ name, url, icon }) => ( + + + + ))}
)}
- ) -} + ); +}; -export default ConnectWalletButton +export default ConnectWalletButton; diff --git a/components/DiscordIcon.tsx b/components/DiscordIcon.tsx index 6283e77ad4..7ca6231b7a 100644 --- a/components/DiscordIcon.tsx +++ b/components/DiscordIcon.tsx @@ -21,7 +21,7 @@ const DiscordIcon = ({ className }) => { mask="url(#path-1-inside-1_1901_7895)" /> - ) -} + ); +}; -export default DiscordIcon +export default DiscordIcon; diff --git a/components/Divider.tsx b/components/Divider.tsx index dc854b5132..ba061a9f81 100644 --- a/components/Divider.tsx +++ b/components/Divider.tsx @@ -1,14 +1,14 @@ -import React from 'react' +import React from 'react'; const Divider: React.FC<{ - className?: string - dashed?: boolean + className?: string; + dashed?: boolean; }> = ({ className, dashed }) => (
-) +); -export default Divider +export default Divider; diff --git a/components/DropdownBtn.tsx b/components/DropdownBtn.tsx index c6ee09c5d5..4256964964 100644 --- a/components/DropdownBtn.tsx +++ b/components/DropdownBtn.tsx @@ -1,25 +1,25 @@ -import { Menu } from '@headlessui/react' -import { ChevronDownIcon } from '@heroicons/react/solid' -import Loading from '@components/Loading' +import { Menu } from '@headlessui/react'; +import { ChevronDownIcon } from '@heroicons/react/solid'; +import Loading from '@components/Loading'; export interface DropdownBtnOptions { - isDefault: boolean | undefined - label: string - callback: () => Promise + isDefault: boolean | undefined; + label: string; + callback: () => Promise; } const DropdownBtn = ({ options, isLoading, }: { - options: DropdownBtnOptions[] - isLoading?: boolean + options: DropdownBtnOptions[]; + isLoading?: boolean; }) => { - const defaultFunction = options.find((x) => x.isDefault) + const defaultFunction = options.find((x) => x.isDefault); if (!defaultFunction) { - throw 'DropdownBtn must have at least one default option' + throw 'DropdownBtn must have at least one default option'; } - const filtredOptions = options.filter((x) => !x.isDefault) + const filtredOptions = options.filter((x) => !x.isDefault); return (
{isLoading ? ( @@ -67,7 +67,7 @@ const DropdownBtn = ({ )}
- ) -} + ); +}; -export default DropdownBtn +export default DropdownBtn; diff --git a/components/ErrorBoundary.tsx b/components/ErrorBoundary.tsx index e40277189e..c0740e3ead 100644 --- a/components/ErrorBoundary.tsx +++ b/components/ErrorBoundary.tsx @@ -1,17 +1,17 @@ -import React from 'react' +import React from 'react'; class ErrorBoundary extends React.Component< any, { hasError: boolean; error: any } > { constructor(props) { - super(props) - this.state = { hasError: false, error: null } + super(props); + this.state = { hasError: false, error: null }; } static getDerivedStateFromError(error) { // Update state so the next render will show the fallback UI. - return { hasError: true, error: error } + return { hasError: true, error: error }; } componentDidCatch(error, errorInfo) { @@ -26,9 +26,9 @@ class ErrorBoundary extends React.Component< body: JSON.stringify({ content: `UI ERROR: ${error} : ${errorInfo?.componentStack}`, }), - }) + }); } catch (err) { - console.error('Error posting to notify webhook:', err) + console.error('Error posting to notify webhook:', err); } } } @@ -45,11 +45,11 @@ class ErrorBoundary extends React.Component<
{this.state.error.stack}
- ) + ); } - return this.props.children + return this.props.children; } } -export default ErrorBoundary +export default ErrorBoundary; diff --git a/components/Footer.tsx b/components/Footer.tsx deleted file mode 100644 index f69f81079d..0000000000 --- a/components/Footer.tsx +++ /dev/null @@ -1,78 +0,0 @@ -import DiscordIcon from './DiscordIcon' -import GithubIcon from './GithubIcon' -import TwitterIcon from './TwitterIcon' - -const Footer = () => { - const { REALM } = process.env - - if (REALM) return null - else - return ( -
- - - - -
-

- Powered by -

- - - Solana - -
-
- ) -} - -export default Footer diff --git a/components/GithubIcon.tsx b/components/GithubIcon.tsx index ffa778eebe..9bfd5633ab 100644 --- a/components/GithubIcon.tsx +++ b/components/GithubIcon.tsx @@ -16,7 +16,7 @@ const GithubIcon = ({ className }) => { strokeLinejoin="round" /> - ) -} + ); +}; -export default GithubIcon +export default GithubIcon; diff --git a/components/GovernedAccountsTabs.tsx b/components/GovernedAccountsTabs.tsx index 97219f2179..271d8146c1 100644 --- a/components/GovernedAccountsTabs.tsx +++ b/components/GovernedAccountsTabs.tsx @@ -1,9 +1,9 @@ -import { FunctionComponent } from 'react' +import { FunctionComponent } from 'react'; interface GovernedAccountsTabsProps { - activeTab: any - onChange: (x) => void - tabs: Array + activeTab: any; + onChange: (x) => void; + tabs: Array; } const GovernedAccountsTabs: FunctionComponent = ({ @@ -18,15 +18,15 @@ const GovernedAccountsTabs: FunctionComponent = ({ style={{ transform: `translateY(${ tabs.findIndex( - (t) => t.pubkey.toBase58() === activeTab?.pubkey.toBase58() + (t) => t.pubkey.toBase58() === activeTab?.pubkey.toBase58(), ) * 100 }%)`, height: `${100 / tabs.length}%`, }} /> {tabs.map((x) => { - const pubKey = x.pubkey - const activePubKey = activeTab?.pubkey + const pubKey = x.pubkey; + const activePubKey = activeTab?.pubkey; return ( - ) + ); })} - ) -} + ); +}; -export default GovernedAccountsTabs +export default GovernedAccountsTabs; diff --git a/components/HotWallet/HotWallet.tsx b/components/HotWallet/HotWallet.tsx new file mode 100644 index 0000000000..fe8599e5b1 --- /dev/null +++ b/components/HotWallet/HotWallet.tsx @@ -0,0 +1,51 @@ +import useHotWallet from '@hooks/useHotWallet'; +import { FireIcon } from '@heroicons/react/solid'; +import HotWalletName from './HotWalletName'; +import HotWalletPluginTribecaGauges from './plugins/TribecaGauges/TribecaGauges'; +import { + saberTribecaConfiguration, + sunnyTribecaConfiguration, +} from '@tools/sdk/tribeca/configurations'; +import HotWalletPluginTokenAccounts from './plugins/TokenAccounts/TokenAccounts'; +import HotWalletPluginSaberStats from './plugins/SaberStats/SaberStats'; +import HotWalletPluginUXDStaking from './plugins/UXDStaking/UXDStaking'; + +const HotWallet = (): JSX.Element => { + const { hotWalletAccount } = useHotWallet(); + + if (!hotWalletAccount) { + return <>; + } + + return ( +
+

+ + Hot Wallet +

+ + + +
+ + + + + + + + + +
+
+ ); +}; + +export default HotWallet; diff --git a/components/HotWallet/HotWalletName.tsx b/components/HotWallet/HotWalletName.tsx new file mode 100644 index 0000000000..e717ec101e --- /dev/null +++ b/components/HotWallet/HotWalletName.tsx @@ -0,0 +1,41 @@ +import { getExplorerUrl } from '@components/explorer/tools'; +import { ExternalLinkIcon } from '@heroicons/react/outline'; +import { PublicKey } from '@solana/web3.js'; +import { abbreviateAddress } from '@utils/formatting'; +import { createRef } from 'react'; +import useWalletStore from 'stores/useWalletStore'; + +const HotWalletName = ({ + hotWalletName, + hotWalletAddress, +}: { + hotWalletName: string; + hotWalletAddress: PublicKey; +}) => { + const connection = useWalletStore((store) => store.connection); + + const linkRef = createRef(); + + return ( +
linkRef.current?.click()} + > +

{hotWalletName}

+

{abbreviateAddress(hotWalletAddress)}

+ + e.stopPropagation()} + > + + +
+ ); +}; + +export default HotWalletName; diff --git a/components/HotWallet/plugins/SaberStats/SaberStat.tsx b/components/HotWallet/plugins/SaberStats/SaberStat.tsx new file mode 100644 index 0000000000..dc3e300443 --- /dev/null +++ b/components/HotWallet/plugins/SaberStats/SaberStat.tsx @@ -0,0 +1,30 @@ +import { SaberStats } from '@hooks/useSaberStats'; + +const SaberStat = ({ + liquidityPoolName, + uiBalance, + pendingRewards, + mintName, +}: SaberStats) => { + return ( +
+ {liquidityPoolName} +
+ Balance + {`${uiBalance.toLocaleString()} ${mintName}`} +
+ +
+ Pending Rewards + {pendingRewards.map(({ name, uiPendingAmount }) => ( + {`${uiPendingAmount.toLocaleString()} ${name}`} + ))} +
+
+ ); +}; + +export default SaberStat; diff --git a/components/HotWallet/plugins/SaberStats/SaberStats.tsx b/components/HotWallet/plugins/SaberStats/SaberStats.tsx new file mode 100644 index 0000000000..e1544dc677 --- /dev/null +++ b/components/HotWallet/plugins/SaberStats/SaberStats.tsx @@ -0,0 +1,33 @@ +import useSaberStats from '@hooks/useSaberStats'; +import { SupportIcon } from '@heroicons/react/outline'; +import SaberStat from './SaberStat'; +import { HotWalletAccount } from '@hooks/useHotWallet'; + +const HotWalletPluginSaberStats = ({ + hotWalletAccount, +}: { + hotWalletAccount: HotWalletAccount; +}) => { + const { saberStats } = useSaberStats(hotWalletAccount); + + if (!saberStats) { + return <>; + } + + return ( +
+

+ + Saber Stats +

+ +
+ {saberStats.map((saberStat) => ( + + ))} +
+
+ ); +}; + +export default HotWalletPluginSaberStats; diff --git a/components/HotWallet/plugins/TokenAccounts/TokenAccount.tsx b/components/HotWallet/plugins/TokenAccounts/TokenAccount.tsx new file mode 100644 index 0000000000..3b1ff112ff --- /dev/null +++ b/components/HotWallet/plugins/TokenAccounts/TokenAccount.tsx @@ -0,0 +1,56 @@ +import { abbreviateAddress } from '@utils/formatting'; +import { HotWalletTokenAccounts } from '@hooks/useHotWalletPluginTokenAccounts'; +import { getExplorerUrl } from '@components/explorer/tools'; +import useWalletStore from 'stores/useWalletStore'; +import { createRef } from 'react'; +import { ExternalLinkIcon } from '@heroicons/react/outline'; +import { nativeAmountToFormattedUiAmount } from '@tools/sdk/units'; + +const TokenAccount = ({ info }: { info: HotWalletTokenAccounts[0] }) => { + const connection = useWalletStore((store) => store.connection); + + const linkRef = createRef(); + + const amountFormatted = nativeAmountToFormattedUiAmount( + info.amount, + info.decimals, + ); + + const usdTotalValueFormatted = info.usdTotalValue.isZero() + ? '' + : `$${nativeAmountToFormattedUiAmount(info.usdTotalValue, info.decimals)}`; + + return ( +
linkRef.current?.click()} + > +
+ + {amountFormatted} + + {info.mintName ?? abbreviateAddress(info.mint)} + + + + {usdTotalValueFormatted} +
+ + e.stopPropagation()} + > + + {abbreviateAddress(info.publicKey)} + + + +
+ ); +}; + +export default TokenAccount; diff --git a/components/HotWallet/plugins/TokenAccounts/TokenAccounts.tsx b/components/HotWallet/plugins/TokenAccounts/TokenAccounts.tsx new file mode 100644 index 0000000000..3facd732d1 --- /dev/null +++ b/components/HotWallet/plugins/TokenAccounts/TokenAccounts.tsx @@ -0,0 +1,39 @@ +import { BookOpenIcon } from '@heroicons/react/solid'; +import { HotWalletAccount } from '@hooks/useHotWallet'; +import useHotWalletPluginTokenAccounts from '@hooks/useHotWalletPluginTokenAccounts'; +import TokenAccount from './TokenAccount'; + +const HotWalletPluginTokenAccounts = ({ + hotWalletAccount, +}: { + hotWalletAccount: HotWalletAccount; +}) => { + const { tokenAccounts } = useHotWalletPluginTokenAccounts(hotWalletAccount); + + if (!hotWalletAccount) { + return <>; + } + + return ( +
+

+ + Token Accounts ({tokenAccounts?.length}) +

+ +
+ {tokenAccounts?.map((tokenAccount) => ( + + ))} +
+
+ ); +}; + +export default HotWalletPluginTokenAccounts; diff --git a/components/HotWallet/plugins/TribecaGauges/ActiveGaugeVotes.tsx b/components/HotWallet/plugins/TribecaGauges/ActiveGaugeVotes.tsx new file mode 100644 index 0000000000..cdfc5d0a0e --- /dev/null +++ b/components/HotWallet/plugins/TribecaGauges/ActiveGaugeVotes.tsx @@ -0,0 +1,37 @@ +import { ActiveGaugeVoteData } from '@hooks/useTribecaGaugesInfos'; + +const ActiveGaugeVotes = ({ + activeGaugeVotesData, +}: { + activeGaugeVotesData?: ActiveGaugeVoteData[] | null; +}) => { + return ( +
+

Vote Weight

+ + {activeGaugeVotesData && activeGaugeVotesData.length > 0 ? ( + activeGaugeVotesData.map( + ({ name, logoURI, weight, weightPercentage }) => ( +
+ {logoURI && } + + {name} + + + {weight}{' '} + ({weightPercentage}%) + +
+ ), + ) + ) : ( +
No weight repartition
+ )} +
+ ); +}; + +export default ActiveGaugeVotes; diff --git a/components/HotWallet/plugins/TribecaGauges/EpochGaugeVoterData.tsx b/components/HotWallet/plugins/TribecaGauges/EpochGaugeVoterData.tsx new file mode 100644 index 0000000000..ce37094a20 --- /dev/null +++ b/components/HotWallet/plugins/TribecaGauges/EpochGaugeVoterData.tsx @@ -0,0 +1,37 @@ +import type { EpochGaugeVoterData } from '@tools/sdk/tribeca/programs'; + +const EpochGaugeVoterDataBloc = ({ + title, + epochGaugeVoterData, +}: { + title: string; + epochGaugeVoterData?: EpochGaugeVoterData | null; +}) => { + return ( +
+

{title}

+ + {epochGaugeVoterData ? ( +
+ + Voting Power{' '} + + {epochGaugeVoterData.votingPower.toNumber().toLocaleString()} + + + + + Allocated Power{' '} + + {epochGaugeVoterData.allocatedPower.toNumber().toLocaleString()} + + +
+ ) : ( +
Non-initialized epoch
+ )} +
+ ); +}; + +export default EpochGaugeVoterDataBloc; diff --git a/components/HotWallet/plugins/TribecaGauges/EscrowData.tsx b/components/HotWallet/plugins/TribecaGauges/EscrowData.tsx new file mode 100644 index 0000000000..90e6160f53 --- /dev/null +++ b/components/HotWallet/plugins/TribecaGauges/EscrowData.tsx @@ -0,0 +1,81 @@ +import { BN } from '@project-serum/anchor'; +import type { EscrowData } from '@tools/sdk/tribeca/programs'; +import { nativeAmountToFormattedUiAmount } from '@tools/sdk/units'; +import { tryGetTokenMint } from '@utils/tokens'; +import { useCallback, useEffect, useState } from 'react'; +import useWalletStore from 'stores/useWalletStore'; + +function formatDate(dateInSec: BN): string { + if (dateInSec.isZero()) { + return '-'; + } + + // mul by 1000 to get ms + return new Date(dateInSec.mul(new BN(1000)).toNumber()).toUTCString(); +} + +const EscrowDataBloc = ({ escrowData }: { escrowData?: EscrowData }) => { + const connection = useWalletStore((s) => s.connection); + const [uiAmount, setUiAmount] = useState('-'); + + const loadUiAmount = useCallback(async (): Promise => { + if (!escrowData) return '-'; + + const tokenInfo = await tryGetTokenMint( + connection.current, + escrowData.tokens, + ); + + if (!tokenInfo) { + console.error( + 'Cannot load information about token mint related to escrow data (tribeca gauges)', + escrowData.tokens, + ); + return '-'; + } + + return nativeAmountToFormattedUiAmount( + escrowData.amount, + tokenInfo.account.decimals, + ); + }, [connection, escrowData]); + + useEffect(() => { + loadUiAmount().then(setUiAmount); + }, [loadUiAmount]); + + return ( +
+

Escrow Data

+ + {escrowData ? ( +
+ + + Number of Locked Tokens + {' '} + {uiAmount} + + + + Lock Date{' '} + + {formatDate(escrowData.escrowStartedAt)} + + + + + Unlocking Date{' '} + + {formatDate(escrowData.escrowEndsAt)} + + +
+ ) : ( + - + )} +
+ ); +}; + +export default EscrowDataBloc; diff --git a/components/HotWallet/plugins/TribecaGauges/TribecaGauges.tsx b/components/HotWallet/plugins/TribecaGauges/TribecaGauges.tsx new file mode 100644 index 0000000000..9e33619e62 --- /dev/null +++ b/components/HotWallet/plugins/TribecaGauges/TribecaGauges.tsx @@ -0,0 +1,50 @@ +import { AdjustmentsIcon } from '@heroicons/react/solid'; +import useTribecaGaugeInfos from '@hooks/useTribecaGaugesInfos'; +import ATribecaConfiguration from '@tools/sdk/tribeca/ATribecaConfiguration'; +import ActiveGaugeVotes from './ActiveGaugeVotes'; +import EpochGaugeVoterData from './EpochGaugeVoterData'; +import EscrowData from './EscrowData'; +import TribecaGaugesEpoch from './TribecaGaugesEpoch'; + +const HotWalletPluginTribecaGauges = ({ + tribecaConfiguration, +}: { + tribecaConfiguration: ATribecaConfiguration; +}) => { + const { infos, escrowOwner } = useTribecaGaugeInfos(tribecaConfiguration); + + if (!escrowOwner) { + return <>; + } + + return ( +
+

+ + {tribecaConfiguration.name} Tribeca Gauges +

+ + + + + + + + + + +
+ ); +}; + +export default HotWalletPluginTribecaGauges; diff --git a/components/HotWallet/plugins/TribecaGauges/TribecaGaugesEpoch.tsx b/components/HotWallet/plugins/TribecaGauges/TribecaGaugesEpoch.tsx new file mode 100644 index 0000000000..602bb0de52 --- /dev/null +++ b/components/HotWallet/plugins/TribecaGauges/TribecaGaugesEpoch.tsx @@ -0,0 +1,70 @@ +import { BN } from '@project-serum/anchor'; +import { useEffect, useState } from 'react'; + +const ONE_DAY_IN_SEC = 3600 * 24; +const ONE_HOUR_IN_SEC = 3600; +const ONE_MIN_IN_SEC = 60; + +function formatNbSec(nbSec: BN) { + const nbDay = nbSec.div(new BN(ONE_DAY_IN_SEC)); + nbSec = nbSec.sub(nbDay.mul(new BN(ONE_DAY_IN_SEC))); + + const nbHour = nbSec.div(new BN(ONE_HOUR_IN_SEC)); + nbSec = nbSec.sub(nbHour.mul(new BN(ONE_HOUR_IN_SEC))); + + const nbMin = nbSec.div(new BN(ONE_MIN_IN_SEC)); + nbSec = nbSec.sub(nbMin.mul(new BN(ONE_MIN_IN_SEC))); + + return `${nbDay} days ${nbHour} hours ${nbMin} minutes ${nbSec} seconds`; +} + +const TribecaGaugesEpoch = ({ + nextEpoch, + rewardsEpoch, + epochDurationSeconds, +}: { + nextEpoch?: BN; + rewardsEpoch?: number; + epochDurationSeconds?: number; +}) => { + const [time, setTime] = useState('-'); + const [formattedEpochDuration, setEpochDuration] = useState('-'); + + useEffect(() => { + if (!nextEpoch) { + setTime('-'); + return; + } + + const descriptor = setInterval(() => { + setTime( + formatNbSec(nextEpoch.sub(new BN(Date.now()).div(new BN(1_000)))), + ); + }, 500); + + return () => { + clearInterval(descriptor); + }; + }, [nextEpoch]); + + useEffect(() => { + setEpochDuration( + epochDurationSeconds ? formatNbSec(new BN(epochDurationSeconds)) : '-', + ); + }, [epochDurationSeconds]); + + return ( +
+

Current Epoch

+

{rewardsEpoch}

+ +

Epoch Duration

+

{formattedEpochDuration}

+ +

Next Epoch

+

{time}

+
+ ); +}; + +export default TribecaGaugesEpoch; diff --git a/components/HotWallet/plugins/UXDStaking/StakingCampaign.tsx b/components/HotWallet/plugins/UXDStaking/StakingCampaign.tsx new file mode 100644 index 0000000000..51a848a7bd --- /dev/null +++ b/components/HotWallet/plugins/UXDStaking/StakingCampaign.tsx @@ -0,0 +1,141 @@ +import { StakingCampaignInfo } from '@hooks/useHotWalletPluginUXDStaking'; +import { nativeAmountToFormattedUiAmount } from '@tools/sdk/units'; +import { getSplTokenNameByMint } from '@utils/splTokens'; + +const StakingCampaign = ({ + stakingCampaignInfo, +}: { + stakingCampaignInfo: StakingCampaignInfo; +}) => { + const startDate = new Date( + Number(stakingCampaignInfo.startTs) * 1_000, + ).toUTCString(); + + const endDate = stakingCampaignInfo.endTs + ? new Date(Number(stakingCampaignInfo.endTs) * 1_000).toUTCString() + : '-'; + + const options = stakingCampaignInfo.stakingOptions.length ? ( + stakingCampaignInfo.stakingOptions + .filter(({ identifier }) => identifier) + .map(({ identifier, lockupSecs, apr }) => ( +
+
+ + option {identifier}: + + + + lock time + + {lockupSecs.toNumber().toLocaleString()}s + + + + + apr + + {apr.toNumber() / 100}% + + +
+
+ )) + ) : ( + No options + ); + + const rewardMintName = getSplTokenNameByMint(stakingCampaignInfo.rewardMint); + const stackedMintName = getSplTokenNameByMint(stakingCampaignInfo.stakedMint); + + return ( + <> +
{stakingCampaignInfo.name}
+ +
+
+ starting date: + {startDate} +
+ +
+ end date: + {endDate} +
+ +
+ finalized: + + {stakingCampaignInfo.isFinalized ? 'yes' : 'no'} + +
+
+ +
+
+ total campaign rewards: + + {nativeAmountToFormattedUiAmount( + stakingCampaignInfo.initialRewardAmount, + stakingCampaignInfo.rewardMintDecimals, + )} + {rewardMintName} + +
+ +
+ unallocated campaign rewards: + + {nativeAmountToFormattedUiAmount( + stakingCampaignInfo.remainingRewardAmount, + stakingCampaignInfo.rewardMintDecimals, + )} + {rewardMintName} + +
+ +
+ total staked amount v1: + + {stakingCampaignInfo.uiStakedTokensV1 + ? `${stakingCampaignInfo.uiStakedTokensV1.toLocaleString()} ${stackedMintName}` + : 'unknown'} + +
+ +
+ total staked amount v2: + + {stakingCampaignInfo.uiStakedTokensV2 + ? `${stakingCampaignInfo.uiStakedTokensV2.toLocaleString()} ${stackedMintName}` + : 'unknown'} + +
+ +
+ total created staking accounts: + + {Number( + stakingCampaignInfo.totalCreatedStakingAccounts, + ).toLocaleString()} + +
+ +
+ total current staking accounts: + + {Number( + stakingCampaignInfo.totalCurrentStakingAccounts, + ).toLocaleString()} + +
+
+ +
+ {options} +
+ + ); +}; + +export default StakingCampaign; diff --git a/components/HotWallet/plugins/UXDStaking/UXDStaking.tsx b/components/HotWallet/plugins/UXDStaking/UXDStaking.tsx new file mode 100644 index 0000000000..5b6ec10697 --- /dev/null +++ b/components/HotWallet/plugins/UXDStaking/UXDStaking.tsx @@ -0,0 +1,44 @@ +import { DatabaseIcon } from '@heroicons/react/outline'; +import { HotWalletAccount } from '@hooks/useHotWallet'; +import useHotWalletPluginUXDStaking from '@hooks/useHotWalletPluginUXDStaking'; +import StakingCampaign from './StakingCampaign'; + +const HotWalletPluginUXDStaking = ({ + hotWalletAccount, +}: { + hotWalletAccount: HotWalletAccount; +}) => { + const { stakingCampaignsInfo } = useHotWalletPluginUXDStaking( + hotWalletAccount, + ); + + if (!stakingCampaignsInfo) { + return <>; + } + + return ( +
+

+ + UXD Staking Stats +

+ +
+ {stakingCampaignsInfo.length ? ( + stakingCampaignsInfo.map((stakingCampaignInfo) => ( + + )) + ) : ( +
+ no configured campaign +
+ )} +
+
+ ); +}; + +export default HotWalletPluginUXDStaking; diff --git a/components/ImageTextSelection.tsx b/components/ImageTextSelection.tsx new file mode 100644 index 0000000000..9900689136 --- /dev/null +++ b/components/ImageTextSelection.tsx @@ -0,0 +1,72 @@ +export type ImageTextElement = { + id: T; + name: string; + image?: string; +}; + +export default function ImageTextSelection({ + selected, + className, + onClick, + imageTextElements, +}: { + selected: T | null; + className?: string; + onClick: (selected: T | null) => void; + imageTextElements: ImageTextElement[]; +}) { + return ( +
+
+ {selected === null + ? 'All' + : imageTextElements.find(({ id }) => id === selected)!.name} +
+ +
+ {imageTextElements.map(({ id, name, image }, index) => { + return ( +
+ {image ? ( + // Image + onClick(id)} + /> + ) : ( + // Text + onClick(id)} + > + {name} + + )} + + {selected === id ? ( + // Selected visual +
+ ) : null} +
+ ); + })} +
+
+ ); +} diff --git a/components/ImgWithLoader.tsx b/components/ImgWithLoader.tsx index c368664e4c..9bd6083984 100644 --- a/components/ImgWithLoader.tsx +++ b/components/ImgWithLoader.tsx @@ -1,8 +1,8 @@ -import { useState } from 'react' -import { PhotographIcon } from '@heroicons/react/outline' +import { useState } from 'react'; +import { PhotographIcon } from '@heroicons/react/outline'; const ImgWithLoader = (props) => { - const [isLoading, setIsLoading] = useState(true) + const [isLoading, setIsLoading] = useState(true); return (
{isLoading && ( @@ -10,7 +10,7 @@ const ImgWithLoader = (props) => { )} setIsLoading(false)} />
- ) -} + ); +}; -export default ImgWithLoader +export default ImgWithLoader; diff --git a/components/InlineNotification.tsx b/components/InlineNotification.tsx index 9a8038c912..743f227129 100644 --- a/components/InlineNotification.tsx +++ b/components/InlineNotification.tsx @@ -1,15 +1,15 @@ -import { FunctionComponent } from 'react' +import { FunctionComponent } from 'react'; import { CheckCircleIcon, ExclamationCircleIcon, ExclamationIcon, InformationCircleIcon, -} from '@heroicons/react/outline' +} from '@heroicons/react/outline'; interface InlineNotificationProps { - desc?: string | (() => string) - title?: string - type: string + desc?: string | (() => string); + title?: string; + type: string; } const InlineNotification: FunctionComponent = ({ @@ -49,6 +49,6 @@ const InlineNotification: FunctionComponent = ({
-) +); -export default InlineNotification +export default InlineNotification; diff --git a/components/InstructionForm.tsx b/components/InstructionForm.tsx new file mode 100644 index 0000000000..17698e1b98 --- /dev/null +++ b/components/InstructionForm.tsx @@ -0,0 +1,69 @@ +import { LinkButton } from '@components/Button'; +import { XCircleIcon } from '@heroicons/react/solid'; +import { InstructionType } from '@hooks/useGovernanceAssets'; +import { GovernedMultiTypeAccount } from '@utils/tokens'; +import { ComponentInstructionData } from '@utils/uiTypes/proposalCreationTypes'; +import SelectedInstruction from 'pages/dao/[symbol]/proposal/components/instructions/SelectedInstruction'; +import InstructionContentContainer from '../pages/dao/[symbol]/proposal/components/InstructionContentContainer'; +import SelectInstructionType from './SelectInstructionType'; + +const InstructionForm = ({ + idx, + availableInstructions, + governedAccount, + selectedInstruction, + setInstructionType, + removeInstruction, +}: { + idx: number; + selectedInstruction: ComponentInstructionData; + governedAccount?: GovernedMultiTypeAccount; + availableInstructions: InstructionType[]; + + setInstructionType: ({ + instructionType, + idx, + }: { + instructionType: InstructionType | null; + idx: number; + }) => void; + + removeInstruction: (idx: number) => void; +}) => { + return ( +
+ + setInstructionType({ instructionType, idx }) + } + selectedInstruction={selectedInstruction.type} + /> + +
+ + {selectedInstruction.type ? ( + + ) : null} + + + {idx != 0 ? ( + removeInstruction(idx)} + > + + Remove + + ) : null} +
+
+ ); +}; + +export default InstructionForm; diff --git a/components/InstructionsForm.tsx b/components/InstructionsForm.tsx new file mode 100644 index 0000000000..d9bab25bcb --- /dev/null +++ b/components/InstructionsForm.tsx @@ -0,0 +1,93 @@ +import { LinkButton } from '@components/Button'; +import { PlusCircleIcon } from '@heroicons/react/solid'; +import { InstructionType } from '@hooks/useGovernanceAssets'; +import { GovernedMultiTypeAccount } from '@utils/tokens'; +import { ComponentInstructionData } from '@utils/uiTypes/proposalCreationTypes'; +import { NewProposalContext } from 'pages/dao/[symbol]/proposal/new'; +import { useState } from 'react'; +import InstructionForm from './InstructionForm'; + +const InstructionsForm = ({ + availableInstructions, + onInstructionsChange, + governedAccount, +}: { + availableInstructions: InstructionType[]; + onInstructionsChange: (instructions: ComponentInstructionData[]) => void; + governedAccount?: GovernedMultiTypeAccount; +}) => { + const [instructions, setInstructions] = useState([ + // One empty instruction at start + {}, + ]); + + const handleSetInstruction = ( + val: Partial, + idx: number, + ) => { + const newInstructions = [...instructions]; + newInstructions[idx] = { ...instructions[idx], ...val }; + setInstructions(newInstructions); + onInstructionsChange(newInstructions); + }; + + const setInstructionType = ({ + instructionType, + idx, + }: { + instructionType: InstructionType | null; + idx: number; + }) => { + handleSetInstruction( + { + type: instructionType ? instructionType : undefined, + }, + idx, + ); + }; + + const addInstruction = () => { + setInstructions([...instructions, { type: undefined }]); + }; + + const removeInstruction = (idx: number) => { + setInstructions([...instructions.filter((x, index) => index !== idx)]); + }; + + return ( + <> + +

Instructions

+ + {instructions.map((instruction, idx) => ( + + ))} +
+ +
+ + + Add instruction + +
+ + ); +}; + +export default InstructionsForm; diff --git a/components/Link.tsx b/components/Link.tsx index ab7216acc4..f66fee8546 100644 --- a/components/Link.tsx +++ b/components/Link.tsx @@ -1,6 +1,6 @@ -import { FunctionComponent } from 'react' -import styled from '@emotion/styled' -import tw from 'twin.macro' +import { FunctionComponent } from 'react'; +import styled from '@emotion/styled'; +import tw from 'twin.macro'; const StyledButton = styled.a` font-weight: 700; @@ -13,7 +13,7 @@ const StyledButton = styled.a` :disabled { ${tw`cursor-not-allowed opacity-60`} } -` +`; // default heroicon does not allow customizing stroke const ChevronRightIcon = ({ className }) => ( @@ -26,10 +26,10 @@ const ChevronRightIcon = ({ className }) => ( > -) +); interface LinkProps { - className?: string + className?: string; } const Link: FunctionComponent = ({ @@ -47,7 +47,7 @@ const Link: FunctionComponent = ({ className={`relative stroke-3 top-1 h-4 w-4 text-fgd-1 ml-1`} /> - ) -} + ); +}; -export default Link +export default Link; diff --git a/components/LinkLeft.tsx b/components/LinkLeft.tsx deleted file mode 100644 index efa1c85165..0000000000 --- a/components/LinkLeft.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import { FunctionComponent } from 'react' -import styled from '@emotion/styled' -import tw from 'twin.macro' - -const StyledButton = styled.a` - font-weight: 700; - cursor: pointer; - - :hover { - ${tw`underline`} - } - - :disabled { - ${tw`cursor-not-allowed opacity-60`} - } -` - -// default heroicon does not allow customizing stroke -const ChevronRightIcon = ({ className }) => ( - - - -) - -interface LinkLeftProps { - className?: string - href?: string -} - -const LinkLeft: FunctionComponent = ({ - children, - className, - ...props -}) => { - return ( - - {children} - - - ) -} - -export default LinkLeft diff --git a/components/LinksCompactWrapper.tsx b/components/LinksCompactWrapper.tsx deleted file mode 100644 index be6cf630ec..0000000000 --- a/components/LinksCompactWrapper.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import { - CloudDownloadIcon, - DocumentIcon, - LinkIcon, -} from '@heroicons/react/outline' - -const LinksCompactWrapper = () => { - return ( - <> -
- - -

Docs that may help you

-
- - -
- - -

Docs & tutorials

-
-
- - -
- - -

About SPL

-
-
- - ) -} - -export default LinksCompactWrapper diff --git a/components/Loading.tsx b/components/Loading.tsx index c9fe6011e1..13233d6853 100644 --- a/components/Loading.tsx +++ b/components/Loading.tsx @@ -1,9 +1,9 @@ -import { FunctionComponent } from 'react' +import { FunctionComponent } from 'react'; interface LoadingProps { - className?: string - w?: string - h?: string + className?: string; + w?: string; + h?: string; } const Loading: FunctionComponent = ({ @@ -34,7 +34,7 @@ const Loading: FunctionComponent = ({ > - ) -} + ); +}; -export default Loading +export default Loading; diff --git a/components/Members/AddMemberForm.tsx b/components/Members/AddMemberForm.tsx deleted file mode 100644 index e07ec3fb2e..0000000000 --- a/components/Members/AddMemberForm.tsx +++ /dev/null @@ -1,356 +0,0 @@ -import { PublicKey } from '@solana/web3.js' -import useRealm from 'hooks/useRealm' -import Input from 'components/inputs/Input' -import Button, { SecondaryButton } from '@components/Button' -import VoteBySwitch from 'pages/dao/[symbol]/proposal/components/VoteBySwitch' -import { getMintMinAmountAsDecimal } from '@tools/sdk/units' -import { abbreviateAddress, precision } from 'utils/formatting' -import useWalletStore from 'stores/useWalletStore' -import { getMintSchema } from 'utils/validations' -import { useEffect, useState } from 'react' -import { MintForm, UiInstruction } from 'utils/uiTypes/proposalCreationTypes' -import useGovernanceAssets from 'hooks/useGovernanceAssets' -import { - getInstructionDataFromBase64, - RpcContext, - Governance, - ProgramAccount, -} from '@solana/spl-governance' -import { useRouter } from 'next/router' -import { createProposal } from 'actions/createProposal' -import { notify } from 'utils/notifications' -import useQueryContext from 'hooks/useQueryContext' -import { getMintInstruction } from 'utils/instructionTools' -import AddMemberIcon from '@components/AddMemberIcon' -import { getProgramVersionForRealm } from '@models/registry/api' -import { - ArrowCircleDownIcon, - ArrowCircleUpIcon, -} from '@heroicons/react/outline' -import useVoteStakeRegistryClientStore from 'VoteStakeRegistry/stores/voteStakeRegistryClientStore' - -interface AddMemberForm extends MintForm { - description: string - title: string -} - -const AddMemberForm = ({ close }) => { - const [voteByCouncil, setVoteByCouncil] = useState(false) - const [showOptions, setShowOptions] = useState(false) - const [isLoading, setIsLoading] = useState(false) - const [formErrors, setFormErrors] = useState({}) - - const router = useRouter() - const client = useVoteStakeRegistryClientStore((s) => s.state.client) - const connection = useWalletStore((s) => s.connection) - const wallet = useWalletStore((s) => s.current) - - const { fmtUrlWithCluster } = useQueryContext() - const { fetchRealmGovernance } = useWalletStore((s) => s.actions) - const { symbol } = router.query - const { getMintWithGovernances } = useGovernanceAssets() - - const { - realmInfo, - canChooseWhoVote, - councilMint, - realm, - ownVoterWeight, - mint, - } = useRealm() - - const programId: PublicKey | undefined = realmInfo?.programId - - const [form, setForm] = useState({ - destinationAccount: '', - amount: 1, - mintAccount: undefined, - programId: programId?.toString(), - description: '', - title: '', - }) - - const schema = getMintSchema({ form, connection }) - - const mintMinAmount = form.mintAccount - ? getMintMinAmountAsDecimal(councilMint!) - : 1 - - const currentPrecision = precision(mintMinAmount) - - const proposalTitle = `Add council member ${ - form.destinationAccount - ? abbreviateAddress(new PublicKey(form.destinationAccount)) - : '' - }` - - const setAmount = (event) => { - const value = event.target.value - - handleSetForm({ - value: value, - propertyName: 'amount', - }) - } - - const handleSetForm = ({ propertyName, value }) => { - setFormErrors({}) - setForm({ ...form, [propertyName]: value }) - } - - 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', - }) - } - - const getInstruction = async (): Promise => { - return getMintInstruction({ - schema, - form, - programId, - connection, - wallet, - governedMintInfoAccount: form.mintAccount, - setFormErrors, - }) - } - - //TODO common handle propose - const handlePropose = async () => { - setIsLoading(true) - - const instruction: UiInstruction = await getInstruction() - - if (instruction.isValid && wallet && realmInfo) { - const governance = form.mintAccount?.governance - - let proposalAddress: PublicKey | null = null - - if (!realm) { - setIsLoading(false) - - throw new Error('No realm selected') - } - - const rpcContext = new RpcContext( - new PublicKey(realm.owner.toString()), - getProgramVersionForRealm(realmInfo), - wallet, - connection.current, - connection.endpoint - ) - - const instructionData = { - data: instruction.serializedInstruction - ? getInstructionDataFromBase64(instruction.serializedInstruction) - : null, - holdUpTime: governance?.account?.config.minInstructionHoldUpTime, - prerequisiteInstructions: instruction.prerequisiteInstructions || [], - } - - try { - const selectedGovernance = (await fetchRealmGovernance( - governance?.pubkey - )) as ProgramAccount - - const ownTokenRecord = ownVoterWeight.getTokenRecordToCreateProposal( - governance!.account.config - ) - - const defaultProposalMint = !mint?.supply.isZero() - ? realm.account.communityMint - : !councilMint?.supply.isZero() - ? realm.account.config.councilMint - : undefined - - const proposalMint = - canChooseWhoVote && voteByCouncil - ? realm.account.config.councilMint - : defaultProposalMint - - if (!proposalMint) { - throw new Error( - 'There is no suitable governing token for the proposal' - ) - } - - proposalAddress = await createProposal( - rpcContext, - realm, - selectedGovernance.pubkey, - ownTokenRecord.pubkey, - form.title ? form.title : proposalTitle, - form.description ? form.description : '', - proposalMint, - selectedGovernance?.account?.proposalCount, - [instructionData], - false, - client - ) - - const url = fmtUrlWithCluster( - `/dao/${symbol}/proposal/${proposalAddress}` - ) - - router.push(url) - } catch (error) { - notify({ - type: 'error', - message: `${error}`, - }) - - close() - } - } - - setIsLoading(false) - } - - useEffect(() => { - const initForm = async () => { - const response = await getMintWithGovernances() - - handleSetForm({ - value: response.find( - (x) => - x.governance?.account.governedAccount.toBase58() === - realm?.account.config.councilMint?.toBase58() - ), - propertyName: 'mintAccount', - }) - } - - initForm() - }, []) - - return ( - <> -
- - -

Add new member to {realmInfo?.displayName}

-
- - - handleSetForm({ - value: event.target.value, - propertyName: 'destinationAccount', - }) - } - noMaxWidth - error={formErrors['destinationAccount']} - /> - -
setShowOptions(!showOptions)} - > - {showOptions ? ( - - ) : ( - - )} - Options -
- - {showOptions && ( - <> - - handleSetForm({ - value: event.target.value, - propertyName: 'title', - }) - } - /> - - - handleSetForm({ - value: event.target.value, - propertyName: 'description', - }) - } - /> - - - - {canChooseWhoVote && ( - { - setVoteByCouncil(!voteByCouncil) - }} - /> - )} - - )} - -
- close()} - > - Cancel - - - -
- - ) -} - -export default AddMemberForm diff --git a/components/Members/MemberItem.tsx b/components/Members/MemberItem.tsx index 0206a331e7..09054d51b1 100644 --- a/components/Members/MemberItem.tsx +++ b/components/Members/MemberItem.tsx @@ -1,21 +1,20 @@ -import { UserCircleIcon, LogoutIcon } from '@heroicons/react/outline' -import useRealm from '@hooks/useRealm' -import { tryParsePublicKey } from '@tools/core/pubkey' -import { fmtMintAmount } from '@tools/sdk/units' -import tokenService from '@utils/services/token' -import useMembersListStore from 'stores/useMembersStore' -import { ViewState } from './types' -import { useMemo } from 'react' -import { Member } from '@utils/uiTypes/members' -import { AddressImage, DisplayAddress } from '@cardinal/namespaces-components' -import useWalletStore from 'stores/useWalletStore' +import { UserCircleIcon, LogoutIcon } from '@heroicons/react/outline'; +import useRealm from '@hooks/useRealm'; +import { tryParsePublicKey } from '@tools/core/pubkey'; +import { fmtMintAmount } from '@tools/sdk/units'; +import { abbreviateAddress } from '@utils/formatting'; +import tokenService from '@utils/services/token'; +import useMembersListStore from 'stores/useMembersStore'; +import { ViewState } from './types'; +import { useMemo } from 'react'; +import { Member } from '@utils/uiTypes/members'; const MemberItem = ({ item }: { item: Member }) => { - const { mint, councilMint, realm } = useRealm() + const { mint, councilMint, realm } = useRealm(); const { setCurrentCompactView, setCurrentCompactViewMember, - } = useMembersListStore() + } = useMembersListStore(); const { walletAddress, councilVotes, @@ -23,67 +22,43 @@ const MemberItem = ({ item }: { item: Member }) => { votesCasted, hasCouncilTokenOutsideRealm, hasCommunityTokenOutsideRealm, - } = item - const { connection } = useWalletStore((s) => s) + } = item; - const walletPublicKey = tryParsePublicKey(walletAddress) + const walletPublicKey = tryParsePublicKey(walletAddress); const tokenName = realm ? tokenService.getTokenInfo(realm?.account.communityMint.toBase58())?.symbol - : '' - const totalVotes = votesCasted + : ''; + const totalVotes = votesCasted; const communityAmount = communityVotes && !communityVotes.isZero() ? useMemo(() => fmtMintAmount(mint, communityVotes), [item.walletAddress]) - : null + : null; const councilAmount = councilVotes && !councilVotes.isZero() ? useMemo(() => fmtMintAmount(councilMint, councilVotes), [ item.walletAddress, ]) - : null + : null; + + const walletAddressFormatted = walletPublicKey + ? abbreviateAddress(walletPublicKey) + : '-'; async function handleGoToMemberOverview() { - setCurrentCompactView(ViewState.MemberOverview) - setCurrentCompactViewMember(item) + setCurrentCompactView(ViewState.MemberOverview); + setCurrentCompactViewMember(item); } - const address = useMemo( - () => ( - } - /> - ), - [item.walletAddress] - ) - - const addressName = useMemo( - () => ( - - ), - [item.walletAddress] - ) - return (
- {address} +
-
{addressName}
+
{walletAddressFormatted}
Votes cast: {totalVotes}
@@ -107,7 +82,7 @@ const MemberItem = ({ item }: { item: Member }) => {
- ) -} + ); +}; -export default MemberItem +export default MemberItem; diff --git a/components/Members/MemberOverview.tsx b/components/Members/MemberOverview.tsx index 83363cb5b1..8cb9e653fc 100644 --- a/components/Members/MemberOverview.tsx +++ b/components/Members/MemberOverview.tsx @@ -1,5 +1,4 @@ -import { AddressImage, DisplayAddress } from '@cardinal/namespaces-components' -import { getExplorerUrl } from '@components/explorer/tools' +import { getExplorerUrl } from '@components/explorer/tools'; import { ArrowLeftIcon, CheckCircleIcon, @@ -7,37 +6,42 @@ import { LogoutIcon, UserCircleIcon, XCircleIcon, -} from '@heroicons/react/outline' -import useQueryContext from '@hooks/useQueryContext' -import useRealm from '@hooks/useRealm' -import { getVoteRecordsByVoterMapByProposal } from '@models/api' -import { isYesVote } from '@models/voteRecords' -import { GOVERNANCE_CHAT_PROGRAM_ID, VoteRecord } from '@solana/spl-governance' -import { ChatMessage, ProgramAccount } from '@solana/spl-governance' -import { getGovernanceChatMessagesByVoter } from '@solana/spl-governance' +} from '@heroicons/react/outline'; +import useQueryContext from '@hooks/useQueryContext'; +import useRealm from '@hooks/useRealm'; +import { getVoteRecordsByVoterMapByProposal } from '@models/api'; +import { isYesVote } from '@models/voteRecords'; +import { GOVERNANCE_CHAT_PROGRAM_ID, VoteRecord } from '@solana/spl-governance'; +import { ChatMessage, ProgramAccount } from '@solana/spl-governance'; +import { getGovernanceChatMessagesByVoter } from '@solana/spl-governance'; -import { PublicKey } from '@solana/web3.js' -import { tryParsePublicKey } from '@tools/core/pubkey' -import { accountsToPubkeyMap } from '@tools/sdk/accounts' -import { fmtMintAmount } from '@tools/sdk/units' -import { notify } from '@utils/notifications' -import tokenService from '@utils/services/token' -import React, { useEffect, useMemo, useState } from 'react' -import useMembersListStore from 'stores/useMembersStore' -import useWalletStore from 'stores/useWalletStore' -import { ViewState, WalletTokenRecordWithProposal } from './types' +import { PublicKey } from '@solana/web3.js'; +import { tryParsePublicKey } from '@tools/core/pubkey'; +import { accountsToPubkeyMap } from '@tools/sdk/accounts'; +import { fmtMintAmount } from '@tools/sdk/units'; +import { notify } from '@utils/notifications'; +import { abbreviateAddress } from '@utils/formatting'; + +import tokenService from '@utils/services/token'; +import React, { useEffect, useMemo, useState } from 'react'; +import useMembersListStore from 'stores/useMembersStore'; +import useWalletStore from 'stores/useWalletStore'; +import { ViewState, WalletTokenRecordWithProposal } from './types'; const MemberOverview = () => { - const { realm } = useRealm() - const member = useMembersListStore((s) => s.compact.currentMember) - const connection = useWalletStore((s) => s.connection) - const selectedRealm = useWalletStore((s) => s.selectedRealm) - const { mint, councilMint, proposals, symbol } = useRealm() - const { setCurrentCompactView, resetCompactViewState } = useMembersListStore() - const { fmtUrlWithCluster } = useQueryContext() + const { realm } = useRealm(); + const member = useMembersListStore((s) => s.compact.currentMember); + const connection = useWalletStore((s) => s.connection); + const selectedRealm = useWalletStore((s) => s.selectedRealm); + const { mint, councilMint, proposals, symbol } = useRealm(); + const { + setCurrentCompactView, + resetCompactViewState, + } = useMembersListStore(); + const { fmtUrlWithCluster } = useQueryContext(); const [ownVoteRecords, setOwnVoteRecords] = useState< WalletTokenRecordWithProposal[] - >([]) + >([]); const { walletAddress, @@ -46,94 +50,98 @@ const MemberOverview = () => { votesCasted, hasCommunityTokenOutsideRealm, hasCouncilTokenOutsideRealm, - } = member! - const walletPublicKey = tryParsePublicKey(walletAddress) + } = member!; + const walletPublicKey = tryParsePublicKey(walletAddress); const tokenName = realm ? tokenService.getTokenInfo(realm?.account.communityMint.toBase58())?.symbol - : '' - const totalVotes = votesCasted + : ''; + const totalVotes = votesCasted; const communityAmount = communityVotes && !communityVotes.isZero() ? useMemo(() => fmtMintAmount(mint, communityVotes), [ member!.walletAddress, ]) - : null + : null; const councilAmount = councilVotes && !councilVotes.isZero() ? useMemo(() => fmtMintAmount(councilMint, councilVotes), [ member!.walletAddress, ]) - : null + : null; + + const walletAddressFormatted = walletPublicKey + ? abbreviateAddress(walletPublicKey) + : '-'; const handleGoBackToMainView = async () => { - setCurrentCompactView(ViewState.MainView) - resetCompactViewState() - } + setCurrentCompactView(ViewState.MainView); + resetCompactViewState(); + }; const getVoteRecordsAndChatMsgs = async () => { - let voteRecords: { [pubKey: string]: ProgramAccount } = {} - let chatMessages: { [pubKey: string]: ProgramAccount } = {} + let voteRecords: { [pubKey: string]: ProgramAccount } = {}; + let chatMessages: { [pubKey: string]: ProgramAccount } = {}; try { const results = await Promise.all([ getVoteRecordsByVoterMapByProposal( connection.current, selectedRealm!.programId!, - new PublicKey(member!.walletAddress) + new PublicKey(member!.walletAddress), ), getGovernanceChatMessagesByVoter( connection!.current, GOVERNANCE_CHAT_PROGRAM_ID, - new PublicKey(member!.walletAddress) + new PublicKey(member!.walletAddress), ), - ]) - voteRecords = results[0] - chatMessages = accountsToPubkeyMap(results[1]) + ]); + voteRecords = results[0]; + chatMessages = accountsToPubkeyMap(results[1]); } catch (e) { notify({ message: 'Unable to fetch vote records for selected wallet address', type: 'error', - }) + }); } - return { voteRecords, chat: chatMessages } - } + return { voteRecords, chat: chatMessages }; + }; useEffect(() => { //we get voteRecords sorted by proposal date and match it with proposal name and chat msgs leaved by token holder. const handleSetVoteRecords = async () => { - const { voteRecords, chat } = await getVoteRecordsAndChatMsgs() + const { voteRecords, chat } = await getVoteRecordsAndChatMsgs(); const voteRecordsArray: WalletTokenRecordWithProposal[] = Object.keys( - voteRecords + voteRecords, ) .sort((a, b) => { - const prevProposal = proposals[a] - const nextProposal = proposals[b] + const prevProposal = proposals[a]; + const nextProposal = proposals[b]; return ( prevProposal?.account.getStateTimestamp() - nextProposal?.account.getStateTimestamp() - ) + ); }) .reverse() .filter((x) => proposals[x]) .flatMap((x) => { - const currentProposal = proposals[x] + const currentProposal = proposals[x]; const currentChatsMsgPk = Object.keys(chat).filter( (c) => chat[c]?.account.proposal.toBase58() === - currentProposal?.pubkey.toBase58() - ) + currentProposal?.pubkey.toBase58(), + ); const currentChatMsgs = currentChatsMsgPk.map( - (c) => chat[c].account.body.value - ) + (c) => chat[c].account.body.value, + ); return { proposalPublicKey: x, proposalName: currentProposal?.account.name, chatMessages: currentChatMsgs, ...voteRecords[x], - } - }) + }; + }); - setOwnVoteRecords(voteRecordsArray) - } - handleSetVoteRecords() - }, [walletAddress]) + setOwnVoteRecords(voteRecordsArray); + }; + handleSetVoteRecords(); + }, [walletAddress]); return ( <> @@ -143,13 +151,7 @@ const MemberOverview = () => { onClick={handleGoBackToMainView} className="h-4 w-4 mr-1 text-primary-light mr-2" /> - + {walletAddressFormatted} {

-
- } - /> -
+
- ) -} + ); +}; -export default MemberOverview +export default MemberOverview; diff --git a/components/Members/MembersCompactWrapper.tsx b/components/Members/MembersCompactWrapper.tsx index d7d966a6ac..8bdccb6c7b 100644 --- a/components/Members/MembersCompactWrapper.tsx +++ b/components/Members/MembersCompactWrapper.tsx @@ -1,50 +1,20 @@ -import useRealm from '@hooks/useRealm' -import React, { useEffect, useState } from 'react' -import useMembersListStore from 'stores/useMembersStore' -import { ViewState } from './types' -import MembersItems from './MembersItems' -import useMembers from './useMembers' -import MemberOverview from './MemberOverview' -import { PlusIcon } from '@heroicons/react/outline' -import useGovernanceAssets from '@hooks/useGovernanceAssets' -import Tooltip from '@components/Tooltip' -import useWalletStore from 'stores/useWalletStore' -import Modal from '@components/Modal' -import AddMemberForm from './AddMemberForm' +import useRealm from '@hooks/useRealm'; +import React, { useEffect } from 'react'; +import useMembersListStore from 'stores/useMembersStore'; +import { ViewState } from './types'; +import MembersItems from './MembersItems'; +import useMembers from './useMembers'; +import MemberOverview from './MemberOverview'; const MembersCompactWrapper = () => { - const { - symbol, - councilMint, - toManyCouncilOutstandingProposalsForUse, - toManyCommunityOutstandingProposalsForUser, - } = useRealm() - const { members, activeMembers } = useMembers() - const connected = useWalletStore((s) => s.connected) - const activeMembersCount = activeMembers.length - const { resetCompactViewState } = useMembersListStore() - const { - canUseMintInstruction, - canMintRealmCouncilToken, - } = useGovernanceAssets() - const currentView = useMembersListStore((s) => s.compact.currentView) + const { symbol } = useRealm(); + const { members, activeMembers } = useMembers(); + const activeMembersCount = activeMembers.length; + const { resetCompactViewState } = useMembersListStore(); + const currentView = useMembersListStore((s) => s.compact.currentView); const totalVotesCast = members.reduce((prev, current) => { - return prev + current.votesCasted - }, 0) - - const [openAddMemberModal, setOpenAddMemberModal] = useState(false) - - const addNewMemberTooltip = !connected - ? 'Connect your wallet to add new council member' - : !canMintRealmCouncilToken() - ? 'Your realm need mint governance for council token to add new member' - : !canUseMintInstruction - ? "You don't have enough governance power to add new council member" - : toManyCommunityOutstandingProposalsForUser - ? 'You have too many community outstanding proposals. You need to finalize them before creating a new council member.' - : toManyCouncilOutstandingProposalsForUse - ? 'You have too many council outstanding proposals. You need to finalize them before creating a new council member.' - : '' + return prev + current.votesCasted; + }, 0); const getCurrentView = () => { switch (currentView) { @@ -53,26 +23,6 @@ const MembersCompactWrapper = () => { <>

Members ({activeMembersCount}) - {councilMint && ( - -
setOpenAddMemberModal(!openAddMemberModal)} - className={`bg-bkg-2 default-transition - flex flex-col items-center justify-center - rounded-lg hover:bg-bkg-3 ml-auto - hover:cursor-pointer ${ - addNewMemberTooltip ? 'opacity-60 pointer-events-none' : '' - }`} - > -
- -
-
-
- )}

@@ -84,31 +34,20 @@ const MembersCompactWrapper = () => {
- - {openAddMemberModal && ( - setOpenAddMemberModal(false)} - isOpen={openAddMemberModal} - > - setOpenAddMemberModal(false)} /> - - )} - ) + ); case ViewState.MemberOverview: - return + return ; } - } + }; useEffect(() => { - resetCompactViewState() - }, [symbol]) + resetCompactViewState(); + }, [symbol]); return (
{getCurrentView()}
- ) -} + ); +}; -export default MembersCompactWrapper +export default MembersCompactWrapper; diff --git a/components/Members/MembersItems.tsx b/components/Members/MembersItems.tsx index 6bd1e6c924..6ed51f2cba 100644 --- a/components/Members/MembersItems.tsx +++ b/components/Members/MembersItems.tsx @@ -1,22 +1,24 @@ -import PaginationComponent from '@components/Pagination' -import { Member } from '@utils/uiTypes/members' -import dynamic from 'next/dynamic' -import { useEffect, useState } from 'react' -const MemberItem = dynamic(() => import('./MemberItem')) +import PaginationComponent from '@components/Pagination'; +import { Member } from '@utils/uiTypes/members'; +import dynamic from 'next/dynamic'; +import { useEffect, useState } from 'react'; +const MemberItem = dynamic(() => import('./MemberItem')); const MembersItems = ({ activeMembers }: { activeMembers: Member[] }) => { - const perPage = 7 - const [members, setMembers] = useState([]) - const totalPages = Math.ceil(activeMembers.length / perPage) + const perPage = 7; + const [members, setMembers] = useState([]); + const totalPages = Math.ceil(activeMembers.length / perPage); const onPageChange = (page) => { - setMembers(paginateMembers(page)) - } + setMembers(paginateMembers(page)); + }; + const paginateMembers = (page) => { - return activeMembers.slice(page * perPage, (page + 1) * perPage) - } + return activeMembers.slice(page * perPage, (page + 1) * perPage); + }; + useEffect(() => { - setMembers(paginateMembers(0)) - }, [activeMembers.length]) + setMembers(paginateMembers(0)); + }, [activeMembers.length]); return ( <> @@ -32,6 +34,6 @@ const MembersItems = ({ activeMembers }: { activeMembers: Member[] }) => { >
- ) -} -export default MembersItems + ); +}; +export default MembersItems; diff --git a/components/Members/types.ts b/components/Members/types.ts index 79bea9dd17..db932f6273 100644 --- a/components/Members/types.ts +++ b/components/Members/types.ts @@ -1,5 +1,5 @@ -import { TokenOwnerRecord, VoteRecord } from '@solana/spl-governance' -import { ProgramAccount } from '@solana/spl-governance' +import { TokenOwnerRecord, VoteRecord } from '@solana/spl-governance'; +import { ProgramAccount } from '@solana/spl-governance'; export enum ViewState { MainView, @@ -8,14 +8,14 @@ export enum ViewState { } export interface TokenRecordsWithWalletAddress { - walletAddress: string - council?: ProgramAccount | undefined - community?: ProgramAccount | undefined + walletAddress: string; + council?: ProgramAccount | undefined; + community?: ProgramAccount | undefined; } export interface WalletTokenRecordWithProposal extends ProgramAccount { - proposalPublicKey: string - proposalName: string - chatMessages: string[] + proposalPublicKey: string; + proposalName: string; + chatMessages: string[]; } diff --git a/components/Members/useMembers.tsx b/components/Members/useMembers.tsx index 62ad969736..e22b78d5c5 100644 --- a/components/Members/useMembers.tsx +++ b/components/Members/useMembers.tsx @@ -1,109 +1,109 @@ -import { TokenRecordsWithWalletAddress } from './types' -import useRealm from '@hooks/useRealm' -import { useEffect, useMemo, useState } from 'react' -import useWalletStore from 'stores/useWalletStore' +import { TokenRecordsWithWalletAddress } from './types'; +import useRealm from '@hooks/useRealm'; +import { useEffect, useMemo, useState } from 'react'; +import useWalletStore from 'stores/useWalletStore'; import { getMultipleAccountInfoChunked, getTokenAccountsByMint, parseTokenAccountData, TokenProgramAccount, -} from '@utils/tokens' +} from '@utils/tokens'; import { AccountInfo, ASSOCIATED_TOKEN_PROGRAM_ID, Token, TOKEN_PROGRAM_ID, -} from '@solana/spl-token' -import { Member } from 'utils/uiTypes/members' -import { BN } from '@project-serum/anchor' -import { PublicKey } from '@solana/web3.js' -import { usePrevious } from '@hooks/usePrevious' -import { capitalize } from '@utils/helpers' +} from '@solana/spl-token'; +import { Member } from 'utils/uiTypes/members'; +import { PublicKey } from '@solana/web3.js'; +import { usePrevious } from '@hooks/usePrevious'; +import { BN_ZERO, capitalize } from '@utils/helpers'; export default function useMembers() { - const { tokenRecords, councilTokenOwnerRecords, realm } = useRealm() - const connection = useWalletStore((s) => s.connection) - const previousRealmPubKey = usePrevious(realm?.pubkey.toBase58()) as string + const { tokenRecords, councilTokenOwnerRecords, realm } = useRealm(); + const connection = useWalletStore((s) => s.connection); + const previousRealmPubKey = usePrevious(realm?.pubkey.toBase58()) as string; const fetchCouncilMembersWithTokensOutsideRealm = async () => { if (realm?.account.config.councilMint) { const tokenAccounts = await getTokenAccountsByMint( connection.current, - realm.account.config.councilMint.toBase58() - ) - const tokenAccountsInfo: TokenProgramAccount[] = [] + realm.account.config.councilMint.toBase58(), + ); + const tokenAccountsInfo: TokenProgramAccount[] = []; for (const acc of tokenAccounts) { - tokenAccountsInfo.push(acc) + tokenAccountsInfo.push(acc); } //we filter out people who dont have any tokens and we filter out accounts owned by realm e.g. //accounts that holds deposited tokens inside realm. return tokenAccountsInfo.filter( (x) => !x.account.amount.isZero() && - x.account.owner.toBase58() !== realm?.pubkey.toBase58() - ) + x.account.owner.toBase58() !== realm?.pubkey.toBase58(), + ); } - return [] - } + return []; + }; //This will need to be rewritten for better performance if some realm hits more then +-5k+ members const fetchCommunityMembersATAS = async () => { if (realm?.account.communityMint) { - const ATAS: PublicKey[] = [] + const ATAS: PublicKey[] = []; //we filter out people who never voted and has tokens inside realm const communityTokenRecordsWallets = tokenRecordArray .filter( (x) => x.community?.account.totalVotesCount && x.community?.account.totalVotesCount > 0 && - x.community.account.governingTokenDepositAmount.isZero() + x.community.account.governingTokenDepositAmount.isZero(), ) - .map((x) => x.walletAddress) + .map((x) => x.walletAddress); for (const walletAddress of communityTokenRecordsWallets) { const ata = await Token.getAssociatedTokenAddress( ASSOCIATED_TOKEN_PROGRAM_ID, // always ASSOCIATED_TOKEN_PROGRAM_ID TOKEN_PROGRAM_ID, // always TOKEN_PROGRAM_ID realm!.account.communityMint, // mint - new PublicKey(walletAddress) // owner - ) - ATAS.push(ata) + new PublicKey(walletAddress), // owner + ); + ATAS.push(ata); } const ownersAtas = await getMultipleAccountInfoChunked( connection.current, - ATAS - ) + ATAS, + ); const ownersAtasParsed: TokenProgramAccount[] = ownersAtas .filter((x) => x) .map((r) => { - const publicKey = r!.owner - const data = Buffer.from(r!.data) - const account = parseTokenAccountData(r!.owner, data) - return { publicKey, account } - }) - return ownersAtasParsed + const publicKey = r!.owner; + // TRICK to make it compile + const data = Buffer.from(r!.data as any); + const account = parseTokenAccountData(r!.owner, data); + return { publicKey, account }; + }); + return ownersAtasParsed; } - return [] - } + return []; + }; const matchMembers = ( membersArray, membersToMatch, type, - pushNonExisting = false + pushNonExisting = false, ) => { - const votesPropoName = `${type.toLowerCase()}Votes` - const hasVotesOutsidePropName = `has${capitalize(type)}TokenOutsideRealm` - const members = [...membersArray] + const votesPropoName = `${type.toLowerCase()}Votes`; + const hasVotesOutsidePropName = `has${capitalize(type)}TokenOutsideRealm`; + const members = [...membersArray]; for (const memberToMatch of membersToMatch) { //We match members that had deposited tokens at least once const member = members.find( - (x) => x.walletAddress === memberToMatch.account.owner.toBase58() - ) + (x) => x.walletAddress === memberToMatch.account.owner.toBase58(), + ); if (member) { member[votesPropoName] = member[votesPropoName].add( - memberToMatch.account.amount - ) + memberToMatch.account.amount, + ); if (!memberToMatch.account.amount.isZero()) { - member[hasVotesOutsidePropName] = true + member[hasVotesOutsidePropName] = true; } } else if (pushNonExisting) { //we add members who never deposited tokens inside realm @@ -111,13 +111,13 @@ export default function useMembers() { walletAddress: memberToMatch.account.owner.toBase58(), votesCasted: 0, [votesPropoName]: memberToMatch.account.amount, - communityVotes: new BN(0), + communityVotes: BN_ZERO, [hasVotesOutsidePropName]: true, - }) + }); } } - return members - } + return members; + }; const tokenRecordArray: TokenRecordsWithWalletAddress[] = useMemo( () => @@ -126,12 +126,12 @@ export default function useMembers() { return { walletAddress: x, community: { ...tokenRecords[x] }, - } + }; }) : [], - [JSON.stringify(tokenRecords)] - ) - const [members, setMembers] = useState([]) + [JSON.stringify(tokenRecords)], + ); + const [members, setMembers] = useState([]); const councilRecordArray: TokenRecordsWithWalletAddress[] = useMemo( () => @@ -140,28 +140,28 @@ export default function useMembers() { return { walletAddress: x, council: { ...councilTokenOwnerRecords[x] }, - } + }; }) : [], - [JSON.stringify(councilTokenOwnerRecords)] - ) + [JSON.stringify(councilTokenOwnerRecords)], + ); //for community we exclude people who never vote const communityAndCouncilTokenRecords = [ ...tokenRecordArray.filter( (x) => x.community?.account.totalVotesCount && - x.community?.account.totalVotesCount > 0 + x.community?.account.totalVotesCount > 0, ), ...councilRecordArray, - ] + ]; //merge community and council vote records to one big array of members //sort them by totalVotes sum of community and council votes const membersWithTokensDeposited = useMemo( () => //remove duplicated walletAddresses Array.from( - new Set(communityAndCouncilTokenRecords.map((s) => s.walletAddress)) + new Set(communityAndCouncilTokenRecords.map((s) => s.walletAddress)), ) //deduplication .map((walletAddress) => { @@ -179,55 +179,56 @@ export default function useMembers() { councilVotes: curr.council ? curr.council.account.governingTokenDepositAmount : acc.councilVotes, - } + }; if (curr.community) { - obj['votesCasted'] += curr.community.account.totalVotesCount + obj['votesCasted'] += + curr.community.account.totalVotesCount; } if (curr.council) { - obj['votesCasted'] += curr.council.account.totalVotesCount + obj['votesCasted'] += curr.council.account.totalVotesCount; } - return obj + return obj; }, { walletAddress: '', votesCasted: 0, - councilVotes: new BN(0), - communityVotes: new BN(0), - } + councilVotes: BN_ZERO, + communityVotes: BN_ZERO, + }, ), - } + }; }) .sort((a, b) => { - return a.votesCasted - b.votesCasted + return a.votesCasted - b.votesCasted; }) .reverse(), - [JSON.stringify(tokenRecordArray), JSON.stringify(councilRecordArray)] - ) + [JSON.stringify(tokenRecordArray), JSON.stringify(councilRecordArray)], + ); //Move to store if will be used more across application useEffect(() => { const handleSetMembers = async () => { - let members = [...membersWithTokensDeposited] - const councilMembers = await fetchCouncilMembersWithTokensOutsideRealm() - const communityMembers = await fetchCommunityMembersATAS() - members = matchMembers(members, councilMembers, 'council', true) - members = matchMembers(members, communityMembers, 'community') - setMembers(members) - } + let members = [...membersWithTokensDeposited]; + const councilMembers = await fetchCouncilMembersWithTokensOutsideRealm(); + const communityMembers = await fetchCommunityMembersATAS(); + members = matchMembers(members, councilMembers, 'council', true); + members = matchMembers(members, communityMembers, 'community'); + setMembers(members); + }; if (previousRealmPubKey !== realm?.pubkey.toBase58()) { - handleSetMembers() + handleSetMembers(); } - }, [realm?.pubkey.toBase58()]) + }, [realm?.pubkey.toBase58()]); const activeMembers: Member[] = members.filter( - (x) => !x.councilVotes.isZero() || !x.communityVotes.isZero() - ) + (x) => !x.councilVotes.isZero() || !x.communityVotes.isZero(), + ); return { tokenRecordArray, councilRecordArray, members, activeMembers, - } + }; } diff --git a/components/Modal.tsx b/components/Modal.tsx index a48e1004be..070ad6e677 100644 --- a/components/Modal.tsx +++ b/components/Modal.tsx @@ -1,5 +1,5 @@ -import { XIcon } from '@heroicons/react/outline' -import { Portal } from 'react-portal' +import { XIcon } from '@heroicons/react/outline'; +import { Portal } from 'react-portal'; const Modal = ({ isOpen, @@ -57,13 +57,13 @@ const Modal = ({
- ) -} + ); +}; const Header = ({ children }) => { - return
{children}
-} + return
{children}
; +}; -Modal.Header = Header +Modal.Header = Header; -export default Modal +export default Modal; diff --git a/components/NFTS/NFTSCompactWrapper.tsx b/components/NFTS/NFTSCompactWrapper.tsx index e394e9bb32..453d465887 100644 --- a/components/NFTS/NFTSCompactWrapper.tsx +++ b/components/NFTS/NFTSCompactWrapper.tsx @@ -1,25 +1,25 @@ -import { getExplorerUrl } from '@components/explorer/tools' -import ImgWithLoader from '@components/ImgWithLoader' -import { DEFAULT_NFT_TREASURY_MINT } from '@components/instructions/tools' -import { PhotographIcon } from '@heroicons/react/outline' -import { ChevronRightIcon } from '@heroicons/react/solid' -import useGovernanceAssets from '@hooks/useGovernanceAssets' -import useQueryContext from '@hooks/useQueryContext' -import useRealm from '@hooks/useRealm' -import { useRouter } from 'next/router' -import React from 'react' -import useTreasuryAccountStore from 'stores/useTreasuryAccountStore' -import useWalletStore from 'stores/useWalletStore' -import { LinkButton } from '@components/Button' +import { getExplorerUrl } from '@components/explorer/tools'; +import ImgWithLoader from '@components/ImgWithLoader'; +import { DEFAULT_NFT_TREASURY_MINT } from '@components/instructions/tools'; +import { PhotographIcon } from '@heroicons/react/outline'; +import { ChevronRightIcon } from '@heroicons/react/solid'; +import useGovernanceAssets from '@hooks/useGovernanceAssets'; +import useQueryContext from '@hooks/useQueryContext'; +import useRealm from '@hooks/useRealm'; +import { useRouter } from 'next/router'; +import React from 'react'; +import useTreasuryAccountStore from 'stores/useTreasuryAccountStore'; +import useWalletStore from 'stores/useWalletStore'; +import { LinkButton } from '@components/Button'; const NFTSCompactWrapper = () => { - const router = useRouter() - const { nftsGovernedTokenAccounts } = useGovernanceAssets() - const connection = useWalletStore((s) => s.connection) - const realmNfts = useTreasuryAccountStore((s) => s.allNfts) - const isLoading = useTreasuryAccountStore((s) => s.isLoadingNfts) - const { symbol } = useRealm() - const { fmtUrlWithCluster } = useQueryContext() + const router = useRouter(); + const { nftsGovernedTokenAccounts } = useGovernanceAssets(); + const connection = useWalletStore((s) => s.connection); + const realmNfts = useTreasuryAccountStore((s) => s.allNfts); + const isLoading = useTreasuryAccountStore((s) => s.isLoadingNfts); + const { symbol } = useRealm(); + const { fmtUrlWithCluster } = useQueryContext(); return nftsGovernedTokenAccounts.length ? (
@@ -28,9 +28,9 @@ const NFTSCompactWrapper = () => { className={`flex items-center text-primary-light`} onClick={() => { const url = fmtUrlWithCluster( - `/dao/${symbol}/gallery/${DEFAULT_NFT_TREASURY_MINT}` - ) - router.push(url) + `/dao/${symbol}/gallery/${DEFAULT_NFT_TREASURY_MINT}`, + ); + router.push(url); }} > View @@ -73,7 +73,7 @@ const NFTSCompactWrapper = () => {
- ) : null -} + ) : null; +}; -export default NFTSCompactWrapper +export default NFTSCompactWrapper; diff --git a/components/NFTS/NFTSelector.tsx b/components/NFTS/NFTSelector.tsx index b83efc7a48..6bb4c6cf26 100644 --- a/components/NFTS/NFTSelector.tsx +++ b/components/NFTS/NFTSelector.tsx @@ -3,18 +3,18 @@ import React, { useEffect, useImperativeHandle, useState, -} from 'react' -import { PhotographIcon } from '@heroicons/react/solid' -import useWalletStore from 'stores/useWalletStore' -import { NFTWithMint } from '@utils/uiTypes/nfts' -import { CheckCircleIcon } from '@heroicons/react/solid' -import { PublicKey } from '@solana/web3.js' -import Loading from '@components/Loading' -import { getNfts } from '@utils/tokens' -import ImgWithLoader from '@components/ImgWithLoader' +} from 'react'; +import { PhotographIcon } from '@heroicons/react/solid'; +import useWalletStore from 'stores/useWalletStore'; +import { NFTWithMint } from '@utils/uiTypes/nfts'; +import { CheckCircleIcon } from '@heroicons/react/solid'; +import { PublicKey } from '@solana/web3.js'; +import Loading from '@components/Loading'; +import { getNfts } from '@utils/tokens'; +import ImgWithLoader from '@components/ImgWithLoader'; export interface NftSelectorFunctions { - handleGetNfts: () => void + handleGetNfts: () => void; } function NFTSelector( @@ -26,49 +26,49 @@ function NFTSelector( selectable = true, familyName = '', }: { - ownerPk: PublicKey - onNftSelect: (nfts: NFTWithMint[]) => void - nftWidth?: string - nftHeight?: string - selectable?: boolean - familyName?: string + ownerPk: PublicKey; + onNftSelect: (nfts: NFTWithMint[]) => void; + nftWidth?: string; + nftHeight?: string; + selectable?: boolean; + familyName?: string; }, - ref: React.Ref + ref: React.Ref, ) { - const [nfts, setNfts] = useState([]) - const [selectedNfts, setSelectedNfts] = useState([]) - const connection = useWalletStore((s) => s.connection) - const [isLoading, setIsLoading] = useState(false) + const [nfts, setNfts] = useState([]); + const [selectedNfts, setSelectedNfts] = useState([]); + const connection = useWalletStore((s) => s.connection); + const [isLoading, setIsLoading] = useState(false); const handleSelectNft = (nft: NFTWithMint) => { - const isSelected = selectedNfts.find((x) => x.mint === nft.mint) + const isSelected = selectedNfts.find((x) => x.mint === nft.mint); if (isSelected) { - setSelectedNfts([...selectedNfts.filter((x) => x.mint !== nft.mint)]) + setSelectedNfts([...selectedNfts.filter((x) => x.mint !== nft.mint)]); } else { //For now only one nft at the time - setSelectedNfts([nft]) + setSelectedNfts([nft]); } - } + }; const handleGetNfts = async () => { - setIsLoading(true) - const nfts = await getNfts(connection.current, ownerPk) + setIsLoading(true); + const nfts = await getNfts(connection.current, ownerPk); if (nfts.length === 1) { - handleSelectNft(nfts[0]) + handleSelectNft(nfts[0]); } - setNfts(nfts) - setIsLoading(false) - } + setNfts(nfts); + setIsLoading(false); + }; useImperativeHandle(ref, () => ({ handleGetNfts, - })) + })); useEffect(() => { if (ownerPk) { - handleGetNfts() + handleGetNfts(); } - }, [ownerPk]) + }, [ownerPk]); useEffect(() => { - onNftSelect(selectedNfts) - }, [selectedNfts]) + onNftSelect(selectedNfts); + }, [selectedNfts]); return ( <>
{nfts .filter( - (x) => !familyName || x.val.collection.family === familyName + (x) => !familyName || x.val.collection.family === familyName, ) .map((x) => (
{selectedNfts.find( - (selectedNfts) => selectedNfts.mint === x.mint + (selectedNfts) => selectedNfts.mint === x.mint, ) && ( )} @@ -117,7 +117,7 @@ function NFTSelector( )}
- ) + ); } -export default forwardRef(NFTSelector) +export default forwardRef(NFTSelector); diff --git a/components/NavBar.tsx b/components/NavBar.tsx index 306795a790..6985ef7dc5 100644 --- a/components/NavBar.tsx +++ b/components/NavBar.tsx @@ -1,24 +1,51 @@ -import useQueryContext from '@hooks/useQueryContext' -import Link from 'next/link' - -import ConnectWalletButton from './ConnectWalletButton' +import useQueryContext from '@hooks/useQueryContext'; +import useRealm from '@hooks/useRealm'; +import { useRouter } from 'next/router'; +import ConnectWalletButton from './ConnectWalletButton'; const NavBar = () => { - const { fmtUrlWithCluster } = useQueryContext() + const { symbol } = useRealm(); + const { fmtUrlWithCluster } = useQueryContext(); + + const router = useRouter(); + + const isDashboardPage = router.pathname.includes('dashboard'); return (
- -
- - {/*

Sierra

*/} -
- +
+ +
+ +
+ + + +
+
- ) -} + ); +}; -export default NavBar +export default NavBar; diff --git a/components/Notification.tsx b/components/Notification.tsx index 38f7495ce9..20e7317ea2 100644 --- a/components/Notification.tsx +++ b/components/Notification.tsx @@ -1,30 +1,30 @@ -import { useEffect, useState } from 'react' +import { useEffect, useState } from 'react'; import { CheckCircleIcon, InformationCircleIcon, XCircleIcon, -} from '@heroicons/react/outline' -import { XIcon } from '@heroicons/react/solid' -import useNotificationStore from '../stores/useNotificationStore' +} from '@heroicons/react/outline'; +import { XIcon } from '@heroicons/react/solid'; +import useNotificationStore from '../stores/useNotificationStore'; const NotificationList = () => { const { notifications, set: setNotificationStore } = useNotificationStore( - (s) => s - ) + (s) => s, + ); useEffect(() => { if (notifications.length > 0) { const id = setInterval(() => { setNotificationStore((state) => { - state.notifications = notifications.slice(1, notifications.length) - }) - }, 6000) + state.notifications = notifications.slice(1, notifications.length); + }); + }, 6000); return () => { - clearInterval(id) - } + clearInterval(id); + }; } - }, [notifications, setNotificationStore]) + }, [notifications, setNotificationStore]); return (
{ ))}
- ) -} + ); +}; const Notification = ({ type, message, description, txid, idx }) => { - const [showNotification, setShowNotification] = useState(true) + const [showNotification, setShowNotification] = useState(true); - if (!showNotification) return null + if (!showNotification) return null; return (
{
- ) -} + ); +}; -export default NotificationList +export default NotificationList; diff --git a/components/PageBodyContainer.tsx b/components/PageBodyContainer.tsx index 368a1d261d..562b90399e 100644 --- a/components/PageBodyContainer.tsx +++ b/components/PageBodyContainer.tsx @@ -4,6 +4,6 @@ const PageBodyContainer = ({ children }) => ( {children} -) +); -export default PageBodyContainer +export default PageBodyContainer; diff --git a/components/Pagination.tsx b/components/Pagination.tsx index efe1d95ee4..0e3e06d594 100644 --- a/components/Pagination.tsx +++ b/components/Pagination.tsx @@ -1,14 +1,14 @@ -import { ArrowLeftIcon, ArrowRightIcon } from '@heroicons/react/outline' -import React from 'react' -import { Pagination } from 'react-headless-pagination' +import { ArrowLeftIcon, ArrowRightIcon } from '@heroicons/react/outline'; +import React from 'react'; +import { Pagination } from 'react-headless-pagination'; const PaginationComponent = ({ totalPages = 5, onPageChange }) => { - const [page, setPage] = React.useState(0) + const [page, setPage] = React.useState(0); const handlePageChange = (page: number) => { - setPage(page) - onPageChange(page) - } + setPage(page); + onPageChange(page); + }; return ( <> @@ -43,7 +43,7 @@ const PaginationComponent = ({ totalPages = 5, onPageChange }) => { ) : null} - ) -} + ); +}; -export default PaginationComponent +export default PaginationComponent; diff --git a/components/PreviousRouteBtn.tsx b/components/PreviousRouteBtn.tsx index 15611cb331..73e91988bd 100644 --- a/components/PreviousRouteBtn.tsx +++ b/components/PreviousRouteBtn.tsx @@ -1,11 +1,11 @@ -import { ArrowLeftIcon } from '@heroicons/react/solid' -import useRouterHistory from '@hooks/useRouterHistory' -import Link from 'next/link' -import React from 'react' +import { ArrowLeftIcon } from '@heroicons/react/solid'; +import useRouterHistory from '@hooks/useRouterHistory'; +import Link from 'next/link'; +import React from 'react'; const PreviousRouteBtn = () => { - const { getLastRoute } = useRouterHistory() - const lastUrl = getLastRoute() as string + const { getLastRoute } = useRouterHistory(); + const lastUrl = getLastRoute() as string; return lastUrl ? (
@@ -13,6 +13,6 @@ const PreviousRouteBtn = () => { Back - ) : null -} -export default PreviousRouteBtn + ) : null; +}; +export default PreviousRouteBtn; diff --git a/components/ProgressBar.tsx b/components/ProgressBar.tsx index b6065f88b8..5ea8742ee8 100644 --- a/components/ProgressBar.tsx +++ b/components/ProgressBar.tsx @@ -1,4 +1,4 @@ -import React from 'react' +import React from 'react'; const ProgressBar = ({ progress, prefix }) => { return ( @@ -22,7 +22,7 @@ const ProgressBar = ({ progress, prefix }) => { > - ) -} + ); +}; -export default ProgressBar +export default ProgressBar; diff --git a/components/ProposalActions.tsx b/components/ProposalActions.tsx index 39df4af30d..b64eb87e6d 100644 --- a/components/ProposalActions.tsx +++ b/components/ProposalActions.tsx @@ -1,40 +1,39 @@ -/* eslint-disable @typescript-eslint/no-non-null-assertion */ -import { useEffect, useState } from 'react' -import { useHasVoteTimeExpired } from '../hooks/useHasVoteTimeExpired' -import useRealm from '../hooks/useRealm' -import { - getSignatoryRecordAddress, - ProposalState, -} from '@solana/spl-governance' -import useWalletStore from '../stores/useWalletStore' -import Button, { SecondaryButton } from './Button' - -import { RpcContext } from '@solana/spl-governance' -import { signOffProposal } from 'actions/signOffProposal' -import { notify } from '@utils/notifications' -import { finalizeVote } from 'actions/finalizeVotes' -import { Proposal } from '@solana/spl-governance' -import { ProgramAccount } from '@solana/spl-governance' -import { cancelProposal } from 'actions/cancelProposal' -import { getProgramVersionForRealm } from '@models/registry/api' +import { useEffect, useState } from 'react'; +import { useHasVoteTimeExpired } from '../hooks/useHasVoteTimeExpired'; +import useRealm from '../hooks/useRealm'; +import { getSignatoryRecordAddress } from '@solana/spl-governance'; +import useWalletStore, { + EnhancedProposalState, +} from '../stores/useWalletStore'; +import Button, { SecondaryButton } from './Button'; + +import { RpcContext } from '@solana/spl-governance'; +import { signOffProposal } from 'actions/signOffProposal'; +import { notify } from '@utils/notifications'; +import { finalizeVote } from 'actions/finalizeVotes'; +import { Proposal } from '@solana/spl-governance'; +import { ProgramAccount } from '@solana/spl-governance'; +import { cancelProposal } from 'actions/cancelProposal'; +import { getProgramVersionForRealm } from '@models/registry/api'; const ProposalActionsPanel = () => { const { governance, proposal, proposalOwner } = useWalletStore( - (s) => s.selectedProposal - ) - const { realmInfo } = useRealm() - const wallet = useWalletStore((s) => s.current) - const connected = useWalletStore((s) => s.connected) - const hasVoteTimeExpired = useHasVoteTimeExpired(governance, proposal!) - const signatories = useWalletStore((s) => s.selectedProposal.signatories) - const connection = useWalletStore((s) => s.connection) - const fetchRealm = useWalletStore((s) => s.actions.fetchRealm) - const [signatoryRecord, setSignatoryRecord] = useState(undefined) + (s) => s.selectedProposal, + ); + const { realmInfo } = useRealm(); + const wallet = useWalletStore((s) => s.current); + const connected = useWalletStore((s) => s.connected); + const hasVoteTimeExpired = useHasVoteTimeExpired(governance, proposal!); + const signatories = useWalletStore((s) => s.selectedProposal.signatories); + const connection = useWalletStore((s) => s.connection); + const fetchRealm = useWalletStore((s) => s.actions.fetchRealm); + const [signatoryRecord, setSignatoryRecord] = useState(undefined); const canFinalizeVote = - hasVoteTimeExpired && proposal?.account.state === ProposalState.Voting + hasVoteTimeExpired && + proposal?.account.state === EnhancedProposalState.Voting; - const walletPk = wallet?.publicKey + const walletPk = wallet?.publicKey; useEffect(() => { const setup = async () => { @@ -42,22 +41,22 @@ const ProposalActionsPanel = () => { const signatoryRecordPk = await getSignatoryRecordAddress( realmInfo.programId, proposal.pubkey, - walletPk - ) + walletPk, + ); if (signatoryRecordPk && signatories) { - setSignatoryRecord(signatories[signatoryRecordPk.toBase58()]) + setSignatoryRecord(signatories[signatoryRecordPk.toBase58()]); } } - } + }; - setup() - }, [proposal, realmInfo, walletPk]) + setup(); + }, [proposal, realmInfo, walletPk]); const canSignOff = signatoryRecord && - (proposal?.account.state === ProposalState.Draft || - proposal?.account.state === ProposalState.SigningOff) + (proposal?.account.state === EnhancedProposalState.Draft || + proposal?.account.state === EnhancedProposalState.SigningOff); const canCancelProposal = proposal && @@ -67,19 +66,19 @@ const ProposalActionsPanel = () => { proposal.account.canWalletCancel( governance.account, proposalOwner.account, - wallet.publicKey - ) + wallet.publicKey, + ); const signOffTooltipContent = !connected ? 'Connect your wallet to sign off this proposal' : !signatoryRecord ? 'Only a signatory of the proposal can sign it off' : !( - proposal?.account.state === ProposalState.Draft || - proposal?.account.state === ProposalState.SigningOff + proposal?.account.state === EnhancedProposalState.Draft || + proposal?.account.state === EnhancedProposalState.SigningOff ) ? 'Invalid proposal state. To sign off a proposal, it must be a draft or be in signing off state after creation.' - : '' + : ''; const cancelTooltipContent = !connected ? 'Connect your wallet to cancel this proposal' @@ -90,18 +89,19 @@ const ProposalActionsPanel = () => { !proposal?.account.canWalletCancel( governance.account, proposalOwner.account, - wallet.publicKey + wallet.publicKey, ) ? 'Only the owner of the proposal can execute this action' - : '' + : ''; const finalizeVoteTooltipContent = !connected ? 'Connect your wallet to finalize this proposal' : !hasVoteTimeExpired ? "Vote time has not expired yet. You can finalize a vote only after it's time has expired." - : proposal?.account.state === ProposalState.Voting && !hasVoteTimeExpired + : proposal?.account.state === EnhancedProposalState.Voting && + !hasVoteTimeExpired ? 'Proposal is being voting right now, you need to wait the vote to finish to be able to finalize it.' - : '' + : ''; const handleFinalizeVote = async () => { try { if (proposal && realmInfo && governance) { @@ -110,22 +110,22 @@ const ProposalActionsPanel = () => { getProgramVersionForRealm(realmInfo), wallet!, connection.current, - connection.endpoint - ) + connection.endpoint, + ); - await finalizeVote(rpcContext, governance?.account.realm, proposal) - await fetchRealm(realmInfo!.programId, realmInfo!.realmId) + await finalizeVote(rpcContext, governance?.account.realm, proposal); + await fetchRealm(realmInfo!.programId, realmInfo!.realmId); } } catch (error) { notify({ type: 'error', message: `Error: Could not finalize vote.`, description: `${error}`, - }) + }); - console.error('error finalizing vote', error) + console.error('error finalizing vote', error); } - } + }; const handleSignOffProposal = async () => { try { @@ -135,30 +135,30 @@ const ProposalActionsPanel = () => { getProgramVersionForRealm(realmInfo), wallet!, connection.current, - connection.endpoint - ) + connection.endpoint, + ); await signOffProposal( rpcContext, realmInfo.realmId, proposal, - signatoryRecord - ) + signatoryRecord, + ); - await fetchRealm(realmInfo!.programId, realmInfo!.realmId) + await fetchRealm(realmInfo!.programId, realmInfo!.realmId); } } catch (error) { notify({ type: 'error', message: `Error: Could not sign off proposal.`, description: `${error}`, - }) + }); - console.error('error sign off', error) + console.error('error sign off', error); } - } + }; const handleCancelProposal = async ( - proposal: ProgramAccount | undefined + proposal: ProgramAccount | undefined, ) => { try { if (proposal && realmInfo) { @@ -167,28 +167,29 @@ const ProposalActionsPanel = () => { getProgramVersionForRealm(realmInfo), wallet!, connection.current, - connection.endpoint - ) + connection.endpoint, + ); - await cancelProposal(rpcContext, realmInfo.realmId, proposal) + await cancelProposal(rpcContext, realmInfo.realmId, proposal); - await fetchRealm(realmInfo!.programId, realmInfo!.realmId) + await fetchRealm(realmInfo!.programId, realmInfo!.realmId); } } catch (error) { notify({ type: 'error', message: `Error: Could not cancel proposal.`, description: `${error}`, - }) + }); - console.error('error cancelling proposal', error) + console.error('error cancelling proposal', error); } - } + }; return ( <> - {ProposalState.Cancelled === proposal?.account.state || - ProposalState.Succeeded === proposal?.account.state || - ProposalState.Defeated === proposal?.account.state || + {EnhancedProposalState.Cancelled === proposal?.account.state || + EnhancedProposalState.Succeeded === proposal?.account.state || + EnhancedProposalState.Outdated === proposal?.account.state || + EnhancedProposalState.Defeated === proposal?.account.state || (!canCancelProposal && !canSignOff && !canFinalizeVote) ? null : (
@@ -228,7 +229,7 @@ const ProposalActionsPanel = () => {
)} - ) -} + ); +}; -export default ProposalActionsPanel +export default ProposalActionsPanel; diff --git a/components/ProposalCard.tsx b/components/ProposalCard.tsx index a6a1c3c761..cb190cb451 100644 --- a/components/ProposalCard.tsx +++ b/components/ProposalCard.tsx @@ -1,23 +1,23 @@ -import styled from '@emotion/styled' -import { ChevronRightIcon } from '@heroicons/react/solid' -import ProposalStateBadge from './ProposalStatusBadge' -import Link from 'next/link' -import { Proposal, ProposalState } from '@solana/spl-governance' -import ApprovalQuorum from './ApprovalQuorum' -import useRealm from '../hooks/useRealm' -import useProposalVotes from '../hooks/useProposalVotes' -import ProposalTimeStatus from './ProposalTimeStatus' +import styled from '@emotion/styled'; +import { ChevronRightIcon } from '@heroicons/react/solid'; +import ProposalStateBadge from './ProposalStatusBadge'; +import Link from 'next/link'; +import { Proposal, ProposalState } from '@solana/spl-governance'; +import ApprovalQuorum from './ApprovalQuorum'; +import useRealm from '../hooks/useRealm'; +import useProposalVotes from '../hooks/useProposalVotes'; +import ProposalTimeStatus from './ProposalTimeStatus'; -import useQueryContext from '../hooks/useQueryContext' -import { PublicKey } from '@solana/web3.js' -import VoteResults from './VoteResults' +import useQueryContext from '../hooks/useQueryContext'; +import { PublicKey } from '@solana/web3.js'; +import VoteResults from './VoteResults'; type ProposalCardProps = { - proposalPk: PublicKey - proposal: Proposal -} + proposalPk: PublicKey; + proposal: Proposal; +}; -const StyledSvg = styled(ChevronRightIcon)`` +const StyledSvg = styled(ChevronRightIcon)``; const StyledCardWrapper = styled.div` :hover { @@ -25,18 +25,18 @@ const StyledCardWrapper = styled.div` transform: translateX(4px); } } -` +`; const ProposalCard = ({ proposalPk, proposal }: ProposalCardProps) => { - const { symbol } = useRealm() - const { fmtUrlWithCluster } = useQueryContext() - const { yesVoteProgress, yesVotesRequired } = useProposalVotes(proposal) + const { symbol } = useRealm(); + const { fmtUrlWithCluster } = useQueryContext(); + const { yesVoteProgress, yesVotesRequired } = useProposalVotes(proposal); return ( - ) -} + ); +}; -export default ProposalCard +export default ProposalCard; diff --git a/components/ProposalFilter.tsx b/components/ProposalFilter.tsx index b453a4dcc7..405d021b80 100644 --- a/components/ProposalFilter.tsx +++ b/components/ProposalFilter.tsx @@ -1,48 +1,60 @@ -import { useEffect, useReducer } from 'react' -import styled from '@emotion/styled' -import { ChevronDownIcon } from '@heroicons/react/solid' -import { Disclosure } from '@headlessui/react' -import Switch from './Switch' -import { ProposalState } from '@solana/spl-governance' +import { useEffect, useReducer } from 'react'; +import styled from '@emotion/styled'; +import { ChevronDownIcon } from '@heroicons/react/solid'; +import { Disclosure } from '@headlessui/react'; +import Switch from './Switch'; +import { EnhancedProposalState } from 'stores/useWalletStore'; -const initialFilterSettings = { - [ProposalState.Draft]: false, - [ProposalState.SigningOff]: true, - [ProposalState.Voting]: true, - [ProposalState.Succeeded]: true, - [ProposalState.Executing]: true, - [ProposalState.Completed]: true, - [ProposalState.Cancelled]: false, - [ProposalState.Defeated]: true, - [ProposalState.ExecutingWithErrors]: true, -} +type Filters = { + [key in EnhancedProposalState]: boolean; +}; + +const initialFilterSettings: Filters = { + [EnhancedProposalState.Draft]: false, + [EnhancedProposalState.SigningOff]: true, + [EnhancedProposalState.Voting]: true, + [EnhancedProposalState.Succeeded]: true, + [EnhancedProposalState.Executing]: true, + [EnhancedProposalState.Completed]: true, + [EnhancedProposalState.Cancelled]: false, + [EnhancedProposalState.Defeated]: true, + [EnhancedProposalState.ExecutingWithErrors]: true, + [EnhancedProposalState.Outdated]: false, +}; const StyledAlertCount = styled.span` font-size: 0.6rem; -` +`; const ProposalFilter = ({ filters, setFilters }) => { - const [filterSettings, setFilterSettings] = useReducer( - (state, newState) => ({ ...state, ...newState }), - initialFilterSettings - ) + const [filterSettings, setFilterSettings] = useReducer< + (state: Filters, newState: Partial) => any + >((state, newState) => ({ ...state, ...newState }), initialFilterSettings); + + const handleFilters = ( + proposalState: EnhancedProposalState, + checked: boolean, + ) => { + setFilterSettings({ + [proposalState]: checked, + }); - const handleFilters = (proposalState, checked) => { - setFilterSettings({ [proposalState]: checked }) if (!checked) { - setFilters([...filters, proposalState]) + setFilters([...filters, proposalState]); } else { - setFilters(filters.filter((n) => n !== proposalState)) + setFilters( + filters.filter((n: EnhancedProposalState) => n !== proposalState), + ); } - } + }; useEffect(() => { const initialFilters = Object.keys(initialFilterSettings) .filter((x) => !initialFilterSettings[x]) - .map(Number) + .map(Number); - setFilters([...initialFilters]) - }, []) + setFilters([...initialFilters]); + }, []); return ( {({ open }) => ( @@ -73,81 +85,95 @@ const ProposalFilter = ({ filters, setFilters }) => {
Cancelled - handleFilters(ProposalState.Cancelled, checked) + handleFilters(EnhancedProposalState.Cancelled, checked) } />
Completed - handleFilters(ProposalState.Completed, checked) + handleFilters(EnhancedProposalState.Completed, checked) } />
Defeated - handleFilters(ProposalState.Defeated, checked) + handleFilters(EnhancedProposalState.Defeated, checked) } />
Draft - handleFilters(ProposalState.Draft, checked) + handleFilters(EnhancedProposalState.Draft, checked) } />
Executing - handleFilters(ProposalState.Executing, checked) + handleFilters(EnhancedProposalState.Executing, checked) } />
ExecutingWithErrors - handleFilters(ProposalState.ExecutingWithErrors, checked) + handleFilters( + EnhancedProposalState.ExecutingWithErrors, + checked, + ) } />
SigningOff - handleFilters(ProposalState.SigningOff, checked) + handleFilters(EnhancedProposalState.SigningOff, checked) } />
Succeeded + handleFilters(EnhancedProposalState.Succeeded, checked) + } + /> +
+
+ Outdated + - handleFilters(ProposalState.Succeeded, checked) + handleFilters(EnhancedProposalState.Outdated, checked) } />
Voting - handleFilters(ProposalState.Voting, checked) + handleFilters(EnhancedProposalState.Voting, checked) } />
@@ -156,7 +182,7 @@ const ProposalFilter = ({ filters, setFilters }) => { )}
- ) -} + ); +}; -export default ProposalFilter +export default ProposalFilter; diff --git a/components/ProposalStatusBadge.tsx b/components/ProposalStatusBadge.tsx index d8cab1d047..7365a37c6b 100644 --- a/components/ProposalStatusBadge.tsx +++ b/components/ProposalStatusBadge.tsx @@ -1,42 +1,57 @@ -import { PublicKey } from '@solana/web3.js' -import useRealmGovernance from '../hooks/useRealmGovernance' -import { Proposal, ProposalState } from '@solana/spl-governance' -import useWalletStore from '../stores/useWalletStore' -import { isYesVote } from '@models/voteRecords' +import { PublicKey } from '@solana/web3.js'; +import useRealmGovernance from '../hooks/useRealmGovernance'; +import useWalletStore, { + EnhancedProposal, + EnhancedProposalState, +} from '../stores/useWalletStore'; +import { isYesVote } from '@models/voteRecords'; -function getProposalStateLabel(state: ProposalState, hasVoteEnded: boolean) { +function getProposalStateLabel( + state: EnhancedProposalState, + hasVoteEnded: boolean, +) { switch (state) { - case ProposalState.ExecutingWithErrors: - return 'Execution Errors' - case ProposalState.Voting: + case EnhancedProposalState.ExecutingWithErrors: + return 'Execution Errors'; + case EnhancedProposalState.Outdated: + return 'Outdated'; + case EnhancedProposalState.Voting: // If there is no tipping point and voting period ends then proposal stays in Voting state and needs to be manually finalized - return hasVoteEnded ? 'Finalizing' : 'Voting' + return hasVoteEnded ? 'Finalizing' : 'Voting'; default: - return ProposalState[state] + return EnhancedProposalState[state]; } } -function getProposalStateStyle(state: ProposalState) { +function getProposalStateStyle(state: EnhancedProposalState) { if ( - state === ProposalState.Voting || - state === ProposalState.Executing || - state === ProposalState.SigningOff + state === EnhancedProposalState.Voting || + state === EnhancedProposalState.Executing || + state === EnhancedProposalState.SigningOff ) { - return 'border border-blue text-blue' - } else if ( - state === ProposalState.Completed || - state === ProposalState.Succeeded + return 'border border-blue text-blue'; + } + + if ( + state === EnhancedProposalState.Completed || + state === EnhancedProposalState.Succeeded ) { - return 'border border-green text-green' - } else if ( - state === ProposalState.Cancelled || - state === ProposalState.Defeated || - state === ProposalState.ExecutingWithErrors + return 'border border-green text-green'; + } + + if ( + state === EnhancedProposalState.Cancelled || + state === EnhancedProposalState.Defeated || + state === EnhancedProposalState.ExecutingWithErrors ) { - return 'border border-red text-red' - } else { - return 'border border-fgd-3 text-fgd-3' + return 'border border-red text-red'; + } + + if (state === EnhancedProposalState.Outdated) { + return 'border border-orange text-orange'; } + + return 'border border-fgd-3 text-fgd-3'; } const ProposalStateBadge = ({ @@ -44,24 +59,24 @@ const ProposalStateBadge = ({ proposal, open, }: { - proposalPk: PublicKey - proposal: Proposal - open: boolean + proposalPk: PublicKey; + proposal: EnhancedProposal; + open: boolean; }) => { - const governance = useRealmGovernance(proposal.governance) + const governance = useRealmGovernance(proposal.governance); const ownVoteRecord = useWalletStore((s) => s.ownVoteRecordsByProposal)[ proposalPk.toBase58() - ] + ]; let statusLabel = getProposalStateLabel( proposal.state, - governance && proposal.getTimeToVoteEnd(governance) < 0 - ) + governance && proposal.getTimeToVoteEnd(governance) < 0, + ); if (ownVoteRecord) { statusLabel = - statusLabel + ': ' + (isYesVote(ownVoteRecord.account) ? 'Yes' : 'No') + statusLabel + ': ' + (isYesVote(ownVoteRecord.account) ? 'Yes' : 'No'); } return ( @@ -71,8 +86,8 @@ const ProposalStateBadge = ({
{statusLabel}
@@ -81,14 +96,14 @@ const ProposalStateBadge = ({ ) : (
{statusLabel}
)} - ) -} + ); +}; -export default ProposalStateBadge +export default ProposalStateBadge; diff --git a/components/ProposalTimeStatus.tsx b/components/ProposalTimeStatus.tsx index b826271701..e40e904ab2 100644 --- a/components/ProposalTimeStatus.tsx +++ b/components/ProposalTimeStatus.tsx @@ -1,21 +1,21 @@ -import useRealm from '../hooks/useRealm' -import { Proposal, ProposalState } from '@solana/spl-governance' -import { fmtUnixTime } from '../utils/formatting' -import { VoteCountdown } from './VoteCountdown' +import useRealm from '../hooks/useRealm'; +import { Proposal, ProposalState } from '@solana/spl-governance'; +import { fmtUnixTime } from '../utils/formatting'; +import { VoteCountdown } from './VoteCountdown'; type ProposalTimeStatusProps = { - proposal: Proposal -} + proposal: Proposal; +}; const ProposalTimeStatus = ({ proposal }: ProposalTimeStatusProps) => { - const { governances } = useRealm() - const governance = governances[proposal?.governance.toBase58()]?.account + const { governances } = useRealm(); + const governance = governances[proposal?.governance.toBase58()]?.account; return proposal && governance ? (
{proposal.votingCompletedAt ? ( `${ProposalState[proposal.state]} ${fmtUnixTime( - proposal.votingCompletedAt + proposal.votingCompletedAt, )}` ) : proposal.votingAt ? ( @@ -23,7 +23,7 @@ const ProposalTimeStatus = ({ proposal }: ProposalTimeStatusProps) => { `Drafted ${fmtUnixTime(proposal.draftAt)}` )}
- ) : null -} + ) : null; +}; -export default ProposalTimeStatus +export default ProposalTimeStatus; diff --git a/components/RealmHeader.tsx b/components/RealmHeader.tsx index 36b2de5089..a0f316f0a9 100644 --- a/components/RealmHeader.tsx +++ b/components/RealmHeader.tsx @@ -1,47 +1,13 @@ -import React from 'react' -import useRealm from 'hooks/useRealm' -import { CogIcon, GlobeAltIcon } from '@heroicons/react/outline' -import { ArrowLeftIcon } from '@heroicons/react/solid' -import Link from 'next/link' -import { TwitterIcon } from './icons' -import useQueryContext from 'hooks/useQueryContext' -import { ExternalLinkIcon } from '@heroicons/react/outline' -import { getRealmExplorerHost } from 'tools/routing' +import React from 'react'; +import useRealm from 'hooks/useRealm'; +import { GlobeAltIcon } from '@heroicons/react/outline'; +import { TwitterIcon } from './icons'; const RealmHeader = () => { - const { fmtUrlWithCluster } = useQueryContext() - const { realmInfo, realmDisplayName, symbol } = useRealm() - const { REALM } = process.env - - const isBackNavVisible = realmInfo?.symbol !== REALM // hide backnav for the default realm - - const explorerHost = getRealmExplorerHost(realmInfo) - const realmUrl = `https://${explorerHost}/#/realm/${realmInfo?.realmId.toBase58()}?programId=${realmInfo?.programId.toBase58()}` + const { realmInfo, realmDisplayName } = useRealm(); return (
-
- {isBackNavVisible ? ( - - - - Back - - - ) : null} - - - -
{realmDisplayName ? (
@@ -63,12 +29,6 @@ const RealmHeader = () => {
)}
- - - - Params - - {realmInfo?.website ? ( {
- ) -} + ); +}; -export default RealmHeader +export default RealmHeader; diff --git a/components/RealmWizard/RealmWizard.tsx b/components/RealmWizard/RealmWizard.tsx deleted file mode 100644 index be0eca25d5..0000000000 --- a/components/RealmWizard/RealmWizard.tsx +++ /dev/null @@ -1,488 +0,0 @@ -import React, { useState } from 'react' -import RealmWizardController from './controller/RealmWizardController' -// import CreateRealmForm from './components/CreateRealmForm' -import Loading from '@components/Loading' -import WizardModeSelect from './components/Steps/WizardModeSelect' -import { notify } from '@utils/notifications' -import { - MultisigOptions, - BespokeConfig, - BespokeCouncil, - BespokeInfo, - RealmCreated, -} from './components/Steps' -import { useMemo } from 'react' -import Button from '@components/Button' -import { - RealmArtifacts, - RealmWizardMode, - RealmWizardStep, - StepDirection, -} from './interfaces/Realm' -import { PublicKey } from '@solana/web3.js' -import useWalletStore from 'stores/useWalletStore' -import { - DEFAULT_GOVERNANCE_PROGRAM_ID, - DEFAULT_TEST_GOVERNANCE_PROGRAM_ID, -} from '@components/instructions/tools' - -import Tooltip from '@components/Tooltip' -import { StyledLabel } from '@components/inputs/styles' -import { createMultisigRealm } from 'actions/createMultisigRealm' -import { ArrowLeftIcon } from '@heroicons/react/solid' -import useQueryContext from '@hooks/useQueryContext' -import router from 'next/router' -import { useEffect } from 'react' -import { CreateFormSchema } from './validators/createRealmValidator' -import { formValidation, isFormValid } from '@utils/formValidation' -import { registerRealm } from 'actions/registerRealm' -import { - getGovernanceProgramVersion, - MintMaxVoteWeightSource, -} from '@solana/spl-governance' -import Switch from '@components/Switch' -import { BN } from '@project-serum/anchor' -import BigNumber from 'bignumber.js' - -enum LoaderMessage { - CREATING_ARTIFACTS = 'Creating the DAO artifacts..', - MINTING_COUNCIL_TOKENS = 'Minting the council tokens..', - MINTING_COMMUNITY_TOKENS = 'Minting the community tokens..', - DEPLOYING_REALM = 'Building your DAO...', - COMPLETING_REALM = 'Finishing the DAO buildings..', - FINISHED = "DAO successfully created. Redirecting to the DAO's page", - ERROR = 'We found an error while creating your DAO :/', -} - -// TODO: split this component - -const RealmWizard: React.FC = () => { - const { fmtUrlWithCluster } = useQueryContext() - // const wallet = useWalletStore((s) => s.current) - const { connection, current: wallet } = useWalletStore((s) => s) - /** - * @var {RealmWizardController} ctl - * The wizard controller instance - */ - const [ctl, setController] = useState() - const [testRealmCheck, setTestRealmCheck] = useState(false) - const [form, setForm] = useState({ - communityMintMaxVoteWeightSource: '1', - }) - const [formErrors, setFormErrors] = useState({}) - const [councilSwitchState, setUseCouncil] = useState(true) - const [isTestProgramId, setIsTestProgramId] = useState(false) - - const [isLoading, setIsLoading] = useState(false) - const [currentStep, setCurrentStep] = useState( - RealmWizardStep.SELECT_MODE - ) - const [realmAddress] = useState('') - const [loaderMessage] = useState(LoaderMessage.DEPLOYING_REALM) - - /** - * Handles and set the form data - * @param data the form data - */ - const handleSetForm = (data: RealmArtifacts) => { - setForm({ - ...form, - ...data, - }) - } - - /** - * Generate realm artifacts - */ - const handleCreateMultisigRealm = async () => { - if (!ctl) return - if (!wallet?.publicKey || !connection.current) return - if (!form.name) - return notify({ - type: 'error', - message: 'You must set a name for the realm!', - }) - - if (!form.teamWallets?.length) - return notify({ - type: 'error', - message: 'Team member wallets are required.', - }) - - if (!form.yesThreshold) { - return notify({ - type: 'error', - message: 'Approval quorum required.', - }) - } - - const programId = testRealmCheck - ? DEFAULT_TEST_GOVERNANCE_PROGRAM_ID - : DEFAULT_GOVERNANCE_PROGRAM_ID - - const governanceProgramId = new PublicKey(programId) - const programVersion = await getGovernanceProgramVersion( - connection.current, - governanceProgramId - ) - - console.log('CREATE REALM Program', { - governanceProgramId: governanceProgramId.toBase58(), - programVersion, - }) - - const results = await createMultisigRealm( - connection.current, - governanceProgramId, - programVersion, - form.name, - form.yesThreshold, - form.teamWallets.map((w) => new PublicKey(w)), - wallet - ) - - if (results) { - router.push(fmtUrlWithCluster(`/dao/${results.realmPk.toBase58()}`)) - return - } - - notify({ - type: 'error', - message: 'Something bad happened during this request.', - }) - } - - /** - * Get the mint max vote weight parsed to `MintMaxVoteWeightSource` - */ - const getMintMaxVoteWeight = () => { - let value = MintMaxVoteWeightSource.FULL_SUPPLY_FRACTION.value - if (form.communityMintMaxVoteWeightSource) { - const fraction = new BigNumber(form.communityMintMaxVoteWeightSource) - .shiftedBy(MintMaxVoteWeightSource.SUPPLY_FRACTION_DECIMALS) - .toString() - value = new BN(fraction) - } - - return new MintMaxVoteWeightSource({ - value, - }) - } - - /** - * Get the array of wallets parsed into public keys or undefined if not eligible - */ - const getTeamWallets = (): PublicKey[] | undefined => - form.teamWallets ? form.teamWallets.map((w) => new PublicKey(w)) : undefined - - const handleCreateBespokeRealm = async () => { - setFormErrors({}) - - const { isValid, validationErrors }: formValidation = await isFormValid( - CreateFormSchema, - form - ) - - if (isValid) { - try { - const governanceProgramId = new PublicKey(form.governanceProgramId!) - const programVersion = await getGovernanceProgramVersion( - connection.current, - governanceProgramId - ) - - console.log('CREATE REALM Program', { - governanceProgramId: governanceProgramId.toBase58(), - programVersion, - }) - - const realmAddress = await registerRealm( - { - connection, - wallet: wallet!, - walletPubkey: wallet!.publicKey!, - }, - governanceProgramId, - programVersion, - form.name!, - form.communityMintId - ? new PublicKey(form.communityMintId) - : undefined, - form.councilMintId ? new PublicKey(form.councilMintId) : undefined, - getMintMaxVoteWeight(), - form.minCommunityTokensToCreateGovernance!, - form.yesThreshold, - form.communityMintId ? form.transferAuthority : true, - form.communityMint ? form.communityMint.account.decimals : undefined, - form.councilMint ? form.councilMint.account.decimals : undefined, - getTeamWallets() - ) - router.push(fmtUrlWithCluster(`/dao/${realmAddress.toBase58()}`)) - } catch (error) { - notify({ - type: 'error', - message: error.message, - }) - } - } else { - console.debug(validationErrors) - setFormErrors(validationErrors) - } - setIsLoading(false) - } - - /** - * Handles and set the selected mode - * @param option the selected mode - */ - const handleModeSelection = (option: RealmWizardMode) => { - try { - const ctl = new RealmWizardController(option) - const nextStep = ctl.getNextStep(currentStep, StepDirection.NEXT) - handleSetForm({ - governanceProgramId: - process.env.DEFAULT_GOVERNANCE_PROGRAM_ID ?? - DEFAULT_GOVERNANCE_PROGRAM_ID, - yesThreshold: 60, - }) - setController(ctl) - setCurrentStep(nextStep) - } catch (error: any) { - notify({ - type: 'error', - message: error.message, - }) - } - } - - const handleStepSelection = (direction: StepDirection) => { - if (ctl) { - try { - const nextStep = ctl.getNextStep(currentStep, direction) - setCurrentStep(nextStep) - } catch (error) { - notify({ - type: 'error', - message: error.message, - }) - } - } - } - - const handleCreateRealm = async () => { - if (!wallet?.publicKey || !connection.current) - return notify({ - type: 'error', - message: 'Wallet not connected', - }) - // Handles the current misuse of the CreateRealmForm - if (ctl) { - try { - setIsLoading(true) - - switch (ctl.getMode()) { - case RealmWizardMode.BASIC: - await handleCreateMultisigRealm() - break - case RealmWizardMode.ADVANCED: - await handleCreateBespokeRealm() - break - default: - throw new Error('Mode not available.') - } - } catch (error) { - const err = error as Error - setIsLoading(false) - notify({ - type: 'error', - message: err.message, - }) - } finally { - setIsLoading(false) - } - } - } - - const handleBackButtonClick = () => { - if (ctl && !ctl.isModeSelect()) { - setCurrentStep(ctl.getNextStep(currentStep, StepDirection.PREV)) - } else { - router.push(fmtUrlWithCluster('/realms')) - } - } - - const isCreateButtonDisabled = () => - ctl && ctl.getMode() === RealmWizardMode.ADVANCED - ? false - : !form.teamWallets?.length || !form.name - - const canGoNext = (step: RealmWizardStep): boolean => { - if (step === RealmWizardStep.BESPOKE_CONFIG) { - const errors: any = {} - !form.name ? (errors.name = 'Name is required') : null - !form.governanceProgramId - ? (errors.governanceProgramId = 'Governance Program ID is required') - : null - - setFormErrors(errors) - - return !Object.values(errors).length - } - - return true - } - - const onClickNext = (): boolean => { - if (ctl) - switch (ctl.getMode()) { - case RealmWizardMode.ADVANCED: - return canGoNext(ctl.getCurrentStep()) - default: - return false - } - return false - } - - /** - * Binds the current step to the matching component - */ - const BoundStepComponent = useMemo(() => { - switch (currentStep) { - case RealmWizardStep.SELECT_MODE: - return - case RealmWizardStep.MULTISIG_CONFIG: - return - case RealmWizardStep.BESPOKE_CONFIG: - return ( - { - setIsTestProgramId(x) - handleSetForm({ - governanceProgramId: x - ? DEFAULT_TEST_GOVERNANCE_PROGRAM_ID - : DEFAULT_GOVERNANCE_PROGRAM_ID, - }) - }} - /> - ) - case RealmWizardStep.BESPOKE_COUNCIL: - return ( - { - setUseCouncil(x) - }} - switchState={councilSwitchState} - /> - ) - case RealmWizardStep.BESPOKE_INFO: - return ( - - ) - case RealmWizardStep.REALM_CREATED: - return - default: - return

Sorry, but this step ran away

- } - }, [currentStep, form, formErrors, councilSwitchState]) - - useEffect(() => { - // Return shouldFireCreate to the base state - if (Object.values(formErrors).length) setFormErrors({}) - }, [form]) - - return ( -
- - {isLoading ? ( -
- - {loaderMessage} -
- ) : ( -
{BoundStepComponent}
- )} - {ctl && !(ctl.isModeSelect() || isLoading) && ( - <> -
- {ctl.getMode() === RealmWizardMode.BASIC && ctl.isLastStep() && ( -
- { - setTestRealmCheck(check) - }} - /> - - - Create a test DAO - - -
- )} - {!ctl.isFirstStep() ? ( - - ) : ( -

 

- )} - - -
- - )} -
- ) -} - -export default RealmWizard diff --git a/components/RealmWizard/components/AddWalletModal.tsx b/components/RealmWizard/components/AddWalletModal.tsx deleted file mode 100644 index 1d49920c1e..0000000000 --- a/components/RealmWizard/components/AddWalletModal.tsx +++ /dev/null @@ -1,101 +0,0 @@ -import React, { useState } from 'react' -import Textarea from '@components/inputs/Textarea' -import Modal from '@components/Modal' -import Button, { SecondaryButton } from '@components/Button' -import { publicKeyValidationTest } from '../validators/createRealmValidator' - -const AddWalletModal: React.FC<{ - onOk: (wallets: string[]) => void - onClose: () => void - isOpen: boolean -}> = ({ isOpen = false, onOk, onClose }) => { - const [walletAddr, setWalletAddr] = useState('') - const [hasErrors, setErrors] = useState() - - const handleAddWallet = () => { - const wallets = walletAddr.replace(/ +/gim, '').split(/,|\n/gim) - const errors: string[] = [] - const parsedWallets: string[] = [] - wallets.forEach((wallet, index) => { - if (wallet.length) { - parsedWallets.push(wallet) - if (!publicKeyValidationTest(wallet)) { - errors.push( - `Entry ${index + 1} (${wallet.substr( - 0, - 8 - )}...) is not a valid public key.` - ) - } - } - }) - if (errors.length) setErrors(errors) - else { - onClose() - setWalletAddr('') - onOk(parsedWallets) - } - } - - const getAddMembersText = () => { - let message = 'Add Member' - const wallets = walletAddr.split(/\n/) - if (wallets.length > 1 && wallets[1].length > 1) message += 's' - return message - } - - return ( - <> - {isOpen && ( - { - setWalletAddr('') - onClose() - }} - > -

Team members wallets

- - - ) -} - -export default CustomBase64 diff --git a/pages/dao/[symbol]/proposal/components/instructions/Deltafi/CreateFarmUserV2.tsx b/pages/dao/[symbol]/proposal/components/instructions/Deltafi/CreateFarmUserV2.tsx new file mode 100644 index 0000000000..57c7281955 --- /dev/null +++ b/pages/dao/[symbol]/proposal/components/instructions/Deltafi/CreateFarmUserV2.tsx @@ -0,0 +1,105 @@ +import * as yup from 'yup'; +import useInstructionFormBuilder from '@hooks/useInstructionFormBuilder'; +import deltafiConfiguration, { + DeltafiDexV2, + PoolInfo, +} from '@tools/sdk/deltafi/configuration'; +import { GovernedMultiTypeAccount } from '@utils/tokens'; +import { DeltafiCreateFarmUserForm } from '@utils/uiTypes/proposalCreationTypes'; +import SelectDeltafiPool, { PoolName } from '@components/SelectDeltafiPool'; +import createFarmUserV2 from '@tools/sdk/deltafi/instructions/createFarmUserV2'; +import { useState } from 'react'; +import useDeltafiProgram from '@hooks/useDeltafiProgram'; + +const schema = yup.object().shape({ + governedAccount: yup + .object() + .nullable() + .required('Governed account is required'), + poolName: yup.string().required('Pool name is required'), +}); + +const DeltafiCreateFarmUserV2 = ({ + index, + governedAccount, +}: { + index: number; + governedAccount?: GovernedMultiTypeAccount; +}) => { + const { poolInfoList } = DeltafiDexV2.configuration; + + const deltafiProgram = useDeltafiProgram(); + + const [poolInfo, setPoolInfo] = useState(null); + + const { + form, + handleSetForm, + } = useInstructionFormBuilder({ + index, + initialFormValues: { + governedAccount, + }, + schema, + buildInstruction: async function ({ + wallet, + cluster, + governedAccountPubkey, + form, + }) { + if (cluster !== 'mainnet') { + throw new Error('Other cluster than mainnet are not supported yet.'); + } + + if (!deltafiProgram) { + throw new Error('Deltafi program not loaded yet'); + } + + const poolInfo = deltafiConfiguration.getPoolInfoByPoolName( + form.poolName!, + ); + + if (!poolInfo) { + throw new Error(`Cannot find pool info with name ${form.poolName!}`); + } + + if (!poolInfo.farmInfo) { + throw new Error('Selected pool does not have a farm'); + } + + return createFarmUserV2({ + deltafiProgram, + authority: governedAccountPubkey, + poolInfo, + farmInfo: poolInfo.farmInfo, + payer: wallet.publicKey!, + }); + }, + }); + + return ( + <> + { + const poolInfo = poolInfoList.find(({ name }) => name === poolName); + + setPoolInfo(poolInfo ?? null); + + handleSetForm({ + value: poolName, + propertyName: 'poolName', + }); + }} + /> + + {poolInfo && !poolInfo.farmInfo ? ( +
This pool does not contains a farm
+ ) : null} + + ); +}; + +export default DeltafiCreateFarmUserV2; diff --git a/pages/dao/[symbol]/proposal/components/instructions/Deltafi/CreateLiquidityProvider.tsx b/pages/dao/[symbol]/proposal/components/instructions/Deltafi/CreateLiquidityProvider.tsx new file mode 100644 index 0000000000..f657d6ef42 --- /dev/null +++ b/pages/dao/[symbol]/proposal/components/instructions/Deltafi/CreateLiquidityProvider.tsx @@ -0,0 +1,86 @@ +import * as yup from 'yup'; +import useInstructionFormBuilder from '@hooks/useInstructionFormBuilder'; +import deltafiConfiguration, { + DeltafiDexV2, +} from '@tools/sdk/deltafi/configuration'; +import { GovernedMultiTypeAccount } from '@utils/tokens'; +import { DeltafiCreateLiquidityProviderForm } from '@utils/uiTypes/proposalCreationTypes'; +import SelectDeltafiPool, { PoolName } from '@components/SelectDeltafiPool'; +import createLiquidityProviderV2 from '@tools/sdk/deltafi/instructions/createLiquidityProviderV2'; +import useDeltafiProgram from '@hooks/useDeltafiProgram'; + +const schema = yup.object().shape({ + governedAccount: yup + .object() + .nullable() + .required('Governed account is required'), + poolName: yup.string().required('Pool name is required'), +}); + +const DeltafiCreateLiquidityProvider = ({ + index, + governedAccount, +}: { + index: number; + governedAccount?: GovernedMultiTypeAccount; +}) => { + const { poolInfoList } = DeltafiDexV2.configuration; + + const deltafiProgram = useDeltafiProgram(); + + const { + form, + handleSetForm, + } = useInstructionFormBuilder({ + index, + initialFormValues: { + governedAccount, + }, + schema, + buildInstruction: async function ({ + wallet, + cluster, + governedAccountPubkey, + form, + }) { + if (cluster !== 'mainnet') { + throw new Error('Other cluster than mainnet are not supported yet.'); + } + + if (!deltafiProgram) { + throw new Error('Deltafi program not loaded yet'); + } + + const poolInfo = deltafiConfiguration.getPoolInfoByPoolName( + form.poolName!, + ); + + if (!poolInfo) { + throw new Error('Pool info is required'); + } + + return createLiquidityProviderV2({ + deltafiProgram, + authority: governedAccountPubkey, + poolInfo, + payer: wallet.publicKey!, + }); + }, + }); + + return ( + + handleSetForm({ + value: poolName, + propertyName: 'poolName', + }) + } + /> + ); +}; + +export default DeltafiCreateLiquidityProvider; diff --git a/pages/dao/[symbol]/proposal/components/instructions/Deltafi/Deposit.tsx b/pages/dao/[symbol]/proposal/components/instructions/Deltafi/Deposit.tsx new file mode 100644 index 0000000000..3959e0835e --- /dev/null +++ b/pages/dao/[symbol]/proposal/components/instructions/Deltafi/Deposit.tsx @@ -0,0 +1,172 @@ +import * as yup from 'yup'; +import useInstructionFormBuilder from '@hooks/useInstructionFormBuilder'; +import deltafiConfiguration, { + DeltafiDexV2, +} from '@tools/sdk/deltafi/configuration'; +import { GovernedMultiTypeAccount } from '@utils/tokens'; +import { DeltafiPoolDepositForm } from '@utils/uiTypes/proposalCreationTypes'; +import deposit from '@tools/sdk/deltafi/instructions/deposit'; +import Input from '@components/inputs/Input'; +import SelectDeltafiPool, { PoolName } from '@components/SelectDeltafiPool'; +import { uiAmountToNativeBN } from '@tools/sdk/units'; +import useDeltafiProgram from '@hooks/useDeltafiProgram'; + +const schema = yup.object().shape({ + governedAccount: yup + .object() + .nullable() + .required('Governed account is required'), + poolName: yup.string().required('Pool name is required'), + uiBaseAmount: yup + .number() + .typeError('Base Amount has to be a number') + .required('Base Amount is required'), + uiQuoteAmount: yup + .number() + .typeError('Quote Amount has to be a number') + .required('Quote Amount is required'), + uiMinBaseShare: yup + .number() + .typeError('Min Base Share has to be a number') + .required('Min Base Share is required'), + uiMinQuoteShare: yup + .number() + .typeError('Min Quote Share has to be a number') + .required('Min Quote Share is required'), +}); + +const DeltafiPoolDeposit = ({ + index, + governedAccount, +}: { + index: number; + governedAccount?: GovernedMultiTypeAccount; +}) => { + const { poolInfoList } = DeltafiDexV2.configuration; + + const deltafiProgram = useDeltafiProgram(); + + const { + form, + handleSetForm, + formErrors, + } = useInstructionFormBuilder({ + index, + initialFormValues: { + governedAccount, + }, + schema, + buildInstruction: async function ({ + cluster, + governedAccountPubkey, + form, + }) { + if (cluster !== 'mainnet') { + throw new Error('Other cluster than mainnet are not supported yet.'); + } + + if (!deltafiProgram) { + throw new Error('Deltafi program not loaded yet'); + } + + const poolInfo = deltafiConfiguration.getPoolInfoByPoolName( + form.poolName!, + ); + + if (!poolInfo) { + throw new Error('Pool info is required'); + } + + const baseDecimals = deltafiConfiguration.getBaseOrQuoteMintDecimals( + poolInfo.mintBase, + ); + const quoteDecimals = deltafiConfiguration.getBaseOrQuoteMintDecimals( + poolInfo.mintQuote, + ); + + return deposit({ + deltafiProgram, + authority: governedAccountPubkey, + poolInfo, + baseAmount: uiAmountToNativeBN(form.uiBaseAmount!, baseDecimals), + quoteAmount: uiAmountToNativeBN(form.uiQuoteAmount!, quoteDecimals), + minBaseShare: uiAmountToNativeBN(form.uiMinBaseShare!, baseDecimals), + minQuoteShare: uiAmountToNativeBN(form.uiMinQuoteShare!, quoteDecimals), + }); + }, + }); + + return ( + <> + + handleSetForm({ + value: poolName, + propertyName: 'poolName', + }) + } + /> + + + handleSetForm({ + value: evt.target.value, + propertyName: 'uiBaseAmount', + }) + } + error={formErrors['uiBaseAmount']} + /> + + { + handleSetForm({ + value: evt.target.value, + propertyName: 'uiQuoteAmount', + }); + }} + error={formErrors['uiQuoteAmount']} + /> + + { + handleSetForm({ + value: evt.target.value, + propertyName: 'uiMinBaseShare', + }); + }} + error={formErrors['uiMinBaseShare']} + /> + + { + handleSetForm({ + value: evt.target.value, + propertyName: 'uiMinQuoteShare', + }); + }} + error={formErrors['uiMinQuoteShare']} + /> + + ); +}; + +export default DeltafiPoolDeposit; diff --git a/pages/dao/[symbol]/proposal/components/instructions/Deltafi/DepositToFarm.tsx b/pages/dao/[symbol]/proposal/components/instructions/Deltafi/DepositToFarm.tsx new file mode 100644 index 0000000000..6db211aa2e --- /dev/null +++ b/pages/dao/[symbol]/proposal/components/instructions/Deltafi/DepositToFarm.tsx @@ -0,0 +1,210 @@ +import * as yup from 'yup'; +import useInstructionFormBuilder from '@hooks/useInstructionFormBuilder'; +import deltafiConfiguration, { + DeltafiDexV2, + PoolInfo, + UserStakeInfo, +} from '@tools/sdk/deltafi/configuration'; +import { GovernedMultiTypeAccount } from '@utils/tokens'; +import { DeltafiFarmDepositForm } from '@utils/uiTypes/proposalCreationTypes'; +import Input from '@components/inputs/Input'; +import SelectDeltafiPool, { PoolName } from '@components/SelectDeltafiPool'; +import { uiAmountToNativeBN } from '@tools/sdk/units'; +import { useEffect, useState } from 'react'; +import depositToFarm from '@tools/sdk/deltafi/instructions/depositToFarm'; +import useDeltafiProgram from '@hooks/useDeltafiProgram'; + +const schema = yup.object().shape({ + governedAccount: yup + .object() + .nullable() + .required('Governed account is required'), + poolName: yup.string().required('Pool name is required'), + uiBaseAmount: yup + .number() + .typeError('Base Amount has to be a number') + .required('Base Amount is required'), + uiQuoteAmount: yup + .number() + .typeError('Quote Amount has to be a number') + .required('Quote Amount is required'), +}); + +const DeltafiFarmDeposit = ({ + index, + governedAccount, +}: { + index: number; + governedAccount?: GovernedMultiTypeAccount; +}) => { + const { poolInfoList } = DeltafiDexV2.configuration; + + const deltafiProgram = useDeltafiProgram(); + + const [poolInfo, setPoolInfo] = useState(null); + + const [userStakeInfo, setUserStakeInfo] = useState( + null, + ); + + const { + form, + handleSetForm, + formErrors, + connection, + governedAccountPubkey, + wallet, + } = useInstructionFormBuilder({ + index, + initialFormValues: { + governedAccount, + }, + schema, + buildInstruction: async function ({ + cluster, + governedAccountPubkey, + form, + }) { + if (cluster !== 'mainnet') { + throw new Error('Other cluster than mainnet are not supported yet.'); + } + + if (!deltafiProgram) { + throw new Error('Deltafi program not loaded yet'); + } + + if (!poolInfo) { + throw new Error('Pool info is required'); + } + + if (!poolInfo.farmInfo) { + throw new Error('Farm info is required'); + } + + const baseDecimals = deltafiConfiguration.getBaseOrQuoteMintDecimals( + poolInfo.mintBase, + ); + const quoteDecimals = deltafiConfiguration.getBaseOrQuoteMintDecimals( + poolInfo.mintQuote, + ); + + return depositToFarm({ + deltafiProgram, + authority: governedAccountPubkey, + poolInfo, + farmInfo: poolInfo.farmInfo, + baseAmount: uiAmountToNativeBN(form.uiBaseAmount!, baseDecimals), + quoteAmount: uiAmountToNativeBN(form.uiQuoteAmount!, quoteDecimals), + }); + }, + }); + + useEffect(() => { + (async () => { + if ( + !poolInfo || + !governedAccountPubkey || + !wallet || + !deltafiProgram || + !connection + ) { + setUserStakeInfo(null); + return; + } + + setUserStakeInfo( + await deltafiConfiguration.getUserStakeInfo({ + poolInfo, + authority: governedAccountPubkey, + deltafiProgram, + }), + ); + })(); + }, [poolInfo, connection, governedAccountPubkey, deltafiProgram]); + + return ( + <> + { + const poolInfo = poolInfoList.find(({ name }) => name === poolName); + + setPoolInfo(poolInfo ?? null); + + handleSetForm({ + value: poolName, + propertyName: 'poolName', + }); + }} + /> + + {poolInfo && !poolInfo.farmInfo ? ( +
This pool does not contains a farm
+ ) : null} + + {poolInfo && poolInfo.farmInfo ? ( + <> + + handleSetForm({ + value: evt.target.value, + propertyName: 'uiBaseAmount', + }) + } + error={formErrors['uiBaseAmount']} + /> + + {userStakeInfo ? ( +
+ handleSetForm({ + value: userStakeInfo.availableToDepositToFarm.uiBase, + propertyName: 'uiBaseAmount', + }) + } + > + Max {userStakeInfo.availableToDepositToFarm.uiBase} +
+ ) : null} + + { + handleSetForm({ + value: evt.target.value, + propertyName: 'uiQuoteAmount', + }); + }} + error={formErrors['uiQuoteAmount']} + /> + + {userStakeInfo ? ( +
+ handleSetForm({ + value: userStakeInfo.availableToDepositToFarm.uiQuote, + propertyName: 'uiQuoteAmount', + }) + } + > + Max {userStakeInfo.availableToDepositToFarm.uiQuote} +
+ ) : null} + + ) : null} + + ); +}; + +export default DeltafiFarmDeposit; diff --git a/pages/dao/[symbol]/proposal/components/instructions/Deltafi/Withdraw.tsx b/pages/dao/[symbol]/proposal/components/instructions/Deltafi/Withdraw.tsx new file mode 100644 index 0000000000..61e9486cc2 --- /dev/null +++ b/pages/dao/[symbol]/proposal/components/instructions/Deltafi/Withdraw.tsx @@ -0,0 +1,243 @@ +import * as yup from 'yup'; +import useInstructionFormBuilder from '@hooks/useInstructionFormBuilder'; +import deltafiConfiguration, { + DeltafiDexV2, + PoolInfo, + UserStakeInfo, +} from '@tools/sdk/deltafi/configuration'; +import { GovernedMultiTypeAccount } from '@utils/tokens'; +import { DeltafiPoolWithdrawForm } from '@utils/uiTypes/proposalCreationTypes'; +import Input from '@components/inputs/Input'; +import SelectDeltafiPool, { PoolName } from '@components/SelectDeltafiPool'; +import { uiAmountToNativeBN } from '@tools/sdk/units'; +import withdraw from '@tools/sdk/deltafi/instructions/withdraw'; +import { useEffect, useState } from 'react'; +import useDeltafiProgram from '@hooks/useDeltafiProgram'; + +const schema = yup.object().shape({ + governedAccount: yup + .object() + .nullable() + .required('Governed account is required'), + poolName: yup.string().required('Pool name is required'), + uiBaseShare: yup + .number() + .typeError('Base Share has to be a number') + .required('Base Share is required'), + uiQuoteShare: yup + .number() + .typeError('Quote Share has to be a number') + .required('Quote Share is required'), + uiMinBaseAmount: yup + .number() + .typeError('Min Base Amount has to be a number') + .required('Min Base Amount is required'), + uiMinQuoteAmount: yup + .number() + .typeError('Min Quote Amount has to be a number') + .required('Min Quote Amount is required'), +}); + +const DeltafiPoolWithdraw = ({ + index, + governedAccount, +}: { + index: number; + governedAccount?: GovernedMultiTypeAccount; +}) => { + const deltafiProgram = useDeltafiProgram(); + + const [poolInfo, setPoolInfo] = useState(null); + + const [userStakeInfo, setUserStakeInfo] = useState( + null, + ); + + const { poolInfoList } = DeltafiDexV2.configuration; + + const { + form, + handleSetForm, + formErrors, + governedAccountPubkey, + connection, + wallet, + } = useInstructionFormBuilder({ + index, + initialFormValues: { + governedAccount, + }, + schema, + buildInstruction: async function ({ + cluster, + governedAccountPubkey, + form, + }) { + if (cluster !== 'mainnet') { + throw new Error('Other cluster than mainnet are not supported yet.'); + } + + if (!deltafiProgram) { + throw new Error('Deltafi program not loaded yet'); + } + + const poolInfo = deltafiConfiguration.getPoolInfoByPoolName( + form.poolName!, + ); + + if (!poolInfo) { + throw new Error('Pool info is required'); + } + + const baseDecimals = deltafiConfiguration.getBaseOrQuoteMintDecimals( + poolInfo.mintBase, + ); + const quoteDecimals = deltafiConfiguration.getBaseOrQuoteMintDecimals( + poolInfo.mintQuote, + ); + + return withdraw({ + deltafiProgram, + authority: governedAccountPubkey, + poolInfo, + + baseShare: uiAmountToNativeBN(form.uiBaseShare!, baseDecimals), + quoteShare: uiAmountToNativeBN(form.uiQuoteShare!, quoteDecimals), + minBaseAmount: uiAmountToNativeBN(form.uiMinBaseAmount!, baseDecimals), + minQuoteAmount: uiAmountToNativeBN( + form.uiMinQuoteAmount!, + quoteDecimals, + ), + }); + }, + }); + + useEffect(() => { + (async () => { + if ( + !poolInfo || + !governedAccountPubkey || + !wallet || + !deltafiProgram || + !connection + ) { + setUserStakeInfo(null); + return; + } + + setUserStakeInfo( + await deltafiConfiguration.getUserStakeInfo({ + poolInfo, + authority: governedAccountPubkey, + deltafiProgram, + }), + ); + })(); + }, [poolInfo, connection, governedAccountPubkey, deltafiProgram]); + + return ( + <> + { + const poolInfo = poolInfoList.find(({ name }) => name === poolName); + + setPoolInfo(poolInfo ?? null); + + handleSetForm({ + value: poolName, + propertyName: 'poolName', + }); + }} + /> + + { + handleSetForm({ + value: evt.target.value, + propertyName: 'uiBaseShare', + }); + }} + error={formErrors['uiBaseShare']} + /> + + {userStakeInfo ? ( +
{ + handleSetForm({ + value: userStakeInfo.inPool.uiBase, + propertyName: 'uiBaseShare', + }); + }} + > + max: {userStakeInfo.inPool.uiBase} +
+ ) : null} + + { + handleSetForm({ + value: evt.target.value, + propertyName: 'uiQuoteShare', + }); + }} + error={formErrors['uiQuoteShare']} + /> + + {userStakeInfo ? ( +
{ + handleSetForm({ + value: userStakeInfo.inPool.uiQuote, + propertyName: 'uiQuoteShare', + }); + }} + > + max: {userStakeInfo.inPool.uiQuote} +
+ ) : null} + + + handleSetForm({ + value: evt.target.value, + propertyName: 'uiMinBaseAmount', + }) + } + error={formErrors['uiMinBaseAmount']} + /> + + { + handleSetForm({ + value: evt.target.value, + propertyName: 'uiMinQuoteAmount', + }); + }} + error={formErrors['uiMinQuoteAmount']} + /> + + ); +}; + +export default DeltafiPoolWithdraw; diff --git a/pages/dao/[symbol]/proposal/components/instructions/Deltafi/WithdrawFromFarm.tsx b/pages/dao/[symbol]/proposal/components/instructions/Deltafi/WithdrawFromFarm.tsx new file mode 100644 index 0000000000..f074fe4558 --- /dev/null +++ b/pages/dao/[symbol]/proposal/components/instructions/Deltafi/WithdrawFromFarm.tsx @@ -0,0 +1,206 @@ +import * as yup from 'yup'; +import useInstructionFormBuilder from '@hooks/useInstructionFormBuilder'; +import deltafiConfiguration, { + DeltafiDexV2, + PoolInfo, + UserStakeInfo, +} from '@tools/sdk/deltafi/configuration'; +import { GovernedMultiTypeAccount } from '@utils/tokens'; +import { DeltafiFarmWithdrawForm } from '@utils/uiTypes/proposalCreationTypes'; +import Input from '@components/inputs/Input'; +import SelectDeltafiPool, { PoolName } from '@components/SelectDeltafiPool'; +import { uiAmountToNativeBN } from '@tools/sdk/units'; +import { useState, useEffect } from 'react'; +import withdrawFromFarm from '@tools/sdk/deltafi/instructions/withdrawFromFarm'; +import useDeltafiProgram from '@hooks/useDeltafiProgram'; + +const schema = yup.object().shape({ + governedAccount: yup + .object() + .nullable() + .required('Governed account is required'), + poolName: yup.string().required('Pool name is required'), + uiBaseAmount: yup + .number() + .typeError('Base Amount has to be a number') + .required('Base Amount is required'), + uiQuoteAmount: yup + .number() + .typeError('Quote Amount has to be a number') + .required('Quote Amount is required'), +}); + +const DeltafiFarmWithdraw = ({ + index, + governedAccount, +}: { + index: number; + governedAccount?: GovernedMultiTypeAccount; +}) => { + const { poolInfoList } = DeltafiDexV2.configuration; + + const deltafiProgram = useDeltafiProgram(); + + const [poolInfo, setPoolInfo] = useState(null); + + const [userStakeInfo, setUserStakeInfo] = useState( + null, + ); + + const { + form, + handleSetForm, + formErrors, + governedAccountPubkey, + connection, + wallet, + } = useInstructionFormBuilder({ + index, + initialFormValues: { + governedAccount, + }, + schema, + buildInstruction: async function ({ + cluster, + governedAccountPubkey, + form, + }) { + if (cluster !== 'mainnet') { + throw new Error('Other cluster than mainnet are not supported yet.'); + } + + if (!deltafiProgram) { + throw new Error('Deltafi program not loaded yet'); + } + + if (!poolInfo) { + throw new Error('Pool info is required'); + } + + if (!poolInfo.farmInfo) { + throw new Error('Farm info is required'); + } + + const baseDecimals = deltafiConfiguration.getBaseOrQuoteMintDecimals( + poolInfo.mintBase, + ); + const quoteDecimals = deltafiConfiguration.getBaseOrQuoteMintDecimals( + poolInfo.mintQuote, + ); + + return withdrawFromFarm({ + deltafiProgram, + authority: governedAccountPubkey, + poolInfo, + farmInfo: poolInfo.farmInfo, + baseAmount: uiAmountToNativeBN(form.uiBaseAmount!, baseDecimals), + quoteAmount: uiAmountToNativeBN(form.uiQuoteAmount!, quoteDecimals), + }); + }, + }); + + useEffect(() => { + (async () => { + if ( + !poolInfo || + !governedAccountPubkey || + !wallet || + !deltafiProgram || + !connection + ) { + setUserStakeInfo(null); + return; + } + + setUserStakeInfo( + await deltafiConfiguration.getUserStakeInfo({ + poolInfo, + authority: governedAccountPubkey, + deltafiProgram, + }), + ); + })(); + }, [poolInfo, connection, governedAccountPubkey, deltafiProgram]); + + return ( + <> + { + const poolInfo = deltafiConfiguration.getPoolInfoByPoolName(poolName); + + setPoolInfo(poolInfo ?? null); + + handleSetForm({ + value: poolName, + propertyName: 'poolName', + }); + }} + /> + + {poolInfo && !poolInfo.farmInfo ? ( + This pool does not contains a farm + ) : null} + + + handleSetForm({ + value: evt.target.value, + propertyName: 'uiBaseAmount', + }) + } + error={formErrors['uiBaseAmount']} + /> + + {userStakeInfo ? ( +
+ handleSetForm({ + value: userStakeInfo.inFarm.uiBase, + propertyName: 'uiBaseAmount', + }) + } + > + Max {userStakeInfo.inFarm.uiBase} +
+ ) : null} + + { + handleSetForm({ + value: evt.target.value, + propertyName: 'uiQuoteAmount', + }); + }} + error={formErrors['uiQuoteAmount']} + /> + + {userStakeInfo ? ( +
+ handleSetForm({ + value: userStakeInfo.inFarm.uiQuote, + propertyName: 'uiQuoteAmount', + }) + } + > + Max {userStakeInfo.inFarm.uiQuote} +
+ ) : null} + + ); +}; + +export default DeltafiFarmWithdraw; diff --git a/pages/dao/[symbol]/proposal/components/instructions/Empty.tsx b/pages/dao/[symbol]/proposal/components/instructions/Empty.tsx deleted file mode 100644 index e5929be873..0000000000 --- a/pages/dao/[symbol]/proposal/components/instructions/Empty.tsx +++ /dev/null @@ -1,67 +0,0 @@ -import React, { useContext, useEffect, useState } from 'react' -import * as yup from 'yup' -import { Governance, ProgramAccount } from '@solana/spl-governance' -import { validateInstruction } from '@utils/instructionTools' -import { - EmptyInstructionForm, - UiInstruction, -} from '@utils/uiTypes/proposalCreationTypes' -import { NewProposalContext } from '../../new' -import GovernedAccountSelect from '../GovernedAccountSelect' -import useGovernedMultiTypeAccounts from '@hooks/useGovernedMultiTypeAccounts' -const Empty = ({ - index, - governance, -}: { - index: number - governance: ProgramAccount | null -}) => { - const [form, setForm] = useState({ - governedAccount: undefined, - }) - const { governedMultiTypeAccounts } = useGovernedMultiTypeAccounts() - const shouldBeGoverned = index !== 0 && governance - const [formErrors, setFormErrors] = useState({}) - const { handleSetInstructions } = useContext(NewProposalContext) - const handleSetForm = ({ propertyName, value }) => { - setFormErrors({}) - setForm({ ...form, [propertyName]: value }) - } - async function getInstruction(): Promise { - const isValid = await validateInstruction({ schema, form, setFormErrors }) - const obj: UiInstruction = { - serializedInstruction: '', - isValid, - governance: form.governedAccount?.governance, - } - return obj - } - - useEffect(() => { - handleSetInstructions( - { governedAccount: form.governedAccount?.governance, getInstruction }, - index - ) - }, [form]) - const schema = yup.object().shape({ - governedAccount: yup - .object() - .nullable() - .required('Governed account is required'), - }) - return ( - { - handleSetForm({ value, propertyName: 'governedAccount' }) - }} - value={form.governedAccount} - error={formErrors['governedAccount']} - shouldBeGoverned={shouldBeGoverned} - governance={governance} - /> - ) -} - -export default Empty diff --git a/pages/dao/[symbol]/proposal/components/instructions/Friktion/Claim.tsx b/pages/dao/[symbol]/proposal/components/instructions/Friktion/Claim.tsx new file mode 100644 index 0000000000..ebdb8719dc --- /dev/null +++ b/pages/dao/[symbol]/proposal/components/instructions/Friktion/Claim.tsx @@ -0,0 +1,160 @@ +import * as yup from 'yup'; +import { PublicKey } from '@solana/web3.js'; +import Select from '@components/inputs/Select'; +import SelectOptionDetailed, { Flag } from '@components/SelectOptionDetailed'; +import useFriktionVolt from '@hooks/usefriktionVolts'; +import useGovernanceUnderlyingTokenAccounts from '@hooks/useGovernanceUnderlyingTokenAccounts'; +import useInstructionFormBuilder from '@hooks/useInstructionFormBuilder'; +import { VoltData } from '@tools/sdk/friktion/friktion'; +import claimPendingWithdrawal from '@tools/sdk/friktion/instructions/claimPendingWithdrawal'; +import { GovernedMultiTypeAccount } from '@utils/tokens'; +import { FriktionClaimWithdrawalForm } from '@utils/uiTypes/proposalCreationTypes'; +import TokenAccountSelect from '../../TokenAccountSelect'; + +const schema = yup.object().shape({ + governedAccount: yup.object().required('Governance is required'), + receiverAccount: yup.string().typeError('Source account is required'), + volt: yup.string().required('Volt is required'), +}); + +const Claim = ({ + index, + governedAccount, +}: { + index: number; + governedAccount?: GovernedMultiTypeAccount; +}) => { + const { + connection, + wallet, + form, + formErrors, + handleSetForm, + governedAccountPubkey, + } = useInstructionFormBuilder({ + index, + initialFormValues: { + governedAccount, + }, + schema, + shouldSplitIntoSeparateTxs: true, + buildInstruction: async function ({ + form, + connection, + wallet, + governedAccountPubkey, + }) { + if (!friktionVolts || !friktionVolts[form.volt!]) { + throw new Error('Could not load Friktion Volt'); + } + + const volt = friktionVolts[form.volt!]; + return claimPendingWithdrawal({ + connection, + wallet, + voltVaultId: volt.voltVaultId, + governancePubkey: governedAccountPubkey, + }); + }, + }); + + const { friktionVolts } = useFriktionVolt({ + connection: connection.current, + wallet, + governedAccountPubkey, + }); + const { ownedTokenAccountsInfo } = useGovernanceUnderlyingTokenAccounts( + governedAccountPubkey ?? undefined, + ); + + const getVoltDetail = (volt: VoltData) => [ + { + label: 'Underlying Mint', + text: volt.underlyingTokenSymbol, + }, + { + label: 'Volt APY', + text: volt.apy.toString() + '%', + }, + { + label: 'Deposited', + text: volt.deposited, + }, + { + label: 'Pending Withdrawal', + text: volt.pendingWithdrawal, + ...(Number(volt.pendingWithdrawal) > 0 && { flag: Flag.Warning }), + }, + ]; + + const getDiffValue = (amount: number) => + amount > 0 + ? { + label: 'Claimable', + text: `Claimable: ${amount}`, + flag: Flag.OK, + } + : { label: 'Claimable', text: 'No Claimable Token', flag: Flag.Danger }; + + return ( + <> + {ownedTokenAccountsInfo && friktionVolts && ( + <> + + {form.volt && friktionVolts[form.volt] && ( + <> + + handleSetForm({ value, propertyName: 'receiverAccount' }) + } + error={formErrors['receiverAccount']} + ownedTokenAccountsInfo={ownedTokenAccountsInfo} + /> + + )} + + )} + + ); +}; + +export default Claim; diff --git a/pages/dao/[symbol]/proposal/components/instructions/Friktion/Deposit.tsx b/pages/dao/[symbol]/proposal/components/instructions/Friktion/Deposit.tsx new file mode 100644 index 0000000000..482ca393ac --- /dev/null +++ b/pages/dao/[symbol]/proposal/components/instructions/Friktion/Deposit.tsx @@ -0,0 +1,178 @@ +import * as yup from 'yup'; +import { PublicKey } from '@solana/web3.js'; +import Input from '@components/inputs/Input'; +import Select from '@components/inputs/Select'; +import SelectOptionDetailed, { Flag } from '@components/SelectOptionDetailed'; +import useFriktionVolt from '@hooks/usefriktionVolts'; +import useGovernanceUnderlyingTokenAccounts from '@hooks/useGovernanceUnderlyingTokenAccounts'; +import useInstructionFormBuilder from '@hooks/useInstructionFormBuilder'; +import { VoltData } from '@tools/sdk/friktion/friktion'; +import depositToVolt from '@tools/sdk/friktion/instructions/depositToVault'; +import { GovernedMultiTypeAccount } from '@utils/tokens'; +import { FriktionDepositForm } from '@utils/uiTypes/proposalCreationTypes'; +import TokenAccountSelect from '../../TokenAccountSelect'; + +const schema = yup.object().shape({ + governedAccount: yup.object().required('Governance is required'), + sourceAccount: yup.string().required('Source account is required'), + volt: yup.string().required('Volt is required'), + uiAmount: yup + .number() + .typeError('Amount has to be a number') + .required('Amount is required'), +}); + +const FriktionDeposit = ({ + index, + governedAccount, +}: { + index: number; + governedAccount?: GovernedMultiTypeAccount; +}) => { + const { + connection, + wallet, + form, + formErrors, + handleSetForm, + governedAccountPubkey, + } = useInstructionFormBuilder({ + index, + initialFormValues: { + governedAccount, + uiAmount: 0, + }, + schema, + shouldSplitIntoSeparateTxs: true, + buildInstruction: async function ({ + form, + governedAccountPubkey, + connection, + wallet, + }) { + if (!friktionVolts || !friktionVolts[form.volt!]) { + throw new Error('Could not load Friktion Volt'); + } + + const volt = friktionVolts[form.volt!]; + return depositToVolt({ + connection, + wallet, + voltVaultId: volt.voltVaultId, + governancePubkey: governedAccountPubkey, + sourceTokenAccount: new PublicKey(form.sourceAccount!), + amount: form.uiAmount!, + decimals: volt.shareTokenDecimals, + }); + }, + }); + + const { friktionVolts } = useFriktionVolt({ + connection: connection.current, + wallet, + governedAccountPubkey, + }); + const { ownedTokenAccountsInfo } = useGovernanceUnderlyingTokenAccounts( + governedAccountPubkey ?? undefined, + ); + + const getVoltDetail = (volt: VoltData) => [ + { + label: 'Underlying Mint', + text: volt.underlyingTokenSymbol, + }, + { + label: 'Volt APY', + text: volt.apy.toString() + '%', + }, + { + label: 'Deposited', + text: volt.deposited, + }, + ]; + + const getDiffValue = (amount: number) => + amount > 0 + ? { + label: 'Pending Deposit', + text: `Pending Deposit: ${amount}`, + flag: Flag.Warning, + } + : { label: 'Pending Deposit', text: 'No Pending Deposit', flag: Flag.OK }; + + return ( + <> + {ownedTokenAccountsInfo && friktionVolts && ( + <> + + {form.volt && friktionVolts[form.volt] && ( + <> + + handleSetForm({ value, propertyName: 'sourceAccount' }) + } + error={formErrors['sourceAccount']} + ownedTokenAccountsInfo={ownedTokenAccountsInfo} + /> + + { + handleSetForm({ + value: evt.target.value, + propertyName: 'uiAmount', + }); + }} + error={formErrors['uiAmount']} + /> + + )} + + )} + + ); +}; + +export default FriktionDeposit; diff --git a/pages/dao/[symbol]/proposal/components/instructions/Friktion/FriktionDeposit.tsx b/pages/dao/[symbol]/proposal/components/instructions/Friktion/FriktionDeposit.tsx deleted file mode 100644 index 0759162e93..0000000000 --- a/pages/dao/[symbol]/proposal/components/instructions/Friktion/FriktionDeposit.tsx +++ /dev/null @@ -1,186 +0,0 @@ -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 { - FriktionDepositForm, - UiInstruction, -} from '@utils/uiTypes/proposalCreationTypes' -import { NewProposalContext } from '../../../new' -import { getFriktionDepositSchema } 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 { getFriktionDepositInstruction } from '@utils/instructionTools' -import Select from '@components/inputs/Select' -import { FriktionSnapshot, VoltSnapshot } from '@friktion-labs/friktion-sdk' - -const FriktionDeposit = ({ - 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: '', - 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 getFriktionDepositInstruction({ - 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 = getFriktionDepositSchema({ form }) - - return ( - <> - { - handleSetForm({ value, propertyName: 'governedTokenAccount' }) - }} - value={form.governedTokenAccount} - error={formErrors['governedTokenAccount']} - shouldBeGoverned={shouldBeGoverned} - governance={governance} - > - - - - ) -} - -export default FriktionDeposit diff --git a/pages/dao/[symbol]/proposal/components/instructions/Friktion/Withdraw.tsx b/pages/dao/[symbol]/proposal/components/instructions/Friktion/Withdraw.tsx new file mode 100644 index 0000000000..b20ec3239b --- /dev/null +++ b/pages/dao/[symbol]/proposal/components/instructions/Friktion/Withdraw.tsx @@ -0,0 +1,201 @@ +import * as yup from 'yup'; +import { PublicKey } from '@solana/web3.js'; +import Input from '@components/inputs/Input'; +import Select from '@components/inputs/Select'; +import SelectOptionDetailed, { Flag } from '@components/SelectOptionDetailed'; +import useFriktionVolt from '@hooks/usefriktionVolts'; +import useGovernanceUnderlyingTokenAccounts from '@hooks/useGovernanceUnderlyingTokenAccounts'; +import useInstructionFormBuilder from '@hooks/useInstructionFormBuilder'; +import { VoltData } from '@tools/sdk/friktion/friktion'; +import withdrawFromVault from '@tools/sdk/friktion/instructions/withdrawFromVault'; +import { uiAmountToNativeBN } from '@tools/sdk/units'; +import { GovernedMultiTypeAccount } from '@utils/tokens'; +import { FriktionWithdrawForm } from '@utils/uiTypes/proposalCreationTypes'; +import TokenAccountSelect from '../../TokenAccountSelect'; + +const schema = yup.object().shape({ + governedAccount: yup.object().required('Governance is required'), + receiverAccount: yup.string().required('Source account is required'), + volt: yup.string().required('Volt is required'), + uiAmount: yup.number().required('Amount is required'), +}); + +const Withdraw = ({ + index, + governedAccount, +}: { + index: number; + governedAccount?: GovernedMultiTypeAccount; +}) => { + const { + connection, + wallet, + form, + formErrors, + handleSetForm, + governedAccountPubkey, + } = useInstructionFormBuilder({ + index, + initialFormValues: { + governedAccount, + uiAmount: 0, + }, + schema, + shouldSplitIntoSeparateTxs: true, + buildInstruction: async function ({ + form, + connection, + wallet, + governedAccountPubkey, + }) { + if (!friktionVolts || !friktionVolts[form.volt!]) { + throw new Error('Could not load Friktion Volt'); + } + + const volt = friktionVolts[form.volt!]; + return withdrawFromVault({ + connection, + wallet, + voltVaultId: volt.voltVaultId, + governancePubkey: governedAccountPubkey, + amount: uiAmountToNativeBN( + form.uiAmount!.toString(), + volt.shareTokenDecimals, + ), + }); + }, + }); + + const { friktionVolts } = useFriktionVolt({ + connection: connection.current, + wallet, + governedAccountPubkey, + }); + + const { ownedTokenAccountsInfo } = useGovernanceUnderlyingTokenAccounts( + governedAccountPubkey ?? undefined, + ); + + const getVoltDetail = (volt: VoltData) => [ + { + label: 'Underlying Mint', + text: volt.underlyingTokenSymbol, + }, + { + label: 'Volt APY', + text: volt.apy.toString() + '%', + }, + { + label: 'Vault Token Amount', + text: + Number(volt.tokenPrice) > 0 + ? (Number(volt.deposited) / Number(volt.tokenPrice)).toString() + : '0', + }, + { + label: 'Pending Withdrawal', + text: volt.pendingWithdrawal, + flag: Number(volt.deposited) > 0 ? Flag.Warning : Flag.OK, + }, + ]; + + const getDiffValue = (amount: number) => + amount > 0 + ? { + label: 'Deposited', + text: `Amount deposited: ${amount}`, + flag: Flag.OK, + } + : { + label: 'Deposited', + text: 'No settled deposit on this volt', + flag: Flag.Danger, + }; + + return ( + <> + {ownedTokenAccountsInfo && friktionVolts && ( + <> + + {form.volt && friktionVolts[form.volt] && ( + <> + + handleSetForm({ value, propertyName: 'receiverAccount' }) + } + error={formErrors['receiverAccount']} + ownedTokenAccountsInfo={ownedTokenAccountsInfo} + /> + + { + handleSetForm({ + value: evt.target.value, + propertyName: 'uiAmount', + }); + }} + error={formErrors['uiAmount']} + /> + + + + )} + + )} + + ); +}; + +export default Withdraw; diff --git a/pages/dao/[symbol]/proposal/components/instructions/Lifinity/DepositToPool.tsx b/pages/dao/[symbol]/proposal/components/instructions/Lifinity/DepositToPool.tsx new file mode 100644 index 0000000000..536672984a --- /dev/null +++ b/pages/dao/[symbol]/proposal/components/instructions/Lifinity/DepositToPool.tsx @@ -0,0 +1,164 @@ +import React, { useEffect } from 'react'; +import * as yup from 'yup'; +import useInstructionFormBuilder from '@hooks/useInstructionFormBuilder'; +import { GovernedMultiTypeAccount } from '@utils/tokens'; +import { LifinityDepositToPoolForm } from '@utils/uiTypes/proposalCreationTypes'; +import depositToPool from '@tools/sdk/lifinity/depositToPool'; +import Select from '@components/inputs/Select'; +import SelectOptionList from '../../SelectOptionList'; +import Input from '@components/inputs/Input'; +import { + calculateDepositAmounts, + poolLabels, +} from '@tools/sdk/lifinity/lifinity'; +import { debounce } from '@utils/debounce'; + +const SLIPPAGE_OPTIONS = [0.5, 1, 2]; + +const schema = yup.object().shape({ + governedAccount: yup + .object() + .nullable() + .required('Governed account is required'), + poolName: yup.string().required('Liquidity Pool is required'), + uiAmountTokenA: yup + .number() + .moreThan(0, 'Token A Amount to deposit must be more than 0') + .required('Token A Amount to deposit value is required'), + uiAmountTokenB: yup + .number() + .moreThan(0, 'Token B Amount to deposit must be more than 0') + .required('Token B Amount to deposit value is required'), + slippage: yup.number().required('Slippage value is required'), +}); + +const DepositToPool = ({ + index, + governedAccount, +}: { + index: number; + governedAccount?: GovernedMultiTypeAccount; +}) => { + const { + connection, + wallet, + form, + handleSetForm, + formErrors, + } = useInstructionFormBuilder({ + index, + initialFormValues: { + governedAccount, + uiAmountTokenA: 0, + uiAmountTokenB: 0, + slippage: 0.5, + }, + schema, + buildInstruction: async function ({ + connection, + wallet, + form, + governedAccountPubkey, + }) { + // let's recalculate at the last moment to get the LP amount. + const { + maximumAmountTokenA, + maximumAmountTokenB, + amountLpToken, + } = await calculateDepositAmounts({ + connection: connection, + wallet, + uiAmountTokenA: form.uiAmountTokenA!, + slippage: form.slippage, + poolName: form.poolName!, + }); + + return depositToPool({ + connection, + wallet, + userTransferAuthority: governedAccountPubkey, + poolName: form.poolName!, + maximumAmountTokenA, + maximumAmountTokenB, + amountLpToken, + slippage: form.slippage, + }); + }, + }); + + useEffect(() => { + debounce.debounceFcn(async () => { + if (!form.uiAmountTokenA || !form.poolName || !wallet) return; + const { maximumUiAmountTokenB } = await calculateDepositAmounts({ + connection: connection.current, + wallet, + uiAmountTokenA: form.uiAmountTokenA, + slippage: form.slippage, + poolName: form.poolName, + }); + + handleSetForm({ + value: maximumUiAmountTokenB, + propertyName: 'uiAmountTokenB', + }); + }); + }, [form.uiAmountTokenA, form.slippage]); + + // Hardcoded gate used to be clear about what cluster is supported for now + if (connection.cluster !== 'mainnet') { + return <>This instruction does not support {connection.cluster}; + } + + return ( + <> + + + {form.poolName && ( + <> + + handleSetForm({ + value: evt.target.value, + propertyName: 'uiAmountTokenA', + }) + } + error={formErrors['uiAmountTokenA']} + /> + + + + + + )} + + ); +}; + +export default DepositToPool; diff --git a/pages/dao/[symbol]/proposal/components/instructions/Lifinity/WithdrawFromPool.tsx b/pages/dao/[symbol]/proposal/components/instructions/Lifinity/WithdrawFromPool.tsx new file mode 100644 index 0000000000..5cad8b863f --- /dev/null +++ b/pages/dao/[symbol]/proposal/components/instructions/Lifinity/WithdrawFromPool.tsx @@ -0,0 +1,189 @@ +import * as yup from 'yup'; +import { GovernedMultiTypeAccount } from '@utils/tokens'; +import useInstructionFormBuilder from '@hooks/useInstructionFormBuilder'; +import { LifinityWithdrawFromPoolForm } from '@utils/uiTypes/proposalCreationTypes'; +import Select from '@components/inputs/Select'; +import { + calculateMinimumWithdrawAmounts, + getUserLiquidityPoolTokenUiBalance, + poolLabels, +} from '@tools/sdk/lifinity/lifinity'; +import SelectOptionList from '../../SelectOptionList'; +import Input from '@components/inputs/Input'; +import { notify } from '@utils/notifications'; +import { useEffect, useState } from 'react'; +import { debounce } from '@utils/debounce'; +import withdrawFromPool from '@tools/sdk/lifinity/withdrawFromPool'; + +const SLIPPAGE_OPTIONS = [0.5, 1, 2]; + +const schema = yup.object().shape({ + governedAccount: yup + .object() + .nullable() + .required('Governed account is required'), + poolName: yup.string().required('Liquidity Pool is required'), + uiAmountTokenLP: yup + .number() + .moreThan(0, 'LP Token Amount to withdraw must be more than 0') + .required('LP Token Amount to withdraw value is required'), + slippage: yup.number().required('Slippage value is required'), +}); + +const WithdrawFromPool = ({ + index, + governedAccount, +}: { + index: number; + governedAccount?: GovernedMultiTypeAccount; +}) => { + const [maxLPTokenAmount, setMaxLPTokenAmount] = useState(0); + const [tokenAmounts, setTokenAmounts] = useState({ + uiAmountTokenA: 0, + uiAmountTokenB: 0, + }); + const { + form, + formErrors, + handleSetForm, + connection, + governedAccountPubkey, + } = useInstructionFormBuilder({ + index, + initialFormValues: { + governedAccount, + uiAmountTokenLP: 0, + slippage: 0.5, + }, + schema, + buildInstruction: async function ({ + connection, + form, + wallet, + governedAccountPubkey, + }) { + const { + minimumAmountTokenA, + minimumAmountTokenB, + lpTokenAmount, + } = await calculateMinimumWithdrawAmounts({ + connection: connection, + poolName: form.poolName!, + uiLpTokenAmount: form.uiAmountTokenLP!, + slippage: form.slippage, + }); + + return withdrawFromPool({ + connection, + wallet, + poolName: form.poolName!, + userTransferAuthority: governedAccountPubkey, + lpTokenAmount, + minimumAmountTokenA, + minimumAmountTokenB, + }); + }, + }); + + useEffect(() => { + async function fetchLpMintInfo() { + if (!governedAccountPubkey || !form.poolName) return; + + try { + const uiBalance = await getUserLiquidityPoolTokenUiBalance({ + wallet: governedAccountPubkey, + poolName: form.poolName, + connection: connection.current, + }); + + setMaxLPTokenAmount(uiBalance); + } catch (e) { + notify({ + type: 'error', + message: 'Could not fetch LP Account', + description: `${form.poolName} LP Token Account could not be found for the selected Governance`, + }); + } + } + fetchLpMintInfo(); + }, [governedAccount?.governance?.pubkey, form.poolName]); + + useEffect(() => { + debounce.debounceFcn(async () => { + if (!form.uiAmountTokenLP || !form.poolName) return; + const { + minimumWithdrawnUiAmountTokenA, + minimumWithdrawnUiAmountTokenB, + } = await calculateMinimumWithdrawAmounts({ + connection: connection.current, + poolName: form.poolName, + uiLpTokenAmount: form.uiAmountTokenLP!, + slippage: form.slippage, + }); + + setTokenAmounts({ + uiAmountTokenA: minimumWithdrawnUiAmountTokenA, + uiAmountTokenB: minimumWithdrawnUiAmountTokenB, + }); + }); + }, [form.poolName, form.uiAmountTokenLP, form.slippage]); + + return ( + <> + + + {form.poolName && ( + <> + + handleSetForm({ + value: evt.target.value, + propertyName: 'uiAmountTokenLP', + }) + } + error={formErrors['uiAmountTokenLP']} + /> + + + + + )} + + ); +}; + +export default WithdrawFromPool; diff --git a/pages/dao/[symbol]/proposal/components/instructions/Mango/MakeChangeMaxAccounts.tsx b/pages/dao/[symbol]/proposal/components/instructions/Mango/MakeChangeMaxAccounts.tsx deleted file mode 100644 index 2557d5e8e8..0000000000 --- a/pages/dao/[symbol]/proposal/components/instructions/Mango/MakeChangeMaxAccounts.tsx +++ /dev/null @@ -1,155 +0,0 @@ -/* eslint-disable @typescript-eslint/no-non-null-assertion */ -import React, { useContext, useEffect, useState } from 'react' -import useRealm from '@hooks/useRealm' -import { PublicKey } from '@solana/web3.js' -import * as yup from 'yup' -import { isFormValid } from '@utils/formValidation' -import { - UiInstruction, - MangoMakeChangeMaxAccountsForm, -} from '@utils/uiTypes/proposalCreationTypes' -import { NewProposalContext } from '../../../new' -import useGovernanceAssets from '@hooks/useGovernanceAssets' -import { Governance, GovernanceAccountType } from '@solana/spl-governance' -import { ProgramAccount } from '@solana/spl-governance' -import useWalletStore from 'stores/useWalletStore' -import { serializeInstructionToBase64 } from '@solana/spl-governance' -import Input from '@components/inputs/Input' -import GovernedAccountSelect from '../../GovernedAccountSelect' -import { GovernedMultiTypeAccount } from '@utils/tokens' -import { - BN, - makeChangeMaxMangoAccountsInstruction, -} from '@blockworks-foundation/mango-client' - -const MakeChangeMaxAccounts = ({ - index, - governance, -}: { - index: number - governance: ProgramAccount | null -}) => { - const wallet = useWalletStore((s) => s.current) - const { realmInfo } = useRealm() - const { getGovernancesByAccountTypes } = useGovernanceAssets() - const governedProgramAccounts = getGovernancesByAccountTypes([ - GovernanceAccountType.ProgramGovernanceV1, - GovernanceAccountType.ProgramGovernanceV2, - ]).map((x) => { - return { - governance: x, - } - }) - const shouldBeGoverned = index !== 0 && governance - const programId: PublicKey | undefined = realmInfo?.programId - const [form, setForm] = useState({ - governedAccount: undefined, - programId: programId?.toString(), - mangoGroupKey: undefined, - maxMangoAccounts: 1, - }) - const [formErrors, setFormErrors] = useState({}) - const { handleSetInstructions } = useContext(NewProposalContext) - const handleSetForm = ({ propertyName, value }) => { - setFormErrors({}) - setForm({ ...form, [propertyName]: value }) - } - const validateInstruction = async (): Promise => { - const { isValid, validationErrors } = await isFormValid(schema, form) - setFormErrors(validationErrors) - return isValid - } - async function getInstruction(): Promise { - const isValid = await validateInstruction() - let serializedInstruction = '' - if ( - isValid && - programId && - form.governedAccount?.governance?.account && - wallet?.publicKey - ) { - //Mango instruction call and serialize - const setMaxMangoAccountsInstr = makeChangeMaxMangoAccountsInstruction( - form.governedAccount.governance.account.governedAccount, - new PublicKey(form.mangoGroupKey!), - form.governedAccount.governance.pubkey, - new BN(form.maxMangoAccounts) - ) - - serializedInstruction = serializeInstructionToBase64( - setMaxMangoAccountsInstr - ) - } - const obj: UiInstruction = { - serializedInstruction: serializedInstruction, - isValid, - governance: form.governedAccount?.governance, - } - return obj - } - useEffect(() => { - handleSetForm({ - propertyName: 'programId', - value: programId?.toString(), - }) - }, [realmInfo?.programId]) - - useEffect(() => { - handleSetInstructions( - { governedAccount: form.governedAccount?.governance, getInstruction }, - index - ) - }, [form]) - const schema = yup.object().shape({ - bufferAddress: yup.number(), - governedAccount: yup - .object() - .nullable() - .required('Program governed account is required'), - }) - - return ( - <> - {/* if you need more fields add theme to interface MangoMakeChangeMaxAccountsForm - then you can add inputs in here */} - { - handleSetForm({ value, propertyName: 'governedAccount' }) - }} - value={form.governedAccount} - error={formErrors['governedAccount']} - shouldBeGoverned={shouldBeGoverned} - governance={governance} - > - - handleSetForm({ - value: evt.target.value, - propertyName: 'mangoGroupKey', - }) - } - error={formErrors['mangoGroupKey']} - /> - - handleSetForm({ - value: evt.target.value, - propertyName: 'maxMangoAccounts', - }) - } - error={formErrors['maxMangoAccounts']} - /> - - ) -} - -export default MakeChangeMaxAccounts diff --git a/pages/dao/[symbol]/proposal/components/instructions/Mango/MakeChangeReferralFeeParams.tsx b/pages/dao/[symbol]/proposal/components/instructions/Mango/MakeChangeReferralFeeParams.tsx deleted file mode 100644 index 57093b1942..0000000000 --- a/pages/dao/[symbol]/proposal/components/instructions/Mango/MakeChangeReferralFeeParams.tsx +++ /dev/null @@ -1,195 +0,0 @@ -/* eslint-disable @typescript-eslint/no-non-null-assertion */ -import React, { useContext, useEffect, useState } from 'react' -import useRealm from '@hooks/useRealm' -import { PublicKey } from '@solana/web3.js' -import * as yup from 'yup' -import { isFormValid } from '@utils/formValidation' -import { - MangoMakeChangeReferralFeeParams, - UiInstruction, -} from '@utils/uiTypes/proposalCreationTypes' -import { NewProposalContext } from '../../../new' -import useGovernanceAssets from '@hooks/useGovernanceAssets' -import { Governance, GovernanceAccountType } from '@solana/spl-governance' -import { ProgramAccount } from '@solana/spl-governance' -import useWalletStore from 'stores/useWalletStore' -import { serializeInstructionToBase64 } from '@solana/spl-governance' -import Input from '@components/inputs/Input' -import GovernedAccountSelect from '../../GovernedAccountSelect' -import { GovernedMultiTypeAccount, tryGetMint } from '@utils/tokens' -import { makeChangeReferralFeeParamsInstruction } from '@blockworks-foundation/mango-client' -import { BN } from '@project-serum/anchor' -import { MANGO_MINT } from 'Strategies/protocols/mango/tools' -import { parseMintNaturalAmountFromDecimal } from '@tools/sdk/units' - -const MakeChangeReferralFeeParams = ({ - index, - governance, -}: { - index: number - governance: ProgramAccount | null -}) => { - const wallet = useWalletStore((s) => s.current) - const { realmInfo } = useRealm() - const { getGovernancesByAccountTypes } = useGovernanceAssets() - const connection = useWalletStore((s) => s.connection) - const governedProgramAccounts = getGovernancesByAccountTypes([ - GovernanceAccountType.ProgramGovernanceV1, - GovernanceAccountType.ProgramGovernanceV2, - ]).map((x) => { - return { - governance: x, - } - }) - const shouldBeGoverned = index !== 0 && governance - const programId: PublicKey | undefined = realmInfo?.programId - const [form, setForm] = useState({ - governedAccount: undefined, - programId: programId?.toString(), - mangoGroupKey: undefined, - refSurchargeCentibps: 0, - refShareCentibps: 0, - refMngoRequired: 0, - }) - const [formErrors, setFormErrors] = useState({}) - const { handleSetInstructions } = useContext(NewProposalContext) - const handleSetForm = ({ propertyName, value }) => { - setFormErrors({}) - setForm({ ...form, [propertyName]: value }) - } - const validateInstruction = async (): Promise => { - const { isValid, validationErrors } = await isFormValid(schema, form) - setFormErrors(validationErrors) - return isValid - } - async function getInstruction(): Promise { - const isValid = await validateInstruction() - let serializedInstruction = '' - if ( - isValid && - programId && - form.governedAccount?.governance?.account && - wallet?.publicKey - ) { - //Mango instruction call and serialize - const mint = await tryGetMint( - connection.current, - new PublicKey(MANGO_MINT) - ) - const refMngoRequiredMintAmount = parseMintNaturalAmountFromDecimal( - form.refMngoRequired!, - mint!.account.decimals - ) - const setMaxMangoAccountsInstr = makeChangeReferralFeeParamsInstruction( - form.governedAccount.governance.account.governedAccount, - new PublicKey(form.mangoGroupKey!), - form.governedAccount.governance.pubkey, - new BN(form.refSurchargeCentibps), - new BN(form.refShareCentibps), - new BN(refMngoRequiredMintAmount) - ) - - serializedInstruction = serializeInstructionToBase64( - setMaxMangoAccountsInstr - ) - } - const obj: UiInstruction = { - serializedInstruction: serializedInstruction, - isValid, - governance: form.governedAccount?.governance, - } - return obj - } - useEffect(() => { - handleSetForm({ - propertyName: 'programId', - value: programId?.toString(), - }) - }, [realmInfo?.programId]) - - useEffect(() => { - handleSetInstructions( - { governedAccount: form.governedAccount?.governance, getInstruction }, - index - ) - }, [form]) - const schema = yup.object().shape({ - governedAccount: yup - .object() - .nullable() - .required('Program governed account is required'), - mangoGroupKey: yup.string().required(), - refShareCentibps: yup.number().required(), - refMngoRequired: yup.number().required(), - refSurchargeCentibps: yup.number().required(), - }) - - return ( - <> - { - handleSetForm({ value, propertyName: 'governedAccount' }) - }} - value={form.governedAccount} - error={formErrors['governedAccount']} - shouldBeGoverned={shouldBeGoverned} - governance={governance} - > - - handleSetForm({ - value: evt.target.value, - propertyName: 'mangoGroupKey', - }) - } - error={formErrors['mangoGroupKey']} - /> - - handleSetForm({ - value: evt.target.value, - propertyName: 'refSurchargeCentibps', - }) - } - error={formErrors['refSurchargeCentibps']} - /> - - handleSetForm({ - value: evt.target.value, - propertyName: 'refShareCentibps', - }) - } - error={formErrors['refShareCentibps']} - /> - - handleSetForm({ - value: evt.target.value, - propertyName: 'refMngoRequired', - }) - } - error={formErrors['refMngoRequired']} - /> - - ) -} - -export default MakeChangeReferralFeeParams diff --git a/pages/dao/[symbol]/proposal/components/instructions/MapleFinance/LenderDeposit.tsx b/pages/dao/[symbol]/proposal/components/instructions/MapleFinance/LenderDeposit.tsx new file mode 100644 index 0000000000..a605650d84 --- /dev/null +++ b/pages/dao/[symbol]/proposal/components/instructions/MapleFinance/LenderDeposit.tsx @@ -0,0 +1,125 @@ +import * as yup from 'yup'; +import Input from '@components/inputs/Input'; +import useInstructionFormBuilder from '@hooks/useInstructionFormBuilder'; +import mapleFinanceConfig, { + MapleFinance, +} from '@tools/sdk/mapleFinance/configuration'; +import { lenderDeposit } from '@tools/sdk/mapleFinance/instructions/lenderDeposit'; +import { GovernedMultiTypeAccount } from '@utils/tokens'; +import { MapleFinanceLenderDepositForm } from '@utils/uiTypes/proposalCreationTypes'; +import { uiAmountToNativeBN } from '@tools/sdk/units'; +import TokenAccountSelect from '../../TokenAccountSelect'; +import useGovernanceUnderlyingTokenAccounts from '@hooks/useGovernanceUnderlyingTokenAccounts'; +import Select from '@components/inputs/Select'; +import { PublicKey } from '@solana/web3.js'; + +const schema = yup.object().shape({ + governedAccount: yup + .object() + .nullable() + .required('Governed account is required'), + uiDepositAmount: yup + .number() + .moreThan(0, 'Deposit amount should be more than 0') + .required('Deposit amount is required'), + poolName: yup.string().required('Pool Name is required'), +}); + +const LenderDeposit = ({ + index, + governedAccount, +}: { + index: number; + governedAccount?: GovernedMultiTypeAccount; +}) => { + const { + form, + handleSetForm, + formErrors, + governedAccountPubkey, + } = useInstructionFormBuilder({ + index, + initialFormValues: { + governedAccount, + }, + schema, + buildInstruction: async function ({ + form, + connection, + wallet, + governedAccountPubkey, + }) { + const programs = mapleFinanceConfig.getMapleFinancePrograms({ + connection, + wallet, + }); + + return lenderDeposit({ + authority: governedAccountPubkey, + programs, + depositAmount: uiAmountToNativeBN( + form.uiDepositAmount!.toString(), + MapleFinance.pools[form.poolName!].baseMint.decimals, + ), + poolName: form.poolName!, + sourceAccount: new PublicKey(form.sourceAccount!), + }); + }, + }); + + // Governance underlying accounts that can be selected as source + const { ownedTokenAccountsInfo } = useGovernanceUnderlyingTokenAccounts( + governedAccountPubkey, + ); + + return ( + <> + + + {ownedTokenAccountsInfo && ( + + handleSetForm({ value, propertyName: 'sourceAccount' }) + } + error={formErrors['sourceAccount']} + ownedTokenAccountsInfo={ownedTokenAccountsInfo} + /> + )} + + + handleSetForm({ + value: evt.target.value, + propertyName: 'uiDepositAmount', + }) + } + error={formErrors['uiDepositAmount']} + /> + + ); +}; + +export default LenderDeposit; diff --git a/pages/dao/[symbol]/proposal/components/instructions/Native/BurnSplTokens.tsx b/pages/dao/[symbol]/proposal/components/instructions/Native/BurnSplTokens.tsx new file mode 100644 index 0000000000..1688a28dd4 --- /dev/null +++ b/pages/dao/[symbol]/proposal/components/instructions/Native/BurnSplTokens.tsx @@ -0,0 +1,117 @@ +import * as yup from 'yup'; +import useInstructionFormBuilder from '@hooks/useInstructionFormBuilder'; +import { + GovernedMultiTypeAccount, + TOKEN_PROGRAM_ID, + tryGetTokenMint, +} from '@utils/tokens'; +import { SPLToken } from '@saberhq/token-utils'; +import useGovernanceUnderlyingTokenAccounts from '@hooks/useGovernanceUnderlyingTokenAccounts'; +import TokenAccountSelect from '../../TokenAccountSelect'; +import { uiAmountToNativeBN } from '@tools/sdk/units'; +import { PublicKey } from '@solana/web3.js'; +import { NativeBurnSplTokensForm } from '@utils/uiTypes/proposalCreationTypes'; +import Input from '@components/inputs/Input'; + +const BurnSplTokens = ({ + index, + governedAccount, +}: { + index: number; + governedAccount?: GovernedMultiTypeAccount; +}) => { + const { + connection, + form, + handleSetForm, + formErrors, + governedAccountPubkey, + } = useInstructionFormBuilder({ + index, + initialFormValues: { + governedAccount, + }, + schema: yup.object().shape({ + governedAccount: yup + .object() + .nullable() + .required('Governed account is required'), + source: yup.string().required('Source Account is required'), + uiAmount: yup + .number() + .moreThan(0, 'Amount should be more than 0') + .required('Amount is required'), + }), + + buildInstruction: async function ({ governedAccountPubkey, form }) { + const source = new PublicKey(form.source!); + + const mintInfo = await tryGetTokenMint(connection.current, source); + + if (!mintInfo) { + throw new Error('Cannot get Mint info'); + } + + const nativeAmount = uiAmountToNativeBN( + form.uiAmount!, + mintInfo.account.decimals, + ); + + // Tricks, we implement the function toBuffer as it is required by the SPLToken library + nativeAmount.toBuffer = () => nativeAmount.toArrayLike(Buffer, 'le', 8); + + return SPLToken.createBurnInstruction( + TOKEN_PROGRAM_ID, + mintInfo.publicKey, + + // SourceKey + source, + + // Owner + governedAccountPubkey, + + // Multi Signers + [], + + nativeAmount, + ); + }, + }); + + // Governance underlying accounts that can be selected as source + const { ownedTokenAccountsInfo } = useGovernanceUnderlyingTokenAccounts( + governedAccountPubkey, + ); + + if (!ownedTokenAccountsInfo) { + return null; + } + + return ( + <> + handleSetForm({ value, propertyName: 'source' })} + error={formErrors['source']} + ownedTokenAccountsInfo={ownedTokenAccountsInfo} + /> + + + handleSetForm({ + value: evt.target.value, + propertyName: 'uiAmount', + }) + } + error={formErrors['uiAmount']} + /> + + ); +}; + +export default BurnSplTokens; diff --git a/pages/dao/[symbol]/proposal/components/instructions/Native/CreateAssociatedTokenAccount.tsx b/pages/dao/[symbol]/proposal/components/instructions/Native/CreateAssociatedTokenAccount.tsx new file mode 100644 index 0000000000..d30a95fea2 --- /dev/null +++ b/pages/dao/[symbol]/proposal/components/instructions/Native/CreateAssociatedTokenAccount.tsx @@ -0,0 +1,72 @@ +import * as yup from 'yup'; +import Select from '@components/inputs/Select'; +import useInstructionFormBuilder from '@hooks/useInstructionFormBuilder'; +import { createAssociatedTokenAccount } from '@utils/associated'; +import { getSplTokenMintAddressByUIName, SPL_TOKENS } from '@utils/splTokens'; +import { GovernedMultiTypeAccount } from '@utils/tokens'; +import { CreateAssociatedTokenAccountForm } from '@utils/uiTypes/proposalCreationTypes'; + +const schema = yup.object().shape({ + governedAccount: yup + .object() + .nullable() + .required('Governed account is required'), + splTokenMintUIName: yup.string().required('SPL Token Mint is required'), +}); + +const CreateAssociatedTokenAccount = ({ + index, + governedAccount, +}: { + index: number; + governedAccount?: GovernedMultiTypeAccount; +}) => { + const { + form, + formErrors, + handleSetForm, + } = useInstructionFormBuilder({ + index, + initialFormValues: { + governedAccount, + }, + schema, + buildInstruction: async function ({ wallet, governedAccountPubkey, form }) { + const [tx] = await createAssociatedTokenAccount( + // fundingAddress + wallet.publicKey!, + + // walletAddress + governedAccountPubkey, + + // splTokenMintAddress + getSplTokenMintAddressByUIName(form.splTokenMintUIName!), + ); + return tx; + }, + }); + + return ( + + ); +}; + +export default CreateAssociatedTokenAccount; diff --git a/pages/dao/[symbol]/proposal/components/instructions/Native/CustomBase64.tsx b/pages/dao/[symbol]/proposal/components/instructions/Native/CustomBase64.tsx new file mode 100644 index 0000000000..b001e7a93a --- /dev/null +++ b/pages/dao/[symbol]/proposal/components/instructions/Native/CustomBase64.tsx @@ -0,0 +1,113 @@ +import React from 'react'; +import * as yup from 'yup'; +import { getInstructionDataFromBase64 } from '@solana/spl-governance'; +import Input from '@components/inputs/Input'; +import Textarea from '@components/inputs/Textarea'; +import { Base64InstructionForm } from '@utils/uiTypes/proposalCreationTypes'; +import { GovernedMultiTypeAccount } from '@utils/tokens'; +import useInstructionFormBuilder, { + SerializedInstruction, +} from '@hooks/useInstructionFormBuilder'; + +const CustomBase64 = ({ + index, + governedAccount, +}: { + index: number; + governedAccount?: GovernedMultiTypeAccount; +}) => { + const { + form, + formErrors, + handleSetForm, + } = useInstructionFormBuilder({ + index, + initialFormValues: { + governedAccount, + base64: '', + holdUpTime: 0, + }, + schema: yup.object().shape({ + governedAccount: yup + .object() + .nullable() + .required('Governed account is required'), + base64: yup + .string() + .required('Instruction is required') + .test('base64Test', 'Invalid base64', function (val: string) { + if (val) { + try { + getInstructionDataFromBase64(val); + return true; + } catch (e) { + return false; + } + } + + return this.createError({ + message: `Instruction is required`, + }); + }), + holdUpTime: yup.number().required('Hold up time is required'), + }), + + getCustomHoldUpTime: async function () { + return form.holdUpTime; + }, + + buildInstruction: async function () { + return form.base64 as SerializedInstruction; + }, + }); + + const validateAmountOnBlur = () => { + const value = form.holdUpTime; + + handleSetForm({ + value: parseFloat( + Math.max( + Number(0), + Math.min(Number(Number.MAX_SAFE_INTEGER), Number(value)), + ).toFixed(), + ), + propertyName: 'holdUpTime', + }); + }; + + return ( + <> + { + handleSetForm({ + value: event.target.value, + propertyName: 'holdUpTime', + }); + }} + step={1} + error={formErrors['holdUpTime']} + onBlur={validateAmountOnBlur} + /> + + - {canChooseWhoVote && ( - { - setVoteByCouncil(!voteByCouncil) - }} - > - )} - +
+ +