diff --git a/.env.sample b/.env.sample index 326208b10a..289d0cd4be 100644 --- a/.env.sample +++ b/.env.sample @@ -5,6 +5,7 @@ DEVNET_RPC=https://mango.devnet.rpcpool.com DEFAULT_GOVERNANCE_PROGRAM_ID=GTesTBiEWE32WHXXE2S4XbZvA5CrEc4xs6ZgRe895dP +NEXT_PUBLIC_JUPTER_SWAP_API_ENDPOINT=https://quote-api.jup.ag/v6 NEXT_PUBLIC_API_ENDPOINT=https://api.realms.today/graphql NEXT_PUBLIC_DISCORD_APPLICATION_CLIENT_ID=1042836142560645130 NEXT_PUBLIC_DISCORD_MATCHDAY_CLIENT_ID=1044361939322683442 diff --git a/.github/workflows/ci-main-tests.yml b/.github/workflows/ci-main-tests.yml index f1944c0358..2f471a90a3 100644 --- a/.github/workflows/ci-main-tests.yml +++ b/.github/workflows/ci-main-tests.yml @@ -15,29 +15,24 @@ jobs: contents: read security-events: write - strategy: - fail-fast: false - matrix: - language: ['javascript'] - steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Initialise CodeQL uses: github/codeql-action/init@v2 with: - languages: ${{ matrix.language }} + languages: 'javascript' - name: Run CodeQL uses: github/codeql-action/analyze@v2 - + sca: name: Dependency Scan runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 # Report all vulnerabilities GitHub security tab - name: Report on all vulnerabilities @@ -59,41 +54,41 @@ jobs: format: 'table' severity: 'CRITICAL' exit-code: '1' - + - name: Upload scan results - uses: github/codeql-action/upload-sarif@v2 + uses: github/codeql-action/upload-sarif@v3 if: always() with: sarif_file: 'trivy-results.sarif' test: - name: Run Tests - runs-on: ubuntu-latest - steps: - - name: Checkout repository - uses: actions/checkout@v3 + name: Run Tests + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 - - name: Setup Node - uses: actions/setup-node@v3 - with: - node-version: lts/* - cache: yarn + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: 20 + cache: yarn - - name: Cache dependencies - uses: actions/cache@v3 - with: - path: '**/node_modules' - key: ${{ runner.os }}-modules-${{ hashFiles('**/yarn.lock') }} + - name: Cache dependencies + uses: actions/cache@v3 + with: + path: '**/node_modules' + key: ${{ runner.os }}-modules-${{ hashFiles('**/yarn.lock') }} - - name: Install dependencies - run: yarn install --frozen-lockfile + - name: Install dependencies + run: yarn ci - - name: Run tests - run: yarn test-all + - name: Run tests + run: yarn test-all pass: name: All tests pass needs: ['sast', 'sca', 'test'] runs-on: ubuntu-latest steps: - - run: echo ok \ No newline at end of file + - run: echo ok diff --git a/.nvmrc b/.nvmrc index 53d838af21..0a47c855eb 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -lts/gallium +lts/iron \ No newline at end of file diff --git a/.vscode/deploy.txt b/.vscode/deploy.txt new file mode 100644 index 0000000000..7949afce22 --- /dev/null +++ b/.vscode/deploy.txt @@ -0,0 +1 @@ +Please deploy \ No newline at end of file diff --git a/.yarnrc b/.yarnrc index 83f65f98bb..b16a8dd6e3 100644 --- a/.yarnrc +++ b/.yarnrc @@ -1,3 +1,2 @@ ---frozen-lockfile true --add.exact true ignore-scripts true diff --git a/@types/types.ts b/@types/types.ts index 9761f749b8..519b525fb9 100644 --- a/@types/types.ts +++ b/@types/types.ts @@ -4,3 +4,5 @@ export interface EndpointInfo { name: EndpointTypes url: string } + +export type GovernanceRole = 'council' | 'community'; \ No newline at end of file diff --git a/DriftStakeVoterPlugin/DriftVoterClient.ts b/DriftStakeVoterPlugin/DriftVoterClient.ts new file mode 100644 index 0000000000..6a83a5c5f3 --- /dev/null +++ b/DriftStakeVoterPlugin/DriftVoterClient.ts @@ -0,0 +1,259 @@ +import { BN, Program, Provider } from '@coral-xyz/anchor' +import { Client } from '@solana/governance-program-library' +import { + getTokenOwnerRecordAddress, + SYSTEM_PROGRAM_ID, +} from '@solana/spl-governance' +import { PublicKey, TransactionInstruction } from '@solana/web3.js' +import { DriftStakeVoter, IDL } from './idl/driftStakeVoter' +import { IDL as DriftIDL } from './idl/drift' +import { + getInsuranceFundStakeAccountPublicKey, + getInsuranceFundVaultPublicKey, + getSpotMarketPublicKey, + unstakeSharesToAmountWithOpenRequest, +} from './driftSdk' +import { fetchTokenAccountByPubkey } from '@hooks/queries/tokenAccount' +import { DRIFT_STAKE_VOTER_PLUGIN } from './constants' +import { fetchRealmByPubkey } from '@hooks/queries/realm' +import queryClient from '@hooks/queries/queryClient' + +export class DriftVoterClient extends Client { + readonly requiresInputVoterWeight = true + + async _fetchRegistrar(realm: PublicKey, mint: PublicKey) { + const { registrar: registrarPk } = this.getRegistrarPDA(realm, mint) + const registrar = await queryClient.fetchQuery( + ['Drift', 'Plugin Registrar', registrarPk], + () => this.program.account.registrar.fetch(registrarPk) + ) + return registrar + } + + constructor( + public program: Program, + public devnet: boolean + ) { + super(program, devnet) + } + + async calculateMaxVoterWeight( + _realm: PublicKey, + _mint: PublicKey + ): Promise { + console.log( + 'drift voter client was just asked to calculate max voter weight' + ) + const { result: realm } = await fetchRealmByPubkey( + this.program.provider.connection, + _realm + ) + console.log('drift voter client realm', realm) + return realm?.account.config?.communityMintMaxVoteWeightSource.value ?? null // TODO this code should not actually be called because this is not a max voter weight plugin + } + + async calculateVoterWeight( + voter: PublicKey, + realm: PublicKey, + mint: PublicKey, + inputVoterWeight: BN + ): Promise { + const registrar = await this._fetchRegistrar(realm, mint) + const spotMarketIndex = registrar.spotMarketIndex // could just hardcode spotmarket pk + const driftProgramId = registrar.driftProgramId // likewise + const drift = new Program(DriftIDL, driftProgramId, this.program.provider) + const spotMarketPk = await getSpotMarketPublicKey( + driftProgramId, + spotMarketIndex + ) + const insuranceFundVaultPk = await getInsuranceFundVaultPublicKey( + driftProgramId, + spotMarketIndex + ) + const insuranceFundStakePk = await getInsuranceFundStakeAccountPublicKey( + driftProgramId, + voter, + spotMarketIndex + ) + + const insuranceFundStake = await queryClient.fetchQuery({ + queryKey: ['Insurance Fund Stake', insuranceFundStakePk.toString()], + queryFn: async () => + drift.account.insuranceFundStake.fetchNullable(insuranceFundStakePk), + }) + + if (insuranceFundStake === null) { + console.log('drift voter client', 'no insurance fund stake account found') + return inputVoterWeight + } + + const spotMarket = await queryClient.fetchQuery({ + queryKey: ['Drift Spot Market', spotMarketPk.toString()], + queryFn: async () => drift.account.spotMarket.fetchNullable(spotMarketPk), + }) + + if (spotMarket === null) { + console.log('Drift spot market not found: ' + spotMarketPk.toString()) + return inputVoterWeight + } + + const insuranceFundVault = await fetchTokenAccountByPubkey( + this.program.provider.connection, + insuranceFundVaultPk + ) + if (insuranceFundVault.result === undefined) { + console.log( + 'Insurance fund vault not found: ' + insuranceFundVaultPk.toString() + ) + return inputVoterWeight + } + + const nShares = insuranceFundStake.ifShares + const withdrawRequestShares = insuranceFundStake.lastWithdrawRequestShares + const withdrawRequestAmount = insuranceFundStake.lastWithdrawRequestValue + const totalIfShares = spotMarket.insuranceFund.totalShares + const insuranceFundVaultBalance = insuranceFundVault.result?.amount + + const amount = unstakeSharesToAmountWithOpenRequest( + nShares, + withdrawRequestShares, + withdrawRequestAmount, + totalIfShares, + insuranceFundVaultBalance + ) + + return amount.add(inputVoterWeight) + } + + async updateVoterWeightRecord( + voter: PublicKey, + realm: PublicKey, + mint: PublicKey + //action?: VoterWeightAction | undefined, + //inputRecordCallback?: (() => Promise) | undefined + ): Promise<{ + pre: TransactionInstruction[] + post?: TransactionInstruction[] | undefined + }> { + const connection = this.program.provider.connection + const { result: realmAccount } = await fetchRealmByPubkey(connection, realm) + if (!realmAccount) throw new Error('Realm not found') + const tokenOwnerRecordPk = await getTokenOwnerRecordAddress( + realmAccount?.owner, + realm, + mint, + voter + ) + const { voterWeightPk } = await this.getVoterWeightRecordPDA( + realm, + mint, + voter + ) + const { registrar: registrarPk } = this.getRegistrarPDA(realm, mint) + const registrar = await this._fetchRegistrar(realm, mint) + const spotMarketIndex = registrar.spotMarketIndex // could just hardcode spotmarket pk + const driftProgramId = registrar.driftProgramId // likewise + const drift = new Program(DriftIDL, driftProgramId, this.program.provider) + + //const drift = new Program(DriftIDL, driftProgramId, this.program.provider) + const spotMarketPk = await getSpotMarketPublicKey( + driftProgramId, + spotMarketIndex + ) + const insuranceFundVaultPk = await getInsuranceFundVaultPublicKey( + driftProgramId, + spotMarketIndex + ) + const insuranceFundStakePk = await getInsuranceFundStakeAccountPublicKey( + driftProgramId, + voter, + spotMarketIndex + ) + + const spotMarket = await queryClient.fetchQuery({ + queryKey: ['Drift Spot Market', spotMarketPk.toString()], + queryFn: async () => drift.account.spotMarket.fetchNullable(spotMarketPk), + }) + const spotMarketPkOrNull = spotMarket === null ? null : spotMarketPk + + const insuranceFundVault = await fetchTokenAccountByPubkey( + this.program.provider.connection, + insuranceFundVaultPk + ) + const insuranceFundVaultPkOrNull = + insuranceFundVault.found === false ? null : insuranceFundVaultPk + + let insuranceFundStake: + | Awaited> + | undefined + try { + insuranceFundStake = await drift.account.insuranceFundStake.fetch( + insuranceFundStakePk + ) + } catch (e) { + console.log('drift voter client', 'no insurance fund stake account found') + insuranceFundStake = undefined + } + const stakePkOrNull = + insuranceFundStake === undefined ? null : insuranceFundStakePk + + const ix = await this.program.methods + .updateVoterWeightRecord() + .accountsStrict({ + voterWeightRecord: voterWeightPk, + registrar: registrarPk, + driftProgram: driftProgramId, + spotMarket: spotMarketPkOrNull, + insuranceFundStake: stakePkOrNull, + insuranceFundVault: insuranceFundVaultPkOrNull, + tokenOwnerRecord: tokenOwnerRecordPk, + }) + .instruction() + + return { pre: [ix] } + } + + // NO-OP + async createMaxVoterWeightRecord(): Promise { + return null + } + + // NO-OP + async updateMaxVoterWeightRecord(): Promise { + return null + } + + static async connect( + provider: Provider, + programId = new PublicKey(DRIFT_STAKE_VOTER_PLUGIN), + devnet = false + ): Promise { + return new DriftVoterClient( + new Program(IDL, programId, provider), + devnet + ) + } + + async createVoterWeightRecord( + voter: PublicKey, + realm: PublicKey, + mint: PublicKey + ): Promise { + const { voterWeightPk } = await this.getVoterWeightRecordPDA( + realm, + mint, + voter + ) + const { registrar } = this.getRegistrarPDA(realm, mint) + + return this.program.methods + .createVoterWeightRecord(voter) + .accounts({ + voterWeightRecord: voterWeightPk, + registrar, + payer: voter, + systemProgram: SYSTEM_PROGRAM_ID, + }) + .instruction() + } +} diff --git a/DriftStakeVoterPlugin/components/DriftDeposit.tsx b/DriftStakeVoterPlugin/components/DriftDeposit.tsx new file mode 100644 index 0000000000..a17d82ec0e --- /dev/null +++ b/DriftStakeVoterPlugin/components/DriftDeposit.tsx @@ -0,0 +1,57 @@ +import { BigNumber } from 'bignumber.js' +import { useRealmQuery } from '@hooks/queries/realm' +import { useMintInfoByPubkeyQuery } from '@hooks/queries/mintInfo' +import useUserGovTokenAccountQuery from '@hooks/useUserGovTokenAccount' +import { DepositTokensButton } from '@components/DepositTokensButton' +import Button from '@components/Button' +import { DRIFT_GOVERNANCE_TICKER } from 'DriftStakeVoterPlugin/constants' + +/** Contextual deposit, shows only if relevant */ +export const DriftDeposit = ({ role }: { role: 'community' | 'council' }) => { + const realm = useRealmQuery().data?.result + const mint = + role === 'community' + ? realm?.account.communityMint + : realm?.account.config.councilMint + + const mintInfo = useMintInfoByPubkeyQuery(mint).data?.result + const userAta = useUserGovTokenAccountQuery(role).data?.result + + const depositAmount = userAta?.amount + ? new BigNumber(userAta.amount.toString()) + : new BigNumber(0) + + return !depositAmount.isGreaterThan(0) ? null : ( + <> +
+ You have{' '} + {mintInfo + ? depositAmount.shiftedBy(-mintInfo.decimals).toFormat() + : depositAmount.toFormat()}{' '} + more {DRIFT_GOVERNANCE_TICKER} in your wallet. You can stake it with + Drift or deposit it into Realms to increase your voting power. +
+
+ + +
+ + ) +} diff --git a/DriftStakeVoterPlugin/components/DriftVotingPower.tsx b/DriftStakeVoterPlugin/components/DriftVotingPower.tsx new file mode 100644 index 0000000000..96509cd95a --- /dev/null +++ b/DriftStakeVoterPlugin/components/DriftVotingPower.tsx @@ -0,0 +1,145 @@ +import classNames from 'classnames' + +import { useMintInfoByPubkeyQuery } from '@hooks/queries/mintInfo' +import { useRealmQuery } from '@hooks/queries/realm' +import { BigNumber } from 'bignumber.js' +import clsx from 'clsx' +import { useMemo } from 'react' +import { useRealmVoterWeightPlugins } from '@hooks/useRealmVoterWeightPlugins' +import { useJoinRealm } from '@hooks/useJoinRealm' +import { Transaction } from '@solana/web3.js' +import useWalletOnePointOh from '@hooks/useWalletOnePointOh' +import { useConnection } from '@solana/wallet-adapter-react' +import Button from '@components/Button' +import { sendTransaction } from '@utils/send' +import { DriftDeposit } from './DriftDeposit' +import { BN } from '@coral-xyz/anchor' + +interface Props { + className?: string + role: 'community' | 'council' +} + +export default function DriftVotingPower({ role, className }: Props) { + const wallet = useWalletOnePointOh() + const connected = !!wallet?.connected + const { connection } = useConnection() + const realm = useRealmQuery().data?.result + const { + userNeedsTokenOwnerRecord, + userNeedsVoterWeightRecords, + handleRegister, + } = useJoinRealm() + const mintInfo = useMintInfoByPubkeyQuery(realm?.account.communityMint).data + ?.result + + const { totalCalculatedVoterWeight, isReady } = useRealmVoterWeightPlugins( + role + ) + + const vanillaValue = totalCalculatedVoterWeight?.initialValue + const stakedValue = totalCalculatedVoterWeight?.value?.sub( + vanillaValue ?? new BN(0) + ) + + const formattedTotal = useMemo( + () => + mintInfo && totalCalculatedVoterWeight?.value + ? new BigNumber(totalCalculatedVoterWeight?.value.toString()) + .shiftedBy(-mintInfo.decimals) + .toFormat(2) + : undefined, + [mintInfo, totalCalculatedVoterWeight?.value] + ) + + const formattedStaked = useMemo( + () => + mintInfo && stakedValue + ? new BigNumber(stakedValue.toString()) + .shiftedBy(-mintInfo.decimals) + .toFormat(2) + : undefined, + [mintInfo, stakedValue] + ) + + const formattedVanilla = useMemo( + () => + mintInfo && vanillaValue + ? new BigNumber(vanillaValue.toString()) + .shiftedBy(-mintInfo.decimals) + .toFormat(2) + : undefined, + [mintInfo, vanillaValue] + ) + + // There are two buttons available on this UI: + // The Deposit button - available if you have tokens to deposit + // The Join button - available if you have already deposited tokens (you have a Token Owner Record) + // but you may not have all your Voter Weight Records yet. + // This latter case may occur if the DAO changes its configuration and new Voter Weight Records are required. + // For example if a new plugin is added. + const showJoinButton = + userNeedsTokenOwnerRecord || userNeedsVoterWeightRecords + + const join = async () => { + const instructions = await handleRegister() + const transaction = new Transaction() + transaction.add(...instructions) + + await sendTransaction({ + transaction: transaction, + wallet: wallet!, + connection, + signers: [], + sendingMessage: `Registering`, + successMessage: `Registered`, + }) + } + + if (!isReady || formattedTotal === undefined) { + return ( +
+ ) + } + + return ( +
+
+
+
+
Votes
+
+ {formattedTotal ?? 0} +
+ {formattedStaked && + stakedValue?.gtn(0) && + formattedVanilla && + vanillaValue?.gtn(0) && ( + <> +
+ {formattedStaked} from Drift insurance staking +
+
+ {formattedVanilla} from Realms deposit +
+ + )} +
+
+
+ {connected && showJoinButton && ( + + )} + +
+
+
+ ) +} diff --git a/DriftStakeVoterPlugin/constants.ts b/DriftStakeVoterPlugin/constants.ts new file mode 100644 index 0000000000..4a48c341f5 --- /dev/null +++ b/DriftStakeVoterPlugin/constants.ts @@ -0,0 +1,8 @@ +export const DRIFT_STAKE_VOTER_PLUGIN = + 'dVoTE1AJqkZVoE1mPbWcqYPmEEvAUBksHY2NiM2UJQe' + +export const DRIFT_PROGRAM_ID = 'dRiftyHA39MWEi3m9aunc5MzRF1JYuBsbn6VPcn33UH' + +//export const DRIFT_GOVERNANCE_PROGRAM_ID = 'dgov7NC8iaumWw3k8TkmLDybvZBCmd1qwxgLAGAsWxf' + +export const DRIFT_GOVERNANCE_TICKER = 'DRFT' diff --git a/DriftStakeVoterPlugin/driftSdk.ts b/DriftStakeVoterPlugin/driftSdk.ts new file mode 100644 index 0000000000..7341b55590 --- /dev/null +++ b/DriftStakeVoterPlugin/driftSdk.ts @@ -0,0 +1,78 @@ +import { PublicKey } from '@solana/web3.js' +import * as anchor from '@coral-xyz/anchor' +import { BN } from '@coral-xyz/anchor' + +export async function getSpotMarketPublicKey( + programId: PublicKey, + marketIndex: number +): Promise { + return ( + await PublicKey.findProgramAddress( + [ + Buffer.from(anchor.utils.bytes.utf8.encode('spot_market')), + new anchor.BN(marketIndex).toArrayLike(Buffer, 'le', 2), + ], + programId + ) + )[0] +} + +export async function getInsuranceFundVaultPublicKey( + programId: PublicKey, + marketIndex: number +): Promise { + return ( + await PublicKey.findProgramAddress( + [ + Buffer.from(anchor.utils.bytes.utf8.encode('insurance_fund_vault')), + new anchor.BN(marketIndex).toArrayLike(Buffer, 'le', 2), + ], + programId + ) + )[0] +} + +export function getInsuranceFundStakeAccountPublicKey( + programId: PublicKey, + authority: PublicKey, + marketIndex: number +): PublicKey { + return PublicKey.findProgramAddressSync( + [ + Buffer.from(anchor.utils.bytes.utf8.encode('insurance_fund_stake')), + authority.toBuffer(), + new anchor.BN(marketIndex).toArrayLike(Buffer, 'le', 2), + ], + programId + )[0] +} + +const ZERO = new BN(0) +export function unstakeSharesToAmountWithOpenRequest( + nShares: BN, + withdrawRequestShares: BN, + withdrawRequestAmount: BN, + totalIfShares: BN, + insuranceFundVaultBalance: BN +): BN { + let stakedAmount: BN + if (totalIfShares.gt(ZERO)) { + stakedAmount = BN.max( + ZERO, + nShares + .sub(withdrawRequestShares) + .mul(insuranceFundVaultBalance) + .div(totalIfShares) + ) + } else { + stakedAmount = ZERO + } + + const withdrawAmount = BN.min( + withdrawRequestAmount, + withdrawRequestShares.mul(insuranceFundVaultBalance).div(totalIfShares) + ) + const amount = withdrawAmount.add(stakedAmount) + + return amount +} diff --git a/DriftStakeVoterPlugin/idl/drift.ts b/DriftStakeVoterPlugin/idl/drift.ts new file mode 100644 index 0000000000..541e857a0c --- /dev/null +++ b/DriftStakeVoterPlugin/idl/drift.ts @@ -0,0 +1,25907 @@ +export type Drift = { + "version": "2.92.0", + "name": "drift", + "instructions": [ + { + "name": "initializeUser", + "accounts": [ + { + "name": "user", + "isMut": true, + "isSigner": false + }, + { + "name": "userStats", + "isMut": true, + "isSigner": false + }, + { + "name": "state", + "isMut": true, + "isSigner": false + }, + { + "name": "authority", + "isMut": false, + "isSigner": true + }, + { + "name": "payer", + "isMut": true, + "isSigner": true + }, + { + "name": "rent", + "isMut": false, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "subAccountId", + "type": "u16" + }, + { + "name": "name", + "type": { + "array": [ + "u8", + 32 + ] + } + } + ] + }, + { + "name": "initializeUserStats", + "accounts": [ + { + "name": "userStats", + "isMut": true, + "isSigner": false + }, + { + "name": "state", + "isMut": true, + "isSigner": false + }, + { + "name": "authority", + "isMut": false, + "isSigner": true + }, + { + "name": "payer", + "isMut": true, + "isSigner": true + }, + { + "name": "rent", + "isMut": false, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [] + }, + { + "name": "initializeReferrerName", + "accounts": [ + { + "name": "referrerName", + "isMut": true, + "isSigner": false + }, + { + "name": "user", + "isMut": true, + "isSigner": false + }, + { + "name": "userStats", + "isMut": true, + "isSigner": false + }, + { + "name": "authority", + "isMut": false, + "isSigner": true + }, + { + "name": "payer", + "isMut": true, + "isSigner": true + }, + { + "name": "rent", + "isMut": false, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "name", + "type": { + "array": [ + "u8", + 32 + ] + } + } + ] + }, + { + "name": "deposit", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "user", + "isMut": true, + "isSigner": false + }, + { + "name": "userStats", + "isMut": true, + "isSigner": false + }, + { + "name": "authority", + "isMut": false, + "isSigner": true + }, + { + "name": "spotMarketVault", + "isMut": true, + "isSigner": false + }, + { + "name": "userTokenAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "marketIndex", + "type": "u16" + }, + { + "name": "amount", + "type": "u64" + }, + { + "name": "reduceOnly", + "type": "bool" + } + ] + }, + { + "name": "withdraw", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "user", + "isMut": true, + "isSigner": false + }, + { + "name": "userStats", + "isMut": true, + "isSigner": false + }, + { + "name": "authority", + "isMut": false, + "isSigner": true + }, + { + "name": "spotMarketVault", + "isMut": true, + "isSigner": false + }, + { + "name": "driftSigner", + "isMut": false, + "isSigner": false + }, + { + "name": "userTokenAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "marketIndex", + "type": "u16" + }, + { + "name": "amount", + "type": "u64" + }, + { + "name": "reduceOnly", + "type": "bool" + } + ] + }, + { + "name": "transferDeposit", + "accounts": [ + { + "name": "fromUser", + "isMut": true, + "isSigner": false + }, + { + "name": "toUser", + "isMut": true, + "isSigner": false + }, + { + "name": "userStats", + "isMut": true, + "isSigner": false + }, + { + "name": "authority", + "isMut": false, + "isSigner": true + }, + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "spotMarketVault", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "marketIndex", + "type": "u16" + }, + { + "name": "amount", + "type": "u64" + } + ] + }, + { + "name": "placePerpOrder", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "user", + "isMut": true, + "isSigner": false + }, + { + "name": "authority", + "isMut": false, + "isSigner": true + } + ], + "args": [ + { + "name": "params", + "type": { + "defined": "OrderParams" + } + } + ] + }, + { + "name": "cancelOrder", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "user", + "isMut": true, + "isSigner": false + }, + { + "name": "authority", + "isMut": false, + "isSigner": true + } + ], + "args": [ + { + "name": "orderId", + "type": { + "option": "u32" + } + } + ] + }, + { + "name": "cancelOrderByUserId", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "user", + "isMut": true, + "isSigner": false + }, + { + "name": "authority", + "isMut": false, + "isSigner": true + } + ], + "args": [ + { + "name": "userOrderId", + "type": "u8" + } + ] + }, + { + "name": "cancelOrders", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "user", + "isMut": true, + "isSigner": false + }, + { + "name": "authority", + "isMut": false, + "isSigner": true + } + ], + "args": [ + { + "name": "marketType", + "type": { + "option": { + "defined": "MarketType" + } + } + }, + { + "name": "marketIndex", + "type": { + "option": "u16" + } + }, + { + "name": "direction", + "type": { + "option": { + "defined": "PositionDirection" + } + } + } + ] + }, + { + "name": "cancelOrdersByIds", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "user", + "isMut": true, + "isSigner": false + }, + { + "name": "authority", + "isMut": false, + "isSigner": true + } + ], + "args": [ + { + "name": "orderIds", + "type": { + "vec": "u32" + } + } + ] + }, + { + "name": "modifyOrder", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "user", + "isMut": true, + "isSigner": false + }, + { + "name": "authority", + "isMut": false, + "isSigner": true + } + ], + "args": [ + { + "name": "orderId", + "type": { + "option": "u32" + } + }, + { + "name": "modifyOrderParams", + "type": { + "defined": "ModifyOrderParams" + } + } + ] + }, + { + "name": "modifyOrderByUserId", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "user", + "isMut": true, + "isSigner": false + }, + { + "name": "authority", + "isMut": false, + "isSigner": true + } + ], + "args": [ + { + "name": "userOrderId", + "type": "u8" + }, + { + "name": "modifyOrderParams", + "type": { + "defined": "ModifyOrderParams" + } + } + ] + }, + { + "name": "placeAndTakePerpOrder", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "user", + "isMut": true, + "isSigner": false + }, + { + "name": "userStats", + "isMut": true, + "isSigner": false + }, + { + "name": "authority", + "isMut": false, + "isSigner": true + } + ], + "args": [ + { + "name": "params", + "type": { + "defined": "OrderParams" + } + }, + { + "name": "makerOrderId", + "type": { + "option": "u32" + } + } + ] + }, + { + "name": "placeAndMakePerpOrder", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "user", + "isMut": true, + "isSigner": false + }, + { + "name": "userStats", + "isMut": true, + "isSigner": false + }, + { + "name": "taker", + "isMut": true, + "isSigner": false + }, + { + "name": "takerStats", + "isMut": true, + "isSigner": false + }, + { + "name": "authority", + "isMut": false, + "isSigner": true + } + ], + "args": [ + { + "name": "params", + "type": { + "defined": "OrderParams" + } + }, + { + "name": "takerOrderId", + "type": "u32" + } + ] + }, + { + "name": "placeSpotOrder", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "user", + "isMut": true, + "isSigner": false + }, + { + "name": "authority", + "isMut": false, + "isSigner": true + } + ], + "args": [ + { + "name": "params", + "type": { + "defined": "OrderParams" + } + } + ] + }, + { + "name": "placeAndTakeSpotOrder", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "user", + "isMut": true, + "isSigner": false + }, + { + "name": "userStats", + "isMut": true, + "isSigner": false + }, + { + "name": "authority", + "isMut": false, + "isSigner": true + } + ], + "args": [ + { + "name": "params", + "type": { + "defined": "OrderParams" + } + }, + { + "name": "fulfillmentType", + "type": { + "option": { + "defined": "SpotFulfillmentType" + } + } + }, + { + "name": "makerOrderId", + "type": { + "option": "u32" + } + } + ] + }, + { + "name": "placeAndMakeSpotOrder", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "user", + "isMut": true, + "isSigner": false + }, + { + "name": "userStats", + "isMut": true, + "isSigner": false + }, + { + "name": "taker", + "isMut": true, + "isSigner": false + }, + { + "name": "takerStats", + "isMut": true, + "isSigner": false + }, + { + "name": "authority", + "isMut": false, + "isSigner": true + } + ], + "args": [ + { + "name": "params", + "type": { + "defined": "OrderParams" + } + }, + { + "name": "takerOrderId", + "type": "u32" + }, + { + "name": "fulfillmentType", + "type": { + "option": { + "defined": "SpotFulfillmentType" + } + } + } + ] + }, + { + "name": "placeOrders", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "user", + "isMut": true, + "isSigner": false + }, + { + "name": "authority", + "isMut": false, + "isSigner": true + } + ], + "args": [ + { + "name": "params", + "type": { + "vec": { + "defined": "OrderParams" + } + } + } + ] + }, + { + "name": "beginSwap", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "user", + "isMut": true, + "isSigner": false + }, + { + "name": "userStats", + "isMut": true, + "isSigner": false + }, + { + "name": "authority", + "isMut": false, + "isSigner": true + }, + { + "name": "outSpotMarketVault", + "isMut": true, + "isSigner": false + }, + { + "name": "inSpotMarketVault", + "isMut": true, + "isSigner": false + }, + { + "name": "outTokenAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "inTokenAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "driftSigner", + "isMut": false, + "isSigner": false + }, + { + "name": "instructions", + "isMut": false, + "isSigner": false, + "docs": [ + "Instructions Sysvar for instruction introspection" + ] + } + ], + "args": [ + { + "name": "inMarketIndex", + "type": "u16" + }, + { + "name": "outMarketIndex", + "type": "u16" + }, + { + "name": "amountIn", + "type": "u64" + } + ] + }, + { + "name": "endSwap", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "user", + "isMut": true, + "isSigner": false + }, + { + "name": "userStats", + "isMut": true, + "isSigner": false + }, + { + "name": "authority", + "isMut": false, + "isSigner": true + }, + { + "name": "outSpotMarketVault", + "isMut": true, + "isSigner": false + }, + { + "name": "inSpotMarketVault", + "isMut": true, + "isSigner": false + }, + { + "name": "outTokenAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "inTokenAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "driftSigner", + "isMut": false, + "isSigner": false + }, + { + "name": "instructions", + "isMut": false, + "isSigner": false, + "docs": [ + "Instructions Sysvar for instruction introspection" + ] + } + ], + "args": [ + { + "name": "inMarketIndex", + "type": "u16" + }, + { + "name": "outMarketIndex", + "type": "u16" + }, + { + "name": "limitPrice", + "type": { + "option": "u64" + } + }, + { + "name": "reduceOnly", + "type": { + "option": { + "defined": "SwapReduceOnly" + } + } + } + ] + }, + { + "name": "addPerpLpShares", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "user", + "isMut": true, + "isSigner": false + }, + { + "name": "authority", + "isMut": false, + "isSigner": true + } + ], + "args": [ + { + "name": "nShares", + "type": "u64" + }, + { + "name": "marketIndex", + "type": "u16" + } + ] + }, + { + "name": "removePerpLpShares", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "user", + "isMut": true, + "isSigner": false + }, + { + "name": "authority", + "isMut": false, + "isSigner": true + } + ], + "args": [ + { + "name": "sharesToBurn", + "type": "u64" + }, + { + "name": "marketIndex", + "type": "u16" + } + ] + }, + { + "name": "removePerpLpSharesInExpiringMarket", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "user", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "sharesToBurn", + "type": "u64" + }, + { + "name": "marketIndex", + "type": "u16" + } + ] + }, + { + "name": "updateUserName", + "accounts": [ + { + "name": "user", + "isMut": true, + "isSigner": false + }, + { + "name": "authority", + "isMut": false, + "isSigner": true + } + ], + "args": [ + { + "name": "subAccountId", + "type": "u16" + }, + { + "name": "name", + "type": { + "array": [ + "u8", + 32 + ] + } + } + ] + }, + { + "name": "updateUserCustomMarginRatio", + "accounts": [ + { + "name": "user", + "isMut": true, + "isSigner": false + }, + { + "name": "authority", + "isMut": false, + "isSigner": true + } + ], + "args": [ + { + "name": "subAccountId", + "type": "u16" + }, + { + "name": "marginRatio", + "type": "u32" + } + ] + }, + { + "name": "updateUserMarginTradingEnabled", + "accounts": [ + { + "name": "user", + "isMut": true, + "isSigner": false + }, + { + "name": "authority", + "isMut": false, + "isSigner": true + } + ], + "args": [ + { + "name": "subAccountId", + "type": "u16" + }, + { + "name": "marginTradingEnabled", + "type": "bool" + } + ] + }, + { + "name": "updateUserDelegate", + "accounts": [ + { + "name": "user", + "isMut": true, + "isSigner": false + }, + { + "name": "authority", + "isMut": false, + "isSigner": true + } + ], + "args": [ + { + "name": "subAccountId", + "type": "u16" + }, + { + "name": "delegate", + "type": "publicKey" + } + ] + }, + { + "name": "updateUserReduceOnly", + "accounts": [ + { + "name": "user", + "isMut": true, + "isSigner": false + }, + { + "name": "authority", + "isMut": false, + "isSigner": true + } + ], + "args": [ + { + "name": "subAccountId", + "type": "u16" + }, + { + "name": "reduceOnly", + "type": "bool" + } + ] + }, + { + "name": "updateUserAdvancedLp", + "accounts": [ + { + "name": "user", + "isMut": true, + "isSigner": false + }, + { + "name": "authority", + "isMut": false, + "isSigner": true + } + ], + "args": [ + { + "name": "subAccountId", + "type": "u16" + }, + { + "name": "advancedLp", + "type": "bool" + } + ] + }, + { + "name": "deleteUser", + "accounts": [ + { + "name": "user", + "isMut": true, + "isSigner": false + }, + { + "name": "userStats", + "isMut": true, + "isSigner": false + }, + { + "name": "state", + "isMut": true, + "isSigner": false + }, + { + "name": "authority", + "isMut": false, + "isSigner": true + } + ], + "args": [] + }, + { + "name": "reclaimRent", + "accounts": [ + { + "name": "user", + "isMut": true, + "isSigner": false + }, + { + "name": "userStats", + "isMut": true, + "isSigner": false + }, + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "authority", + "isMut": false, + "isSigner": true + }, + { + "name": "rent", + "isMut": false, + "isSigner": false + } + ], + "args": [] + }, + { + "name": "fillPerpOrder", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "authority", + "isMut": false, + "isSigner": true + }, + { + "name": "filler", + "isMut": true, + "isSigner": false + }, + { + "name": "fillerStats", + "isMut": true, + "isSigner": false + }, + { + "name": "user", + "isMut": true, + "isSigner": false + }, + { + "name": "userStats", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "orderId", + "type": { + "option": "u32" + } + }, + { + "name": "makerOrderId", + "type": { + "option": "u32" + } + } + ] + }, + { + "name": "revertFill", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "authority", + "isMut": false, + "isSigner": true + }, + { + "name": "filler", + "isMut": true, + "isSigner": false + }, + { + "name": "fillerStats", + "isMut": true, + "isSigner": false + } + ], + "args": [] + }, + { + "name": "fillSpotOrder", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "authority", + "isMut": false, + "isSigner": true + }, + { + "name": "filler", + "isMut": true, + "isSigner": false + }, + { + "name": "fillerStats", + "isMut": true, + "isSigner": false + }, + { + "name": "user", + "isMut": true, + "isSigner": false + }, + { + "name": "userStats", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "orderId", + "type": { + "option": "u32" + } + }, + { + "name": "fulfillmentType", + "type": { + "option": { + "defined": "SpotFulfillmentType" + } + } + }, + { + "name": "makerOrderId", + "type": { + "option": "u32" + } + } + ] + }, + { + "name": "triggerOrder", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "authority", + "isMut": false, + "isSigner": true + }, + { + "name": "filler", + "isMut": true, + "isSigner": false + }, + { + "name": "user", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "orderId", + "type": "u32" + } + ] + }, + { + "name": "forceCancelOrders", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "authority", + "isMut": false, + "isSigner": true + }, + { + "name": "filler", + "isMut": true, + "isSigner": false + }, + { + "name": "user", + "isMut": true, + "isSigner": false + } + ], + "args": [] + }, + { + "name": "updateUserIdle", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "authority", + "isMut": false, + "isSigner": true + }, + { + "name": "filler", + "isMut": true, + "isSigner": false + }, + { + "name": "user", + "isMut": true, + "isSigner": false + } + ], + "args": [] + }, + { + "name": "updateUserOpenOrdersCount", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "authority", + "isMut": false, + "isSigner": true + }, + { + "name": "filler", + "isMut": true, + "isSigner": false + }, + { + "name": "user", + "isMut": true, + "isSigner": false + } + ], + "args": [] + }, + { + "name": "adminDisableUpdatePerpBidAskTwap", + "accounts": [ + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "userStats", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "disable", + "type": "bool" + } + ] + }, + { + "name": "settlePnl", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "user", + "isMut": true, + "isSigner": false + }, + { + "name": "authority", + "isMut": false, + "isSigner": true + }, + { + "name": "spotMarketVault", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "marketIndex", + "type": "u16" + } + ] + }, + { + "name": "settleMultiplePnls", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "user", + "isMut": true, + "isSigner": false + }, + { + "name": "authority", + "isMut": false, + "isSigner": true + }, + { + "name": "spotMarketVault", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "marketIndexes", + "type": { + "vec": "u16" + } + }, + { + "name": "mode", + "type": { + "defined": "SettlePnlMode" + } + } + ] + }, + { + "name": "settleFundingPayment", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "user", + "isMut": true, + "isSigner": false + } + ], + "args": [] + }, + { + "name": "settleLp", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "user", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "marketIndex", + "type": "u16" + } + ] + }, + { + "name": "settleExpiredMarket", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "authority", + "isMut": false, + "isSigner": true + } + ], + "args": [ + { + "name": "marketIndex", + "type": "u16" + } + ] + }, + { + "name": "liquidatePerp", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "authority", + "isMut": false, + "isSigner": true + }, + { + "name": "liquidator", + "isMut": true, + "isSigner": false + }, + { + "name": "liquidatorStats", + "isMut": true, + "isSigner": false + }, + { + "name": "user", + "isMut": true, + "isSigner": false + }, + { + "name": "userStats", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "marketIndex", + "type": "u16" + }, + { + "name": "liquidatorMaxBaseAssetAmount", + "type": "u64" + }, + { + "name": "limitPrice", + "type": { + "option": "u64" + } + } + ] + }, + { + "name": "liquidatePerpWithFill", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "authority", + "isMut": false, + "isSigner": true + }, + { + "name": "liquidator", + "isMut": true, + "isSigner": false + }, + { + "name": "liquidatorStats", + "isMut": true, + "isSigner": false + }, + { + "name": "user", + "isMut": true, + "isSigner": false + }, + { + "name": "userStats", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "marketIndex", + "type": "u16" + } + ] + }, + { + "name": "liquidateSpot", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "authority", + "isMut": false, + "isSigner": true + }, + { + "name": "liquidator", + "isMut": true, + "isSigner": false + }, + { + "name": "liquidatorStats", + "isMut": true, + "isSigner": false + }, + { + "name": "user", + "isMut": true, + "isSigner": false + }, + { + "name": "userStats", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "assetMarketIndex", + "type": "u16" + }, + { + "name": "liabilityMarketIndex", + "type": "u16" + }, + { + "name": "liquidatorMaxLiabilityTransfer", + "type": "u128" + }, + { + "name": "limitPrice", + "type": { + "option": "u64" + } + } + ] + }, + { + "name": "liquidateBorrowForPerpPnl", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "authority", + "isMut": false, + "isSigner": true + }, + { + "name": "liquidator", + "isMut": true, + "isSigner": false + }, + { + "name": "liquidatorStats", + "isMut": true, + "isSigner": false + }, + { + "name": "user", + "isMut": true, + "isSigner": false + }, + { + "name": "userStats", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "perpMarketIndex", + "type": "u16" + }, + { + "name": "spotMarketIndex", + "type": "u16" + }, + { + "name": "liquidatorMaxLiabilityTransfer", + "type": "u128" + }, + { + "name": "limitPrice", + "type": { + "option": "u64" + } + } + ] + }, + { + "name": "liquidatePerpPnlForDeposit", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "authority", + "isMut": false, + "isSigner": true + }, + { + "name": "liquidator", + "isMut": true, + "isSigner": false + }, + { + "name": "liquidatorStats", + "isMut": true, + "isSigner": false + }, + { + "name": "user", + "isMut": true, + "isSigner": false + }, + { + "name": "userStats", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "perpMarketIndex", + "type": "u16" + }, + { + "name": "spotMarketIndex", + "type": "u16" + }, + { + "name": "liquidatorMaxPnlTransfer", + "type": "u128" + }, + { + "name": "limitPrice", + "type": { + "option": "u64" + } + } + ] + }, + { + "name": "setUserStatusToBeingLiquidated", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "user", + "isMut": true, + "isSigner": false + } + ], + "args": [] + }, + { + "name": "resolvePerpPnlDeficit", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "authority", + "isMut": false, + "isSigner": true + }, + { + "name": "spotMarketVault", + "isMut": true, + "isSigner": false + }, + { + "name": "insuranceFundVault", + "isMut": true, + "isSigner": false + }, + { + "name": "driftSigner", + "isMut": false, + "isSigner": false + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "spotMarketIndex", + "type": "u16" + }, + { + "name": "perpMarketIndex", + "type": "u16" + } + ] + }, + { + "name": "resolvePerpBankruptcy", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "authority", + "isMut": false, + "isSigner": true + }, + { + "name": "liquidator", + "isMut": true, + "isSigner": false + }, + { + "name": "liquidatorStats", + "isMut": true, + "isSigner": false + }, + { + "name": "user", + "isMut": true, + "isSigner": false + }, + { + "name": "userStats", + "isMut": true, + "isSigner": false + }, + { + "name": "spotMarketVault", + "isMut": true, + "isSigner": false + }, + { + "name": "insuranceFundVault", + "isMut": true, + "isSigner": false + }, + { + "name": "driftSigner", + "isMut": false, + "isSigner": false + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "quoteSpotMarketIndex", + "type": "u16" + }, + { + "name": "marketIndex", + "type": "u16" + } + ] + }, + { + "name": "resolveSpotBankruptcy", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "authority", + "isMut": false, + "isSigner": true + }, + { + "name": "liquidator", + "isMut": true, + "isSigner": false + }, + { + "name": "liquidatorStats", + "isMut": true, + "isSigner": false + }, + { + "name": "user", + "isMut": true, + "isSigner": false + }, + { + "name": "userStats", + "isMut": true, + "isSigner": false + }, + { + "name": "spotMarketVault", + "isMut": true, + "isSigner": false + }, + { + "name": "insuranceFundVault", + "isMut": true, + "isSigner": false + }, + { + "name": "driftSigner", + "isMut": false, + "isSigner": false + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "marketIndex", + "type": "u16" + } + ] + }, + { + "name": "settleRevenueToInsuranceFund", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "spotMarket", + "isMut": true, + "isSigner": false + }, + { + "name": "spotMarketVault", + "isMut": true, + "isSigner": false + }, + { + "name": "driftSigner", + "isMut": false, + "isSigner": false + }, + { + "name": "insuranceFundVault", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "spotMarketIndex", + "type": "u16" + } + ] + }, + { + "name": "updateFundingRate", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "perpMarket", + "isMut": true, + "isSigner": false + }, + { + "name": "oracle", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "marketIndex", + "type": "u16" + } + ] + }, + { + "name": "updatePrelaunchOracle", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "perpMarket", + "isMut": false, + "isSigner": false + }, + { + "name": "oracle", + "isMut": true, + "isSigner": false + } + ], + "args": [] + }, + { + "name": "updatePerpBidAskTwap", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "perpMarket", + "isMut": true, + "isSigner": false + }, + { + "name": "oracle", + "isMut": false, + "isSigner": false + }, + { + "name": "keeperStats", + "isMut": false, + "isSigner": false + }, + { + "name": "authority", + "isMut": false, + "isSigner": true + } + ], + "args": [] + }, + { + "name": "updateSpotMarketCumulativeInterest", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "spotMarket", + "isMut": true, + "isSigner": false + }, + { + "name": "oracle", + "isMut": false, + "isSigner": false + }, + { + "name": "spotMarketVault", + "isMut": false, + "isSigner": false + } + ], + "args": [] + }, + { + "name": "updateAmms", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "authority", + "isMut": false, + "isSigner": true + } + ], + "args": [ + { + "name": "marketIndexes", + "type": { + "array": [ + "u16", + 5 + ] + } + } + ] + }, + { + "name": "updateSpotMarketExpiry", + "accounts": [ + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "spotMarket", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "expiryTs", + "type": "i64" + } + ] + }, + { + "name": "updateUserQuoteAssetInsuranceStake", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "spotMarket", + "isMut": true, + "isSigner": false + }, + { + "name": "insuranceFundStake", + "isMut": true, + "isSigner": false + }, + { + "name": "userStats", + "isMut": true, + "isSigner": false + }, + { + "name": "signer", + "isMut": false, + "isSigner": true + }, + { + "name": "insuranceFundVault", + "isMut": true, + "isSigner": false + } + ], + "args": [] + }, + { + "name": "updateUserGovTokenInsuranceStake", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "spotMarket", + "isMut": true, + "isSigner": false + }, + { + "name": "insuranceFundStake", + "isMut": true, + "isSigner": false + }, + { + "name": "userStats", + "isMut": true, + "isSigner": false + }, + { + "name": "signer", + "isMut": false, + "isSigner": true + }, + { + "name": "insuranceFundVault", + "isMut": true, + "isSigner": false + } + ], + "args": [] + }, + { + "name": "initializeInsuranceFundStake", + "accounts": [ + { + "name": "spotMarket", + "isMut": false, + "isSigner": false + }, + { + "name": "insuranceFundStake", + "isMut": true, + "isSigner": false + }, + { + "name": "userStats", + "isMut": true, + "isSigner": false + }, + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "authority", + "isMut": false, + "isSigner": true + }, + { + "name": "payer", + "isMut": true, + "isSigner": true + }, + { + "name": "rent", + "isMut": false, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "marketIndex", + "type": "u16" + } + ] + }, + { + "name": "addInsuranceFundStake", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "spotMarket", + "isMut": true, + "isSigner": false + }, + { + "name": "insuranceFundStake", + "isMut": true, + "isSigner": false + }, + { + "name": "userStats", + "isMut": true, + "isSigner": false + }, + { + "name": "authority", + "isMut": false, + "isSigner": true + }, + { + "name": "spotMarketVault", + "isMut": true, + "isSigner": false + }, + { + "name": "insuranceFundVault", + "isMut": true, + "isSigner": false + }, + { + "name": "driftSigner", + "isMut": false, + "isSigner": false + }, + { + "name": "userTokenAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "marketIndex", + "type": "u16" + }, + { + "name": "amount", + "type": "u64" + } + ] + }, + { + "name": "requestRemoveInsuranceFundStake", + "accounts": [ + { + "name": "spotMarket", + "isMut": true, + "isSigner": false + }, + { + "name": "insuranceFundStake", + "isMut": true, + "isSigner": false + }, + { + "name": "userStats", + "isMut": true, + "isSigner": false + }, + { + "name": "authority", + "isMut": false, + "isSigner": true + }, + { + "name": "insuranceFundVault", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "marketIndex", + "type": "u16" + }, + { + "name": "amount", + "type": "u64" + } + ] + }, + { + "name": "cancelRequestRemoveInsuranceFundStake", + "accounts": [ + { + "name": "spotMarket", + "isMut": true, + "isSigner": false + }, + { + "name": "insuranceFundStake", + "isMut": true, + "isSigner": false + }, + { + "name": "userStats", + "isMut": true, + "isSigner": false + }, + { + "name": "authority", + "isMut": false, + "isSigner": true + }, + { + "name": "insuranceFundVault", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "marketIndex", + "type": "u16" + } + ] + }, + { + "name": "removeInsuranceFundStake", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "spotMarket", + "isMut": true, + "isSigner": false + }, + { + "name": "insuranceFundStake", + "isMut": true, + "isSigner": false + }, + { + "name": "userStats", + "isMut": true, + "isSigner": false + }, + { + "name": "authority", + "isMut": false, + "isSigner": true + }, + { + "name": "insuranceFundVault", + "isMut": true, + "isSigner": false + }, + { + "name": "driftSigner", + "isMut": false, + "isSigner": false + }, + { + "name": "userTokenAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "marketIndex", + "type": "u16" + } + ] + }, + { + "name": "transferProtocolIfShares", + "accounts": [ + { + "name": "signer", + "isMut": false, + "isSigner": true + }, + { + "name": "transferConfig", + "isMut": true, + "isSigner": false + }, + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "spotMarket", + "isMut": true, + "isSigner": false + }, + { + "name": "insuranceFundStake", + "isMut": true, + "isSigner": false + }, + { + "name": "userStats", + "isMut": true, + "isSigner": false + }, + { + "name": "authority", + "isMut": false, + "isSigner": true + }, + { + "name": "insuranceFundVault", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "marketIndex", + "type": "u16" + }, + { + "name": "shares", + "type": "u128" + } + ] + }, + { + "name": "updatePythPullOracle", + "accounts": [ + { + "name": "keeper", + "isMut": true, + "isSigner": true + }, + { + "name": "pythSolanaReceiver", + "isMut": false, + "isSigner": false + }, + { + "name": "encodedVaa", + "isMut": false, + "isSigner": false + }, + { + "name": "priceFeed", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "feedId", + "type": { + "array": [ + "u8", + 32 + ] + } + }, + { + "name": "params", + "type": "bytes" + } + ] + }, + { + "name": "postPythPullOracleUpdateAtomic", + "accounts": [ + { + "name": "keeper", + "isMut": true, + "isSigner": true + }, + { + "name": "pythSolanaReceiver", + "isMut": false, + "isSigner": false + }, + { + "name": "guardianSet", + "isMut": false, + "isSigner": false + }, + { + "name": "priceFeed", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "feedId", + "type": { + "array": [ + "u8", + 32 + ] + } + }, + { + "name": "params", + "type": "bytes" + } + ] + }, + { + "name": "postMultiPythPullOracleUpdatesAtomic", + "accounts": [ + { + "name": "keeper", + "isMut": true, + "isSigner": true + }, + { + "name": "pythSolanaReceiver", + "isMut": false, + "isSigner": false + }, + { + "name": "guardianSet", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "params", + "type": "bytes" + } + ] + }, + { + "name": "initialize", + "accounts": [ + { + "name": "admin", + "isMut": true, + "isSigner": true + }, + { + "name": "state", + "isMut": true, + "isSigner": false + }, + { + "name": "quoteAssetMint", + "isMut": false, + "isSigner": false + }, + { + "name": "driftSigner", + "isMut": false, + "isSigner": false + }, + { + "name": "rent", + "isMut": false, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [] + }, + { + "name": "initializeSpotMarket", + "accounts": [ + { + "name": "spotMarket", + "isMut": true, + "isSigner": false + }, + { + "name": "spotMarketMint", + "isMut": false, + "isSigner": false + }, + { + "name": "spotMarketVault", + "isMut": true, + "isSigner": false + }, + { + "name": "insuranceFundVault", + "isMut": true, + "isSigner": false + }, + { + "name": "driftSigner", + "isMut": false, + "isSigner": false + }, + { + "name": "state", + "isMut": true, + "isSigner": false + }, + { + "name": "oracle", + "isMut": false, + "isSigner": false + }, + { + "name": "admin", + "isMut": true, + "isSigner": true + }, + { + "name": "rent", + "isMut": false, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "optimalUtilization", + "type": "u32" + }, + { + "name": "optimalBorrowRate", + "type": "u32" + }, + { + "name": "maxBorrowRate", + "type": "u32" + }, + { + "name": "oracleSource", + "type": { + "defined": "OracleSource" + } + }, + { + "name": "initialAssetWeight", + "type": "u32" + }, + { + "name": "maintenanceAssetWeight", + "type": "u32" + }, + { + "name": "initialLiabilityWeight", + "type": "u32" + }, + { + "name": "maintenanceLiabilityWeight", + "type": "u32" + }, + { + "name": "imfFactor", + "type": "u32" + }, + { + "name": "liquidatorFee", + "type": "u32" + }, + { + "name": "ifLiquidationFee", + "type": "u32" + }, + { + "name": "activeStatus", + "type": "bool" + }, + { + "name": "assetTier", + "type": { + "defined": "AssetTier" + } + }, + { + "name": "scaleInitialAssetWeightStart", + "type": "u64" + }, + { + "name": "withdrawGuardThreshold", + "type": "u64" + }, + { + "name": "orderTickSize", + "type": "u64" + }, + { + "name": "orderStepSize", + "type": "u64" + }, + { + "name": "ifTotalFactor", + "type": "u32" + }, + { + "name": "name", + "type": { + "array": [ + "u8", + 32 + ] + } + } + ] + }, + { + "name": "deleteInitializedSpotMarket", + "accounts": [ + { + "name": "admin", + "isMut": true, + "isSigner": true + }, + { + "name": "state", + "isMut": true, + "isSigner": false + }, + { + "name": "spotMarket", + "isMut": true, + "isSigner": false + }, + { + "name": "spotMarketVault", + "isMut": true, + "isSigner": false + }, + { + "name": "insuranceFundVault", + "isMut": true, + "isSigner": false + }, + { + "name": "driftSigner", + "isMut": false, + "isSigner": false + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "marketIndex", + "type": "u16" + } + ] + }, + { + "name": "initializeSerumFulfillmentConfig", + "accounts": [ + { + "name": "baseSpotMarket", + "isMut": false, + "isSigner": false + }, + { + "name": "quoteSpotMarket", + "isMut": false, + "isSigner": false + }, + { + "name": "state", + "isMut": true, + "isSigner": false + }, + { + "name": "serumProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "serumMarket", + "isMut": false, + "isSigner": false + }, + { + "name": "serumOpenOrders", + "isMut": true, + "isSigner": false + }, + { + "name": "driftSigner", + "isMut": false, + "isSigner": false + }, + { + "name": "serumFulfillmentConfig", + "isMut": true, + "isSigner": false + }, + { + "name": "admin", + "isMut": true, + "isSigner": true + }, + { + "name": "rent", + "isMut": false, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "marketIndex", + "type": "u16" + } + ] + }, + { + "name": "updateSerumFulfillmentConfigStatus", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "serumFulfillmentConfig", + "isMut": true, + "isSigner": false + }, + { + "name": "admin", + "isMut": true, + "isSigner": true + } + ], + "args": [ + { + "name": "status", + "type": { + "defined": "SpotFulfillmentConfigStatus" + } + } + ] + }, + { + "name": "initializeOpenbookV2FulfillmentConfig", + "accounts": [ + { + "name": "baseSpotMarket", + "isMut": false, + "isSigner": false + }, + { + "name": "quoteSpotMarket", + "isMut": false, + "isSigner": false + }, + { + "name": "state", + "isMut": true, + "isSigner": false + }, + { + "name": "openbookV2Program", + "isMut": false, + "isSigner": false + }, + { + "name": "openbookV2Market", + "isMut": false, + "isSigner": false + }, + { + "name": "driftSigner", + "isMut": false, + "isSigner": false + }, + { + "name": "openbookV2FulfillmentConfig", + "isMut": true, + "isSigner": false + }, + { + "name": "admin", + "isMut": true, + "isSigner": true + }, + { + "name": "rent", + "isMut": false, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "marketIndex", + "type": "u16" + } + ] + }, + { + "name": "openbookV2FulfillmentConfigStatus", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "openbookV2FulfillmentConfig", + "isMut": true, + "isSigner": false + }, + { + "name": "admin", + "isMut": true, + "isSigner": true + } + ], + "args": [ + { + "name": "status", + "type": { + "defined": "SpotFulfillmentConfigStatus" + } + } + ] + }, + { + "name": "initializePhoenixFulfillmentConfig", + "accounts": [ + { + "name": "baseSpotMarket", + "isMut": false, + "isSigner": false + }, + { + "name": "quoteSpotMarket", + "isMut": false, + "isSigner": false + }, + { + "name": "state", + "isMut": true, + "isSigner": false + }, + { + "name": "phoenixProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "phoenixMarket", + "isMut": false, + "isSigner": false + }, + { + "name": "driftSigner", + "isMut": false, + "isSigner": false + }, + { + "name": "phoenixFulfillmentConfig", + "isMut": true, + "isSigner": false + }, + { + "name": "admin", + "isMut": true, + "isSigner": true + }, + { + "name": "rent", + "isMut": false, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "marketIndex", + "type": "u16" + } + ] + }, + { + "name": "phoenixFulfillmentConfigStatus", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "phoenixFulfillmentConfig", + "isMut": true, + "isSigner": false + }, + { + "name": "admin", + "isMut": true, + "isSigner": true + } + ], + "args": [ + { + "name": "status", + "type": { + "defined": "SpotFulfillmentConfigStatus" + } + } + ] + }, + { + "name": "updateSerumVault", + "accounts": [ + { + "name": "state", + "isMut": true, + "isSigner": false + }, + { + "name": "admin", + "isMut": true, + "isSigner": true + }, + { + "name": "srmVault", + "isMut": false, + "isSigner": false + } + ], + "args": [] + }, + { + "name": "initializePerpMarket", + "accounts": [ + { + "name": "admin", + "isMut": true, + "isSigner": true + }, + { + "name": "state", + "isMut": true, + "isSigner": false + }, + { + "name": "perpMarket", + "isMut": true, + "isSigner": false + }, + { + "name": "oracle", + "isMut": false, + "isSigner": false + }, + { + "name": "rent", + "isMut": false, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "marketIndex", + "type": "u16" + }, + { + "name": "ammBaseAssetReserve", + "type": "u128" + }, + { + "name": "ammQuoteAssetReserve", + "type": "u128" + }, + { + "name": "ammPeriodicity", + "type": "i64" + }, + { + "name": "ammPegMultiplier", + "type": "u128" + }, + { + "name": "oracleSource", + "type": { + "defined": "OracleSource" + } + }, + { + "name": "contractTier", + "type": { + "defined": "ContractTier" + } + }, + { + "name": "marginRatioInitial", + "type": "u32" + }, + { + "name": "marginRatioMaintenance", + "type": "u32" + }, + { + "name": "liquidatorFee", + "type": "u32" + }, + { + "name": "ifLiquidationFee", + "type": "u32" + }, + { + "name": "imfFactor", + "type": "u32" + }, + { + "name": "activeStatus", + "type": "bool" + }, + { + "name": "baseSpread", + "type": "u32" + }, + { + "name": "maxSpread", + "type": "u32" + }, + { + "name": "maxOpenInterest", + "type": "u128" + }, + { + "name": "maxRevenueWithdrawPerPeriod", + "type": "u64" + }, + { + "name": "quoteMaxInsurance", + "type": "u64" + }, + { + "name": "orderStepSize", + "type": "u64" + }, + { + "name": "orderTickSize", + "type": "u64" + }, + { + "name": "minOrderSize", + "type": "u64" + }, + { + "name": "concentrationCoefScale", + "type": "u128" + }, + { + "name": "curveUpdateIntensity", + "type": "u8" + }, + { + "name": "ammJitIntensity", + "type": "u8" + }, + { + "name": "name", + "type": { + "array": [ + "u8", + 32 + ] + } + } + ] + }, + { + "name": "initializePredictionMarket", + "accounts": [ + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "perpMarket", + "isMut": true, + "isSigner": false + } + ], + "args": [] + }, + { + "name": "deleteInitializedPerpMarket", + "accounts": [ + { + "name": "admin", + "isMut": true, + "isSigner": true + }, + { + "name": "state", + "isMut": true, + "isSigner": false + }, + { + "name": "perpMarket", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "marketIndex", + "type": "u16" + } + ] + }, + { + "name": "moveAmmPrice", + "accounts": [ + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "perpMarket", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "baseAssetReserve", + "type": "u128" + }, + { + "name": "quoteAssetReserve", + "type": "u128" + }, + { + "name": "sqrtK", + "type": "u128" + } + ] + }, + { + "name": "recenterPerpMarketAmm", + "accounts": [ + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "perpMarket", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "pegMultiplier", + "type": "u128" + }, + { + "name": "sqrtK", + "type": "u128" + } + ] + }, + { + "name": "updatePerpMarketAmmSummaryStats", + "accounts": [ + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "perpMarket", + "isMut": true, + "isSigner": false + }, + { + "name": "spotMarket", + "isMut": false, + "isSigner": false + }, + { + "name": "oracle", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "params", + "type": { + "defined": "UpdatePerpMarketSummaryStatsParams" + } + } + ] + }, + { + "name": "updatePerpMarketExpiry", + "accounts": [ + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "perpMarket", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "expiryTs", + "type": "i64" + } + ] + }, + { + "name": "settleExpiredMarketPoolsToRevenuePool", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "spotMarket", + "isMut": true, + "isSigner": false + }, + { + "name": "perpMarket", + "isMut": true, + "isSigner": false + } + ], + "args": [] + }, + { + "name": "depositIntoPerpMarketFeePool", + "accounts": [ + { + "name": "state", + "isMut": true, + "isSigner": false + }, + { + "name": "perpMarket", + "isMut": true, + "isSigner": false + }, + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "sourceVault", + "isMut": true, + "isSigner": false + }, + { + "name": "driftSigner", + "isMut": false, + "isSigner": false + }, + { + "name": "quoteSpotMarket", + "isMut": true, + "isSigner": false + }, + { + "name": "spotMarketVault", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "amount", + "type": "u64" + } + ] + }, + { + "name": "depositIntoSpotMarketVault", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "spotMarket", + "isMut": true, + "isSigner": false + }, + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "sourceVault", + "isMut": true, + "isSigner": false + }, + { + "name": "spotMarketVault", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "amount", + "type": "u64" + } + ] + }, + { + "name": "depositIntoSpotMarketRevenuePool", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "spotMarket", + "isMut": true, + "isSigner": false + }, + { + "name": "authority", + "isMut": true, + "isSigner": true + }, + { + "name": "spotMarketVault", + "isMut": true, + "isSigner": false + }, + { + "name": "userTokenAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "amount", + "type": "u64" + } + ] + }, + { + "name": "repegAmmCurve", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "perpMarket", + "isMut": true, + "isSigner": false + }, + { + "name": "oracle", + "isMut": false, + "isSigner": false + }, + { + "name": "admin", + "isMut": false, + "isSigner": true + } + ], + "args": [ + { + "name": "newPegCandidate", + "type": "u128" + } + ] + }, + { + "name": "updatePerpMarketAmmOracleTwap", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "perpMarket", + "isMut": true, + "isSigner": false + }, + { + "name": "oracle", + "isMut": false, + "isSigner": false + }, + { + "name": "admin", + "isMut": false, + "isSigner": true + } + ], + "args": [] + }, + { + "name": "resetPerpMarketAmmOracleTwap", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "perpMarket", + "isMut": true, + "isSigner": false + }, + { + "name": "oracle", + "isMut": false, + "isSigner": false + }, + { + "name": "admin", + "isMut": false, + "isSigner": true + } + ], + "args": [] + }, + { + "name": "updateK", + "accounts": [ + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "perpMarket", + "isMut": true, + "isSigner": false + }, + { + "name": "oracle", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "sqrtK", + "type": "u128" + } + ] + }, + { + "name": "updatePerpMarketMarginRatio", + "accounts": [ + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "perpMarket", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "marginRatioInitial", + "type": "u32" + }, + { + "name": "marginRatioMaintenance", + "type": "u32" + } + ] + }, + { + "name": "updatePerpMarketFundingPeriod", + "accounts": [ + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "perpMarket", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "fundingPeriod", + "type": "i64" + } + ] + }, + { + "name": "updatePerpMarketMaxImbalances", + "accounts": [ + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "perpMarket", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "unrealizedMaxImbalance", + "type": "u64" + }, + { + "name": "maxRevenueWithdrawPerPeriod", + "type": "u64" + }, + { + "name": "quoteMaxInsurance", + "type": "u64" + } + ] + }, + { + "name": "updatePerpMarketLiquidationFee", + "accounts": [ + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "perpMarket", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "liquidatorFee", + "type": "u32" + }, + { + "name": "ifLiquidationFee", + "type": "u32" + } + ] + }, + { + "name": "updateInsuranceFundUnstakingPeriod", + "accounts": [ + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "spotMarket", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "insuranceFundUnstakingPeriod", + "type": "i64" + } + ] + }, + { + "name": "updateSpotMarketLiquidationFee", + "accounts": [ + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "spotMarket", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "liquidatorFee", + "type": "u32" + }, + { + "name": "ifLiquidationFee", + "type": "u32" + } + ] + }, + { + "name": "updateWithdrawGuardThreshold", + "accounts": [ + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "spotMarket", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "withdrawGuardThreshold", + "type": "u64" + } + ] + }, + { + "name": "updateSpotMarketIfFactor", + "accounts": [ + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "spotMarket", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "spotMarketIndex", + "type": "u16" + }, + { + "name": "userIfFactor", + "type": "u32" + }, + { + "name": "totalIfFactor", + "type": "u32" + } + ] + }, + { + "name": "updateSpotMarketRevenueSettlePeriod", + "accounts": [ + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "spotMarket", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "revenueSettlePeriod", + "type": "i64" + } + ] + }, + { + "name": "updateSpotMarketStatus", + "accounts": [ + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "spotMarket", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "status", + "type": { + "defined": "MarketStatus" + } + } + ] + }, + { + "name": "updateSpotMarketPausedOperations", + "accounts": [ + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "spotMarket", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "pausedOperations", + "type": "u8" + } + ] + }, + { + "name": "updateSpotMarketAssetTier", + "accounts": [ + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "spotMarket", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "assetTier", + "type": { + "defined": "AssetTier" + } + } + ] + }, + { + "name": "updateSpotMarketMarginWeights", + "accounts": [ + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "spotMarket", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "initialAssetWeight", + "type": "u32" + }, + { + "name": "maintenanceAssetWeight", + "type": "u32" + }, + { + "name": "initialLiabilityWeight", + "type": "u32" + }, + { + "name": "maintenanceLiabilityWeight", + "type": "u32" + }, + { + "name": "imfFactor", + "type": "u32" + } + ] + }, + { + "name": "updateSpotMarketBorrowRate", + "accounts": [ + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "spotMarket", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "optimalUtilization", + "type": "u32" + }, + { + "name": "optimalBorrowRate", + "type": "u32" + }, + { + "name": "maxBorrowRate", + "type": "u32" + }, + { + "name": "minBorrowRate", + "type": { + "option": "u8" + } + } + ] + }, + { + "name": "updateSpotMarketMaxTokenDeposits", + "accounts": [ + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "spotMarket", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "maxTokenDeposits", + "type": "u64" + } + ] + }, + { + "name": "updateSpotMarketMaxTokenBorrows", + "accounts": [ + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "spotMarket", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "maxTokenBorrowsFraction", + "type": "u16" + } + ] + }, + { + "name": "updateSpotMarketScaleInitialAssetWeightStart", + "accounts": [ + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "spotMarket", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "scaleInitialAssetWeightStart", + "type": "u64" + } + ] + }, + { + "name": "updateSpotMarketOracle", + "accounts": [ + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "spotMarket", + "isMut": true, + "isSigner": false + }, + { + "name": "oracle", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "oracle", + "type": "publicKey" + }, + { + "name": "oracleSource", + "type": { + "defined": "OracleSource" + } + } + ] + }, + { + "name": "updateSpotMarketStepSizeAndTickSize", + "accounts": [ + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "spotMarket", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "stepSize", + "type": "u64" + }, + { + "name": "tickSize", + "type": "u64" + } + ] + }, + { + "name": "updateSpotMarketMinOrderSize", + "accounts": [ + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "spotMarket", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "orderSize", + "type": "u64" + } + ] + }, + { + "name": "updateSpotMarketOrdersEnabled", + "accounts": [ + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "spotMarket", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "ordersEnabled", + "type": "bool" + } + ] + }, + { + "name": "updateSpotMarketIfPausedOperations", + "accounts": [ + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "spotMarket", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "pausedOperations", + "type": "u8" + } + ] + }, + { + "name": "updateSpotMarketName", + "accounts": [ + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "spotMarket", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "name", + "type": { + "array": [ + "u8", + 32 + ] + } + } + ] + }, + { + "name": "updatePerpMarketStatus", + "accounts": [ + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "perpMarket", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "status", + "type": { + "defined": "MarketStatus" + } + } + ] + }, + { + "name": "updatePerpMarketPausedOperations", + "accounts": [ + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "perpMarket", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "pausedOperations", + "type": "u8" + } + ] + }, + { + "name": "updatePerpMarketContractTier", + "accounts": [ + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "perpMarket", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "contractTier", + "type": { + "defined": "ContractTier" + } + } + ] + }, + { + "name": "updatePerpMarketImfFactor", + "accounts": [ + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "perpMarket", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "imfFactor", + "type": "u32" + }, + { + "name": "unrealizedPnlImfFactor", + "type": "u32" + } + ] + }, + { + "name": "updatePerpMarketUnrealizedAssetWeight", + "accounts": [ + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "perpMarket", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "unrealizedInitialAssetWeight", + "type": "u32" + }, + { + "name": "unrealizedMaintenanceAssetWeight", + "type": "u32" + } + ] + }, + { + "name": "updatePerpMarketConcentrationCoef", + "accounts": [ + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "perpMarket", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "concentrationScale", + "type": "u128" + } + ] + }, + { + "name": "updatePerpMarketCurveUpdateIntensity", + "accounts": [ + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "perpMarket", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "curveUpdateIntensity", + "type": "u8" + } + ] + }, + { + "name": "updatePerpMarketTargetBaseAssetAmountPerLp", + "accounts": [ + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "perpMarket", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "targetBaseAssetAmountPerLp", + "type": "i32" + } + ] + }, + { + "name": "updatePerpMarketPerLpBase", + "accounts": [ + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "perpMarket", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "perLpBase", + "type": "i8" + } + ] + }, + { + "name": "updateLpCooldownTime", + "accounts": [ + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "state", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "lpCooldownTime", + "type": "u64" + } + ] + }, + { + "name": "updatePerpFeeStructure", + "accounts": [ + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "state", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "feeStructure", + "type": { + "defined": "FeeStructure" + } + } + ] + }, + { + "name": "updateSpotFeeStructure", + "accounts": [ + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "state", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "feeStructure", + "type": { + "defined": "FeeStructure" + } + } + ] + }, + { + "name": "updateInitialPctToLiquidate", + "accounts": [ + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "state", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "initialPctToLiquidate", + "type": "u16" + } + ] + }, + { + "name": "updateLiquidationDuration", + "accounts": [ + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "state", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "liquidationDuration", + "type": "u8" + } + ] + }, + { + "name": "updateLiquidationMarginBufferRatio", + "accounts": [ + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "state", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "liquidationMarginBufferRatio", + "type": "u32" + } + ] + }, + { + "name": "updateOracleGuardRails", + "accounts": [ + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "state", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "oracleGuardRails", + "type": { + "defined": "OracleGuardRails" + } + } + ] + }, + { + "name": "updateStateSettlementDuration", + "accounts": [ + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "state", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "settlementDuration", + "type": "u16" + } + ] + }, + { + "name": "updateStateMaxNumberOfSubAccounts", + "accounts": [ + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "state", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "maxNumberOfSubAccounts", + "type": "u16" + } + ] + }, + { + "name": "updateStateMaxInitializeUserFee", + "accounts": [ + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "state", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "maxInitializeUserFee", + "type": "u16" + } + ] + }, + { + "name": "updatePerpMarketOracle", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "perpMarket", + "isMut": true, + "isSigner": false + }, + { + "name": "oracle", + "isMut": false, + "isSigner": false + }, + { + "name": "admin", + "isMut": false, + "isSigner": true + } + ], + "args": [ + { + "name": "oracle", + "type": "publicKey" + }, + { + "name": "oracleSource", + "type": { + "defined": "OracleSource" + } + } + ] + }, + { + "name": "updatePerpMarketBaseSpread", + "accounts": [ + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "perpMarket", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "baseSpread", + "type": "u32" + } + ] + }, + { + "name": "updateAmmJitIntensity", + "accounts": [ + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "perpMarket", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "ammJitIntensity", + "type": "u8" + } + ] + }, + { + "name": "updatePerpMarketMaxSpread", + "accounts": [ + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "perpMarket", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "maxSpread", + "type": "u32" + } + ] + }, + { + "name": "updatePerpMarketStepSizeAndTickSize", + "accounts": [ + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "perpMarket", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "stepSize", + "type": "u64" + }, + { + "name": "tickSize", + "type": "u64" + } + ] + }, + { + "name": "updatePerpMarketName", + "accounts": [ + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "perpMarket", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "name", + "type": { + "array": [ + "u8", + 32 + ] + } + } + ] + }, + { + "name": "updatePerpMarketMinOrderSize", + "accounts": [ + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "perpMarket", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "orderSize", + "type": "u64" + } + ] + }, + { + "name": "updatePerpMarketMaxSlippageRatio", + "accounts": [ + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "perpMarket", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "maxSlippageRatio", + "type": "u16" + } + ] + }, + { + "name": "updatePerpMarketMaxFillReserveFraction", + "accounts": [ + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "perpMarket", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "maxFillReserveFraction", + "type": "u16" + } + ] + }, + { + "name": "updatePerpMarketMaxOpenInterest", + "accounts": [ + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "perpMarket", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "maxOpenInterest", + "type": "u128" + } + ] + }, + { + "name": "updatePerpMarketNumberOfUsers", + "accounts": [ + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "perpMarket", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "numberOfUsers", + "type": { + "option": "u32" + } + }, + { + "name": "numberOfUsersWithBase", + "type": { + "option": "u32" + } + } + ] + }, + { + "name": "updatePerpMarketFeeAdjustment", + "accounts": [ + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "perpMarket", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "feeAdjustment", + "type": "i16" + } + ] + }, + { + "name": "updateSpotMarketFeeAdjustment", + "accounts": [ + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "spotMarket", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "feeAdjustment", + "type": "i16" + } + ] + }, + { + "name": "updatePerpMarketFuel", + "accounts": [ + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "perpMarket", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "fuelBoostTaker", + "type": { + "option": "u8" + } + }, + { + "name": "fuelBoostMaker", + "type": { + "option": "u8" + } + }, + { + "name": "fuelBoostPosition", + "type": { + "option": "u8" + } + } + ] + }, + { + "name": "updateSpotMarketFuel", + "accounts": [ + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "spotMarket", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "fuelBoostDeposits", + "type": { + "option": "u8" + } + }, + { + "name": "fuelBoostBorrows", + "type": { + "option": "u8" + } + }, + { + "name": "fuelBoostTaker", + "type": { + "option": "u8" + } + }, + { + "name": "fuelBoostMaker", + "type": { + "option": "u8" + } + }, + { + "name": "fuelBoostInsurance", + "type": { + "option": "u8" + } + } + ] + }, + { + "name": "initUserFuel", + "accounts": [ + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "user", + "isMut": true, + "isSigner": false + }, + { + "name": "userStats", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "fuelBoostDeposits", + "type": { + "option": "u32" + } + }, + { + "name": "fuelBoostBorrows", + "type": { + "option": "u32" + } + }, + { + "name": "fuelBoostTaker", + "type": { + "option": "u32" + } + }, + { + "name": "fuelBoostMaker", + "type": { + "option": "u32" + } + }, + { + "name": "fuelBoostInsurance", + "type": { + "option": "u32" + } + } + ] + }, + { + "name": "updateAdmin", + "accounts": [ + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "state", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "admin", + "type": "publicKey" + } + ] + }, + { + "name": "updateWhitelistMint", + "accounts": [ + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "state", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "whitelistMint", + "type": "publicKey" + } + ] + }, + { + "name": "updateDiscountMint", + "accounts": [ + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "state", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "discountMint", + "type": "publicKey" + } + ] + }, + { + "name": "updateExchangeStatus", + "accounts": [ + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "state", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "exchangeStatus", + "type": "u8" + } + ] + }, + { + "name": "updatePerpAuctionDuration", + "accounts": [ + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "state", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "minPerpAuctionDuration", + "type": "u8" + } + ] + }, + { + "name": "updateSpotAuctionDuration", + "accounts": [ + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "state", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "defaultSpotAuctionDuration", + "type": "u8" + } + ] + }, + { + "name": "initializeProtocolIfSharesTransferConfig", + "accounts": [ + { + "name": "admin", + "isMut": true, + "isSigner": true + }, + { + "name": "protocolIfSharesTransferConfig", + "isMut": true, + "isSigner": false + }, + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "rent", + "isMut": false, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [] + }, + { + "name": "updateProtocolIfSharesTransferConfig", + "accounts": [ + { + "name": "admin", + "isMut": true, + "isSigner": true + }, + { + "name": "protocolIfSharesTransferConfig", + "isMut": true, + "isSigner": false + }, + { + "name": "state", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "whitelistedSigners", + "type": { + "option": { + "array": [ + "publicKey", + 4 + ] + } + } + }, + { + "name": "maxTransferPerEpoch", + "type": { + "option": "u128" + } + } + ] + }, + { + "name": "initializePrelaunchOracle", + "accounts": [ + { + "name": "admin", + "isMut": true, + "isSigner": true + }, + { + "name": "prelaunchOracle", + "isMut": true, + "isSigner": false + }, + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "rent", + "isMut": false, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "params", + "type": { + "defined": "PrelaunchOracleParams" + } + } + ] + }, + { + "name": "updatePrelaunchOracleParams", + "accounts": [ + { + "name": "admin", + "isMut": true, + "isSigner": true + }, + { + "name": "prelaunchOracle", + "isMut": true, + "isSigner": false + }, + { + "name": "perpMarket", + "isMut": true, + "isSigner": false + }, + { + "name": "state", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "params", + "type": { + "defined": "PrelaunchOracleParams" + } + } + ] + }, + { + "name": "deletePrelaunchOracle", + "accounts": [ + { + "name": "admin", + "isMut": true, + "isSigner": true + }, + { + "name": "prelaunchOracle", + "isMut": true, + "isSigner": false + }, + { + "name": "perpMarket", + "isMut": false, + "isSigner": false + }, + { + "name": "state", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "perpMarketIndex", + "type": "u16" + } + ] + }, + { + "name": "initializePythPullOracle", + "accounts": [ + { + "name": "admin", + "isMut": true, + "isSigner": true + }, + { + "name": "pythSolanaReceiver", + "isMut": false, + "isSigner": false + }, + { + "name": "priceFeed", + "isMut": true, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "state", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "feedId", + "type": { + "array": [ + "u8", + 32 + ] + } + } + ] + } + ], + "accounts": [ + { + "name": "OpenbookV2FulfillmentConfig", + "type": { + "kind": "struct", + "fields": [ + { + "name": "pubkey", + "type": "publicKey" + }, + { + "name": "openbookV2ProgramId", + "type": "publicKey" + }, + { + "name": "openbookV2Market", + "type": "publicKey" + }, + { + "name": "openbookV2MarketAuthority", + "type": "publicKey" + }, + { + "name": "openbookV2EventHeap", + "type": "publicKey" + }, + { + "name": "openbookV2Bids", + "type": "publicKey" + }, + { + "name": "openbookV2Asks", + "type": "publicKey" + }, + { + "name": "openbookV2BaseVault", + "type": "publicKey" + }, + { + "name": "openbookV2QuoteVault", + "type": "publicKey" + }, + { + "name": "marketIndex", + "type": "u16" + }, + { + "name": "fulfillmentType", + "type": { + "defined": "SpotFulfillmentType" + } + }, + { + "name": "status", + "type": { + "defined": "SpotFulfillmentConfigStatus" + } + }, + { + "name": "padding", + "type": { + "array": [ + "u8", + 4 + ] + } + } + ] + } + }, + { + "name": "PhoenixV1FulfillmentConfig", + "type": { + "kind": "struct", + "fields": [ + { + "name": "pubkey", + "type": "publicKey" + }, + { + "name": "phoenixProgramId", + "type": "publicKey" + }, + { + "name": "phoenixLogAuthority", + "type": "publicKey" + }, + { + "name": "phoenixMarket", + "type": "publicKey" + }, + { + "name": "phoenixBaseVault", + "type": "publicKey" + }, + { + "name": "phoenixQuoteVault", + "type": "publicKey" + }, + { + "name": "marketIndex", + "type": "u16" + }, + { + "name": "fulfillmentType", + "type": { + "defined": "SpotFulfillmentType" + } + }, + { + "name": "status", + "type": { + "defined": "SpotFulfillmentConfigStatus" + } + }, + { + "name": "padding", + "type": { + "array": [ + "u8", + 4 + ] + } + } + ] + } + }, + { + "name": "SerumV3FulfillmentConfig", + "type": { + "kind": "struct", + "fields": [ + { + "name": "pubkey", + "type": "publicKey" + }, + { + "name": "serumProgramId", + "type": "publicKey" + }, + { + "name": "serumMarket", + "type": "publicKey" + }, + { + "name": "serumRequestQueue", + "type": "publicKey" + }, + { + "name": "serumEventQueue", + "type": "publicKey" + }, + { + "name": "serumBids", + "type": "publicKey" + }, + { + "name": "serumAsks", + "type": "publicKey" + }, + { + "name": "serumBaseVault", + "type": "publicKey" + }, + { + "name": "serumQuoteVault", + "type": "publicKey" + }, + { + "name": "serumOpenOrders", + "type": "publicKey" + }, + { + "name": "serumSignerNonce", + "type": "u64" + }, + { + "name": "marketIndex", + "type": "u16" + }, + { + "name": "fulfillmentType", + "type": { + "defined": "SpotFulfillmentType" + } + }, + { + "name": "status", + "type": { + "defined": "SpotFulfillmentConfigStatus" + } + }, + { + "name": "padding", + "type": { + "array": [ + "u8", + 4 + ] + } + } + ] + } + }, + { + "name": "insuranceFundStake", + "type": { + "kind": "struct", + "fields": [ + { + "name": "authority", + "type": "publicKey" + }, + { + "name": "ifShares", + "type": "u128" + }, + { + "name": "lastWithdrawRequestShares", + "type": "u128" + }, + { + "name": "ifBase", + "type": "u128" + }, + { + "name": "lastValidTs", + "type": "i64" + }, + { + "name": "lastWithdrawRequestValue", + "type": "u64" + }, + { + "name": "lastWithdrawRequestTs", + "type": "i64" + }, + { + "name": "costBasis", + "type": "i64" + }, + { + "name": "marketIndex", + "type": "u16" + }, + { + "name": "padding", + "type": { + "array": [ + "u8", + 14 + ] + } + } + ] + } + }, + { + "name": "ProtocolIfSharesTransferConfig", + "type": { + "kind": "struct", + "fields": [ + { + "name": "whitelistedSigners", + "type": { + "array": [ + "publicKey", + 4 + ] + } + }, + { + "name": "maxTransferPerEpoch", + "type": "u128" + }, + { + "name": "currentEpochTransfer", + "type": "u128" + }, + { + "name": "nextEpochTs", + "type": "i64" + }, + { + "name": "padding", + "type": { + "array": [ + "u128", + 8 + ] + } + } + ] + } + }, + { + "name": "PrelaunchOracle", + "type": { + "kind": "struct", + "fields": [ + { + "name": "price", + "type": "i64" + }, + { + "name": "maxPrice", + "type": "i64" + }, + { + "name": "confidence", + "type": "u64" + }, + { + "name": "lastUpdateSlot", + "type": "u64" + }, + { + "name": "ammLastUpdateSlot", + "type": "u64" + }, + { + "name": "perpMarketIndex", + "type": "u16" + }, + { + "name": "padding", + "type": { + "array": [ + "u8", + 70 + ] + } + } + ] + } + }, + { + "name": "PerpMarket", + "type": { + "kind": "struct", + "fields": [ + { + "name": "pubkey", + "docs": [ + "The perp market's address. It is a pda of the market index" + ], + "type": "publicKey" + }, + { + "name": "amm", + "docs": [ + "The automated market maker" + ], + "type": { + "defined": "AMM" + } + }, + { + "name": "pnlPool", + "docs": [ + "The market's pnl pool. When users settle negative pnl, the balance increases.", + "When users settle positive pnl, the balance decreases. Can not go negative." + ], + "type": { + "defined": "PoolBalance" + } + }, + { + "name": "name", + "docs": [ + "Encoded display name for the perp market e.g. SOL-PERP" + ], + "type": { + "array": [ + "u8", + 32 + ] + } + }, + { + "name": "insuranceClaim", + "docs": [ + "The perp market's claim on the insurance fund" + ], + "type": { + "defined": "InsuranceClaim" + } + }, + { + "name": "unrealizedPnlMaxImbalance", + "docs": [ + "The max pnl imbalance before positive pnl asset weight is discounted", + "pnl imbalance is the difference between long and short pnl. When it's greater than 0,", + "the amm has negative pnl and the initial asset weight for positive pnl is discounted", + "precision = QUOTE_PRECISION" + ], + "type": "u64" + }, + { + "name": "expiryTs", + "docs": [ + "The ts when the market will be expired. Only set if market is in reduce only mode" + ], + "type": "i64" + }, + { + "name": "expiryPrice", + "docs": [ + "The price at which positions will be settled. Only set if market is expired", + "precision = PRICE_PRECISION" + ], + "type": "i64" + }, + { + "name": "nextFillRecordId", + "docs": [ + "Every trade has a fill record id. This is the next id to be used" + ], + "type": "u64" + }, + { + "name": "nextFundingRateRecordId", + "docs": [ + "Every funding rate update has a record id. This is the next id to be used" + ], + "type": "u64" + }, + { + "name": "nextCurveRecordId", + "docs": [ + "Every amm k updated has a record id. This is the next id to be used" + ], + "type": "u64" + }, + { + "name": "imfFactor", + "docs": [ + "The initial margin fraction factor. Used to increase margin ratio for large positions", + "precision: MARGIN_PRECISION" + ], + "type": "u32" + }, + { + "name": "unrealizedPnlImfFactor", + "docs": [ + "The imf factor for unrealized pnl. Used to discount asset weight for large positive pnl", + "precision: MARGIN_PRECISION" + ], + "type": "u32" + }, + { + "name": "liquidatorFee", + "docs": [ + "The fee the liquidator is paid for taking over perp position", + "precision: LIQUIDATOR_FEE_PRECISION" + ], + "type": "u32" + }, + { + "name": "ifLiquidationFee", + "docs": [ + "The fee the insurance fund receives from liquidation", + "precision: LIQUIDATOR_FEE_PRECISION" + ], + "type": "u32" + }, + { + "name": "marginRatioInitial", + "docs": [ + "The margin ratio which determines how much collateral is required to open a position", + "e.g. margin ratio of .1 means a user must have $100 of total collateral to open a $1000 position", + "precision: MARGIN_PRECISION" + ], + "type": "u32" + }, + { + "name": "marginRatioMaintenance", + "docs": [ + "The margin ratio which determines when a user will be liquidated", + "e.g. margin ratio of .05 means a user must have $50 of total collateral to maintain a $1000 position", + "else they will be liquidated", + "precision: MARGIN_PRECISION" + ], + "type": "u32" + }, + { + "name": "unrealizedPnlInitialAssetWeight", + "docs": [ + "The initial asset weight for positive pnl. Negative pnl always has an asset weight of 1", + "precision: SPOT_WEIGHT_PRECISION" + ], + "type": "u32" + }, + { + "name": "unrealizedPnlMaintenanceAssetWeight", + "docs": [ + "The maintenance asset weight for positive pnl. Negative pnl always has an asset weight of 1", + "precision: SPOT_WEIGHT_PRECISION" + ], + "type": "u32" + }, + { + "name": "numberOfUsersWithBase", + "docs": [ + "number of users in a position (base)" + ], + "type": "u32" + }, + { + "name": "numberOfUsers", + "docs": [ + "number of users in a position (pnl) or pnl (quote)" + ], + "type": "u32" + }, + { + "name": "marketIndex", + "type": "u16" + }, + { + "name": "status", + "docs": [ + "Whether a market is active, reduce only, expired, etc", + "Affects whether users can open/close positions" + ], + "type": { + "defined": "MarketStatus" + } + }, + { + "name": "contractType", + "docs": [ + "Currently only Perpetual markets are supported" + ], + "type": { + "defined": "ContractType" + } + }, + { + "name": "contractTier", + "docs": [ + "The contract tier determines how much insurance a market can receive, with more speculative markets receiving less insurance", + "It also influences the order perp markets can be liquidated, with less speculative markets being liquidated first" + ], + "type": { + "defined": "ContractTier" + } + }, + { + "name": "pausedOperations", + "type": "u8" + }, + { + "name": "quoteSpotMarketIndex", + "docs": [ + "The spot market that pnl is settled in" + ], + "type": "u16" + }, + { + "name": "feeAdjustment", + "docs": [ + "Between -100 and 100, represents what % to increase/decrease the fee by", + "E.g. if this is -50 and the fee is 5bps, the new fee will be 2.5bps", + "if this is 50 and the fee is 5bps, the new fee will be 7.5bps" + ], + "type": "i16" + }, + { + "name": "fuelBoostPosition", + "docs": [ + "fuel multiplier for perp funding", + "precision: 10" + ], + "type": "u8" + }, + { + "name": "fuelBoostTaker", + "docs": [ + "fuel multiplier for perp taker", + "precision: 10" + ], + "type": "u8" + }, + { + "name": "fuelBoostMaker", + "docs": [ + "fuel multiplier for perp maker", + "precision: 10" + ], + "type": "u8" + }, + { + "name": "padding", + "type": { + "array": [ + "u8", + 43 + ] + } + } + ] + } + }, + { + "name": "spotMarket", + "type": { + "kind": "struct", + "fields": [ + { + "name": "pubkey", + "docs": [ + "The address of the spot market. It is a pda of the market index" + ], + "type": "publicKey" + }, + { + "name": "oracle", + "docs": [ + "The oracle used to price the markets deposits/borrows" + ], + "type": "publicKey" + }, + { + "name": "mint", + "docs": [ + "The token mint of the market" + ], + "type": "publicKey" + }, + { + "name": "vault", + "docs": [ + "The vault used to store the market's deposits", + "The amount in the vault should be equal to or greater than deposits - borrows" + ], + "type": "publicKey" + }, + { + "name": "name", + "docs": [ + "The encoded display name for the market e.g. SOL" + ], + "type": { + "array": [ + "u8", + 32 + ] + } + }, + { + "name": "historicalOracleData", + "type": { + "defined": "HistoricalOracleData" + } + }, + { + "name": "historicalIndexData", + "type": { + "defined": "HistoricalIndexData" + } + }, + { + "name": "revenuePool", + "docs": [ + "Revenue the protocol has collected in this markets token", + "e.g. for SOL-PERP, funds can be settled in usdc and will flow into the USDC revenue pool" + ], + "type": { + "defined": "PoolBalance" + } + }, + { + "name": "spotFeePool", + "docs": [ + "The fees collected from swaps between this market and the quote market", + "Is settled to the quote markets revenue pool" + ], + "type": { + "defined": "PoolBalance" + } + }, + { + "name": "insuranceFund", + "docs": [ + "Details on the insurance fund covering bankruptcies in this markets token", + "Covers bankruptcies for borrows with this markets token and perps settling in this markets token" + ], + "type": { + "defined": "InsuranceFund" + } + }, + { + "name": "totalSpotFee", + "docs": [ + "The total spot fees collected for this market", + "precision: QUOTE_PRECISION" + ], + "type": "u128" + }, + { + "name": "depositBalance", + "docs": [ + "The sum of the scaled balances for deposits across users and pool balances", + "To convert to the deposit token amount, multiply by the cumulative deposit interest", + "precision: SPOT_BALANCE_PRECISION" + ], + "type": "u128" + }, + { + "name": "borrowBalance", + "docs": [ + "The sum of the scaled balances for borrows across users and pool balances", + "To convert to the borrow token amount, multiply by the cumulative borrow interest", + "precision: SPOT_BALANCE_PRECISION" + ], + "type": "u128" + }, + { + "name": "cumulativeDepositInterest", + "docs": [ + "The cumulative interest earned by depositors", + "Used to calculate the deposit token amount from the deposit balance", + "precision: SPOT_CUMULATIVE_INTEREST_PRECISION" + ], + "type": "u128" + }, + { + "name": "cumulativeBorrowInterest", + "docs": [ + "The cumulative interest earned by borrowers", + "Used to calculate the borrow token amount from the borrow balance", + "precision: SPOT_CUMULATIVE_INTEREST_PRECISION" + ], + "type": "u128" + }, + { + "name": "totalSocialLoss", + "docs": [ + "The total socialized loss from borrows, in the mint's token", + "precision: token mint precision" + ], + "type": "u128" + }, + { + "name": "totalQuoteSocialLoss", + "docs": [ + "The total socialized loss from borrows, in the quote market's token", + "preicision: QUOTE_PRECISION" + ], + "type": "u128" + }, + { + "name": "withdrawGuardThreshold", + "docs": [ + "no withdraw limits/guards when deposits below this threshold", + "precision: token mint precision" + ], + "type": "u64" + }, + { + "name": "maxTokenDeposits", + "docs": [ + "The max amount of token deposits in this market", + "0 if there is no limit", + "precision: token mint precision" + ], + "type": "u64" + }, + { + "name": "depositTokenTwap", + "docs": [ + "24hr average of deposit token amount", + "precision: token mint precision" + ], + "type": "u64" + }, + { + "name": "borrowTokenTwap", + "docs": [ + "24hr average of borrow token amount", + "precision: token mint precision" + ], + "type": "u64" + }, + { + "name": "utilizationTwap", + "docs": [ + "24hr average of utilization", + "which is borrow amount over token amount", + "precision: SPOT_UTILIZATION_PRECISION" + ], + "type": "u64" + }, + { + "name": "lastInterestTs", + "docs": [ + "Last time the cumulative deposit and borrow interest was updated" + ], + "type": "u64" + }, + { + "name": "lastTwapTs", + "docs": [ + "Last time the deposit/borrow/utilization averages were updated" + ], + "type": "u64" + }, + { + "name": "expiryTs", + "docs": [ + "The time the market is set to expire. Only set if market is in reduce only mode" + ], + "type": "i64" + }, + { + "name": "orderStepSize", + "docs": [ + "Spot orders must be a multiple of the step size", + "precision: token mint precision" + ], + "type": "u64" + }, + { + "name": "orderTickSize", + "docs": [ + "Spot orders must be a multiple of the tick size", + "precision: PRICE_PRECISION" + ], + "type": "u64" + }, + { + "name": "minOrderSize", + "docs": [ + "The minimum order size", + "precision: token mint precision" + ], + "type": "u64" + }, + { + "name": "maxPositionSize", + "docs": [ + "The maximum spot position size", + "if the limit is 0, there is no limit", + "precision: token mint precision" + ], + "type": "u64" + }, + { + "name": "nextFillRecordId", + "docs": [ + "Every spot trade has a fill record id. This is the next id to use" + ], + "type": "u64" + }, + { + "name": "nextDepositRecordId", + "docs": [ + "Every deposit has a deposit record id. This is the next id to use" + ], + "type": "u64" + }, + { + "name": "initialAssetWeight", + "docs": [ + "The initial asset weight used to calculate a deposits contribution to a users initial total collateral", + "e.g. if the asset weight is .8, $100 of deposits contributes $80 to the users initial total collateral", + "precision: SPOT_WEIGHT_PRECISION" + ], + "type": "u32" + }, + { + "name": "maintenanceAssetWeight", + "docs": [ + "The maintenance asset weight used to calculate a deposits contribution to a users maintenance total collateral", + "e.g. if the asset weight is .9, $100 of deposits contributes $90 to the users maintenance total collateral", + "precision: SPOT_WEIGHT_PRECISION" + ], + "type": "u32" + }, + { + "name": "initialLiabilityWeight", + "docs": [ + "The initial liability weight used to calculate a borrows contribution to a users initial margin requirement", + "e.g. if the liability weight is .9, $100 of borrows contributes $90 to the users initial margin requirement", + "precision: SPOT_WEIGHT_PRECISION" + ], + "type": "u32" + }, + { + "name": "maintenanceLiabilityWeight", + "docs": [ + "The maintenance liability weight used to calculate a borrows contribution to a users maintenance margin requirement", + "e.g. if the liability weight is .8, $100 of borrows contributes $80 to the users maintenance margin requirement", + "precision: SPOT_WEIGHT_PRECISION" + ], + "type": "u32" + }, + { + "name": "imfFactor", + "docs": [ + "The initial margin fraction factor. Used to increase liability weight/decrease asset weight for large positions", + "precision: MARGIN_PRECISION" + ], + "type": "u32" + }, + { + "name": "liquidatorFee", + "docs": [ + "The fee the liquidator is paid for taking over borrow/deposit", + "precision: LIQUIDATOR_FEE_PRECISION" + ], + "type": "u32" + }, + { + "name": "ifLiquidationFee", + "docs": [ + "The fee the insurance fund receives from liquidation", + "precision: LIQUIDATOR_FEE_PRECISION" + ], + "type": "u32" + }, + { + "name": "optimalUtilization", + "docs": [ + "The optimal utilization rate for this market.", + "Used to determine the markets borrow rate", + "precision: SPOT_UTILIZATION_PRECISION" + ], + "type": "u32" + }, + { + "name": "optimalBorrowRate", + "docs": [ + "The borrow rate for this market when the market has optimal utilization", + "precision: SPOT_RATE_PRECISION" + ], + "type": "u32" + }, + { + "name": "maxBorrowRate", + "docs": [ + "The borrow rate for this market when the market has 1000 utilization", + "precision: SPOT_RATE_PRECISION" + ], + "type": "u32" + }, + { + "name": "decimals", + "docs": [ + "The market's token mint's decimals. To from decimals to a precision, 10^decimals" + ], + "type": "u32" + }, + { + "name": "marketIndex", + "type": "u16" + }, + { + "name": "ordersEnabled", + "docs": [ + "Whether or not spot trading is enabled" + ], + "type": "bool" + }, + { + "name": "oracleSource", + "type": { + "defined": "OracleSource" + } + }, + { + "name": "status", + "type": { + "defined": "MarketStatus" + } + }, + { + "name": "assetTier", + "docs": [ + "The asset tier affects how a deposit can be used as collateral and the priority for a borrow being liquidated" + ], + "type": { + "defined": "AssetTier" + } + }, + { + "name": "pausedOperations", + "type": "u8" + }, + { + "name": "ifPausedOperations", + "type": "u8" + }, + { + "name": "feeAdjustment", + "type": "i16" + }, + { + "name": "maxTokenBorrowsFraction", + "docs": [ + "What fraction of max_token_deposits", + "disabled when 0, 1 => 1/10000 => .01% of max_token_deposits", + "precision: X/10000" + ], + "type": "u16" + }, + { + "name": "flashLoanAmount", + "docs": [ + "For swaps, the amount of token loaned out in the begin_swap ix", + "precision: token mint precision" + ], + "type": "u64" + }, + { + "name": "flashLoanInitialTokenAmount", + "docs": [ + "For swaps, the amount in the users token account in the begin_swap ix", + "Used to calculate how much of the token left the system in end_swap ix", + "precision: token mint precision" + ], + "type": "u64" + }, + { + "name": "totalSwapFee", + "docs": [ + "The total fees received from swaps", + "precision: token mint precision" + ], + "type": "u64" + }, + { + "name": "scaleInitialAssetWeightStart", + "docs": [ + "When to begin scaling down the initial asset weight", + "disabled when 0", + "precision: QUOTE_PRECISION" + ], + "type": "u64" + }, + { + "name": "minBorrowRate", + "docs": [ + "The min borrow rate for this market when the market regardless of utilization", + "1 => 1/200 => .5%", + "precision: X/200" + ], + "type": "u8" + }, + { + "name": "fuelBoostDeposits", + "docs": [ + "fuel multiplier for spot deposits", + "precision: 10" + ], + "type": "u8" + }, + { + "name": "fuelBoostBorrows", + "docs": [ + "fuel multiplier for spot borrows", + "precision: 10" + ], + "type": "u8" + }, + { + "name": "fuelBoostTaker", + "docs": [ + "fuel multiplier for spot taker", + "precision: 10" + ], + "type": "u8" + }, + { + "name": "fuelBoostMaker", + "docs": [ + "fuel multiplier for spot maker", + "precision: 10" + ], + "type": "u8" + }, + { + "name": "fuelBoostInsurance", + "docs": [ + "fuel multiplier for spot insurance stake", + "precision: 10" + ], + "type": "u8" + }, + { + "name": "tokenProgram", + "type": "u8" + }, + { + "name": "padding", + "type": { + "array": [ + "u8", + 41 + ] + } + } + ] + } + }, + { + "name": "State", + "type": { + "kind": "struct", + "fields": [ + { + "name": "admin", + "type": "publicKey" + }, + { + "name": "whitelistMint", + "type": "publicKey" + }, + { + "name": "discountMint", + "type": "publicKey" + }, + { + "name": "signer", + "type": "publicKey" + }, + { + "name": "srmVault", + "type": "publicKey" + }, + { + "name": "perpFeeStructure", + "type": { + "defined": "FeeStructure" + } + }, + { + "name": "spotFeeStructure", + "type": { + "defined": "FeeStructure" + } + }, + { + "name": "oracleGuardRails", + "type": { + "defined": "OracleGuardRails" + } + }, + { + "name": "numberOfAuthorities", + "type": "u64" + }, + { + "name": "numberOfSubAccounts", + "type": "u64" + }, + { + "name": "lpCooldownTime", + "type": "u64" + }, + { + "name": "liquidationMarginBufferRatio", + "type": "u32" + }, + { + "name": "settlementDuration", + "type": "u16" + }, + { + "name": "numberOfMarkets", + "type": "u16" + }, + { + "name": "numberOfSpotMarkets", + "type": "u16" + }, + { + "name": "signerNonce", + "type": "u8" + }, + { + "name": "minPerpAuctionDuration", + "type": "u8" + }, + { + "name": "defaultMarketOrderTimeInForce", + "type": "u8" + }, + { + "name": "defaultSpotAuctionDuration", + "type": "u8" + }, + { + "name": "exchangeStatus", + "type": "u8" + }, + { + "name": "liquidationDuration", + "type": "u8" + }, + { + "name": "initialPctToLiquidate", + "type": "u16" + }, + { + "name": "maxNumberOfSubAccounts", + "type": "u16" + }, + { + "name": "maxInitializeUserFee", + "type": "u16" + }, + { + "name": "padding", + "type": { + "array": [ + "u8", + 10 + ] + } + } + ] + } + }, + { + "name": "User", + "type": { + "kind": "struct", + "fields": [ + { + "name": "authority", + "docs": [ + "The owner/authority of the account" + ], + "type": "publicKey" + }, + { + "name": "delegate", + "docs": [ + "An addresses that can control the account on the authority's behalf. Has limited power, cant withdraw" + ], + "type": "publicKey" + }, + { + "name": "name", + "docs": [ + "Encoded display name e.g. \"toly\"" + ], + "type": { + "array": [ + "u8", + 32 + ] + } + }, + { + "name": "spotPositions", + "docs": [ + "The user's spot positions" + ], + "type": { + "array": [ + { + "defined": "SpotPosition" + }, + 8 + ] + } + }, + { + "name": "perpPositions", + "docs": [ + "The user's perp positions" + ], + "type": { + "array": [ + { + "defined": "PerpPosition" + }, + 8 + ] + } + }, + { + "name": "orders", + "docs": [ + "The user's orders" + ], + "type": { + "array": [ + { + "defined": "Order" + }, + 32 + ] + } + }, + { + "name": "lastAddPerpLpSharesTs", + "docs": [ + "The last time the user added perp lp positions" + ], + "type": "i64" + }, + { + "name": "totalDeposits", + "docs": [ + "The total values of deposits the user has made", + "precision: QUOTE_PRECISION" + ], + "type": "u64" + }, + { + "name": "totalWithdraws", + "docs": [ + "The total values of withdrawals the user has made", + "precision: QUOTE_PRECISION" + ], + "type": "u64" + }, + { + "name": "totalSocialLoss", + "docs": [ + "The total socialized loss the users has incurred upon the protocol", + "precision: QUOTE_PRECISION" + ], + "type": "u64" + }, + { + "name": "settledPerpPnl", + "docs": [ + "Fees (taker fees, maker rebate, referrer reward, filler reward) and pnl for perps", + "precision: QUOTE_PRECISION" + ], + "type": "i64" + }, + { + "name": "cumulativeSpotFees", + "docs": [ + "Fees (taker fees, maker rebate, filler reward) for spot", + "precision: QUOTE_PRECISION" + ], + "type": "i64" + }, + { + "name": "cumulativePerpFunding", + "docs": [ + "Cumulative funding paid/received for perps", + "precision: QUOTE_PRECISION" + ], + "type": "i64" + }, + { + "name": "liquidationMarginFreed", + "docs": [ + "The amount of margin freed during liquidation. Used to force the liquidation to occur over a period of time", + "Defaults to zero when not being liquidated", + "precision: QUOTE_PRECISION" + ], + "type": "u64" + }, + { + "name": "lastActiveSlot", + "docs": [ + "The last slot a user was active. Used to determine if a user is idle" + ], + "type": "u64" + }, + { + "name": "nextOrderId", + "docs": [ + "Every user order has an order id. This is the next order id to be used" + ], + "type": "u32" + }, + { + "name": "maxMarginRatio", + "docs": [ + "Custom max initial margin ratio for the user" + ], + "type": "u32" + }, + { + "name": "nextLiquidationId", + "docs": [ + "The next liquidation id to be used for user" + ], + "type": "u16" + }, + { + "name": "subAccountId", + "docs": [ + "The sub account id for this user" + ], + "type": "u16" + }, + { + "name": "status", + "docs": [ + "Whether the user is active, being liquidated or bankrupt" + ], + "type": "u8" + }, + { + "name": "isMarginTradingEnabled", + "docs": [ + "Whether the user has enabled margin trading" + ], + "type": "bool" + }, + { + "name": "idle", + "docs": [ + "User is idle if they haven't interacted with the protocol in 1 week and they have no orders, perp positions or borrows", + "Off-chain keeper bots can ignore users that are idle" + ], + "type": "bool" + }, + { + "name": "openOrders", + "docs": [ + "number of open orders" + ], + "type": "u8" + }, + { + "name": "hasOpenOrder", + "docs": [ + "Whether or not user has open order" + ], + "type": "bool" + }, + { + "name": "openAuctions", + "docs": [ + "number of open orders with auction" + ], + "type": "u8" + }, + { + "name": "hasOpenAuction", + "docs": [ + "Whether or not user has open order with auction" + ], + "type": "bool" + }, + { + "name": "padding1", + "type": { + "array": [ + "u8", + 5 + ] + } + }, + { + "name": "lastFuelBonusUpdateTs", + "type": "u32" + }, + { + "name": "padding", + "type": { + "array": [ + "u8", + 12 + ] + } + } + ] + } + }, + { + "name": "UserStats", + "type": { + "kind": "struct", + "fields": [ + { + "name": "authority", + "docs": [ + "The authority for all of a users sub accounts" + ], + "type": "publicKey" + }, + { + "name": "referrer", + "docs": [ + "The address that referred this user" + ], + "type": "publicKey" + }, + { + "name": "fees", + "docs": [ + "Stats on the fees paid by the user" + ], + "type": { + "defined": "UserFees" + } + }, + { + "name": "nextEpochTs", + "docs": [ + "The timestamp of the next epoch", + "Epoch is used to limit referrer rewards earned in single epoch" + ], + "type": "i64" + }, + { + "name": "makerVolume30d", + "docs": [ + "Rolling 30day maker volume for user", + "precision: QUOTE_PRECISION" + ], + "type": "u64" + }, + { + "name": "takerVolume30d", + "docs": [ + "Rolling 30day taker volume for user", + "precision: QUOTE_PRECISION" + ], + "type": "u64" + }, + { + "name": "fillerVolume30d", + "docs": [ + "Rolling 30day filler volume for user", + "precision: QUOTE_PRECISION" + ], + "type": "u64" + }, + { + "name": "lastMakerVolume30dTs", + "docs": [ + "last time the maker volume was updated" + ], + "type": "i64" + }, + { + "name": "lastTakerVolume30dTs", + "docs": [ + "last time the taker volume was updated" + ], + "type": "i64" + }, + { + "name": "lastFillerVolume30dTs", + "docs": [ + "last time the filler volume was updated" + ], + "type": "i64" + }, + { + "name": "ifStakedQuoteAssetAmount", + "docs": [ + "The amount of tokens staked in the quote spot markets if" + ], + "type": "u64" + }, + { + "name": "numberOfSubAccounts", + "docs": [ + "The current number of sub accounts" + ], + "type": "u16" + }, + { + "name": "numberOfSubAccountsCreated", + "docs": [ + "The number of sub accounts created. Can be greater than the number of sub accounts if user", + "has deleted sub accounts" + ], + "type": "u16" + }, + { + "name": "isReferrer", + "docs": [ + "Whether the user is a referrer. Sub account 0 can not be deleted if user is a referrer" + ], + "type": "bool" + }, + { + "name": "disableUpdatePerpBidAskTwap", + "type": "bool" + }, + { + "name": "padding1", + "type": { + "array": [ + "u8", + 2 + ] + } + }, + { + "name": "fuelInsurance", + "docs": [ + "accumulated fuel for token amounts of insurance" + ], + "type": "u32" + }, + { + "name": "fuelDeposits", + "docs": [ + "accumulated fuel for notional of deposits" + ], + "type": "u32" + }, + { + "name": "fuelBorrows", + "docs": [ + "accumulate fuel bonus for notional of borrows" + ], + "type": "u32" + }, + { + "name": "fuelPositions", + "docs": [ + "accumulated fuel for perp open interest" + ], + "type": "u32" + }, + { + "name": "fuelTaker", + "docs": [ + "accumulate fuel bonus for taker volume" + ], + "type": "u32" + }, + { + "name": "fuelMaker", + "docs": [ + "accumulate fuel bonus for maker volume" + ], + "type": "u32" + }, + { + "name": "ifStakedGovTokenAmount", + "docs": [ + "The amount of tokens staked in the governance spot markets if" + ], + "type": "u64" + }, + { + "name": "lastFuelIfBonusUpdateTs", + "docs": [ + "last unix ts user stats data was used to update if fuel (u32 to save space)" + ], + "type": "u32" + }, + { + "name": "padding", + "type": { + "array": [ + "u8", + 12 + ] + } + } + ] + } + }, + { + "name": "ReferrerName", + "type": { + "kind": "struct", + "fields": [ + { + "name": "authority", + "type": "publicKey" + }, + { + "name": "user", + "type": "publicKey" + }, + { + "name": "userStats", + "type": "publicKey" + }, + { + "name": "name", + "type": { + "array": [ + "u8", + 32 + ] + } + } + ] + } + } + ], + "types": [ + { + "name": "UpdatePerpMarketSummaryStatsParams", + "type": { + "kind": "struct", + "fields": [ + { + "name": "quoteAssetAmountWithUnsettledLp", + "type": { + "option": "i64" + } + }, + { + "name": "netUnsettledFundingPnl", + "type": { + "option": "i64" + } + }, + { + "name": "updateAmmSummaryStats", + "type": { + "option": "bool" + } + } + ] + } + }, + { + "name": "LiquidatePerpRecord", + "type": { + "kind": "struct", + "fields": [ + { + "name": "marketIndex", + "type": "u16" + }, + { + "name": "oraclePrice", + "type": "i64" + }, + { + "name": "baseAssetAmount", + "type": "i64" + }, + { + "name": "quoteAssetAmount", + "type": "i64" + }, + { + "name": "lpShares", + "docs": [ + "precision: AMM_RESERVE_PRECISION" + ], + "type": "u64" + }, + { + "name": "fillRecordId", + "type": "u64" + }, + { + "name": "userOrderId", + "type": "u32" + }, + { + "name": "liquidatorOrderId", + "type": "u32" + }, + { + "name": "liquidatorFee", + "docs": [ + "precision: QUOTE_PRECISION" + ], + "type": "u64" + }, + { + "name": "ifFee", + "docs": [ + "precision: QUOTE_PRECISION" + ], + "type": "u64" + } + ] + } + }, + { + "name": "LiquidateSpotRecord", + "type": { + "kind": "struct", + "fields": [ + { + "name": "assetMarketIndex", + "type": "u16" + }, + { + "name": "assetPrice", + "type": "i64" + }, + { + "name": "assetTransfer", + "type": "u128" + }, + { + "name": "liabilityMarketIndex", + "type": "u16" + }, + { + "name": "liabilityPrice", + "type": "i64" + }, + { + "name": "liabilityTransfer", + "docs": [ + "precision: token mint precision" + ], + "type": "u128" + }, + { + "name": "ifFee", + "docs": [ + "precision: token mint precision" + ], + "type": "u64" + } + ] + } + }, + { + "name": "LiquidateBorrowForPerpPnlRecord", + "type": { + "kind": "struct", + "fields": [ + { + "name": "perpMarketIndex", + "type": "u16" + }, + { + "name": "marketOraclePrice", + "type": "i64" + }, + { + "name": "pnlTransfer", + "type": "u128" + }, + { + "name": "liabilityMarketIndex", + "type": "u16" + }, + { + "name": "liabilityPrice", + "type": "i64" + }, + { + "name": "liabilityTransfer", + "type": "u128" + } + ] + } + }, + { + "name": "LiquidatePerpPnlForDepositRecord", + "type": { + "kind": "struct", + "fields": [ + { + "name": "perpMarketIndex", + "type": "u16" + }, + { + "name": "marketOraclePrice", + "type": "i64" + }, + { + "name": "pnlTransfer", + "type": "u128" + }, + { + "name": "assetMarketIndex", + "type": "u16" + }, + { + "name": "assetPrice", + "type": "i64" + }, + { + "name": "assetTransfer", + "type": "u128" + } + ] + } + }, + { + "name": "PerpBankruptcyRecord", + "type": { + "kind": "struct", + "fields": [ + { + "name": "marketIndex", + "type": "u16" + }, + { + "name": "pnl", + "type": "i128" + }, + { + "name": "ifPayment", + "type": "u128" + }, + { + "name": "clawbackUser", + "type": { + "option": "publicKey" + } + }, + { + "name": "clawbackUserPayment", + "type": { + "option": "u128" + } + }, + { + "name": "cumulativeFundingRateDelta", + "type": "i128" + } + ] + } + }, + { + "name": "SpotBankruptcyRecord", + "type": { + "kind": "struct", + "fields": [ + { + "name": "marketIndex", + "type": "u16" + }, + { + "name": "borrowAmount", + "type": "u128" + }, + { + "name": "ifPayment", + "type": "u128" + }, + { + "name": "cumulativeDepositInterestDelta", + "type": "u128" + } + ] + } + }, + { + "name": "MarketIdentifier", + "type": { + "kind": "struct", + "fields": [ + { + "name": "marketType", + "type": { + "defined": "MarketType" + } + }, + { + "name": "marketIndex", + "type": "u16" + } + ] + } + }, + { + "name": "HistoricalOracleData", + "type": { + "kind": "struct", + "fields": [ + { + "name": "lastOraclePrice", + "docs": [ + "precision: PRICE_PRECISION" + ], + "type": "i64" + }, + { + "name": "lastOracleConf", + "docs": [ + "precision: PRICE_PRECISION" + ], + "type": "u64" + }, + { + "name": "lastOracleDelay", + "docs": [ + "number of slots since last update" + ], + "type": "i64" + }, + { + "name": "lastOraclePriceTwap", + "docs": [ + "precision: PRICE_PRECISION" + ], + "type": "i64" + }, + { + "name": "lastOraclePriceTwap5min", + "docs": [ + "precision: PRICE_PRECISION" + ], + "type": "i64" + }, + { + "name": "lastOraclePriceTwapTs", + "docs": [ + "unix_timestamp of last snapshot" + ], + "type": "i64" + } + ] + } + }, + { + "name": "HistoricalIndexData", + "type": { + "kind": "struct", + "fields": [ + { + "name": "lastIndexBidPrice", + "docs": [ + "precision: PRICE_PRECISION" + ], + "type": "u64" + }, + { + "name": "lastIndexAskPrice", + "docs": [ + "precision: PRICE_PRECISION" + ], + "type": "u64" + }, + { + "name": "lastIndexPriceTwap", + "docs": [ + "precision: PRICE_PRECISION" + ], + "type": "u64" + }, + { + "name": "lastIndexPriceTwap5min", + "docs": [ + "precision: PRICE_PRECISION" + ], + "type": "u64" + }, + { + "name": "lastIndexPriceTwapTs", + "docs": [ + "unix_timestamp of last snapshot" + ], + "type": "i64" + } + ] + } + }, + { + "name": "PrelaunchOracleParams", + "type": { + "kind": "struct", + "fields": [ + { + "name": "perpMarketIndex", + "type": "u16" + }, + { + "name": "price", + "type": { + "option": "i64" + } + }, + { + "name": "maxPrice", + "type": { + "option": "i64" + } + } + ] + } + }, + { + "name": "OrderParams", + "type": { + "kind": "struct", + "fields": [ + { + "name": "orderType", + "type": { + "defined": "OrderType" + } + }, + { + "name": "marketType", + "type": { + "defined": "MarketType" + } + }, + { + "name": "direction", + "type": { + "defined": "PositionDirection" + } + }, + { + "name": "userOrderId", + "type": "u8" + }, + { + "name": "baseAssetAmount", + "type": "u64" + }, + { + "name": "price", + "type": "u64" + }, + { + "name": "marketIndex", + "type": "u16" + }, + { + "name": "reduceOnly", + "type": "bool" + }, + { + "name": "postOnly", + "type": { + "defined": "PostOnlyParam" + } + }, + { + "name": "immediateOrCancel", + "type": "bool" + }, + { + "name": "maxTs", + "type": { + "option": "i64" + } + }, + { + "name": "triggerPrice", + "type": { + "option": "u64" + } + }, + { + "name": "triggerCondition", + "type": { + "defined": "OrderTriggerCondition" + } + }, + { + "name": "oraclePriceOffset", + "type": { + "option": "i32" + } + }, + { + "name": "auctionDuration", + "type": { + "option": "u8" + } + }, + { + "name": "auctionStartPrice", + "type": { + "option": "i64" + } + }, + { + "name": "auctionEndPrice", + "type": { + "option": "i64" + } + } + ] + } + }, + { + "name": "ModifyOrderParams", + "type": { + "kind": "struct", + "fields": [ + { + "name": "direction", + "type": { + "option": { + "defined": "PositionDirection" + } + } + }, + { + "name": "baseAssetAmount", + "type": { + "option": "u64" + } + }, + { + "name": "price", + "type": { + "option": "u64" + } + }, + { + "name": "reduceOnly", + "type": { + "option": "bool" + } + }, + { + "name": "postOnly", + "type": { + "option": { + "defined": "PostOnlyParam" + } + } + }, + { + "name": "immediateOrCancel", + "type": { + "option": "bool" + } + }, + { + "name": "maxTs", + "type": { + "option": "i64" + } + }, + { + "name": "triggerPrice", + "type": { + "option": "u64" + } + }, + { + "name": "triggerCondition", + "type": { + "option": { + "defined": "OrderTriggerCondition" + } + } + }, + { + "name": "oraclePriceOffset", + "type": { + "option": "i32" + } + }, + { + "name": "auctionDuration", + "type": { + "option": "u8" + } + }, + { + "name": "auctionStartPrice", + "type": { + "option": "i64" + } + }, + { + "name": "auctionEndPrice", + "type": { + "option": "i64" + } + }, + { + "name": "policy", + "type": { + "option": { + "defined": "ModifyOrderPolicy" + } + } + } + ] + } + }, + { + "name": "InsuranceClaim", + "type": { + "kind": "struct", + "fields": [ + { + "name": "revenueWithdrawSinceLastSettle", + "docs": [ + "The amount of revenue last settled", + "Positive if funds left the perp market,", + "negative if funds were pulled into the perp market", + "precision: QUOTE_PRECISION" + ], + "type": "i64" + }, + { + "name": "maxRevenueWithdrawPerPeriod", + "docs": [ + "The max amount of revenue that can be withdrawn per period", + "precision: QUOTE_PRECISION" + ], + "type": "u64" + }, + { + "name": "quoteMaxInsurance", + "docs": [ + "The max amount of insurance that perp market can use to resolve bankruptcy and pnl deficits", + "precision: QUOTE_PRECISION" + ], + "type": "u64" + }, + { + "name": "quoteSettledInsurance", + "docs": [ + "The amount of insurance that has been used to resolve bankruptcy and pnl deficits", + "precision: QUOTE_PRECISION" + ], + "type": "u64" + }, + { + "name": "lastRevenueWithdrawTs", + "docs": [ + "The last time revenue was settled in/out of market" + ], + "type": "i64" + } + ] + } + }, + { + "name": "PoolBalance", + "type": { + "kind": "struct", + "fields": [ + { + "name": "scaledBalance", + "docs": [ + "To get the pool's token amount, you must multiply the scaled balance by the market's cumulative", + "deposit interest", + "precision: SPOT_BALANCE_PRECISION" + ], + "type": "u128" + }, + { + "name": "marketIndex", + "docs": [ + "The spot market the pool is for" + ], + "type": "u16" + }, + { + "name": "padding", + "type": { + "array": [ + "u8", + 6 + ] + } + } + ] + } + }, + { + "name": "AMM", + "type": { + "kind": "struct", + "fields": [ + { + "name": "oracle", + "docs": [ + "oracle price data public key" + ], + "type": "publicKey" + }, + { + "name": "historicalOracleData", + "docs": [ + "stores historically witnessed oracle data" + ], + "type": { + "defined": "HistoricalOracleData" + } + }, + { + "name": "baseAssetAmountPerLp", + "docs": [ + "accumulated base asset amount since inception per lp share", + "precision: QUOTE_PRECISION" + ], + "type": "i128" + }, + { + "name": "quoteAssetAmountPerLp", + "docs": [ + "accumulated quote asset amount since inception per lp share", + "precision: QUOTE_PRECISION" + ], + "type": "i128" + }, + { + "name": "feePool", + "docs": [ + "partition of fees from perp market trading moved from pnl settlements" + ], + "type": { + "defined": "PoolBalance" + } + }, + { + "name": "baseAssetReserve", + "docs": [ + "`x` reserves for constant product mm formula (x * y = k)", + "precision: AMM_RESERVE_PRECISION" + ], + "type": "u128" + }, + { + "name": "quoteAssetReserve", + "docs": [ + "`y` reserves for constant product mm formula (x * y = k)", + "precision: AMM_RESERVE_PRECISION" + ], + "type": "u128" + }, + { + "name": "concentrationCoef", + "docs": [ + "determines how close the min/max base asset reserve sit vs base reserves", + "allow for decreasing slippage without increasing liquidity and v.v.", + "precision: PERCENTAGE_PRECISION" + ], + "type": "u128" + }, + { + "name": "minBaseAssetReserve", + "docs": [ + "minimum base_asset_reserve allowed before AMM is unavailable", + "precision: AMM_RESERVE_PRECISION" + ], + "type": "u128" + }, + { + "name": "maxBaseAssetReserve", + "docs": [ + "maximum base_asset_reserve allowed before AMM is unavailable", + "precision: AMM_RESERVE_PRECISION" + ], + "type": "u128" + }, + { + "name": "sqrtK", + "docs": [ + "`sqrt(k)` in constant product mm formula (x * y = k). stored to avoid drift caused by integer math issues", + "precision: AMM_RESERVE_PRECISION" + ], + "type": "u128" + }, + { + "name": "pegMultiplier", + "docs": [ + "normalizing numerical factor for y, its use offers lowest slippage in cp-curve when market is balanced", + "precision: PEG_PRECISION" + ], + "type": "u128" + }, + { + "name": "terminalQuoteAssetReserve", + "docs": [ + "y when market is balanced. stored to save computation", + "precision: AMM_RESERVE_PRECISION" + ], + "type": "u128" + }, + { + "name": "baseAssetAmountLong", + "docs": [ + "always non-negative. tracks number of total longs in market (regardless of counterparty)", + "precision: BASE_PRECISION" + ], + "type": "i128" + }, + { + "name": "baseAssetAmountShort", + "docs": [ + "always non-positive. tracks number of total shorts in market (regardless of counterparty)", + "precision: BASE_PRECISION" + ], + "type": "i128" + }, + { + "name": "baseAssetAmountWithAmm", + "docs": [ + "tracks net position (longs-shorts) in market with AMM as counterparty", + "precision: BASE_PRECISION" + ], + "type": "i128" + }, + { + "name": "baseAssetAmountWithUnsettledLp", + "docs": [ + "tracks net position (longs-shorts) in market with LPs as counterparty", + "precision: BASE_PRECISION" + ], + "type": "i128" + }, + { + "name": "maxOpenInterest", + "docs": [ + "max allowed open interest, blocks trades that breach this value", + "precision: BASE_PRECISION" + ], + "type": "u128" + }, + { + "name": "quoteAssetAmount", + "docs": [ + "sum of all user's perp quote_asset_amount in market", + "precision: QUOTE_PRECISION" + ], + "type": "i128" + }, + { + "name": "quoteEntryAmountLong", + "docs": [ + "sum of all long user's quote_entry_amount in market", + "precision: QUOTE_PRECISION" + ], + "type": "i128" + }, + { + "name": "quoteEntryAmountShort", + "docs": [ + "sum of all short user's quote_entry_amount in market", + "precision: QUOTE_PRECISION" + ], + "type": "i128" + }, + { + "name": "quoteBreakEvenAmountLong", + "docs": [ + "sum of all long user's quote_break_even_amount in market", + "precision: QUOTE_PRECISION" + ], + "type": "i128" + }, + { + "name": "quoteBreakEvenAmountShort", + "docs": [ + "sum of all short user's quote_break_even_amount in market", + "precision: QUOTE_PRECISION" + ], + "type": "i128" + }, + { + "name": "userLpShares", + "docs": [ + "total user lp shares of sqrt_k (protocol owned liquidity = sqrt_k - last_funding_rate)", + "precision: AMM_RESERVE_PRECISION" + ], + "type": "u128" + }, + { + "name": "lastFundingRate", + "docs": [ + "last funding rate in this perp market (unit is quote per base)", + "precision: QUOTE_PRECISION" + ], + "type": "i64" + }, + { + "name": "lastFundingRateLong", + "docs": [ + "last funding rate for longs in this perp market (unit is quote per base)", + "precision: QUOTE_PRECISION" + ], + "type": "i64" + }, + { + "name": "lastFundingRateShort", + "docs": [ + "last funding rate for shorts in this perp market (unit is quote per base)", + "precision: QUOTE_PRECISION" + ], + "type": "i64" + }, + { + "name": "last24hAvgFundingRate", + "docs": [ + "estimate of last 24h of funding rate perp market (unit is quote per base)", + "precision: QUOTE_PRECISION" + ], + "type": "i64" + }, + { + "name": "totalFee", + "docs": [ + "total fees collected by this perp market", + "precision: QUOTE_PRECISION" + ], + "type": "i128" + }, + { + "name": "totalMmFee", + "docs": [ + "total fees collected by the vAMM's bid/ask spread", + "precision: QUOTE_PRECISION" + ], + "type": "i128" + }, + { + "name": "totalExchangeFee", + "docs": [ + "total fees collected by exchange fee schedule", + "precision: QUOTE_PRECISION" + ], + "type": "u128" + }, + { + "name": "totalFeeMinusDistributions", + "docs": [ + "total fees minus any recognized upnl and pool withdraws", + "precision: QUOTE_PRECISION" + ], + "type": "i128" + }, + { + "name": "totalFeeWithdrawn", + "docs": [ + "sum of all fees from fee pool withdrawn to revenue pool", + "precision: QUOTE_PRECISION" + ], + "type": "u128" + }, + { + "name": "totalLiquidationFee", + "docs": [ + "all fees collected by market for liquidations", + "precision: QUOTE_PRECISION" + ], + "type": "u128" + }, + { + "name": "cumulativeFundingRateLong", + "docs": [ + "accumulated funding rate for longs since inception in market" + ], + "type": "i128" + }, + { + "name": "cumulativeFundingRateShort", + "docs": [ + "accumulated funding rate for shorts since inception in market" + ], + "type": "i128" + }, + { + "name": "totalSocialLoss", + "docs": [ + "accumulated social loss paid by users since inception in market" + ], + "type": "u128" + }, + { + "name": "askBaseAssetReserve", + "docs": [ + "transformed base_asset_reserve for users going long", + "precision: AMM_RESERVE_PRECISION" + ], + "type": "u128" + }, + { + "name": "askQuoteAssetReserve", + "docs": [ + "transformed quote_asset_reserve for users going long", + "precision: AMM_RESERVE_PRECISION" + ], + "type": "u128" + }, + { + "name": "bidBaseAssetReserve", + "docs": [ + "transformed base_asset_reserve for users going short", + "precision: AMM_RESERVE_PRECISION" + ], + "type": "u128" + }, + { + "name": "bidQuoteAssetReserve", + "docs": [ + "transformed quote_asset_reserve for users going short", + "precision: AMM_RESERVE_PRECISION" + ], + "type": "u128" + }, + { + "name": "lastOracleNormalisedPrice", + "docs": [ + "the last seen oracle price partially shrunk toward the amm reserve price", + "precision: PRICE_PRECISION" + ], + "type": "i64" + }, + { + "name": "lastOracleReservePriceSpreadPct", + "docs": [ + "the gap between the oracle price and the reserve price = y * peg_multiplier / x" + ], + "type": "i64" + }, + { + "name": "lastBidPriceTwap", + "docs": [ + "average estimate of bid price over funding_period", + "precision: PRICE_PRECISION" + ], + "type": "u64" + }, + { + "name": "lastAskPriceTwap", + "docs": [ + "average estimate of ask price over funding_period", + "precision: PRICE_PRECISION" + ], + "type": "u64" + }, + { + "name": "lastMarkPriceTwap", + "docs": [ + "average estimate of (bid+ask)/2 price over funding_period", + "precision: PRICE_PRECISION" + ], + "type": "u64" + }, + { + "name": "lastMarkPriceTwap5min", + "docs": [ + "average estimate of (bid+ask)/2 price over FIVE_MINUTES" + ], + "type": "u64" + }, + { + "name": "lastUpdateSlot", + "docs": [ + "the last blockchain slot the amm was updated" + ], + "type": "u64" + }, + { + "name": "lastOracleConfPct", + "docs": [ + "the pct size of the oracle confidence interval", + "precision: PERCENTAGE_PRECISION" + ], + "type": "u64" + }, + { + "name": "netRevenueSinceLastFunding", + "docs": [ + "the total_fee_minus_distribution change since the last funding update", + "precision: QUOTE_PRECISION" + ], + "type": "i64" + }, + { + "name": "lastFundingRateTs", + "docs": [ + "the last funding rate update unix_timestamp" + ], + "type": "i64" + }, + { + "name": "fundingPeriod", + "docs": [ + "the peridocity of the funding rate updates" + ], + "type": "i64" + }, + { + "name": "orderStepSize", + "docs": [ + "the base step size (increment) of orders", + "precision: BASE_PRECISION" + ], + "type": "u64" + }, + { + "name": "orderTickSize", + "docs": [ + "the price tick size of orders", + "precision: PRICE_PRECISION" + ], + "type": "u64" + }, + { + "name": "minOrderSize", + "docs": [ + "the minimum base size of an order", + "precision: BASE_PRECISION" + ], + "type": "u64" + }, + { + "name": "maxPositionSize", + "docs": [ + "the max base size a single user can have", + "precision: BASE_PRECISION" + ], + "type": "u64" + }, + { + "name": "volume24h", + "docs": [ + "estimated total of volume in market", + "QUOTE_PRECISION" + ], + "type": "u64" + }, + { + "name": "longIntensityVolume", + "docs": [ + "the volume intensity of long fills against AMM" + ], + "type": "u64" + }, + { + "name": "shortIntensityVolume", + "docs": [ + "the volume intensity of short fills against AMM" + ], + "type": "u64" + }, + { + "name": "lastTradeTs", + "docs": [ + "the blockchain unix timestamp at the time of the last trade" + ], + "type": "i64" + }, + { + "name": "markStd", + "docs": [ + "estimate of standard deviation of the fill (mark) prices", + "precision: PRICE_PRECISION" + ], + "type": "u64" + }, + { + "name": "oracleStd", + "docs": [ + "estimate of standard deviation of the oracle price at each update", + "precision: PRICE_PRECISION" + ], + "type": "u64" + }, + { + "name": "lastMarkPriceTwapTs", + "docs": [ + "the last unix_timestamp the mark twap was updated" + ], + "type": "i64" + }, + { + "name": "baseSpread", + "docs": [ + "the minimum spread the AMM can quote. also used as step size for some spread logic increases." + ], + "type": "u32" + }, + { + "name": "maxSpread", + "docs": [ + "the maximum spread the AMM can quote" + ], + "type": "u32" + }, + { + "name": "longSpread", + "docs": [ + "the spread for asks vs the reserve price" + ], + "type": "u32" + }, + { + "name": "shortSpread", + "docs": [ + "the spread for bids vs the reserve price" + ], + "type": "u32" + }, + { + "name": "longIntensityCount", + "docs": [ + "the count intensity of long fills against AMM" + ], + "type": "u32" + }, + { + "name": "shortIntensityCount", + "docs": [ + "the count intensity of short fills against AMM" + ], + "type": "u32" + }, + { + "name": "maxFillReserveFraction", + "docs": [ + "the fraction of total available liquidity a single fill on the AMM can consume" + ], + "type": "u16" + }, + { + "name": "maxSlippageRatio", + "docs": [ + "the maximum slippage a single fill on the AMM can push" + ], + "type": "u16" + }, + { + "name": "curveUpdateIntensity", + "docs": [ + "the update intensity of AMM formulaic updates (adjusting k). 0-100" + ], + "type": "u8" + }, + { + "name": "ammJitIntensity", + "docs": [ + "the jit intensity of AMM. larger intensity means larger participation in jit. 0 means no jit participation.", + "(0, 100] is intensity for protocol-owned AMM. (100, 200] is intensity for user LP-owned AMM." + ], + "type": "u8" + }, + { + "name": "oracleSource", + "docs": [ + "the oracle provider information. used to decode/scale the oracle public key" + ], + "type": { + "defined": "OracleSource" + } + }, + { + "name": "lastOracleValid", + "docs": [ + "tracks whether the oracle was considered valid at the last AMM update" + ], + "type": "bool" + }, + { + "name": "targetBaseAssetAmountPerLp", + "docs": [ + "the target value for `base_asset_amount_per_lp`, used during AMM JIT with LP split", + "precision: BASE_PRECISION" + ], + "type": "i32" + }, + { + "name": "perLpBase", + "docs": [ + "expo for unit of per_lp, base 10 (if per_lp_base=X, then per_lp unit is 10^X)" + ], + "type": "i8" + }, + { + "name": "padding1", + "type": "u8" + }, + { + "name": "padding2", + "type": "u16" + }, + { + "name": "totalFeeEarnedPerLp", + "type": "u64" + }, + { + "name": "netUnsettledFundingPnl", + "type": "i64" + }, + { + "name": "quoteAssetAmountWithUnsettledLp", + "type": "i64" + }, + { + "name": "referencePriceOffset", + "type": "i32" + }, + { + "name": "padding", + "type": { + "array": [ + "u8", + 12 + ] + } + } + ] + } + }, + { + "name": "InsuranceFund", + "type": { + "kind": "struct", + "fields": [ + { + "name": "vault", + "type": "publicKey" + }, + { + "name": "totalShares", + "type": "u128" + }, + { + "name": "userShares", + "type": "u128" + }, + { + "name": "sharesBase", + "type": "u128" + }, + { + "name": "unstakingPeriod", + "type": "i64" + }, + { + "name": "lastRevenueSettleTs", + "type": "i64" + }, + { + "name": "revenueSettlePeriod", + "type": "i64" + }, + { + "name": "totalFactor", + "type": "u32" + }, + { + "name": "userFactor", + "type": "u32" + } + ] + } + }, + { + "name": "OracleGuardRails", + "type": { + "kind": "struct", + "fields": [ + { + "name": "priceDivergence", + "type": { + "defined": "PriceDivergenceGuardRails" + } + }, + { + "name": "validity", + "type": { + "defined": "ValidityGuardRails" + } + } + ] + } + }, + { + "name": "PriceDivergenceGuardRails", + "type": { + "kind": "struct", + "fields": [ + { + "name": "markOraclePercentDivergence", + "type": "u64" + }, + { + "name": "oracleTwap5minPercentDivergence", + "type": "u64" + } + ] + } + }, + { + "name": "ValidityGuardRails", + "type": { + "kind": "struct", + "fields": [ + { + "name": "slotsBeforeStaleForAmm", + "type": "i64" + }, + { + "name": "slotsBeforeStaleForMargin", + "type": "i64" + }, + { + "name": "confidenceIntervalMaxSize", + "type": "u64" + }, + { + "name": "tooVolatileRatio", + "type": "i64" + } + ] + } + }, + { + "name": "FeeStructure", + "type": { + "kind": "struct", + "fields": [ + { + "name": "feeTiers", + "type": { + "array": [ + { + "defined": "FeeTier" + }, + 10 + ] + } + }, + { + "name": "fillerRewardStructure", + "type": { + "defined": "OrderFillerRewardStructure" + } + }, + { + "name": "referrerRewardEpochUpperBound", + "type": "u64" + }, + { + "name": "flatFillerFee", + "type": "u64" + } + ] + } + }, + { + "name": "FeeTier", + "type": { + "kind": "struct", + "fields": [ + { + "name": "feeNumerator", + "type": "u32" + }, + { + "name": "feeDenominator", + "type": "u32" + }, + { + "name": "makerRebateNumerator", + "type": "u32" + }, + { + "name": "makerRebateDenominator", + "type": "u32" + }, + { + "name": "referrerRewardNumerator", + "type": "u32" + }, + { + "name": "referrerRewardDenominator", + "type": "u32" + }, + { + "name": "refereeFeeNumerator", + "type": "u32" + }, + { + "name": "refereeFeeDenominator", + "type": "u32" + } + ] + } + }, + { + "name": "OrderFillerRewardStructure", + "type": { + "kind": "struct", + "fields": [ + { + "name": "rewardNumerator", + "type": "u32" + }, + { + "name": "rewardDenominator", + "type": "u32" + }, + { + "name": "timeBasedRewardLowerBound", + "type": "u128" + } + ] + } + }, + { + "name": "UserFees", + "type": { + "kind": "struct", + "fields": [ + { + "name": "totalFeePaid", + "docs": [ + "Total taker fee paid", + "precision: QUOTE_PRECISION" + ], + "type": "u64" + }, + { + "name": "totalFeeRebate", + "docs": [ + "Total maker fee rebate", + "precision: QUOTE_PRECISION" + ], + "type": "u64" + }, + { + "name": "totalTokenDiscount", + "docs": [ + "Total discount from holding token", + "precision: QUOTE_PRECISION" + ], + "type": "u64" + }, + { + "name": "totalRefereeDiscount", + "docs": [ + "Total discount from being referred", + "precision: QUOTE_PRECISION" + ], + "type": "u64" + }, + { + "name": "totalReferrerReward", + "docs": [ + "Total reward to referrer", + "precision: QUOTE_PRECISION" + ], + "type": "u64" + }, + { + "name": "currentEpochReferrerReward", + "docs": [ + "Total reward to referrer this epoch", + "precision: QUOTE_PRECISION" + ], + "type": "u64" + } + ] + } + }, + { + "name": "SpotPosition", + "type": { + "kind": "struct", + "fields": [ + { + "name": "scaledBalance", + "docs": [ + "The scaled balance of the position. To get the token amount, multiply by the cumulative deposit/borrow", + "interest of corresponding market.", + "precision: SPOT_BALANCE_PRECISION" + ], + "type": "u64" + }, + { + "name": "openBids", + "docs": [ + "How many spot bids the user has open", + "precision: token mint precision" + ], + "type": "i64" + }, + { + "name": "openAsks", + "docs": [ + "How many spot asks the user has open", + "precision: token mint precision" + ], + "type": "i64" + }, + { + "name": "cumulativeDeposits", + "docs": [ + "The cumulative deposits/borrows a user has made into a market", + "precision: token mint precision" + ], + "type": "i64" + }, + { + "name": "marketIndex", + "docs": [ + "The market index of the corresponding spot market" + ], + "type": "u16" + }, + { + "name": "balanceType", + "docs": [ + "Whether the position is deposit or borrow" + ], + "type": { + "defined": "SpotBalanceType" + } + }, + { + "name": "openOrders", + "docs": [ + "Number of open orders" + ], + "type": "u8" + }, + { + "name": "padding", + "type": { + "array": [ + "u8", + 4 + ] + } + } + ] + } + }, + { + "name": "PerpPosition", + "type": { + "kind": "struct", + "fields": [ + { + "name": "lastCumulativeFundingRate", + "docs": [ + "The perp market's last cumulative funding rate. Used to calculate the funding payment owed to user", + "precision: FUNDING_RATE_PRECISION" + ], + "type": "i64" + }, + { + "name": "baseAssetAmount", + "docs": [ + "the size of the users perp position", + "precision: BASE_PRECISION" + ], + "type": "i64" + }, + { + "name": "quoteAssetAmount", + "docs": [ + "Used to calculate the users pnl. Upon entry, is equal to base_asset_amount * avg entry price - fees", + "Updated when the user open/closes position or settles pnl. Includes fees/funding", + "precision: QUOTE_PRECISION" + ], + "type": "i64" + }, + { + "name": "quoteBreakEvenAmount", + "docs": [ + "The amount of quote the user would need to exit their position at to break even", + "Updated when the user open/closes position or settles pnl. Includes fees/funding", + "precision: QUOTE_PRECISION" + ], + "type": "i64" + }, + { + "name": "quoteEntryAmount", + "docs": [ + "The amount quote the user entered the position with. Equal to base asset amount * avg entry price", + "Updated when the user open/closes position. Excludes fees/funding", + "precision: QUOTE_PRECISION" + ], + "type": "i64" + }, + { + "name": "openBids", + "docs": [ + "The amount of open bids the user has in this perp market", + "precision: BASE_PRECISION" + ], + "type": "i64" + }, + { + "name": "openAsks", + "docs": [ + "The amount of open asks the user has in this perp market", + "precision: BASE_PRECISION" + ], + "type": "i64" + }, + { + "name": "settledPnl", + "docs": [ + "The amount of pnl settled in this market since opening the position", + "precision: QUOTE_PRECISION" + ], + "type": "i64" + }, + { + "name": "lpShares", + "docs": [ + "The number of lp (liquidity provider) shares the user has in this perp market", + "LP shares allow users to provide liquidity via the AMM", + "precision: BASE_PRECISION" + ], + "type": "u64" + }, + { + "name": "lastBaseAssetAmountPerLp", + "docs": [ + "The last base asset amount per lp the amm had", + "Used to settle the users lp position", + "precision: BASE_PRECISION" + ], + "type": "i64" + }, + { + "name": "lastQuoteAssetAmountPerLp", + "docs": [ + "The last quote asset amount per lp the amm had", + "Used to settle the users lp position", + "precision: QUOTE_PRECISION" + ], + "type": "i64" + }, + { + "name": "remainderBaseAssetAmount", + "docs": [ + "Settling LP position can lead to a small amount of base asset being left over smaller than step size", + "This records that remainder so it can be settled later on", + "precision: BASE_PRECISION" + ], + "type": "i32" + }, + { + "name": "marketIndex", + "docs": [ + "The market index for the perp market" + ], + "type": "u16" + }, + { + "name": "openOrders", + "docs": [ + "The number of open orders" + ], + "type": "u8" + }, + { + "name": "perLpBase", + "type": "i8" + } + ] + } + }, + { + "name": "Order", + "type": { + "kind": "struct", + "fields": [ + { + "name": "slot", + "docs": [ + "The slot the order was placed" + ], + "type": "u64" + }, + { + "name": "price", + "docs": [ + "The limit price for the order (can be 0 for market orders)", + "For orders with an auction, this price isn't used until the auction is complete", + "precision: PRICE_PRECISION" + ], + "type": "u64" + }, + { + "name": "baseAssetAmount", + "docs": [ + "The size of the order", + "precision for perps: BASE_PRECISION", + "precision for spot: token mint precision" + ], + "type": "u64" + }, + { + "name": "baseAssetAmountFilled", + "docs": [ + "The amount of the order filled", + "precision for perps: BASE_PRECISION", + "precision for spot: token mint precision" + ], + "type": "u64" + }, + { + "name": "quoteAssetAmountFilled", + "docs": [ + "The amount of quote filled for the order", + "precision: QUOTE_PRECISION" + ], + "type": "u64" + }, + { + "name": "triggerPrice", + "docs": [ + "At what price the order will be triggered. Only relevant for trigger orders", + "precision: PRICE_PRECISION" + ], + "type": "u64" + }, + { + "name": "auctionStartPrice", + "docs": [ + "The start price for the auction. Only relevant for market/oracle orders", + "precision: PRICE_PRECISION" + ], + "type": "i64" + }, + { + "name": "auctionEndPrice", + "docs": [ + "The end price for the auction. Only relevant for market/oracle orders", + "precision: PRICE_PRECISION" + ], + "type": "i64" + }, + { + "name": "maxTs", + "docs": [ + "The time when the order will expire" + ], + "type": "i64" + }, + { + "name": "oraclePriceOffset", + "docs": [ + "If set, the order limit price is the oracle price + this offset", + "precision: PRICE_PRECISION" + ], + "type": "i32" + }, + { + "name": "orderId", + "docs": [ + "The id for the order. Each users has their own order id space" + ], + "type": "u32" + }, + { + "name": "marketIndex", + "docs": [ + "The perp/spot market index" + ], + "type": "u16" + }, + { + "name": "status", + "docs": [ + "Whether the order is open or unused" + ], + "type": { + "defined": "OrderStatus" + } + }, + { + "name": "orderType", + "docs": [ + "The type of order" + ], + "type": { + "defined": "OrderType" + } + }, + { + "name": "marketType", + "docs": [ + "Whether market is spot or perp" + ], + "type": { + "defined": "MarketType" + } + }, + { + "name": "userOrderId", + "docs": [ + "User generated order id. Can make it easier to place/cancel orders" + ], + "type": "u8" + }, + { + "name": "existingPositionDirection", + "docs": [ + "What the users position was when the order was placed" + ], + "type": { + "defined": "PositionDirection" + } + }, + { + "name": "direction", + "docs": [ + "Whether the user is going long or short. LONG = bid, SHORT = ask" + ], + "type": { + "defined": "PositionDirection" + } + }, + { + "name": "reduceOnly", + "docs": [ + "Whether the order is allowed to only reduce position size" + ], + "type": "bool" + }, + { + "name": "postOnly", + "docs": [ + "Whether the order must be a maker" + ], + "type": "bool" + }, + { + "name": "immediateOrCancel", + "docs": [ + "Whether the order must be canceled the same slot it is placed" + ], + "type": "bool" + }, + { + "name": "triggerCondition", + "docs": [ + "Whether the order is triggered above or below the trigger price. Only relevant for trigger orders" + ], + "type": { + "defined": "OrderTriggerCondition" + } + }, + { + "name": "auctionDuration", + "docs": [ + "How many slots the auction lasts" + ], + "type": "u8" + }, + { + "name": "padding", + "type": { + "array": [ + "u8", + 3 + ] + } + } + ] + } + }, + { + "name": "SwapDirection", + "type": { + "kind": "enum", + "variants": [ + { + "name": "Add" + }, + { + "name": "Remove" + } + ] + } + }, + { + "name": "ModifyOrderId", + "type": { + "kind": "enum", + "variants": [ + { + "name": "UserOrderId", + "fields": [ + "u8" + ] + }, + { + "name": "OrderId", + "fields": [ + "u32" + ] + } + ] + } + }, + { + "name": "PositionDirection", + "type": { + "kind": "enum", + "variants": [ + { + "name": "Long" + }, + { + "name": "Short" + } + ] + } + }, + { + "name": "SpotFulfillmentType", + "type": { + "kind": "enum", + "variants": [ + { + "name": "SerumV3" + }, + { + "name": "Match" + }, + { + "name": "PhoenixV1" + }, + { + "name": "OpenbookV2" + } + ] + } + }, + { + "name": "SwapReduceOnly", + "type": { + "kind": "enum", + "variants": [ + { + "name": "In" + }, + { + "name": "Out" + } + ] + } + }, + { + "name": "TwapPeriod", + "type": { + "kind": "enum", + "variants": [ + { + "name": "FundingPeriod" + }, + { + "name": "FiveMin" + } + ] + } + }, + { + "name": "LiquidationMultiplierType", + "type": { + "kind": "enum", + "variants": [ + { + "name": "Discount" + }, + { + "name": "Premium" + } + ] + } + }, + { + "name": "MarginRequirementType", + "type": { + "kind": "enum", + "variants": [ + { + "name": "Initial" + }, + { + "name": "Fill" + }, + { + "name": "Maintenance" + } + ] + } + }, + { + "name": "OracleValidity", + "type": { + "kind": "enum", + "variants": [ + { + "name": "NonPositive" + }, + { + "name": "TooVolatile" + }, + { + "name": "TooUncertain" + }, + { + "name": "StaleForMargin" + }, + { + "name": "InsufficientDataPoints" + }, + { + "name": "StaleForAMM" + }, + { + "name": "Valid" + } + ] + } + }, + { + "name": "DriftAction", + "type": { + "kind": "enum", + "variants": [ + { + "name": "UpdateFunding" + }, + { + "name": "SettlePnl" + }, + { + "name": "TriggerOrder" + }, + { + "name": "FillOrderMatch" + }, + { + "name": "FillOrderAmm" + }, + { + "name": "Liquidate" + }, + { + "name": "MarginCalc" + }, + { + "name": "UpdateTwap" + }, + { + "name": "UpdateAMMCurve" + }, + { + "name": "OracleOrderPrice" + } + ] + } + }, + { + "name": "PositionUpdateType", + "type": { + "kind": "enum", + "variants": [ + { + "name": "Open" + }, + { + "name": "Increase" + }, + { + "name": "Reduce" + }, + { + "name": "Close" + }, + { + "name": "Flip" + } + ] + } + }, + { + "name": "DepositExplanation", + "type": { + "kind": "enum", + "variants": [ + { + "name": "None" + }, + { + "name": "Transfer" + }, + { + "name": "Borrow" + }, + { + "name": "RepayBorrow" + } + ] + } + }, + { + "name": "DepositDirection", + "type": { + "kind": "enum", + "variants": [ + { + "name": "Deposit" + }, + { + "name": "Withdraw" + } + ] + } + }, + { + "name": "OrderAction", + "type": { + "kind": "enum", + "variants": [ + { + "name": "Place" + }, + { + "name": "Cancel" + }, + { + "name": "Fill" + }, + { + "name": "Trigger" + }, + { + "name": "Expire" + } + ] + } + }, + { + "name": "OrderActionExplanation", + "type": { + "kind": "enum", + "variants": [ + { + "name": "None" + }, + { + "name": "InsufficientFreeCollateral" + }, + { + "name": "OraclePriceBreachedLimitPrice" + }, + { + "name": "MarketOrderFilledToLimitPrice" + }, + { + "name": "OrderExpired" + }, + { + "name": "Liquidation" + }, + { + "name": "OrderFilledWithAMM" + }, + { + "name": "OrderFilledWithAMMJit" + }, + { + "name": "OrderFilledWithMatch" + }, + { + "name": "OrderFilledWithMatchJit" + }, + { + "name": "MarketExpired" + }, + { + "name": "RiskingIncreasingOrder" + }, + { + "name": "ReduceOnlyOrderIncreasedPosition" + }, + { + "name": "OrderFillWithSerum" + }, + { + "name": "NoBorrowLiquidity" + }, + { + "name": "OrderFillWithPhoenix" + }, + { + "name": "OrderFilledWithAMMJitLPSplit" + }, + { + "name": "OrderFilledWithLPJit" + }, + { + "name": "DeriskLp" + }, + { + "name": "OrderFilledWithOpenbookV2" + } + ] + } + }, + { + "name": "LPAction", + "type": { + "kind": "enum", + "variants": [ + { + "name": "AddLiquidity" + }, + { + "name": "RemoveLiquidity" + }, + { + "name": "SettleLiquidity" + }, + { + "name": "RemoveLiquidityDerisk" + } + ] + } + }, + { + "name": "LiquidationType", + "type": { + "kind": "enum", + "variants": [ + { + "name": "LiquidatePerp" + }, + { + "name": "LiquidateSpot" + }, + { + "name": "LiquidateBorrowForPerpPnl" + }, + { + "name": "LiquidatePerpPnlForDeposit" + }, + { + "name": "PerpBankruptcy" + }, + { + "name": "SpotBankruptcy" + } + ] + } + }, + { + "name": "SettlePnlExplanation", + "type": { + "kind": "enum", + "variants": [ + { + "name": "None" + }, + { + "name": "ExpiredPosition" + } + ] + } + }, + { + "name": "StakeAction", + "type": { + "kind": "enum", + "variants": [ + { + "name": "Stake" + }, + { + "name": "UnstakeRequest" + }, + { + "name": "UnstakeCancelRequest" + }, + { + "name": "Unstake" + }, + { + "name": "UnstakeTransfer" + }, + { + "name": "StakeTransfer" + } + ] + } + }, + { + "name": "FillMode", + "type": { + "kind": "enum", + "variants": [ + { + "name": "Fill" + }, + { + "name": "PlaceAndMake" + }, + { + "name": "PlaceAndTake" + }, + { + "name": "Liquidation" + } + ] + } + }, + { + "name": "PerpFulfillmentMethod", + "type": { + "kind": "enum", + "variants": [ + { + "name": "AMM", + "fields": [ + { + "option": "u64" + } + ] + }, + { + "name": "Match", + "fields": [ + "publicKey", + "u16" + ] + } + ] + } + }, + { + "name": "SpotFulfillmentMethod", + "type": { + "kind": "enum", + "variants": [ + { + "name": "ExternalMarket" + }, + { + "name": "Match", + "fields": [ + "publicKey", + "u16" + ] + } + ] + } + }, + { + "name": "MarginCalculationMode", + "type": { + "kind": "enum", + "variants": [ + { + "name": "Standard", + "fields": [ + { + "name": "trackOpenOrdersFraction", + "type": "bool" + } + ] + }, + { + "name": "Liquidation", + "fields": [ + { + "name": "marketToTrackMarginRequirement", + "type": { + "option": { + "defined": "MarketIdentifier" + } + } + } + ] + } + ] + } + }, + { + "name": "OracleSource", + "type": { + "kind": "enum", + "variants": [ + { + "name": "Pyth" + }, + { + "name": "Switchboard" + }, + { + "name": "QuoteAsset" + }, + { + "name": "Pyth1K" + }, + { + "name": "Pyth1M" + }, + { + "name": "PythStableCoin" + }, + { + "name": "Prelaunch" + }, + { + "name": "PythPull" + }, + { + "name": "Pyth1KPull" + }, + { + "name": "Pyth1MPull" + }, + { + "name": "PythStableCoinPull" + }, + { + "name": "SwitchboardOnDemand" + } + ] + } + }, + { + "name": "PostOnlyParam", + "type": { + "kind": "enum", + "variants": [ + { + "name": "None" + }, + { + "name": "MustPostOnly" + }, + { + "name": "TryPostOnly" + }, + { + "name": "Slide" + } + ] + } + }, + { + "name": "ModifyOrderPolicy", + "type": { + "kind": "enum", + "variants": [ + { + "name": "TryModify" + }, + { + "name": "MustModify" + } + ] + } + }, + { + "name": "PerpOperation", + "type": { + "kind": "enum", + "variants": [ + { + "name": "UpdateFunding" + }, + { + "name": "AmmFill" + }, + { + "name": "Fill" + }, + { + "name": "SettlePnl" + }, + { + "name": "SettlePnlWithPosition" + }, + { + "name": "Liquidation" + } + ] + } + }, + { + "name": "SpotOperation", + "type": { + "kind": "enum", + "variants": [ + { + "name": "UpdateCumulativeInterest" + }, + { + "name": "Fill" + }, + { + "name": "Deposit" + }, + { + "name": "Withdraw" + }, + { + "name": "Liquidation" + } + ] + } + }, + { + "name": "InsuranceFundOperation", + "type": { + "kind": "enum", + "variants": [ + { + "name": "Init" + }, + { + "name": "Add" + }, + { + "name": "RequestRemove" + }, + { + "name": "Remove" + } + ] + } + }, + { + "name": "MarketStatus", + "type": { + "kind": "enum", + "variants": [ + { + "name": "Initialized" + }, + { + "name": "Active" + }, + { + "name": "FundingPaused" + }, + { + "name": "AmmPaused" + }, + { + "name": "FillPaused" + }, + { + "name": "WithdrawPaused" + }, + { + "name": "ReduceOnly" + }, + { + "name": "Settlement" + }, + { + "name": "Delisted" + } + ] + } + }, + { + "name": "ContractType", + "type": { + "kind": "enum", + "variants": [ + { + "name": "Perpetual" + }, + { + "name": "Future" + }, + { + "name": "Prediction" + } + ] + } + }, + { + "name": "ContractTier", + "type": { + "kind": "enum", + "variants": [ + { + "name": "A" + }, + { + "name": "B" + }, + { + "name": "C" + }, + { + "name": "Speculative" + }, + { + "name": "HighlySpeculative" + }, + { + "name": "Isolated" + } + ] + } + }, + { + "name": "AMMLiquiditySplit", + "type": { + "kind": "enum", + "variants": [ + { + "name": "ProtocolOwned" + }, + { + "name": "LPOwned" + }, + { + "name": "Shared" + } + ] + } + }, + { + "name": "SettlePnlMode", + "type": { + "kind": "enum", + "variants": [ + { + "name": "MustSettle" + }, + { + "name": "TrySettle" + } + ] + } + }, + { + "name": "SpotBalanceType", + "type": { + "kind": "enum", + "variants": [ + { + "name": "Deposit" + }, + { + "name": "Borrow" + } + ] + } + }, + { + "name": "SpotFulfillmentConfigStatus", + "type": { + "kind": "enum", + "variants": [ + { + "name": "Enabled" + }, + { + "name": "Disabled" + } + ] + } + }, + { + "name": "AssetTier", + "type": { + "kind": "enum", + "variants": [ + { + "name": "Collateral" + }, + { + "name": "Protected" + }, + { + "name": "Cross" + }, + { + "name": "Isolated" + }, + { + "name": "Unlisted" + } + ] + } + }, + { + "name": "ExchangeStatus", + "type": { + "kind": "enum", + "variants": [ + { + "name": "DepositPaused" + }, + { + "name": "WithdrawPaused" + }, + { + "name": "AmmPaused" + }, + { + "name": "FillPaused" + }, + { + "name": "LiqPaused" + }, + { + "name": "FundingPaused" + }, + { + "name": "SettlePnlPaused" + } + ] + } + }, + { + "name": "UserStatus", + "type": { + "kind": "enum", + "variants": [ + { + "name": "BeingLiquidated" + }, + { + "name": "Bankrupt" + }, + { + "name": "ReduceOnly" + }, + { + "name": "AdvancedLp" + } + ] + } + }, + { + "name": "AssetType", + "type": { + "kind": "enum", + "variants": [ + { + "name": "Base" + }, + { + "name": "Quote" + } + ] + } + }, + { + "name": "OrderStatus", + "type": { + "kind": "enum", + "variants": [ + { + "name": "Init" + }, + { + "name": "Open" + }, + { + "name": "Filled" + }, + { + "name": "Canceled" + } + ] + } + }, + { + "name": "OrderType", + "type": { + "kind": "enum", + "variants": [ + { + "name": "Market" + }, + { + "name": "Limit" + }, + { + "name": "TriggerMarket" + }, + { + "name": "TriggerLimit" + }, + { + "name": "Oracle" + } + ] + } + }, + { + "name": "OrderTriggerCondition", + "type": { + "kind": "enum", + "variants": [ + { + "name": "Above" + }, + { + "name": "Below" + }, + { + "name": "TriggeredAbove" + }, + { + "name": "TriggeredBelow" + } + ] + } + }, + { + "name": "MarketType", + "type": { + "kind": "enum", + "variants": [ + { + "name": "Spot" + }, + { + "name": "Perp" + } + ] + } + } + ], + "events": [ + { + "name": "NewUserRecord", + "fields": [ + { + "name": "ts", + "type": "i64", + "index": false + }, + { + "name": "userAuthority", + "type": "publicKey", + "index": false + }, + { + "name": "user", + "type": "publicKey", + "index": false + }, + { + "name": "subAccountId", + "type": "u16", + "index": false + }, + { + "name": "name", + "type": { + "array": [ + "u8", + 32 + ] + }, + "index": false + }, + { + "name": "referrer", + "type": "publicKey", + "index": false + } + ] + }, + { + "name": "DepositRecord", + "fields": [ + { + "name": "ts", + "type": "i64", + "index": false + }, + { + "name": "userAuthority", + "type": "publicKey", + "index": false + }, + { + "name": "user", + "type": "publicKey", + "index": false + }, + { + "name": "direction", + "type": { + "defined": "DepositDirection" + }, + "index": false + }, + { + "name": "depositRecordId", + "type": "u64", + "index": false + }, + { + "name": "amount", + "type": "u64", + "index": false + }, + { + "name": "marketIndex", + "type": "u16", + "index": false + }, + { + "name": "oraclePrice", + "type": "i64", + "index": false + }, + { + "name": "marketDepositBalance", + "type": "u128", + "index": false + }, + { + "name": "marketWithdrawBalance", + "type": "u128", + "index": false + }, + { + "name": "marketCumulativeDepositInterest", + "type": "u128", + "index": false + }, + { + "name": "marketCumulativeBorrowInterest", + "type": "u128", + "index": false + }, + { + "name": "totalDepositsAfter", + "type": "u64", + "index": false + }, + { + "name": "totalWithdrawsAfter", + "type": "u64", + "index": false + }, + { + "name": "explanation", + "type": { + "defined": "DepositExplanation" + }, + "index": false + }, + { + "name": "transferUser", + "type": { + "option": "publicKey" + }, + "index": false + } + ] + }, + { + "name": "SpotInterestRecord", + "fields": [ + { + "name": "ts", + "type": "i64", + "index": false + }, + { + "name": "marketIndex", + "type": "u16", + "index": false + }, + { + "name": "depositBalance", + "type": "u128", + "index": false + }, + { + "name": "cumulativeDepositInterest", + "type": "u128", + "index": false + }, + { + "name": "borrowBalance", + "type": "u128", + "index": false + }, + { + "name": "cumulativeBorrowInterest", + "type": "u128", + "index": false + }, + { + "name": "optimalUtilization", + "type": "u32", + "index": false + }, + { + "name": "optimalBorrowRate", + "type": "u32", + "index": false + }, + { + "name": "maxBorrowRate", + "type": "u32", + "index": false + } + ] + }, + { + "name": "FundingPaymentRecord", + "fields": [ + { + "name": "ts", + "type": "i64", + "index": false + }, + { + "name": "userAuthority", + "type": "publicKey", + "index": false + }, + { + "name": "user", + "type": "publicKey", + "index": false + }, + { + "name": "marketIndex", + "type": "u16", + "index": false + }, + { + "name": "fundingPayment", + "type": "i64", + "index": false + }, + { + "name": "baseAssetAmount", + "type": "i64", + "index": false + }, + { + "name": "userLastCumulativeFunding", + "type": "i64", + "index": false + }, + { + "name": "ammCumulativeFundingLong", + "type": "i128", + "index": false + }, + { + "name": "ammCumulativeFundingShort", + "type": "i128", + "index": false + } + ] + }, + { + "name": "FundingRateRecord", + "fields": [ + { + "name": "ts", + "type": "i64", + "index": false + }, + { + "name": "recordId", + "type": "u64", + "index": false + }, + { + "name": "marketIndex", + "type": "u16", + "index": false + }, + { + "name": "fundingRate", + "type": "i64", + "index": false + }, + { + "name": "fundingRateLong", + "type": "i128", + "index": false + }, + { + "name": "fundingRateShort", + "type": "i128", + "index": false + }, + { + "name": "cumulativeFundingRateLong", + "type": "i128", + "index": false + }, + { + "name": "cumulativeFundingRateShort", + "type": "i128", + "index": false + }, + { + "name": "oraclePriceTwap", + "type": "i64", + "index": false + }, + { + "name": "markPriceTwap", + "type": "u64", + "index": false + }, + { + "name": "periodRevenue", + "type": "i64", + "index": false + }, + { + "name": "baseAssetAmountWithAmm", + "type": "i128", + "index": false + }, + { + "name": "baseAssetAmountWithUnsettledLp", + "type": "i128", + "index": false + } + ] + }, + { + "name": "CurveRecord", + "fields": [ + { + "name": "ts", + "type": "i64", + "index": false + }, + { + "name": "recordId", + "type": "u64", + "index": false + }, + { + "name": "pegMultiplierBefore", + "type": "u128", + "index": false + }, + { + "name": "baseAssetReserveBefore", + "type": "u128", + "index": false + }, + { + "name": "quoteAssetReserveBefore", + "type": "u128", + "index": false + }, + { + "name": "sqrtKBefore", + "type": "u128", + "index": false + }, + { + "name": "pegMultiplierAfter", + "type": "u128", + "index": false + }, + { + "name": "baseAssetReserveAfter", + "type": "u128", + "index": false + }, + { + "name": "quoteAssetReserveAfter", + "type": "u128", + "index": false + }, + { + "name": "sqrtKAfter", + "type": "u128", + "index": false + }, + { + "name": "baseAssetAmountLong", + "type": "u128", + "index": false + }, + { + "name": "baseAssetAmountShort", + "type": "u128", + "index": false + }, + { + "name": "baseAssetAmountWithAmm", + "type": "i128", + "index": false + }, + { + "name": "totalFee", + "type": "i128", + "index": false + }, + { + "name": "totalFeeMinusDistributions", + "type": "i128", + "index": false + }, + { + "name": "adjustmentCost", + "type": "i128", + "index": false + }, + { + "name": "oraclePrice", + "type": "i64", + "index": false + }, + { + "name": "fillRecord", + "type": "u128", + "index": false + }, + { + "name": "numberOfUsers", + "type": "u32", + "index": false + }, + { + "name": "marketIndex", + "type": "u16", + "index": false + } + ] + }, + { + "name": "OrderRecord", + "fields": [ + { + "name": "ts", + "type": "i64", + "index": false + }, + { + "name": "user", + "type": "publicKey", + "index": false + }, + { + "name": "order", + "type": { + "defined": "Order" + }, + "index": false + } + ] + }, + { + "name": "OrderActionRecord", + "fields": [ + { + "name": "ts", + "type": "i64", + "index": false + }, + { + "name": "action", + "type": { + "defined": "OrderAction" + }, + "index": false + }, + { + "name": "actionExplanation", + "type": { + "defined": "OrderActionExplanation" + }, + "index": false + }, + { + "name": "marketIndex", + "type": "u16", + "index": false + }, + { + "name": "marketType", + "type": { + "defined": "MarketType" + }, + "index": false + }, + { + "name": "filler", + "type": { + "option": "publicKey" + }, + "index": false + }, + { + "name": "fillerReward", + "type": { + "option": "u64" + }, + "index": false + }, + { + "name": "fillRecordId", + "type": { + "option": "u64" + }, + "index": false + }, + { + "name": "baseAssetAmountFilled", + "type": { + "option": "u64" + }, + "index": false + }, + { + "name": "quoteAssetAmountFilled", + "type": { + "option": "u64" + }, + "index": false + }, + { + "name": "takerFee", + "type": { + "option": "u64" + }, + "index": false + }, + { + "name": "makerFee", + "type": { + "option": "i64" + }, + "index": false + }, + { + "name": "referrerReward", + "type": { + "option": "u32" + }, + "index": false + }, + { + "name": "quoteAssetAmountSurplus", + "type": { + "option": "i64" + }, + "index": false + }, + { + "name": "spotFulfillmentMethodFee", + "type": { + "option": "u64" + }, + "index": false + }, + { + "name": "taker", + "type": { + "option": "publicKey" + }, + "index": false + }, + { + "name": "takerOrderId", + "type": { + "option": "u32" + }, + "index": false + }, + { + "name": "takerOrderDirection", + "type": { + "option": { + "defined": "PositionDirection" + } + }, + "index": false + }, + { + "name": "takerOrderBaseAssetAmount", + "type": { + "option": "u64" + }, + "index": false + }, + { + "name": "takerOrderCumulativeBaseAssetAmountFilled", + "type": { + "option": "u64" + }, + "index": false + }, + { + "name": "takerOrderCumulativeQuoteAssetAmountFilled", + "type": { + "option": "u64" + }, + "index": false + }, + { + "name": "maker", + "type": { + "option": "publicKey" + }, + "index": false + }, + { + "name": "makerOrderId", + "type": { + "option": "u32" + }, + "index": false + }, + { + "name": "makerOrderDirection", + "type": { + "option": { + "defined": "PositionDirection" + } + }, + "index": false + }, + { + "name": "makerOrderBaseAssetAmount", + "type": { + "option": "u64" + }, + "index": false + }, + { + "name": "makerOrderCumulativeBaseAssetAmountFilled", + "type": { + "option": "u64" + }, + "index": false + }, + { + "name": "makerOrderCumulativeQuoteAssetAmountFilled", + "type": { + "option": "u64" + }, + "index": false + }, + { + "name": "oraclePrice", + "type": "i64", + "index": false + } + ] + }, + { + "name": "LPRecord", + "fields": [ + { + "name": "ts", + "type": "i64", + "index": false + }, + { + "name": "user", + "type": "publicKey", + "index": false + }, + { + "name": "action", + "type": { + "defined": "LPAction" + }, + "index": false + }, + { + "name": "nShares", + "type": "u64", + "index": false + }, + { + "name": "marketIndex", + "type": "u16", + "index": false + }, + { + "name": "deltaBaseAssetAmount", + "type": "i64", + "index": false + }, + { + "name": "deltaQuoteAssetAmount", + "type": "i64", + "index": false + }, + { + "name": "pnl", + "type": "i64", + "index": false + } + ] + }, + { + "name": "LiquidationRecord", + "fields": [ + { + "name": "ts", + "type": "i64", + "index": false + }, + { + "name": "liquidationType", + "type": { + "defined": "LiquidationType" + }, + "index": false + }, + { + "name": "user", + "type": "publicKey", + "index": false + }, + { + "name": "liquidator", + "type": "publicKey", + "index": false + }, + { + "name": "marginRequirement", + "type": "u128", + "index": false + }, + { + "name": "totalCollateral", + "type": "i128", + "index": false + }, + { + "name": "marginFreed", + "type": "u64", + "index": false + }, + { + "name": "liquidationId", + "type": "u16", + "index": false + }, + { + "name": "bankrupt", + "type": "bool", + "index": false + }, + { + "name": "canceledOrderIds", + "type": { + "vec": "u32" + }, + "index": false + }, + { + "name": "liquidatePerp", + "type": { + "defined": "LiquidatePerpRecord" + }, + "index": false + }, + { + "name": "liquidateSpot", + "type": { + "defined": "LiquidateSpotRecord" + }, + "index": false + }, + { + "name": "liquidateBorrowForPerpPnl", + "type": { + "defined": "LiquidateBorrowForPerpPnlRecord" + }, + "index": false + }, + { + "name": "liquidatePerpPnlForDeposit", + "type": { + "defined": "LiquidatePerpPnlForDepositRecord" + }, + "index": false + }, + { + "name": "perpBankruptcy", + "type": { + "defined": "PerpBankruptcyRecord" + }, + "index": false + }, + { + "name": "spotBankruptcy", + "type": { + "defined": "SpotBankruptcyRecord" + }, + "index": false + } + ] + }, + { + "name": "SettlePnlRecord", + "fields": [ + { + "name": "ts", + "type": "i64", + "index": false + }, + { + "name": "user", + "type": "publicKey", + "index": false + }, + { + "name": "marketIndex", + "type": "u16", + "index": false + }, + { + "name": "pnl", + "type": "i128", + "index": false + }, + { + "name": "baseAssetAmount", + "type": "i64", + "index": false + }, + { + "name": "quoteAssetAmountAfter", + "type": "i64", + "index": false + }, + { + "name": "quoteEntryAmount", + "type": "i64", + "index": false + }, + { + "name": "settlePrice", + "type": "i64", + "index": false + }, + { + "name": "explanation", + "type": { + "defined": "SettlePnlExplanation" + }, + "index": false + } + ] + }, + { + "name": "InsuranceFundRecord", + "fields": [ + { + "name": "ts", + "type": "i64", + "index": false + }, + { + "name": "spotMarketIndex", + "type": "u16", + "index": false + }, + { + "name": "perpMarketIndex", + "type": "u16", + "index": false + }, + { + "name": "userIfFactor", + "type": "u32", + "index": false + }, + { + "name": "totalIfFactor", + "type": "u32", + "index": false + }, + { + "name": "vaultAmountBefore", + "type": "u64", + "index": false + }, + { + "name": "insuranceVaultAmountBefore", + "type": "u64", + "index": false + }, + { + "name": "totalIfSharesBefore", + "type": "u128", + "index": false + }, + { + "name": "totalIfSharesAfter", + "type": "u128", + "index": false + }, + { + "name": "amount", + "type": "i64", + "index": false + } + ] + }, + { + "name": "InsuranceFundStakeRecord", + "fields": [ + { + "name": "ts", + "type": "i64", + "index": false + }, + { + "name": "userAuthority", + "type": "publicKey", + "index": false + }, + { + "name": "action", + "type": { + "defined": "StakeAction" + }, + "index": false + }, + { + "name": "amount", + "type": "u64", + "index": false + }, + { + "name": "marketIndex", + "type": "u16", + "index": false + }, + { + "name": "insuranceVaultAmountBefore", + "type": "u64", + "index": false + }, + { + "name": "ifSharesBefore", + "type": "u128", + "index": false + }, + { + "name": "userIfSharesBefore", + "type": "u128", + "index": false + }, + { + "name": "totalIfSharesBefore", + "type": "u128", + "index": false + }, + { + "name": "ifSharesAfter", + "type": "u128", + "index": false + }, + { + "name": "userIfSharesAfter", + "type": "u128", + "index": false + }, + { + "name": "totalIfSharesAfter", + "type": "u128", + "index": false + } + ] + }, + { + "name": "SwapRecord", + "fields": [ + { + "name": "ts", + "type": "i64", + "index": false + }, + { + "name": "user", + "type": "publicKey", + "index": false + }, + { + "name": "amountOut", + "type": "u64", + "index": false + }, + { + "name": "amountIn", + "type": "u64", + "index": false + }, + { + "name": "outMarketIndex", + "type": "u16", + "index": false + }, + { + "name": "inMarketIndex", + "type": "u16", + "index": false + }, + { + "name": "outOraclePrice", + "type": "i64", + "index": false + }, + { + "name": "inOraclePrice", + "type": "i64", + "index": false + }, + { + "name": "fee", + "type": "u64", + "index": false + } + ] + }, + { + "name": "SpotMarketVaultDepositRecord", + "fields": [ + { + "name": "ts", + "type": "i64", + "index": false + }, + { + "name": "marketIndex", + "type": "u16", + "index": false + }, + { + "name": "depositBalance", + "type": "u128", + "index": false + }, + { + "name": "cumulativeDepositInterestBefore", + "type": "u128", + "index": false + }, + { + "name": "cumulativeDepositInterestAfter", + "type": "u128", + "index": false + }, + { + "name": "depositTokenAmountBefore", + "type": "u64", + "index": false + }, + { + "name": "amount", + "type": "u64", + "index": false + } + ] + } + ], + "errors": [ + { + "code": 6000, + "name": "InvalidSpotMarketAuthority", + "msg": "Invalid Spot Market Authority" + }, + { + "code": 6001, + "name": "InvalidInsuranceFundAuthority", + "msg": "Clearing house not insurance fund authority" + }, + { + "code": 6002, + "name": "InsufficientDeposit", + "msg": "Insufficient deposit" + }, + { + "code": 6003, + "name": "InsufficientCollateral", + "msg": "Insufficient collateral" + }, + { + "code": 6004, + "name": "SufficientCollateral", + "msg": "Sufficient collateral" + }, + { + "code": 6005, + "name": "MaxNumberOfPositions", + "msg": "Max number of positions taken" + }, + { + "code": 6006, + "name": "AdminControlsPricesDisabled", + "msg": "Admin Controls Prices Disabled" + }, + { + "code": 6007, + "name": "MarketDelisted", + "msg": "Market Delisted" + }, + { + "code": 6008, + "name": "MarketIndexAlreadyInitialized", + "msg": "Market Index Already Initialized" + }, + { + "code": 6009, + "name": "UserAccountAndUserPositionsAccountMismatch", + "msg": "User Account And User Positions Account Mismatch" + }, + { + "code": 6010, + "name": "UserHasNoPositionInMarket", + "msg": "User Has No Position In Market" + }, + { + "code": 6011, + "name": "InvalidInitialPeg", + "msg": "Invalid Initial Peg" + }, + { + "code": 6012, + "name": "InvalidRepegRedundant", + "msg": "AMM repeg already configured with amt given" + }, + { + "code": 6013, + "name": "InvalidRepegDirection", + "msg": "AMM repeg incorrect repeg direction" + }, + { + "code": 6014, + "name": "InvalidRepegProfitability", + "msg": "AMM repeg out of bounds pnl" + }, + { + "code": 6015, + "name": "SlippageOutsideLimit", + "msg": "Slippage Outside Limit Price" + }, + { + "code": 6016, + "name": "OrderSizeTooSmall", + "msg": "Order Size Too Small" + }, + { + "code": 6017, + "name": "InvalidUpdateK", + "msg": "Price change too large when updating K" + }, + { + "code": 6018, + "name": "AdminWithdrawTooLarge", + "msg": "Admin tried to withdraw amount larger than fees collected" + }, + { + "code": 6019, + "name": "MathError", + "msg": "Math Error" + }, + { + "code": 6020, + "name": "BnConversionError", + "msg": "Conversion to u128/u64 failed with an overflow or underflow" + }, + { + "code": 6021, + "name": "ClockUnavailable", + "msg": "Clock unavailable" + }, + { + "code": 6022, + "name": "UnableToLoadOracle", + "msg": "Unable To Load Oracles" + }, + { + "code": 6023, + "name": "PriceBandsBreached", + "msg": "Price Bands Breached" + }, + { + "code": 6024, + "name": "ExchangePaused", + "msg": "Exchange is paused" + }, + { + "code": 6025, + "name": "InvalidWhitelistToken", + "msg": "Invalid whitelist token" + }, + { + "code": 6026, + "name": "WhitelistTokenNotFound", + "msg": "Whitelist token not found" + }, + { + "code": 6027, + "name": "InvalidDiscountToken", + "msg": "Invalid discount token" + }, + { + "code": 6028, + "name": "DiscountTokenNotFound", + "msg": "Discount token not found" + }, + { + "code": 6029, + "name": "ReferrerNotFound", + "msg": "Referrer not found" + }, + { + "code": 6030, + "name": "ReferrerStatsNotFound", + "msg": "ReferrerNotFound" + }, + { + "code": 6031, + "name": "ReferrerMustBeWritable", + "msg": "ReferrerMustBeWritable" + }, + { + "code": 6032, + "name": "ReferrerStatsMustBeWritable", + "msg": "ReferrerMustBeWritable" + }, + { + "code": 6033, + "name": "ReferrerAndReferrerStatsAuthorityUnequal", + "msg": "ReferrerAndReferrerStatsAuthorityUnequal" + }, + { + "code": 6034, + "name": "InvalidReferrer", + "msg": "InvalidReferrer" + }, + { + "code": 6035, + "name": "InvalidOracle", + "msg": "InvalidOracle" + }, + { + "code": 6036, + "name": "OracleNotFound", + "msg": "OracleNotFound" + }, + { + "code": 6037, + "name": "LiquidationsBlockedByOracle", + "msg": "Liquidations Blocked By Oracle" + }, + { + "code": 6038, + "name": "MaxDeposit", + "msg": "Can not deposit more than max deposit" + }, + { + "code": 6039, + "name": "CantDeleteUserWithCollateral", + "msg": "Can not delete user that still has collateral" + }, + { + "code": 6040, + "name": "InvalidFundingProfitability", + "msg": "AMM funding out of bounds pnl" + }, + { + "code": 6041, + "name": "CastingFailure", + "msg": "Casting Failure" + }, + { + "code": 6042, + "name": "InvalidOrder", + "msg": "InvalidOrder" + }, + { + "code": 6043, + "name": "InvalidOrderMaxTs", + "msg": "InvalidOrderMaxTs" + }, + { + "code": 6044, + "name": "InvalidOrderMarketType", + "msg": "InvalidOrderMarketType" + }, + { + "code": 6045, + "name": "InvalidOrderForInitialMarginReq", + "msg": "InvalidOrderForInitialMarginReq" + }, + { + "code": 6046, + "name": "InvalidOrderNotRiskReducing", + "msg": "InvalidOrderNotRiskReducing" + }, + { + "code": 6047, + "name": "InvalidOrderSizeTooSmall", + "msg": "InvalidOrderSizeTooSmall" + }, + { + "code": 6048, + "name": "InvalidOrderNotStepSizeMultiple", + "msg": "InvalidOrderNotStepSizeMultiple" + }, + { + "code": 6049, + "name": "InvalidOrderBaseQuoteAsset", + "msg": "InvalidOrderBaseQuoteAsset" + }, + { + "code": 6050, + "name": "InvalidOrderIOC", + "msg": "InvalidOrderIOC" + }, + { + "code": 6051, + "name": "InvalidOrderPostOnly", + "msg": "InvalidOrderPostOnly" + }, + { + "code": 6052, + "name": "InvalidOrderIOCPostOnly", + "msg": "InvalidOrderIOCPostOnly" + }, + { + "code": 6053, + "name": "InvalidOrderTrigger", + "msg": "InvalidOrderTrigger" + }, + { + "code": 6054, + "name": "InvalidOrderAuction", + "msg": "InvalidOrderAuction" + }, + { + "code": 6055, + "name": "InvalidOrderOracleOffset", + "msg": "InvalidOrderOracleOffset" + }, + { + "code": 6056, + "name": "InvalidOrderMinOrderSize", + "msg": "InvalidOrderMinOrderSize" + }, + { + "code": 6057, + "name": "PlacePostOnlyLimitFailure", + "msg": "Failed to Place Post-Only Limit Order" + }, + { + "code": 6058, + "name": "UserHasNoOrder", + "msg": "User has no order" + }, + { + "code": 6059, + "name": "OrderAmountTooSmall", + "msg": "Order Amount Too Small" + }, + { + "code": 6060, + "name": "MaxNumberOfOrders", + "msg": "Max number of orders taken" + }, + { + "code": 6061, + "name": "OrderDoesNotExist", + "msg": "Order does not exist" + }, + { + "code": 6062, + "name": "OrderNotOpen", + "msg": "Order not open" + }, + { + "code": 6063, + "name": "FillOrderDidNotUpdateState", + "msg": "FillOrderDidNotUpdateState" + }, + { + "code": 6064, + "name": "ReduceOnlyOrderIncreasedRisk", + "msg": "Reduce only order increased risk" + }, + { + "code": 6065, + "name": "UnableToLoadAccountLoader", + "msg": "Unable to load AccountLoader" + }, + { + "code": 6066, + "name": "TradeSizeTooLarge", + "msg": "Trade Size Too Large" + }, + { + "code": 6067, + "name": "UserCantReferThemselves", + "msg": "User cant refer themselves" + }, + { + "code": 6068, + "name": "DidNotReceiveExpectedReferrer", + "msg": "Did not receive expected referrer" + }, + { + "code": 6069, + "name": "CouldNotDeserializeReferrer", + "msg": "Could not deserialize referrer" + }, + { + "code": 6070, + "name": "CouldNotDeserializeReferrerStats", + "msg": "Could not deserialize referrer stats" + }, + { + "code": 6071, + "name": "UserOrderIdAlreadyInUse", + "msg": "User Order Id Already In Use" + }, + { + "code": 6072, + "name": "NoPositionsLiquidatable", + "msg": "No positions liquidatable" + }, + { + "code": 6073, + "name": "InvalidMarginRatio", + "msg": "Invalid Margin Ratio" + }, + { + "code": 6074, + "name": "CantCancelPostOnlyOrder", + "msg": "Cant Cancel Post Only Order" + }, + { + "code": 6075, + "name": "InvalidOracleOffset", + "msg": "InvalidOracleOffset" + }, + { + "code": 6076, + "name": "CantExpireOrders", + "msg": "CantExpireOrders" + }, + { + "code": 6077, + "name": "CouldNotLoadMarketData", + "msg": "CouldNotLoadMarketData" + }, + { + "code": 6078, + "name": "PerpMarketNotFound", + "msg": "PerpMarketNotFound" + }, + { + "code": 6079, + "name": "InvalidMarketAccount", + "msg": "InvalidMarketAccount" + }, + { + "code": 6080, + "name": "UnableToLoadPerpMarketAccount", + "msg": "UnableToLoadMarketAccount" + }, + { + "code": 6081, + "name": "MarketWrongMutability", + "msg": "MarketWrongMutability" + }, + { + "code": 6082, + "name": "UnableToCastUnixTime", + "msg": "UnableToCastUnixTime" + }, + { + "code": 6083, + "name": "CouldNotFindSpotPosition", + "msg": "CouldNotFindSpotPosition" + }, + { + "code": 6084, + "name": "NoSpotPositionAvailable", + "msg": "NoSpotPositionAvailable" + }, + { + "code": 6085, + "name": "InvalidSpotMarketInitialization", + "msg": "InvalidSpotMarketInitialization" + }, + { + "code": 6086, + "name": "CouldNotLoadSpotMarketData", + "msg": "CouldNotLoadSpotMarketData" + }, + { + "code": 6087, + "name": "SpotMarketNotFound", + "msg": "SpotMarketNotFound" + }, + { + "code": 6088, + "name": "InvalidSpotMarketAccount", + "msg": "InvalidSpotMarketAccount" + }, + { + "code": 6089, + "name": "UnableToLoadSpotMarketAccount", + "msg": "UnableToLoadSpotMarketAccount" + }, + { + "code": 6090, + "name": "SpotMarketWrongMutability", + "msg": "SpotMarketWrongMutability" + }, + { + "code": 6091, + "name": "SpotMarketInterestNotUpToDate", + "msg": "SpotInterestNotUpToDate" + }, + { + "code": 6092, + "name": "SpotMarketInsufficientDeposits", + "msg": "SpotMarketInsufficientDeposits" + }, + { + "code": 6093, + "name": "UserMustSettleTheirOwnPositiveUnsettledPNL", + "msg": "UserMustSettleTheirOwnPositiveUnsettledPNL" + }, + { + "code": 6094, + "name": "CantUpdatePoolBalanceType", + "msg": "CantUpdatePoolBalanceType" + }, + { + "code": 6095, + "name": "InsufficientCollateralForSettlingPNL", + "msg": "InsufficientCollateralForSettlingPNL" + }, + { + "code": 6096, + "name": "AMMNotUpdatedInSameSlot", + "msg": "AMMNotUpdatedInSameSlot" + }, + { + "code": 6097, + "name": "AuctionNotComplete", + "msg": "AuctionNotComplete" + }, + { + "code": 6098, + "name": "MakerNotFound", + "msg": "MakerNotFound" + }, + { + "code": 6099, + "name": "MakerStatsNotFound", + "msg": "MakerNotFound" + }, + { + "code": 6100, + "name": "MakerMustBeWritable", + "msg": "MakerMustBeWritable" + }, + { + "code": 6101, + "name": "MakerStatsMustBeWritable", + "msg": "MakerMustBeWritable" + }, + { + "code": 6102, + "name": "MakerOrderNotFound", + "msg": "MakerOrderNotFound" + }, + { + "code": 6103, + "name": "CouldNotDeserializeMaker", + "msg": "CouldNotDeserializeMaker" + }, + { + "code": 6104, + "name": "CouldNotDeserializeMakerStats", + "msg": "CouldNotDeserializeMaker" + }, + { + "code": 6105, + "name": "AuctionPriceDoesNotSatisfyMaker", + "msg": "AuctionPriceDoesNotSatisfyMaker" + }, + { + "code": 6106, + "name": "MakerCantFulfillOwnOrder", + "msg": "MakerCantFulfillOwnOrder" + }, + { + "code": 6107, + "name": "MakerOrderMustBePostOnly", + "msg": "MakerOrderMustBePostOnly" + }, + { + "code": 6108, + "name": "CantMatchTwoPostOnlys", + "msg": "CantMatchTwoPostOnlys" + }, + { + "code": 6109, + "name": "OrderBreachesOraclePriceLimits", + "msg": "OrderBreachesOraclePriceLimits" + }, + { + "code": 6110, + "name": "OrderMustBeTriggeredFirst", + "msg": "OrderMustBeTriggeredFirst" + }, + { + "code": 6111, + "name": "OrderNotTriggerable", + "msg": "OrderNotTriggerable" + }, + { + "code": 6112, + "name": "OrderDidNotSatisfyTriggerCondition", + "msg": "OrderDidNotSatisfyTriggerCondition" + }, + { + "code": 6113, + "name": "PositionAlreadyBeingLiquidated", + "msg": "PositionAlreadyBeingLiquidated" + }, + { + "code": 6114, + "name": "PositionDoesntHaveOpenPositionOrOrders", + "msg": "PositionDoesntHaveOpenPositionOrOrders" + }, + { + "code": 6115, + "name": "AllOrdersAreAlreadyLiquidations", + "msg": "AllOrdersAreAlreadyLiquidations" + }, + { + "code": 6116, + "name": "CantCancelLiquidationOrder", + "msg": "CantCancelLiquidationOrder" + }, + { + "code": 6117, + "name": "UserIsBeingLiquidated", + "msg": "UserIsBeingLiquidated" + }, + { + "code": 6118, + "name": "LiquidationsOngoing", + "msg": "LiquidationsOngoing" + }, + { + "code": 6119, + "name": "WrongSpotBalanceType", + "msg": "WrongSpotBalanceType" + }, + { + "code": 6120, + "name": "UserCantLiquidateThemself", + "msg": "UserCantLiquidateThemself" + }, + { + "code": 6121, + "name": "InvalidPerpPositionToLiquidate", + "msg": "InvalidPerpPositionToLiquidate" + }, + { + "code": 6122, + "name": "InvalidBaseAssetAmountForLiquidatePerp", + "msg": "InvalidBaseAssetAmountForLiquidatePerp" + }, + { + "code": 6123, + "name": "InvalidPositionLastFundingRate", + "msg": "InvalidPositionLastFundingRate" + }, + { + "code": 6124, + "name": "InvalidPositionDelta", + "msg": "InvalidPositionDelta" + }, + { + "code": 6125, + "name": "UserBankrupt", + "msg": "UserBankrupt" + }, + { + "code": 6126, + "name": "UserNotBankrupt", + "msg": "UserNotBankrupt" + }, + { + "code": 6127, + "name": "UserHasInvalidBorrow", + "msg": "UserHasInvalidBorrow" + }, + { + "code": 6128, + "name": "DailyWithdrawLimit", + "msg": "DailyWithdrawLimit" + }, + { + "code": 6129, + "name": "DefaultError", + "msg": "DefaultError" + }, + { + "code": 6130, + "name": "InsufficientLPTokens", + "msg": "Insufficient LP tokens" + }, + { + "code": 6131, + "name": "CantLPWithPerpPosition", + "msg": "Cant LP with a market position" + }, + { + "code": 6132, + "name": "UnableToBurnLPTokens", + "msg": "Unable to burn LP tokens" + }, + { + "code": 6133, + "name": "TryingToRemoveLiquidityTooFast", + "msg": "Trying to remove liqudity too fast after adding it" + }, + { + "code": 6134, + "name": "InvalidSpotMarketVault", + "msg": "Invalid Spot Market Vault" + }, + { + "code": 6135, + "name": "InvalidSpotMarketState", + "msg": "Invalid Spot Market State" + }, + { + "code": 6136, + "name": "InvalidSerumProgram", + "msg": "InvalidSerumProgram" + }, + { + "code": 6137, + "name": "InvalidSerumMarket", + "msg": "InvalidSerumMarket" + }, + { + "code": 6138, + "name": "InvalidSerumBids", + "msg": "InvalidSerumBids" + }, + { + "code": 6139, + "name": "InvalidSerumAsks", + "msg": "InvalidSerumAsks" + }, + { + "code": 6140, + "name": "InvalidSerumOpenOrders", + "msg": "InvalidSerumOpenOrders" + }, + { + "code": 6141, + "name": "FailedSerumCPI", + "msg": "FailedSerumCPI" + }, + { + "code": 6142, + "name": "FailedToFillOnExternalMarket", + "msg": "FailedToFillOnExternalMarket" + }, + { + "code": 6143, + "name": "InvalidFulfillmentConfig", + "msg": "InvalidFulfillmentConfig" + }, + { + "code": 6144, + "name": "InvalidFeeStructure", + "msg": "InvalidFeeStructure" + }, + { + "code": 6145, + "name": "InsufficientIFShares", + "msg": "Insufficient IF shares" + }, + { + "code": 6146, + "name": "MarketActionPaused", + "msg": "the Market has paused this action" + }, + { + "code": 6147, + "name": "MarketPlaceOrderPaused", + "msg": "the Market status doesnt allow placing orders" + }, + { + "code": 6148, + "name": "MarketFillOrderPaused", + "msg": "the Market status doesnt allow filling orders" + }, + { + "code": 6149, + "name": "MarketWithdrawPaused", + "msg": "the Market status doesnt allow withdraws" + }, + { + "code": 6150, + "name": "ProtectedAssetTierViolation", + "msg": "Action violates the Protected Asset Tier rules" + }, + { + "code": 6151, + "name": "IsolatedAssetTierViolation", + "msg": "Action violates the Isolated Asset Tier rules" + }, + { + "code": 6152, + "name": "UserCantBeDeleted", + "msg": "User Cant Be Deleted" + }, + { + "code": 6153, + "name": "ReduceOnlyWithdrawIncreasedRisk", + "msg": "Reduce Only Withdraw Increased Risk" + }, + { + "code": 6154, + "name": "MaxOpenInterest", + "msg": "Max Open Interest" + }, + { + "code": 6155, + "name": "CantResolvePerpBankruptcy", + "msg": "Cant Resolve Perp Bankruptcy" + }, + { + "code": 6156, + "name": "LiquidationDoesntSatisfyLimitPrice", + "msg": "Liquidation Doesnt Satisfy Limit Price" + }, + { + "code": 6157, + "name": "MarginTradingDisabled", + "msg": "Margin Trading Disabled" + }, + { + "code": 6158, + "name": "InvalidMarketStatusToSettlePnl", + "msg": "Invalid Market Status to Settle Perp Pnl" + }, + { + "code": 6159, + "name": "PerpMarketNotInSettlement", + "msg": "PerpMarketNotInSettlement" + }, + { + "code": 6160, + "name": "PerpMarketNotInReduceOnly", + "msg": "PerpMarketNotInReduceOnly" + }, + { + "code": 6161, + "name": "PerpMarketSettlementBufferNotReached", + "msg": "PerpMarketSettlementBufferNotReached" + }, + { + "code": 6162, + "name": "PerpMarketSettlementUserHasOpenOrders", + "msg": "PerpMarketSettlementUserHasOpenOrders" + }, + { + "code": 6163, + "name": "PerpMarketSettlementUserHasActiveLP", + "msg": "PerpMarketSettlementUserHasActiveLP" + }, + { + "code": 6164, + "name": "UnableToSettleExpiredUserPosition", + "msg": "UnableToSettleExpiredUserPosition" + }, + { + "code": 6165, + "name": "UnequalMarketIndexForSpotTransfer", + "msg": "UnequalMarketIndexForSpotTransfer" + }, + { + "code": 6166, + "name": "InvalidPerpPositionDetected", + "msg": "InvalidPerpPositionDetected" + }, + { + "code": 6167, + "name": "InvalidSpotPositionDetected", + "msg": "InvalidSpotPositionDetected" + }, + { + "code": 6168, + "name": "InvalidAmmDetected", + "msg": "InvalidAmmDetected" + }, + { + "code": 6169, + "name": "InvalidAmmForFillDetected", + "msg": "InvalidAmmForFillDetected" + }, + { + "code": 6170, + "name": "InvalidAmmLimitPriceOverride", + "msg": "InvalidAmmLimitPriceOverride" + }, + { + "code": 6171, + "name": "InvalidOrderFillPrice", + "msg": "InvalidOrderFillPrice" + }, + { + "code": 6172, + "name": "SpotMarketBalanceInvariantViolated", + "msg": "SpotMarketBalanceInvariantViolated" + }, + { + "code": 6173, + "name": "SpotMarketVaultInvariantViolated", + "msg": "SpotMarketVaultInvariantViolated" + }, + { + "code": 6174, + "name": "InvalidPDA", + "msg": "InvalidPDA" + }, + { + "code": 6175, + "name": "InvalidPDASigner", + "msg": "InvalidPDASigner" + }, + { + "code": 6176, + "name": "RevenueSettingsCannotSettleToIF", + "msg": "RevenueSettingsCannotSettleToIF" + }, + { + "code": 6177, + "name": "NoRevenueToSettleToIF", + "msg": "NoRevenueToSettleToIF" + }, + { + "code": 6178, + "name": "NoAmmPerpPnlDeficit", + "msg": "NoAmmPerpPnlDeficit" + }, + { + "code": 6179, + "name": "SufficientPerpPnlPool", + "msg": "SufficientPerpPnlPool" + }, + { + "code": 6180, + "name": "InsufficientPerpPnlPool", + "msg": "InsufficientPerpPnlPool" + }, + { + "code": 6181, + "name": "PerpPnlDeficitBelowThreshold", + "msg": "PerpPnlDeficitBelowThreshold" + }, + { + "code": 6182, + "name": "MaxRevenueWithdrawPerPeriodReached", + "msg": "MaxRevenueWithdrawPerPeriodReached" + }, + { + "code": 6183, + "name": "MaxIFWithdrawReached", + "msg": "InvalidSpotPositionDetected" + }, + { + "code": 6184, + "name": "NoIFWithdrawAvailable", + "msg": "NoIFWithdrawAvailable" + }, + { + "code": 6185, + "name": "InvalidIFUnstake", + "msg": "InvalidIFUnstake" + }, + { + "code": 6186, + "name": "InvalidIFUnstakeSize", + "msg": "InvalidIFUnstakeSize" + }, + { + "code": 6187, + "name": "InvalidIFUnstakeCancel", + "msg": "InvalidIFUnstakeCancel" + }, + { + "code": 6188, + "name": "InvalidIFForNewStakes", + "msg": "InvalidIFForNewStakes" + }, + { + "code": 6189, + "name": "InvalidIFRebase", + "msg": "InvalidIFRebase" + }, + { + "code": 6190, + "name": "InvalidInsuranceUnstakeSize", + "msg": "InvalidInsuranceUnstakeSize" + }, + { + "code": 6191, + "name": "InvalidOrderLimitPrice", + "msg": "InvalidOrderLimitPrice" + }, + { + "code": 6192, + "name": "InvalidIFDetected", + "msg": "InvalidIFDetected" + }, + { + "code": 6193, + "name": "InvalidAmmMaxSpreadDetected", + "msg": "InvalidAmmMaxSpreadDetected" + }, + { + "code": 6194, + "name": "InvalidConcentrationCoef", + "msg": "InvalidConcentrationCoef" + }, + { + "code": 6195, + "name": "InvalidSrmVault", + "msg": "InvalidSrmVault" + }, + { + "code": 6196, + "name": "InvalidVaultOwner", + "msg": "InvalidVaultOwner" + }, + { + "code": 6197, + "name": "InvalidMarketStatusForFills", + "msg": "InvalidMarketStatusForFills" + }, + { + "code": 6198, + "name": "IFWithdrawRequestInProgress", + "msg": "IFWithdrawRequestInProgress" + }, + { + "code": 6199, + "name": "NoIFWithdrawRequestInProgress", + "msg": "NoIFWithdrawRequestInProgress" + }, + { + "code": 6200, + "name": "IFWithdrawRequestTooSmall", + "msg": "IFWithdrawRequestTooSmall" + }, + { + "code": 6201, + "name": "IncorrectSpotMarketAccountPassed", + "msg": "IncorrectSpotMarketAccountPassed" + }, + { + "code": 6202, + "name": "BlockchainClockInconsistency", + "msg": "BlockchainClockInconsistency" + }, + { + "code": 6203, + "name": "InvalidIFSharesDetected", + "msg": "InvalidIFSharesDetected" + }, + { + "code": 6204, + "name": "NewLPSizeTooSmall", + "msg": "NewLPSizeTooSmall" + }, + { + "code": 6205, + "name": "MarketStatusInvalidForNewLP", + "msg": "MarketStatusInvalidForNewLP" + }, + { + "code": 6206, + "name": "InvalidMarkTwapUpdateDetected", + "msg": "InvalidMarkTwapUpdateDetected" + }, + { + "code": 6207, + "name": "MarketSettlementAttemptOnActiveMarket", + "msg": "MarketSettlementAttemptOnActiveMarket" + }, + { + "code": 6208, + "name": "MarketSettlementRequiresSettledLP", + "msg": "MarketSettlementRequiresSettledLP" + }, + { + "code": 6209, + "name": "MarketSettlementAttemptTooEarly", + "msg": "MarketSettlementAttemptTooEarly" + }, + { + "code": 6210, + "name": "MarketSettlementTargetPriceInvalid", + "msg": "MarketSettlementTargetPriceInvalid" + }, + { + "code": 6211, + "name": "UnsupportedSpotMarket", + "msg": "UnsupportedSpotMarket" + }, + { + "code": 6212, + "name": "SpotOrdersDisabled", + "msg": "SpotOrdersDisabled" + }, + { + "code": 6213, + "name": "MarketBeingInitialized", + "msg": "Market Being Initialized" + }, + { + "code": 6214, + "name": "InvalidUserSubAccountId", + "msg": "Invalid Sub Account Id" + }, + { + "code": 6215, + "name": "InvalidTriggerOrderCondition", + "msg": "Invalid Trigger Order Condition" + }, + { + "code": 6216, + "name": "InvalidSpotPosition", + "msg": "Invalid Spot Position" + }, + { + "code": 6217, + "name": "CantTransferBetweenSameUserAccount", + "msg": "Cant transfer between same user account" + }, + { + "code": 6218, + "name": "InvalidPerpPosition", + "msg": "Invalid Perp Position" + }, + { + "code": 6219, + "name": "UnableToGetLimitPrice", + "msg": "Unable To Get Limit Price" + }, + { + "code": 6220, + "name": "InvalidLiquidation", + "msg": "Invalid Liquidation" + }, + { + "code": 6221, + "name": "SpotFulfillmentConfigDisabled", + "msg": "Spot Fulfillment Config Disabled" + }, + { + "code": 6222, + "name": "InvalidMaker", + "msg": "Invalid Maker" + }, + { + "code": 6223, + "name": "FailedUnwrap", + "msg": "Failed Unwrap" + }, + { + "code": 6224, + "name": "MaxNumberOfUsers", + "msg": "Max Number Of Users" + }, + { + "code": 6225, + "name": "InvalidOracleForSettlePnl", + "msg": "InvalidOracleForSettlePnl" + }, + { + "code": 6226, + "name": "MarginOrdersOpen", + "msg": "MarginOrdersOpen" + }, + { + "code": 6227, + "name": "TierViolationLiquidatingPerpPnl", + "msg": "TierViolationLiquidatingPerpPnl" + }, + { + "code": 6228, + "name": "CouldNotLoadUserData", + "msg": "CouldNotLoadUserData" + }, + { + "code": 6229, + "name": "UserWrongMutability", + "msg": "UserWrongMutability" + }, + { + "code": 6230, + "name": "InvalidUserAccount", + "msg": "InvalidUserAccount" + }, + { + "code": 6231, + "name": "CouldNotLoadUserStatsData", + "msg": "CouldNotLoadUserData" + }, + { + "code": 6232, + "name": "UserStatsWrongMutability", + "msg": "UserWrongMutability" + }, + { + "code": 6233, + "name": "InvalidUserStatsAccount", + "msg": "InvalidUserAccount" + }, + { + "code": 6234, + "name": "UserNotFound", + "msg": "UserNotFound" + }, + { + "code": 6235, + "name": "UnableToLoadUserAccount", + "msg": "UnableToLoadUserAccount" + }, + { + "code": 6236, + "name": "UserStatsNotFound", + "msg": "UserStatsNotFound" + }, + { + "code": 6237, + "name": "UnableToLoadUserStatsAccount", + "msg": "UnableToLoadUserStatsAccount" + }, + { + "code": 6238, + "name": "UserNotInactive", + "msg": "User Not Inactive" + }, + { + "code": 6239, + "name": "RevertFill", + "msg": "RevertFill" + }, + { + "code": 6240, + "name": "InvalidMarketAccountforDeletion", + "msg": "Invalid MarketAccount for Deletion" + }, + { + "code": 6241, + "name": "InvalidSpotFulfillmentParams", + "msg": "Invalid Spot Fulfillment Params" + }, + { + "code": 6242, + "name": "FailedToGetMint", + "msg": "Failed to Get Mint" + }, + { + "code": 6243, + "name": "FailedPhoenixCPI", + "msg": "FailedPhoenixCPI" + }, + { + "code": 6244, + "name": "FailedToDeserializePhoenixMarket", + "msg": "FailedToDeserializePhoenixMarket" + }, + { + "code": 6245, + "name": "InvalidPricePrecision", + "msg": "InvalidPricePrecision" + }, + { + "code": 6246, + "name": "InvalidPhoenixProgram", + "msg": "InvalidPhoenixProgram" + }, + { + "code": 6247, + "name": "InvalidPhoenixMarket", + "msg": "InvalidPhoenixMarket" + }, + { + "code": 6248, + "name": "InvalidSwap", + "msg": "InvalidSwap" + }, + { + "code": 6249, + "name": "SwapLimitPriceBreached", + "msg": "SwapLimitPriceBreached" + }, + { + "code": 6250, + "name": "SpotMarketReduceOnly", + "msg": "SpotMarketReduceOnly" + }, + { + "code": 6251, + "name": "FundingWasNotUpdated", + "msg": "FundingWasNotUpdated" + }, + { + "code": 6252, + "name": "ImpossibleFill", + "msg": "ImpossibleFill" + }, + { + "code": 6253, + "name": "CantUpdatePerpBidAskTwap", + "msg": "CantUpdatePerpBidAskTwap" + }, + { + "code": 6254, + "name": "UserReduceOnly", + "msg": "UserReduceOnly" + }, + { + "code": 6255, + "name": "InvalidMarginCalculation", + "msg": "InvalidMarginCalculation" + }, + { + "code": 6256, + "name": "CantPayUserInitFee", + "msg": "CantPayUserInitFee" + }, + { + "code": 6257, + "name": "CantReclaimRent", + "msg": "CantReclaimRent" + }, + { + "code": 6258, + "name": "InsuranceFundOperationPaused", + "msg": "InsuranceFundOperationPaused" + }, + { + "code": 6259, + "name": "NoUnsettledPnl", + "msg": "NoUnsettledPnl" + }, + { + "code": 6260, + "name": "PnlPoolCantSettleUser", + "msg": "PnlPoolCantSettleUser" + }, + { + "code": 6261, + "name": "OracleNonPositive", + "msg": "OracleInvalid" + }, + { + "code": 6262, + "name": "OracleTooVolatile", + "msg": "OracleTooVolatile" + }, + { + "code": 6263, + "name": "OracleTooUncertain", + "msg": "OracleTooUncertain" + }, + { + "code": 6264, + "name": "OracleStaleForMargin", + "msg": "OracleStaleForMargin" + }, + { + "code": 6265, + "name": "OracleInsufficientDataPoints", + "msg": "OracleInsufficientDataPoints" + }, + { + "code": 6266, + "name": "OracleStaleForAMM", + "msg": "OracleStaleForAMM" + }, + { + "code": 6267, + "name": "UnableToParsePullOracleMessage", + "msg": "Unable to parse pull oracle message" + }, + { + "code": 6268, + "name": "MaxBorrows", + "msg": "Can not borow more than max borrows" + }, + { + "code": 6269, + "name": "OracleUpdatesNotMonotonic", + "msg": "Updates must be monotonically increasing" + }, + { + "code": 6270, + "name": "OraclePriceFeedMessageMismatch", + "msg": "Trying to update price feed with the wrong feed id" + }, + { + "code": 6271, + "name": "OracleUnsupportedMessageType", + "msg": "The message in the update must be a PriceFeedMessage" + }, + { + "code": 6272, + "name": "OracleDeserializeMessageFailed", + "msg": "Could not deserialize the message in the update" + }, + { + "code": 6273, + "name": "OracleWrongGuardianSetOwner", + "msg": "Wrong guardian set owner in update price atomic" + }, + { + "code": 6274, + "name": "OracleWrongWriteAuthority", + "msg": "Oracle post update atomic price feed account must be drift program" + }, + { + "code": 6275, + "name": "OracleWrongVaaOwner", + "msg": "Oracle vaa owner must be wormhole program" + }, + { + "code": 6276, + "name": "OracleTooManyPriceAccountUpdates", + "msg": "Multi updates must have 2 or fewer accounts passed in remaining accounts" + }, + { + "code": 6277, + "name": "OracleMismatchedVaaAndPriceUpdates", + "msg": "Don't have the same remaining accounts number and merkle price updates left" + }, + { + "code": 6278, + "name": "OracleBadRemainingAccountPublicKey", + "msg": "Remaining account passed is not a valid pda" + }, + { + "code": 6279, + "name": "FailedOpenbookV2CPI", + "msg": "FailedOpenbookV2CPI" + }, + { + "code": 6280, + "name": "InvalidOpenbookV2Program", + "msg": "InvalidOpenbookV2Program" + }, + { + "code": 6281, + "name": "InvalidOpenbookV2Market", + "msg": "InvalidOpenbookV2Market" + }, + { + "code": 6282, + "name": "NonZeroTransferFee", + "msg": "Non zero transfer fee" + }, + { + "code": 6283, + "name": "LiquidationOrderFailedToFill", + "msg": "Liquidation order failed to fill" + }, + { + "code": 6284, + "name": "InvalidPredictionMarketOrder", + "msg": "Invalid prediction market order" + } + ] +} + +export const IDL: Drift = { + "version": "2.92.0", + "name": "drift", + "instructions": [ + { + "name": "initializeUser", + "accounts": [ + { + "name": "user", + "isMut": true, + "isSigner": false + }, + { + "name": "userStats", + "isMut": true, + "isSigner": false + }, + { + "name": "state", + "isMut": true, + "isSigner": false + }, + { + "name": "authority", + "isMut": false, + "isSigner": true + }, + { + "name": "payer", + "isMut": true, + "isSigner": true + }, + { + "name": "rent", + "isMut": false, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "subAccountId", + "type": "u16" + }, + { + "name": "name", + "type": { + "array": [ + "u8", + 32 + ] + } + } + ] + }, + { + "name": "initializeUserStats", + "accounts": [ + { + "name": "userStats", + "isMut": true, + "isSigner": false + }, + { + "name": "state", + "isMut": true, + "isSigner": false + }, + { + "name": "authority", + "isMut": false, + "isSigner": true + }, + { + "name": "payer", + "isMut": true, + "isSigner": true + }, + { + "name": "rent", + "isMut": false, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [] + }, + { + "name": "initializeReferrerName", + "accounts": [ + { + "name": "referrerName", + "isMut": true, + "isSigner": false + }, + { + "name": "user", + "isMut": true, + "isSigner": false + }, + { + "name": "userStats", + "isMut": true, + "isSigner": false + }, + { + "name": "authority", + "isMut": false, + "isSigner": true + }, + { + "name": "payer", + "isMut": true, + "isSigner": true + }, + { + "name": "rent", + "isMut": false, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "name", + "type": { + "array": [ + "u8", + 32 + ] + } + } + ] + }, + { + "name": "deposit", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "user", + "isMut": true, + "isSigner": false + }, + { + "name": "userStats", + "isMut": true, + "isSigner": false + }, + { + "name": "authority", + "isMut": false, + "isSigner": true + }, + { + "name": "spotMarketVault", + "isMut": true, + "isSigner": false + }, + { + "name": "userTokenAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "marketIndex", + "type": "u16" + }, + { + "name": "amount", + "type": "u64" + }, + { + "name": "reduceOnly", + "type": "bool" + } + ] + }, + { + "name": "withdraw", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "user", + "isMut": true, + "isSigner": false + }, + { + "name": "userStats", + "isMut": true, + "isSigner": false + }, + { + "name": "authority", + "isMut": false, + "isSigner": true + }, + { + "name": "spotMarketVault", + "isMut": true, + "isSigner": false + }, + { + "name": "driftSigner", + "isMut": false, + "isSigner": false + }, + { + "name": "userTokenAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "marketIndex", + "type": "u16" + }, + { + "name": "amount", + "type": "u64" + }, + { + "name": "reduceOnly", + "type": "bool" + } + ] + }, + { + "name": "transferDeposit", + "accounts": [ + { + "name": "fromUser", + "isMut": true, + "isSigner": false + }, + { + "name": "toUser", + "isMut": true, + "isSigner": false + }, + { + "name": "userStats", + "isMut": true, + "isSigner": false + }, + { + "name": "authority", + "isMut": false, + "isSigner": true + }, + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "spotMarketVault", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "marketIndex", + "type": "u16" + }, + { + "name": "amount", + "type": "u64" + } + ] + }, + { + "name": "placePerpOrder", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "user", + "isMut": true, + "isSigner": false + }, + { + "name": "authority", + "isMut": false, + "isSigner": true + } + ], + "args": [ + { + "name": "params", + "type": { + "defined": "OrderParams" + } + } + ] + }, + { + "name": "cancelOrder", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "user", + "isMut": true, + "isSigner": false + }, + { + "name": "authority", + "isMut": false, + "isSigner": true + } + ], + "args": [ + { + "name": "orderId", + "type": { + "option": "u32" + } + } + ] + }, + { + "name": "cancelOrderByUserId", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "user", + "isMut": true, + "isSigner": false + }, + { + "name": "authority", + "isMut": false, + "isSigner": true + } + ], + "args": [ + { + "name": "userOrderId", + "type": "u8" + } + ] + }, + { + "name": "cancelOrders", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "user", + "isMut": true, + "isSigner": false + }, + { + "name": "authority", + "isMut": false, + "isSigner": true + } + ], + "args": [ + { + "name": "marketType", + "type": { + "option": { + "defined": "MarketType" + } + } + }, + { + "name": "marketIndex", + "type": { + "option": "u16" + } + }, + { + "name": "direction", + "type": { + "option": { + "defined": "PositionDirection" + } + } + } + ] + }, + { + "name": "cancelOrdersByIds", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "user", + "isMut": true, + "isSigner": false + }, + { + "name": "authority", + "isMut": false, + "isSigner": true + } + ], + "args": [ + { + "name": "orderIds", + "type": { + "vec": "u32" + } + } + ] + }, + { + "name": "modifyOrder", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "user", + "isMut": true, + "isSigner": false + }, + { + "name": "authority", + "isMut": false, + "isSigner": true + } + ], + "args": [ + { + "name": "orderId", + "type": { + "option": "u32" + } + }, + { + "name": "modifyOrderParams", + "type": { + "defined": "ModifyOrderParams" + } + } + ] + }, + { + "name": "modifyOrderByUserId", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "user", + "isMut": true, + "isSigner": false + }, + { + "name": "authority", + "isMut": false, + "isSigner": true + } + ], + "args": [ + { + "name": "userOrderId", + "type": "u8" + }, + { + "name": "modifyOrderParams", + "type": { + "defined": "ModifyOrderParams" + } + } + ] + }, + { + "name": "placeAndTakePerpOrder", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "user", + "isMut": true, + "isSigner": false + }, + { + "name": "userStats", + "isMut": true, + "isSigner": false + }, + { + "name": "authority", + "isMut": false, + "isSigner": true + } + ], + "args": [ + { + "name": "params", + "type": { + "defined": "OrderParams" + } + }, + { + "name": "makerOrderId", + "type": { + "option": "u32" + } + } + ] + }, + { + "name": "placeAndMakePerpOrder", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "user", + "isMut": true, + "isSigner": false + }, + { + "name": "userStats", + "isMut": true, + "isSigner": false + }, + { + "name": "taker", + "isMut": true, + "isSigner": false + }, + { + "name": "takerStats", + "isMut": true, + "isSigner": false + }, + { + "name": "authority", + "isMut": false, + "isSigner": true + } + ], + "args": [ + { + "name": "params", + "type": { + "defined": "OrderParams" + } + }, + { + "name": "takerOrderId", + "type": "u32" + } + ] + }, + { + "name": "placeSpotOrder", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "user", + "isMut": true, + "isSigner": false + }, + { + "name": "authority", + "isMut": false, + "isSigner": true + } + ], + "args": [ + { + "name": "params", + "type": { + "defined": "OrderParams" + } + } + ] + }, + { + "name": "placeAndTakeSpotOrder", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "user", + "isMut": true, + "isSigner": false + }, + { + "name": "userStats", + "isMut": true, + "isSigner": false + }, + { + "name": "authority", + "isMut": false, + "isSigner": true + } + ], + "args": [ + { + "name": "params", + "type": { + "defined": "OrderParams" + } + }, + { + "name": "fulfillmentType", + "type": { + "option": { + "defined": "SpotFulfillmentType" + } + } + }, + { + "name": "makerOrderId", + "type": { + "option": "u32" + } + } + ] + }, + { + "name": "placeAndMakeSpotOrder", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "user", + "isMut": true, + "isSigner": false + }, + { + "name": "userStats", + "isMut": true, + "isSigner": false + }, + { + "name": "taker", + "isMut": true, + "isSigner": false + }, + { + "name": "takerStats", + "isMut": true, + "isSigner": false + }, + { + "name": "authority", + "isMut": false, + "isSigner": true + } + ], + "args": [ + { + "name": "params", + "type": { + "defined": "OrderParams" + } + }, + { + "name": "takerOrderId", + "type": "u32" + }, + { + "name": "fulfillmentType", + "type": { + "option": { + "defined": "SpotFulfillmentType" + } + } + } + ] + }, + { + "name": "placeOrders", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "user", + "isMut": true, + "isSigner": false + }, + { + "name": "authority", + "isMut": false, + "isSigner": true + } + ], + "args": [ + { + "name": "params", + "type": { + "vec": { + "defined": "OrderParams" + } + } + } + ] + }, + { + "name": "beginSwap", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "user", + "isMut": true, + "isSigner": false + }, + { + "name": "userStats", + "isMut": true, + "isSigner": false + }, + { + "name": "authority", + "isMut": false, + "isSigner": true + }, + { + "name": "outSpotMarketVault", + "isMut": true, + "isSigner": false + }, + { + "name": "inSpotMarketVault", + "isMut": true, + "isSigner": false + }, + { + "name": "outTokenAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "inTokenAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "driftSigner", + "isMut": false, + "isSigner": false + }, + { + "name": "instructions", + "isMut": false, + "isSigner": false, + "docs": [ + "Instructions Sysvar for instruction introspection" + ] + } + ], + "args": [ + { + "name": "inMarketIndex", + "type": "u16" + }, + { + "name": "outMarketIndex", + "type": "u16" + }, + { + "name": "amountIn", + "type": "u64" + } + ] + }, + { + "name": "endSwap", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "user", + "isMut": true, + "isSigner": false + }, + { + "name": "userStats", + "isMut": true, + "isSigner": false + }, + { + "name": "authority", + "isMut": false, + "isSigner": true + }, + { + "name": "outSpotMarketVault", + "isMut": true, + "isSigner": false + }, + { + "name": "inSpotMarketVault", + "isMut": true, + "isSigner": false + }, + { + "name": "outTokenAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "inTokenAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "driftSigner", + "isMut": false, + "isSigner": false + }, + { + "name": "instructions", + "isMut": false, + "isSigner": false, + "docs": [ + "Instructions Sysvar for instruction introspection" + ] + } + ], + "args": [ + { + "name": "inMarketIndex", + "type": "u16" + }, + { + "name": "outMarketIndex", + "type": "u16" + }, + { + "name": "limitPrice", + "type": { + "option": "u64" + } + }, + { + "name": "reduceOnly", + "type": { + "option": { + "defined": "SwapReduceOnly" + } + } + } + ] + }, + { + "name": "addPerpLpShares", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "user", + "isMut": true, + "isSigner": false + }, + { + "name": "authority", + "isMut": false, + "isSigner": true + } + ], + "args": [ + { + "name": "nShares", + "type": "u64" + }, + { + "name": "marketIndex", + "type": "u16" + } + ] + }, + { + "name": "removePerpLpShares", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "user", + "isMut": true, + "isSigner": false + }, + { + "name": "authority", + "isMut": false, + "isSigner": true + } + ], + "args": [ + { + "name": "sharesToBurn", + "type": "u64" + }, + { + "name": "marketIndex", + "type": "u16" + } + ] + }, + { + "name": "removePerpLpSharesInExpiringMarket", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "user", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "sharesToBurn", + "type": "u64" + }, + { + "name": "marketIndex", + "type": "u16" + } + ] + }, + { + "name": "updateUserName", + "accounts": [ + { + "name": "user", + "isMut": true, + "isSigner": false + }, + { + "name": "authority", + "isMut": false, + "isSigner": true + } + ], + "args": [ + { + "name": "subAccountId", + "type": "u16" + }, + { + "name": "name", + "type": { + "array": [ + "u8", + 32 + ] + } + } + ] + }, + { + "name": "updateUserCustomMarginRatio", + "accounts": [ + { + "name": "user", + "isMut": true, + "isSigner": false + }, + { + "name": "authority", + "isMut": false, + "isSigner": true + } + ], + "args": [ + { + "name": "subAccountId", + "type": "u16" + }, + { + "name": "marginRatio", + "type": "u32" + } + ] + }, + { + "name": "updateUserMarginTradingEnabled", + "accounts": [ + { + "name": "user", + "isMut": true, + "isSigner": false + }, + { + "name": "authority", + "isMut": false, + "isSigner": true + } + ], + "args": [ + { + "name": "subAccountId", + "type": "u16" + }, + { + "name": "marginTradingEnabled", + "type": "bool" + } + ] + }, + { + "name": "updateUserDelegate", + "accounts": [ + { + "name": "user", + "isMut": true, + "isSigner": false + }, + { + "name": "authority", + "isMut": false, + "isSigner": true + } + ], + "args": [ + { + "name": "subAccountId", + "type": "u16" + }, + { + "name": "delegate", + "type": "publicKey" + } + ] + }, + { + "name": "updateUserReduceOnly", + "accounts": [ + { + "name": "user", + "isMut": true, + "isSigner": false + }, + { + "name": "authority", + "isMut": false, + "isSigner": true + } + ], + "args": [ + { + "name": "subAccountId", + "type": "u16" + }, + { + "name": "reduceOnly", + "type": "bool" + } + ] + }, + { + "name": "updateUserAdvancedLp", + "accounts": [ + { + "name": "user", + "isMut": true, + "isSigner": false + }, + { + "name": "authority", + "isMut": false, + "isSigner": true + } + ], + "args": [ + { + "name": "subAccountId", + "type": "u16" + }, + { + "name": "advancedLp", + "type": "bool" + } + ] + }, + { + "name": "deleteUser", + "accounts": [ + { + "name": "user", + "isMut": true, + "isSigner": false + }, + { + "name": "userStats", + "isMut": true, + "isSigner": false + }, + { + "name": "state", + "isMut": true, + "isSigner": false + }, + { + "name": "authority", + "isMut": false, + "isSigner": true + } + ], + "args": [] + }, + { + "name": "reclaimRent", + "accounts": [ + { + "name": "user", + "isMut": true, + "isSigner": false + }, + { + "name": "userStats", + "isMut": true, + "isSigner": false + }, + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "authority", + "isMut": false, + "isSigner": true + }, + { + "name": "rent", + "isMut": false, + "isSigner": false + } + ], + "args": [] + }, + { + "name": "fillPerpOrder", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "authority", + "isMut": false, + "isSigner": true + }, + { + "name": "filler", + "isMut": true, + "isSigner": false + }, + { + "name": "fillerStats", + "isMut": true, + "isSigner": false + }, + { + "name": "user", + "isMut": true, + "isSigner": false + }, + { + "name": "userStats", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "orderId", + "type": { + "option": "u32" + } + }, + { + "name": "makerOrderId", + "type": { + "option": "u32" + } + } + ] + }, + { + "name": "revertFill", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "authority", + "isMut": false, + "isSigner": true + }, + { + "name": "filler", + "isMut": true, + "isSigner": false + }, + { + "name": "fillerStats", + "isMut": true, + "isSigner": false + } + ], + "args": [] + }, + { + "name": "fillSpotOrder", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "authority", + "isMut": false, + "isSigner": true + }, + { + "name": "filler", + "isMut": true, + "isSigner": false + }, + { + "name": "fillerStats", + "isMut": true, + "isSigner": false + }, + { + "name": "user", + "isMut": true, + "isSigner": false + }, + { + "name": "userStats", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "orderId", + "type": { + "option": "u32" + } + }, + { + "name": "fulfillmentType", + "type": { + "option": { + "defined": "SpotFulfillmentType" + } + } + }, + { + "name": "makerOrderId", + "type": { + "option": "u32" + } + } + ] + }, + { + "name": "triggerOrder", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "authority", + "isMut": false, + "isSigner": true + }, + { + "name": "filler", + "isMut": true, + "isSigner": false + }, + { + "name": "user", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "orderId", + "type": "u32" + } + ] + }, + { + "name": "forceCancelOrders", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "authority", + "isMut": false, + "isSigner": true + }, + { + "name": "filler", + "isMut": true, + "isSigner": false + }, + { + "name": "user", + "isMut": true, + "isSigner": false + } + ], + "args": [] + }, + { + "name": "updateUserIdle", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "authority", + "isMut": false, + "isSigner": true + }, + { + "name": "filler", + "isMut": true, + "isSigner": false + }, + { + "name": "user", + "isMut": true, + "isSigner": false + } + ], + "args": [] + }, + { + "name": "updateUserOpenOrdersCount", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "authority", + "isMut": false, + "isSigner": true + }, + { + "name": "filler", + "isMut": true, + "isSigner": false + }, + { + "name": "user", + "isMut": true, + "isSigner": false + } + ], + "args": [] + }, + { + "name": "adminDisableUpdatePerpBidAskTwap", + "accounts": [ + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "userStats", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "disable", + "type": "bool" + } + ] + }, + { + "name": "settlePnl", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "user", + "isMut": true, + "isSigner": false + }, + { + "name": "authority", + "isMut": false, + "isSigner": true + }, + { + "name": "spotMarketVault", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "marketIndex", + "type": "u16" + } + ] + }, + { + "name": "settleMultiplePnls", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "user", + "isMut": true, + "isSigner": false + }, + { + "name": "authority", + "isMut": false, + "isSigner": true + }, + { + "name": "spotMarketVault", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "marketIndexes", + "type": { + "vec": "u16" + } + }, + { + "name": "mode", + "type": { + "defined": "SettlePnlMode" + } + } + ] + }, + { + "name": "settleFundingPayment", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "user", + "isMut": true, + "isSigner": false + } + ], + "args": [] + }, + { + "name": "settleLp", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "user", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "marketIndex", + "type": "u16" + } + ] + }, + { + "name": "settleExpiredMarket", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "authority", + "isMut": false, + "isSigner": true + } + ], + "args": [ + { + "name": "marketIndex", + "type": "u16" + } + ] + }, + { + "name": "liquidatePerp", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "authority", + "isMut": false, + "isSigner": true + }, + { + "name": "liquidator", + "isMut": true, + "isSigner": false + }, + { + "name": "liquidatorStats", + "isMut": true, + "isSigner": false + }, + { + "name": "user", + "isMut": true, + "isSigner": false + }, + { + "name": "userStats", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "marketIndex", + "type": "u16" + }, + { + "name": "liquidatorMaxBaseAssetAmount", + "type": "u64" + }, + { + "name": "limitPrice", + "type": { + "option": "u64" + } + } + ] + }, + { + "name": "liquidatePerpWithFill", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "authority", + "isMut": false, + "isSigner": true + }, + { + "name": "liquidator", + "isMut": true, + "isSigner": false + }, + { + "name": "liquidatorStats", + "isMut": true, + "isSigner": false + }, + { + "name": "user", + "isMut": true, + "isSigner": false + }, + { + "name": "userStats", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "marketIndex", + "type": "u16" + } + ] + }, + { + "name": "liquidateSpot", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "authority", + "isMut": false, + "isSigner": true + }, + { + "name": "liquidator", + "isMut": true, + "isSigner": false + }, + { + "name": "liquidatorStats", + "isMut": true, + "isSigner": false + }, + { + "name": "user", + "isMut": true, + "isSigner": false + }, + { + "name": "userStats", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "assetMarketIndex", + "type": "u16" + }, + { + "name": "liabilityMarketIndex", + "type": "u16" + }, + { + "name": "liquidatorMaxLiabilityTransfer", + "type": "u128" + }, + { + "name": "limitPrice", + "type": { + "option": "u64" + } + } + ] + }, + { + "name": "liquidateBorrowForPerpPnl", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "authority", + "isMut": false, + "isSigner": true + }, + { + "name": "liquidator", + "isMut": true, + "isSigner": false + }, + { + "name": "liquidatorStats", + "isMut": true, + "isSigner": false + }, + { + "name": "user", + "isMut": true, + "isSigner": false + }, + { + "name": "userStats", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "perpMarketIndex", + "type": "u16" + }, + { + "name": "spotMarketIndex", + "type": "u16" + }, + { + "name": "liquidatorMaxLiabilityTransfer", + "type": "u128" + }, + { + "name": "limitPrice", + "type": { + "option": "u64" + } + } + ] + }, + { + "name": "liquidatePerpPnlForDeposit", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "authority", + "isMut": false, + "isSigner": true + }, + { + "name": "liquidator", + "isMut": true, + "isSigner": false + }, + { + "name": "liquidatorStats", + "isMut": true, + "isSigner": false + }, + { + "name": "user", + "isMut": true, + "isSigner": false + }, + { + "name": "userStats", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "perpMarketIndex", + "type": "u16" + }, + { + "name": "spotMarketIndex", + "type": "u16" + }, + { + "name": "liquidatorMaxPnlTransfer", + "type": "u128" + }, + { + "name": "limitPrice", + "type": { + "option": "u64" + } + } + ] + }, + { + "name": "setUserStatusToBeingLiquidated", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "user", + "isMut": true, + "isSigner": false + } + ], + "args": [] + }, + { + "name": "resolvePerpPnlDeficit", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "authority", + "isMut": false, + "isSigner": true + }, + { + "name": "spotMarketVault", + "isMut": true, + "isSigner": false + }, + { + "name": "insuranceFundVault", + "isMut": true, + "isSigner": false + }, + { + "name": "driftSigner", + "isMut": false, + "isSigner": false + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "spotMarketIndex", + "type": "u16" + }, + { + "name": "perpMarketIndex", + "type": "u16" + } + ] + }, + { + "name": "resolvePerpBankruptcy", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "authority", + "isMut": false, + "isSigner": true + }, + { + "name": "liquidator", + "isMut": true, + "isSigner": false + }, + { + "name": "liquidatorStats", + "isMut": true, + "isSigner": false + }, + { + "name": "user", + "isMut": true, + "isSigner": false + }, + { + "name": "userStats", + "isMut": true, + "isSigner": false + }, + { + "name": "spotMarketVault", + "isMut": true, + "isSigner": false + }, + { + "name": "insuranceFundVault", + "isMut": true, + "isSigner": false + }, + { + "name": "driftSigner", + "isMut": false, + "isSigner": false + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "quoteSpotMarketIndex", + "type": "u16" + }, + { + "name": "marketIndex", + "type": "u16" + } + ] + }, + { + "name": "resolveSpotBankruptcy", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "authority", + "isMut": false, + "isSigner": true + }, + { + "name": "liquidator", + "isMut": true, + "isSigner": false + }, + { + "name": "liquidatorStats", + "isMut": true, + "isSigner": false + }, + { + "name": "user", + "isMut": true, + "isSigner": false + }, + { + "name": "userStats", + "isMut": true, + "isSigner": false + }, + { + "name": "spotMarketVault", + "isMut": true, + "isSigner": false + }, + { + "name": "insuranceFundVault", + "isMut": true, + "isSigner": false + }, + { + "name": "driftSigner", + "isMut": false, + "isSigner": false + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "marketIndex", + "type": "u16" + } + ] + }, + { + "name": "settleRevenueToInsuranceFund", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "spotMarket", + "isMut": true, + "isSigner": false + }, + { + "name": "spotMarketVault", + "isMut": true, + "isSigner": false + }, + { + "name": "driftSigner", + "isMut": false, + "isSigner": false + }, + { + "name": "insuranceFundVault", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "spotMarketIndex", + "type": "u16" + } + ] + }, + { + "name": "updateFundingRate", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "perpMarket", + "isMut": true, + "isSigner": false + }, + { + "name": "oracle", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "marketIndex", + "type": "u16" + } + ] + }, + { + "name": "updatePrelaunchOracle", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "perpMarket", + "isMut": false, + "isSigner": false + }, + { + "name": "oracle", + "isMut": true, + "isSigner": false + } + ], + "args": [] + }, + { + "name": "updatePerpBidAskTwap", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "perpMarket", + "isMut": true, + "isSigner": false + }, + { + "name": "oracle", + "isMut": false, + "isSigner": false + }, + { + "name": "keeperStats", + "isMut": false, + "isSigner": false + }, + { + "name": "authority", + "isMut": false, + "isSigner": true + } + ], + "args": [] + }, + { + "name": "updateSpotMarketCumulativeInterest", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "spotMarket", + "isMut": true, + "isSigner": false + }, + { + "name": "oracle", + "isMut": false, + "isSigner": false + }, + { + "name": "spotMarketVault", + "isMut": false, + "isSigner": false + } + ], + "args": [] + }, + { + "name": "updateAmms", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "authority", + "isMut": false, + "isSigner": true + } + ], + "args": [ + { + "name": "marketIndexes", + "type": { + "array": [ + "u16", + 5 + ] + } + } + ] + }, + { + "name": "updateSpotMarketExpiry", + "accounts": [ + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "spotMarket", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "expiryTs", + "type": "i64" + } + ] + }, + { + "name": "updateUserQuoteAssetInsuranceStake", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "spotMarket", + "isMut": true, + "isSigner": false + }, + { + "name": "insuranceFundStake", + "isMut": true, + "isSigner": false + }, + { + "name": "userStats", + "isMut": true, + "isSigner": false + }, + { + "name": "signer", + "isMut": false, + "isSigner": true + }, + { + "name": "insuranceFundVault", + "isMut": true, + "isSigner": false + } + ], + "args": [] + }, + { + "name": "updateUserGovTokenInsuranceStake", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "spotMarket", + "isMut": true, + "isSigner": false + }, + { + "name": "insuranceFundStake", + "isMut": true, + "isSigner": false + }, + { + "name": "userStats", + "isMut": true, + "isSigner": false + }, + { + "name": "signer", + "isMut": false, + "isSigner": true + }, + { + "name": "insuranceFundVault", + "isMut": true, + "isSigner": false + } + ], + "args": [] + }, + { + "name": "initializeInsuranceFundStake", + "accounts": [ + { + "name": "spotMarket", + "isMut": false, + "isSigner": false + }, + { + "name": "insuranceFundStake", + "isMut": true, + "isSigner": false + }, + { + "name": "userStats", + "isMut": true, + "isSigner": false + }, + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "authority", + "isMut": false, + "isSigner": true + }, + { + "name": "payer", + "isMut": true, + "isSigner": true + }, + { + "name": "rent", + "isMut": false, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "marketIndex", + "type": "u16" + } + ] + }, + { + "name": "addInsuranceFundStake", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "spotMarket", + "isMut": true, + "isSigner": false + }, + { + "name": "insuranceFundStake", + "isMut": true, + "isSigner": false + }, + { + "name": "userStats", + "isMut": true, + "isSigner": false + }, + { + "name": "authority", + "isMut": false, + "isSigner": true + }, + { + "name": "spotMarketVault", + "isMut": true, + "isSigner": false + }, + { + "name": "insuranceFundVault", + "isMut": true, + "isSigner": false + }, + { + "name": "driftSigner", + "isMut": false, + "isSigner": false + }, + { + "name": "userTokenAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "marketIndex", + "type": "u16" + }, + { + "name": "amount", + "type": "u64" + } + ] + }, + { + "name": "requestRemoveInsuranceFundStake", + "accounts": [ + { + "name": "spotMarket", + "isMut": true, + "isSigner": false + }, + { + "name": "insuranceFundStake", + "isMut": true, + "isSigner": false + }, + { + "name": "userStats", + "isMut": true, + "isSigner": false + }, + { + "name": "authority", + "isMut": false, + "isSigner": true + }, + { + "name": "insuranceFundVault", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "marketIndex", + "type": "u16" + }, + { + "name": "amount", + "type": "u64" + } + ] + }, + { + "name": "cancelRequestRemoveInsuranceFundStake", + "accounts": [ + { + "name": "spotMarket", + "isMut": true, + "isSigner": false + }, + { + "name": "insuranceFundStake", + "isMut": true, + "isSigner": false + }, + { + "name": "userStats", + "isMut": true, + "isSigner": false + }, + { + "name": "authority", + "isMut": false, + "isSigner": true + }, + { + "name": "insuranceFundVault", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "marketIndex", + "type": "u16" + } + ] + }, + { + "name": "removeInsuranceFundStake", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "spotMarket", + "isMut": true, + "isSigner": false + }, + { + "name": "insuranceFundStake", + "isMut": true, + "isSigner": false + }, + { + "name": "userStats", + "isMut": true, + "isSigner": false + }, + { + "name": "authority", + "isMut": false, + "isSigner": true + }, + { + "name": "insuranceFundVault", + "isMut": true, + "isSigner": false + }, + { + "name": "driftSigner", + "isMut": false, + "isSigner": false + }, + { + "name": "userTokenAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "marketIndex", + "type": "u16" + } + ] + }, + { + "name": "transferProtocolIfShares", + "accounts": [ + { + "name": "signer", + "isMut": false, + "isSigner": true + }, + { + "name": "transferConfig", + "isMut": true, + "isSigner": false + }, + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "spotMarket", + "isMut": true, + "isSigner": false + }, + { + "name": "insuranceFundStake", + "isMut": true, + "isSigner": false + }, + { + "name": "userStats", + "isMut": true, + "isSigner": false + }, + { + "name": "authority", + "isMut": false, + "isSigner": true + }, + { + "name": "insuranceFundVault", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "marketIndex", + "type": "u16" + }, + { + "name": "shares", + "type": "u128" + } + ] + }, + { + "name": "updatePythPullOracle", + "accounts": [ + { + "name": "keeper", + "isMut": true, + "isSigner": true + }, + { + "name": "pythSolanaReceiver", + "isMut": false, + "isSigner": false + }, + { + "name": "encodedVaa", + "isMut": false, + "isSigner": false + }, + { + "name": "priceFeed", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "feedId", + "type": { + "array": [ + "u8", + 32 + ] + } + }, + { + "name": "params", + "type": "bytes" + } + ] + }, + { + "name": "postPythPullOracleUpdateAtomic", + "accounts": [ + { + "name": "keeper", + "isMut": true, + "isSigner": true + }, + { + "name": "pythSolanaReceiver", + "isMut": false, + "isSigner": false + }, + { + "name": "guardianSet", + "isMut": false, + "isSigner": false + }, + { + "name": "priceFeed", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "feedId", + "type": { + "array": [ + "u8", + 32 + ] + } + }, + { + "name": "params", + "type": "bytes" + } + ] + }, + { + "name": "postMultiPythPullOracleUpdatesAtomic", + "accounts": [ + { + "name": "keeper", + "isMut": true, + "isSigner": true + }, + { + "name": "pythSolanaReceiver", + "isMut": false, + "isSigner": false + }, + { + "name": "guardianSet", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "params", + "type": "bytes" + } + ] + }, + { + "name": "initialize", + "accounts": [ + { + "name": "admin", + "isMut": true, + "isSigner": true + }, + { + "name": "state", + "isMut": true, + "isSigner": false + }, + { + "name": "quoteAssetMint", + "isMut": false, + "isSigner": false + }, + { + "name": "driftSigner", + "isMut": false, + "isSigner": false + }, + { + "name": "rent", + "isMut": false, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [] + }, + { + "name": "initializeSpotMarket", + "accounts": [ + { + "name": "spotMarket", + "isMut": true, + "isSigner": false + }, + { + "name": "spotMarketMint", + "isMut": false, + "isSigner": false + }, + { + "name": "spotMarketVault", + "isMut": true, + "isSigner": false + }, + { + "name": "insuranceFundVault", + "isMut": true, + "isSigner": false + }, + { + "name": "driftSigner", + "isMut": false, + "isSigner": false + }, + { + "name": "state", + "isMut": true, + "isSigner": false + }, + { + "name": "oracle", + "isMut": false, + "isSigner": false + }, + { + "name": "admin", + "isMut": true, + "isSigner": true + }, + { + "name": "rent", + "isMut": false, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "optimalUtilization", + "type": "u32" + }, + { + "name": "optimalBorrowRate", + "type": "u32" + }, + { + "name": "maxBorrowRate", + "type": "u32" + }, + { + "name": "oracleSource", + "type": { + "defined": "OracleSource" + } + }, + { + "name": "initialAssetWeight", + "type": "u32" + }, + { + "name": "maintenanceAssetWeight", + "type": "u32" + }, + { + "name": "initialLiabilityWeight", + "type": "u32" + }, + { + "name": "maintenanceLiabilityWeight", + "type": "u32" + }, + { + "name": "imfFactor", + "type": "u32" + }, + { + "name": "liquidatorFee", + "type": "u32" + }, + { + "name": "ifLiquidationFee", + "type": "u32" + }, + { + "name": "activeStatus", + "type": "bool" + }, + { + "name": "assetTier", + "type": { + "defined": "AssetTier" + } + }, + { + "name": "scaleInitialAssetWeightStart", + "type": "u64" + }, + { + "name": "withdrawGuardThreshold", + "type": "u64" + }, + { + "name": "orderTickSize", + "type": "u64" + }, + { + "name": "orderStepSize", + "type": "u64" + }, + { + "name": "ifTotalFactor", + "type": "u32" + }, + { + "name": "name", + "type": { + "array": [ + "u8", + 32 + ] + } + } + ] + }, + { + "name": "deleteInitializedSpotMarket", + "accounts": [ + { + "name": "admin", + "isMut": true, + "isSigner": true + }, + { + "name": "state", + "isMut": true, + "isSigner": false + }, + { + "name": "spotMarket", + "isMut": true, + "isSigner": false + }, + { + "name": "spotMarketVault", + "isMut": true, + "isSigner": false + }, + { + "name": "insuranceFundVault", + "isMut": true, + "isSigner": false + }, + { + "name": "driftSigner", + "isMut": false, + "isSigner": false + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "marketIndex", + "type": "u16" + } + ] + }, + { + "name": "initializeSerumFulfillmentConfig", + "accounts": [ + { + "name": "baseSpotMarket", + "isMut": false, + "isSigner": false + }, + { + "name": "quoteSpotMarket", + "isMut": false, + "isSigner": false + }, + { + "name": "state", + "isMut": true, + "isSigner": false + }, + { + "name": "serumProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "serumMarket", + "isMut": false, + "isSigner": false + }, + { + "name": "serumOpenOrders", + "isMut": true, + "isSigner": false + }, + { + "name": "driftSigner", + "isMut": false, + "isSigner": false + }, + { + "name": "serumFulfillmentConfig", + "isMut": true, + "isSigner": false + }, + { + "name": "admin", + "isMut": true, + "isSigner": true + }, + { + "name": "rent", + "isMut": false, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "marketIndex", + "type": "u16" + } + ] + }, + { + "name": "updateSerumFulfillmentConfigStatus", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "serumFulfillmentConfig", + "isMut": true, + "isSigner": false + }, + { + "name": "admin", + "isMut": true, + "isSigner": true + } + ], + "args": [ + { + "name": "status", + "type": { + "defined": "SpotFulfillmentConfigStatus" + } + } + ] + }, + { + "name": "initializeOpenbookV2FulfillmentConfig", + "accounts": [ + { + "name": "baseSpotMarket", + "isMut": false, + "isSigner": false + }, + { + "name": "quoteSpotMarket", + "isMut": false, + "isSigner": false + }, + { + "name": "state", + "isMut": true, + "isSigner": false + }, + { + "name": "openbookV2Program", + "isMut": false, + "isSigner": false + }, + { + "name": "openbookV2Market", + "isMut": false, + "isSigner": false + }, + { + "name": "driftSigner", + "isMut": false, + "isSigner": false + }, + { + "name": "openbookV2FulfillmentConfig", + "isMut": true, + "isSigner": false + }, + { + "name": "admin", + "isMut": true, + "isSigner": true + }, + { + "name": "rent", + "isMut": false, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "marketIndex", + "type": "u16" + } + ] + }, + { + "name": "openbookV2FulfillmentConfigStatus", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "openbookV2FulfillmentConfig", + "isMut": true, + "isSigner": false + }, + { + "name": "admin", + "isMut": true, + "isSigner": true + } + ], + "args": [ + { + "name": "status", + "type": { + "defined": "SpotFulfillmentConfigStatus" + } + } + ] + }, + { + "name": "initializePhoenixFulfillmentConfig", + "accounts": [ + { + "name": "baseSpotMarket", + "isMut": false, + "isSigner": false + }, + { + "name": "quoteSpotMarket", + "isMut": false, + "isSigner": false + }, + { + "name": "state", + "isMut": true, + "isSigner": false + }, + { + "name": "phoenixProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "phoenixMarket", + "isMut": false, + "isSigner": false + }, + { + "name": "driftSigner", + "isMut": false, + "isSigner": false + }, + { + "name": "phoenixFulfillmentConfig", + "isMut": true, + "isSigner": false + }, + { + "name": "admin", + "isMut": true, + "isSigner": true + }, + { + "name": "rent", + "isMut": false, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "marketIndex", + "type": "u16" + } + ] + }, + { + "name": "phoenixFulfillmentConfigStatus", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "phoenixFulfillmentConfig", + "isMut": true, + "isSigner": false + }, + { + "name": "admin", + "isMut": true, + "isSigner": true + } + ], + "args": [ + { + "name": "status", + "type": { + "defined": "SpotFulfillmentConfigStatus" + } + } + ] + }, + { + "name": "updateSerumVault", + "accounts": [ + { + "name": "state", + "isMut": true, + "isSigner": false + }, + { + "name": "admin", + "isMut": true, + "isSigner": true + }, + { + "name": "srmVault", + "isMut": false, + "isSigner": false + } + ], + "args": [] + }, + { + "name": "initializePerpMarket", + "accounts": [ + { + "name": "admin", + "isMut": true, + "isSigner": true + }, + { + "name": "state", + "isMut": true, + "isSigner": false + }, + { + "name": "perpMarket", + "isMut": true, + "isSigner": false + }, + { + "name": "oracle", + "isMut": false, + "isSigner": false + }, + { + "name": "rent", + "isMut": false, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "marketIndex", + "type": "u16" + }, + { + "name": "ammBaseAssetReserve", + "type": "u128" + }, + { + "name": "ammQuoteAssetReserve", + "type": "u128" + }, + { + "name": "ammPeriodicity", + "type": "i64" + }, + { + "name": "ammPegMultiplier", + "type": "u128" + }, + { + "name": "oracleSource", + "type": { + "defined": "OracleSource" + } + }, + { + "name": "contractTier", + "type": { + "defined": "ContractTier" + } + }, + { + "name": "marginRatioInitial", + "type": "u32" + }, + { + "name": "marginRatioMaintenance", + "type": "u32" + }, + { + "name": "liquidatorFee", + "type": "u32" + }, + { + "name": "ifLiquidationFee", + "type": "u32" + }, + { + "name": "imfFactor", + "type": "u32" + }, + { + "name": "activeStatus", + "type": "bool" + }, + { + "name": "baseSpread", + "type": "u32" + }, + { + "name": "maxSpread", + "type": "u32" + }, + { + "name": "maxOpenInterest", + "type": "u128" + }, + { + "name": "maxRevenueWithdrawPerPeriod", + "type": "u64" + }, + { + "name": "quoteMaxInsurance", + "type": "u64" + }, + { + "name": "orderStepSize", + "type": "u64" + }, + { + "name": "orderTickSize", + "type": "u64" + }, + { + "name": "minOrderSize", + "type": "u64" + }, + { + "name": "concentrationCoefScale", + "type": "u128" + }, + { + "name": "curveUpdateIntensity", + "type": "u8" + }, + { + "name": "ammJitIntensity", + "type": "u8" + }, + { + "name": "name", + "type": { + "array": [ + "u8", + 32 + ] + } + } + ] + }, + { + "name": "initializePredictionMarket", + "accounts": [ + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "perpMarket", + "isMut": true, + "isSigner": false + } + ], + "args": [] + }, + { + "name": "deleteInitializedPerpMarket", + "accounts": [ + { + "name": "admin", + "isMut": true, + "isSigner": true + }, + { + "name": "state", + "isMut": true, + "isSigner": false + }, + { + "name": "perpMarket", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "marketIndex", + "type": "u16" + } + ] + }, + { + "name": "moveAmmPrice", + "accounts": [ + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "perpMarket", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "baseAssetReserve", + "type": "u128" + }, + { + "name": "quoteAssetReserve", + "type": "u128" + }, + { + "name": "sqrtK", + "type": "u128" + } + ] + }, + { + "name": "recenterPerpMarketAmm", + "accounts": [ + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "perpMarket", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "pegMultiplier", + "type": "u128" + }, + { + "name": "sqrtK", + "type": "u128" + } + ] + }, + { + "name": "updatePerpMarketAmmSummaryStats", + "accounts": [ + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "perpMarket", + "isMut": true, + "isSigner": false + }, + { + "name": "spotMarket", + "isMut": false, + "isSigner": false + }, + { + "name": "oracle", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "params", + "type": { + "defined": "UpdatePerpMarketSummaryStatsParams" + } + } + ] + }, + { + "name": "updatePerpMarketExpiry", + "accounts": [ + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "perpMarket", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "expiryTs", + "type": "i64" + } + ] + }, + { + "name": "settleExpiredMarketPoolsToRevenuePool", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "spotMarket", + "isMut": true, + "isSigner": false + }, + { + "name": "perpMarket", + "isMut": true, + "isSigner": false + } + ], + "args": [] + }, + { + "name": "depositIntoPerpMarketFeePool", + "accounts": [ + { + "name": "state", + "isMut": true, + "isSigner": false + }, + { + "name": "perpMarket", + "isMut": true, + "isSigner": false + }, + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "sourceVault", + "isMut": true, + "isSigner": false + }, + { + "name": "driftSigner", + "isMut": false, + "isSigner": false + }, + { + "name": "quoteSpotMarket", + "isMut": true, + "isSigner": false + }, + { + "name": "spotMarketVault", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "amount", + "type": "u64" + } + ] + }, + { + "name": "depositIntoSpotMarketVault", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "spotMarket", + "isMut": true, + "isSigner": false + }, + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "sourceVault", + "isMut": true, + "isSigner": false + }, + { + "name": "spotMarketVault", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "amount", + "type": "u64" + } + ] + }, + { + "name": "depositIntoSpotMarketRevenuePool", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "spotMarket", + "isMut": true, + "isSigner": false + }, + { + "name": "authority", + "isMut": true, + "isSigner": true + }, + { + "name": "spotMarketVault", + "isMut": true, + "isSigner": false + }, + { + "name": "userTokenAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "amount", + "type": "u64" + } + ] + }, + { + "name": "repegAmmCurve", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "perpMarket", + "isMut": true, + "isSigner": false + }, + { + "name": "oracle", + "isMut": false, + "isSigner": false + }, + { + "name": "admin", + "isMut": false, + "isSigner": true + } + ], + "args": [ + { + "name": "newPegCandidate", + "type": "u128" + } + ] + }, + { + "name": "updatePerpMarketAmmOracleTwap", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "perpMarket", + "isMut": true, + "isSigner": false + }, + { + "name": "oracle", + "isMut": false, + "isSigner": false + }, + { + "name": "admin", + "isMut": false, + "isSigner": true + } + ], + "args": [] + }, + { + "name": "resetPerpMarketAmmOracleTwap", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "perpMarket", + "isMut": true, + "isSigner": false + }, + { + "name": "oracle", + "isMut": false, + "isSigner": false + }, + { + "name": "admin", + "isMut": false, + "isSigner": true + } + ], + "args": [] + }, + { + "name": "updateK", + "accounts": [ + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "perpMarket", + "isMut": true, + "isSigner": false + }, + { + "name": "oracle", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "sqrtK", + "type": "u128" + } + ] + }, + { + "name": "updatePerpMarketMarginRatio", + "accounts": [ + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "perpMarket", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "marginRatioInitial", + "type": "u32" + }, + { + "name": "marginRatioMaintenance", + "type": "u32" + } + ] + }, + { + "name": "updatePerpMarketFundingPeriod", + "accounts": [ + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "perpMarket", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "fundingPeriod", + "type": "i64" + } + ] + }, + { + "name": "updatePerpMarketMaxImbalances", + "accounts": [ + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "perpMarket", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "unrealizedMaxImbalance", + "type": "u64" + }, + { + "name": "maxRevenueWithdrawPerPeriod", + "type": "u64" + }, + { + "name": "quoteMaxInsurance", + "type": "u64" + } + ] + }, + { + "name": "updatePerpMarketLiquidationFee", + "accounts": [ + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "perpMarket", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "liquidatorFee", + "type": "u32" + }, + { + "name": "ifLiquidationFee", + "type": "u32" + } + ] + }, + { + "name": "updateInsuranceFundUnstakingPeriod", + "accounts": [ + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "spotMarket", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "insuranceFundUnstakingPeriod", + "type": "i64" + } + ] + }, + { + "name": "updateSpotMarketLiquidationFee", + "accounts": [ + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "spotMarket", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "liquidatorFee", + "type": "u32" + }, + { + "name": "ifLiquidationFee", + "type": "u32" + } + ] + }, + { + "name": "updateWithdrawGuardThreshold", + "accounts": [ + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "spotMarket", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "withdrawGuardThreshold", + "type": "u64" + } + ] + }, + { + "name": "updateSpotMarketIfFactor", + "accounts": [ + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "spotMarket", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "spotMarketIndex", + "type": "u16" + }, + { + "name": "userIfFactor", + "type": "u32" + }, + { + "name": "totalIfFactor", + "type": "u32" + } + ] + }, + { + "name": "updateSpotMarketRevenueSettlePeriod", + "accounts": [ + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "spotMarket", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "revenueSettlePeriod", + "type": "i64" + } + ] + }, + { + "name": "updateSpotMarketStatus", + "accounts": [ + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "spotMarket", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "status", + "type": { + "defined": "MarketStatus" + } + } + ] + }, + { + "name": "updateSpotMarketPausedOperations", + "accounts": [ + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "spotMarket", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "pausedOperations", + "type": "u8" + } + ] + }, + { + "name": "updateSpotMarketAssetTier", + "accounts": [ + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "spotMarket", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "assetTier", + "type": { + "defined": "AssetTier" + } + } + ] + }, + { + "name": "updateSpotMarketMarginWeights", + "accounts": [ + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "spotMarket", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "initialAssetWeight", + "type": "u32" + }, + { + "name": "maintenanceAssetWeight", + "type": "u32" + }, + { + "name": "initialLiabilityWeight", + "type": "u32" + }, + { + "name": "maintenanceLiabilityWeight", + "type": "u32" + }, + { + "name": "imfFactor", + "type": "u32" + } + ] + }, + { + "name": "updateSpotMarketBorrowRate", + "accounts": [ + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "spotMarket", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "optimalUtilization", + "type": "u32" + }, + { + "name": "optimalBorrowRate", + "type": "u32" + }, + { + "name": "maxBorrowRate", + "type": "u32" + }, + { + "name": "minBorrowRate", + "type": { + "option": "u8" + } + } + ] + }, + { + "name": "updateSpotMarketMaxTokenDeposits", + "accounts": [ + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "spotMarket", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "maxTokenDeposits", + "type": "u64" + } + ] + }, + { + "name": "updateSpotMarketMaxTokenBorrows", + "accounts": [ + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "spotMarket", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "maxTokenBorrowsFraction", + "type": "u16" + } + ] + }, + { + "name": "updateSpotMarketScaleInitialAssetWeightStart", + "accounts": [ + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "spotMarket", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "scaleInitialAssetWeightStart", + "type": "u64" + } + ] + }, + { + "name": "updateSpotMarketOracle", + "accounts": [ + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "spotMarket", + "isMut": true, + "isSigner": false + }, + { + "name": "oracle", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "oracle", + "type": "publicKey" + }, + { + "name": "oracleSource", + "type": { + "defined": "OracleSource" + } + } + ] + }, + { + "name": "updateSpotMarketStepSizeAndTickSize", + "accounts": [ + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "spotMarket", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "stepSize", + "type": "u64" + }, + { + "name": "tickSize", + "type": "u64" + } + ] + }, + { + "name": "updateSpotMarketMinOrderSize", + "accounts": [ + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "spotMarket", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "orderSize", + "type": "u64" + } + ] + }, + { + "name": "updateSpotMarketOrdersEnabled", + "accounts": [ + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "spotMarket", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "ordersEnabled", + "type": "bool" + } + ] + }, + { + "name": "updateSpotMarketIfPausedOperations", + "accounts": [ + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "spotMarket", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "pausedOperations", + "type": "u8" + } + ] + }, + { + "name": "updateSpotMarketName", + "accounts": [ + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "spotMarket", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "name", + "type": { + "array": [ + "u8", + 32 + ] + } + } + ] + }, + { + "name": "updatePerpMarketStatus", + "accounts": [ + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "perpMarket", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "status", + "type": { + "defined": "MarketStatus" + } + } + ] + }, + { + "name": "updatePerpMarketPausedOperations", + "accounts": [ + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "perpMarket", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "pausedOperations", + "type": "u8" + } + ] + }, + { + "name": "updatePerpMarketContractTier", + "accounts": [ + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "perpMarket", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "contractTier", + "type": { + "defined": "ContractTier" + } + } + ] + }, + { + "name": "updatePerpMarketImfFactor", + "accounts": [ + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "perpMarket", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "imfFactor", + "type": "u32" + }, + { + "name": "unrealizedPnlImfFactor", + "type": "u32" + } + ] + }, + { + "name": "updatePerpMarketUnrealizedAssetWeight", + "accounts": [ + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "perpMarket", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "unrealizedInitialAssetWeight", + "type": "u32" + }, + { + "name": "unrealizedMaintenanceAssetWeight", + "type": "u32" + } + ] + }, + { + "name": "updatePerpMarketConcentrationCoef", + "accounts": [ + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "perpMarket", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "concentrationScale", + "type": "u128" + } + ] + }, + { + "name": "updatePerpMarketCurveUpdateIntensity", + "accounts": [ + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "perpMarket", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "curveUpdateIntensity", + "type": "u8" + } + ] + }, + { + "name": "updatePerpMarketTargetBaseAssetAmountPerLp", + "accounts": [ + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "perpMarket", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "targetBaseAssetAmountPerLp", + "type": "i32" + } + ] + }, + { + "name": "updatePerpMarketPerLpBase", + "accounts": [ + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "perpMarket", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "perLpBase", + "type": "i8" + } + ] + }, + { + "name": "updateLpCooldownTime", + "accounts": [ + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "state", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "lpCooldownTime", + "type": "u64" + } + ] + }, + { + "name": "updatePerpFeeStructure", + "accounts": [ + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "state", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "feeStructure", + "type": { + "defined": "FeeStructure" + } + } + ] + }, + { + "name": "updateSpotFeeStructure", + "accounts": [ + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "state", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "feeStructure", + "type": { + "defined": "FeeStructure" + } + } + ] + }, + { + "name": "updateInitialPctToLiquidate", + "accounts": [ + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "state", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "initialPctToLiquidate", + "type": "u16" + } + ] + }, + { + "name": "updateLiquidationDuration", + "accounts": [ + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "state", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "liquidationDuration", + "type": "u8" + } + ] + }, + { + "name": "updateLiquidationMarginBufferRatio", + "accounts": [ + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "state", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "liquidationMarginBufferRatio", + "type": "u32" + } + ] + }, + { + "name": "updateOracleGuardRails", + "accounts": [ + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "state", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "oracleGuardRails", + "type": { + "defined": "OracleGuardRails" + } + } + ] + }, + { + "name": "updateStateSettlementDuration", + "accounts": [ + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "state", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "settlementDuration", + "type": "u16" + } + ] + }, + { + "name": "updateStateMaxNumberOfSubAccounts", + "accounts": [ + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "state", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "maxNumberOfSubAccounts", + "type": "u16" + } + ] + }, + { + "name": "updateStateMaxInitializeUserFee", + "accounts": [ + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "state", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "maxInitializeUserFee", + "type": "u16" + } + ] + }, + { + "name": "updatePerpMarketOracle", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "perpMarket", + "isMut": true, + "isSigner": false + }, + { + "name": "oracle", + "isMut": false, + "isSigner": false + }, + { + "name": "admin", + "isMut": false, + "isSigner": true + } + ], + "args": [ + { + "name": "oracle", + "type": "publicKey" + }, + { + "name": "oracleSource", + "type": { + "defined": "OracleSource" + } + } + ] + }, + { + "name": "updatePerpMarketBaseSpread", + "accounts": [ + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "perpMarket", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "baseSpread", + "type": "u32" + } + ] + }, + { + "name": "updateAmmJitIntensity", + "accounts": [ + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "perpMarket", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "ammJitIntensity", + "type": "u8" + } + ] + }, + { + "name": "updatePerpMarketMaxSpread", + "accounts": [ + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "perpMarket", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "maxSpread", + "type": "u32" + } + ] + }, + { + "name": "updatePerpMarketStepSizeAndTickSize", + "accounts": [ + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "perpMarket", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "stepSize", + "type": "u64" + }, + { + "name": "tickSize", + "type": "u64" + } + ] + }, + { + "name": "updatePerpMarketName", + "accounts": [ + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "perpMarket", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "name", + "type": { + "array": [ + "u8", + 32 + ] + } + } + ] + }, + { + "name": "updatePerpMarketMinOrderSize", + "accounts": [ + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "perpMarket", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "orderSize", + "type": "u64" + } + ] + }, + { + "name": "updatePerpMarketMaxSlippageRatio", + "accounts": [ + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "perpMarket", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "maxSlippageRatio", + "type": "u16" + } + ] + }, + { + "name": "updatePerpMarketMaxFillReserveFraction", + "accounts": [ + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "perpMarket", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "maxFillReserveFraction", + "type": "u16" + } + ] + }, + { + "name": "updatePerpMarketMaxOpenInterest", + "accounts": [ + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "perpMarket", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "maxOpenInterest", + "type": "u128" + } + ] + }, + { + "name": "updatePerpMarketNumberOfUsers", + "accounts": [ + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "perpMarket", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "numberOfUsers", + "type": { + "option": "u32" + } + }, + { + "name": "numberOfUsersWithBase", + "type": { + "option": "u32" + } + } + ] + }, + { + "name": "updatePerpMarketFeeAdjustment", + "accounts": [ + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "perpMarket", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "feeAdjustment", + "type": "i16" + } + ] + }, + { + "name": "updateSpotMarketFeeAdjustment", + "accounts": [ + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "spotMarket", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "feeAdjustment", + "type": "i16" + } + ] + }, + { + "name": "updatePerpMarketFuel", + "accounts": [ + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "perpMarket", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "fuelBoostTaker", + "type": { + "option": "u8" + } + }, + { + "name": "fuelBoostMaker", + "type": { + "option": "u8" + } + }, + { + "name": "fuelBoostPosition", + "type": { + "option": "u8" + } + } + ] + }, + { + "name": "updateSpotMarketFuel", + "accounts": [ + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "spotMarket", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "fuelBoostDeposits", + "type": { + "option": "u8" + } + }, + { + "name": "fuelBoostBorrows", + "type": { + "option": "u8" + } + }, + { + "name": "fuelBoostTaker", + "type": { + "option": "u8" + } + }, + { + "name": "fuelBoostMaker", + "type": { + "option": "u8" + } + }, + { + "name": "fuelBoostInsurance", + "type": { + "option": "u8" + } + } + ] + }, + { + "name": "initUserFuel", + "accounts": [ + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "user", + "isMut": true, + "isSigner": false + }, + { + "name": "userStats", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "fuelBoostDeposits", + "type": { + "option": "u32" + } + }, + { + "name": "fuelBoostBorrows", + "type": { + "option": "u32" + } + }, + { + "name": "fuelBoostTaker", + "type": { + "option": "u32" + } + }, + { + "name": "fuelBoostMaker", + "type": { + "option": "u32" + } + }, + { + "name": "fuelBoostInsurance", + "type": { + "option": "u32" + } + } + ] + }, + { + "name": "updateAdmin", + "accounts": [ + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "state", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "admin", + "type": "publicKey" + } + ] + }, + { + "name": "updateWhitelistMint", + "accounts": [ + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "state", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "whitelistMint", + "type": "publicKey" + } + ] + }, + { + "name": "updateDiscountMint", + "accounts": [ + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "state", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "discountMint", + "type": "publicKey" + } + ] + }, + { + "name": "updateExchangeStatus", + "accounts": [ + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "state", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "exchangeStatus", + "type": "u8" + } + ] + }, + { + "name": "updatePerpAuctionDuration", + "accounts": [ + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "state", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "minPerpAuctionDuration", + "type": "u8" + } + ] + }, + { + "name": "updateSpotAuctionDuration", + "accounts": [ + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "state", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "defaultSpotAuctionDuration", + "type": "u8" + } + ] + }, + { + "name": "initializeProtocolIfSharesTransferConfig", + "accounts": [ + { + "name": "admin", + "isMut": true, + "isSigner": true + }, + { + "name": "protocolIfSharesTransferConfig", + "isMut": true, + "isSigner": false + }, + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "rent", + "isMut": false, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [] + }, + { + "name": "updateProtocolIfSharesTransferConfig", + "accounts": [ + { + "name": "admin", + "isMut": true, + "isSigner": true + }, + { + "name": "protocolIfSharesTransferConfig", + "isMut": true, + "isSigner": false + }, + { + "name": "state", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "whitelistedSigners", + "type": { + "option": { + "array": [ + "publicKey", + 4 + ] + } + } + }, + { + "name": "maxTransferPerEpoch", + "type": { + "option": "u128" + } + } + ] + }, + { + "name": "initializePrelaunchOracle", + "accounts": [ + { + "name": "admin", + "isMut": true, + "isSigner": true + }, + { + "name": "prelaunchOracle", + "isMut": true, + "isSigner": false + }, + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "rent", + "isMut": false, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "params", + "type": { + "defined": "PrelaunchOracleParams" + } + } + ] + }, + { + "name": "updatePrelaunchOracleParams", + "accounts": [ + { + "name": "admin", + "isMut": true, + "isSigner": true + }, + { + "name": "prelaunchOracle", + "isMut": true, + "isSigner": false + }, + { + "name": "perpMarket", + "isMut": true, + "isSigner": false + }, + { + "name": "state", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "params", + "type": { + "defined": "PrelaunchOracleParams" + } + } + ] + }, + { + "name": "deletePrelaunchOracle", + "accounts": [ + { + "name": "admin", + "isMut": true, + "isSigner": true + }, + { + "name": "prelaunchOracle", + "isMut": true, + "isSigner": false + }, + { + "name": "perpMarket", + "isMut": false, + "isSigner": false + }, + { + "name": "state", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "perpMarketIndex", + "type": "u16" + } + ] + }, + { + "name": "initializePythPullOracle", + "accounts": [ + { + "name": "admin", + "isMut": true, + "isSigner": true + }, + { + "name": "pythSolanaReceiver", + "isMut": false, + "isSigner": false + }, + { + "name": "priceFeed", + "isMut": true, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "state", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "feedId", + "type": { + "array": [ + "u8", + 32 + ] + } + } + ] + } + ], + "accounts": [ + { + "name": "OpenbookV2FulfillmentConfig", + "type": { + "kind": "struct", + "fields": [ + { + "name": "pubkey", + "type": "publicKey" + }, + { + "name": "openbookV2ProgramId", + "type": "publicKey" + }, + { + "name": "openbookV2Market", + "type": "publicKey" + }, + { + "name": "openbookV2MarketAuthority", + "type": "publicKey" + }, + { + "name": "openbookV2EventHeap", + "type": "publicKey" + }, + { + "name": "openbookV2Bids", + "type": "publicKey" + }, + { + "name": "openbookV2Asks", + "type": "publicKey" + }, + { + "name": "openbookV2BaseVault", + "type": "publicKey" + }, + { + "name": "openbookV2QuoteVault", + "type": "publicKey" + }, + { + "name": "marketIndex", + "type": "u16" + }, + { + "name": "fulfillmentType", + "type": { + "defined": "SpotFulfillmentType" + } + }, + { + "name": "status", + "type": { + "defined": "SpotFulfillmentConfigStatus" + } + }, + { + "name": "padding", + "type": { + "array": [ + "u8", + 4 + ] + } + } + ] + } + }, + { + "name": "PhoenixV1FulfillmentConfig", + "type": { + "kind": "struct", + "fields": [ + { + "name": "pubkey", + "type": "publicKey" + }, + { + "name": "phoenixProgramId", + "type": "publicKey" + }, + { + "name": "phoenixLogAuthority", + "type": "publicKey" + }, + { + "name": "phoenixMarket", + "type": "publicKey" + }, + { + "name": "phoenixBaseVault", + "type": "publicKey" + }, + { + "name": "phoenixQuoteVault", + "type": "publicKey" + }, + { + "name": "marketIndex", + "type": "u16" + }, + { + "name": "fulfillmentType", + "type": { + "defined": "SpotFulfillmentType" + } + }, + { + "name": "status", + "type": { + "defined": "SpotFulfillmentConfigStatus" + } + }, + { + "name": "padding", + "type": { + "array": [ + "u8", + 4 + ] + } + } + ] + } + }, + { + "name": "SerumV3FulfillmentConfig", + "type": { + "kind": "struct", + "fields": [ + { + "name": "pubkey", + "type": "publicKey" + }, + { + "name": "serumProgramId", + "type": "publicKey" + }, + { + "name": "serumMarket", + "type": "publicKey" + }, + { + "name": "serumRequestQueue", + "type": "publicKey" + }, + { + "name": "serumEventQueue", + "type": "publicKey" + }, + { + "name": "serumBids", + "type": "publicKey" + }, + { + "name": "serumAsks", + "type": "publicKey" + }, + { + "name": "serumBaseVault", + "type": "publicKey" + }, + { + "name": "serumQuoteVault", + "type": "publicKey" + }, + { + "name": "serumOpenOrders", + "type": "publicKey" + }, + { + "name": "serumSignerNonce", + "type": "u64" + }, + { + "name": "marketIndex", + "type": "u16" + }, + { + "name": "fulfillmentType", + "type": { + "defined": "SpotFulfillmentType" + } + }, + { + "name": "status", + "type": { + "defined": "SpotFulfillmentConfigStatus" + } + }, + { + "name": "padding", + "type": { + "array": [ + "u8", + 4 + ] + } + } + ] + } + }, + { + "name": "insuranceFundStake", + "type": { + "kind": "struct", + "fields": [ + { + "name": "authority", + "type": "publicKey" + }, + { + "name": "ifShares", + "type": "u128" + }, + { + "name": "lastWithdrawRequestShares", + "type": "u128" + }, + { + "name": "ifBase", + "type": "u128" + }, + { + "name": "lastValidTs", + "type": "i64" + }, + { + "name": "lastWithdrawRequestValue", + "type": "u64" + }, + { + "name": "lastWithdrawRequestTs", + "type": "i64" + }, + { + "name": "costBasis", + "type": "i64" + }, + { + "name": "marketIndex", + "type": "u16" + }, + { + "name": "padding", + "type": { + "array": [ + "u8", + 14 + ] + } + } + ] + } + }, + { + "name": "ProtocolIfSharesTransferConfig", + "type": { + "kind": "struct", + "fields": [ + { + "name": "whitelistedSigners", + "type": { + "array": [ + "publicKey", + 4 + ] + } + }, + { + "name": "maxTransferPerEpoch", + "type": "u128" + }, + { + "name": "currentEpochTransfer", + "type": "u128" + }, + { + "name": "nextEpochTs", + "type": "i64" + }, + { + "name": "padding", + "type": { + "array": [ + "u128", + 8 + ] + } + } + ] + } + }, + { + "name": "PrelaunchOracle", + "type": { + "kind": "struct", + "fields": [ + { + "name": "price", + "type": "i64" + }, + { + "name": "maxPrice", + "type": "i64" + }, + { + "name": "confidence", + "type": "u64" + }, + { + "name": "lastUpdateSlot", + "type": "u64" + }, + { + "name": "ammLastUpdateSlot", + "type": "u64" + }, + { + "name": "perpMarketIndex", + "type": "u16" + }, + { + "name": "padding", + "type": { + "array": [ + "u8", + 70 + ] + } + } + ] + } + }, + { + "name": "PerpMarket", + "type": { + "kind": "struct", + "fields": [ + { + "name": "pubkey", + "docs": [ + "The perp market's address. It is a pda of the market index" + ], + "type": "publicKey" + }, + { + "name": "amm", + "docs": [ + "The automated market maker" + ], + "type": { + "defined": "AMM" + } + }, + { + "name": "pnlPool", + "docs": [ + "The market's pnl pool. When users settle negative pnl, the balance increases.", + "When users settle positive pnl, the balance decreases. Can not go negative." + ], + "type": { + "defined": "PoolBalance" + } + }, + { + "name": "name", + "docs": [ + "Encoded display name for the perp market e.g. SOL-PERP" + ], + "type": { + "array": [ + "u8", + 32 + ] + } + }, + { + "name": "insuranceClaim", + "docs": [ + "The perp market's claim on the insurance fund" + ], + "type": { + "defined": "InsuranceClaim" + } + }, + { + "name": "unrealizedPnlMaxImbalance", + "docs": [ + "The max pnl imbalance before positive pnl asset weight is discounted", + "pnl imbalance is the difference between long and short pnl. When it's greater than 0,", + "the amm has negative pnl and the initial asset weight for positive pnl is discounted", + "precision = QUOTE_PRECISION" + ], + "type": "u64" + }, + { + "name": "expiryTs", + "docs": [ + "The ts when the market will be expired. Only set if market is in reduce only mode" + ], + "type": "i64" + }, + { + "name": "expiryPrice", + "docs": [ + "The price at which positions will be settled. Only set if market is expired", + "precision = PRICE_PRECISION" + ], + "type": "i64" + }, + { + "name": "nextFillRecordId", + "docs": [ + "Every trade has a fill record id. This is the next id to be used" + ], + "type": "u64" + }, + { + "name": "nextFundingRateRecordId", + "docs": [ + "Every funding rate update has a record id. This is the next id to be used" + ], + "type": "u64" + }, + { + "name": "nextCurveRecordId", + "docs": [ + "Every amm k updated has a record id. This is the next id to be used" + ], + "type": "u64" + }, + { + "name": "imfFactor", + "docs": [ + "The initial margin fraction factor. Used to increase margin ratio for large positions", + "precision: MARGIN_PRECISION" + ], + "type": "u32" + }, + { + "name": "unrealizedPnlImfFactor", + "docs": [ + "The imf factor for unrealized pnl. Used to discount asset weight for large positive pnl", + "precision: MARGIN_PRECISION" + ], + "type": "u32" + }, + { + "name": "liquidatorFee", + "docs": [ + "The fee the liquidator is paid for taking over perp position", + "precision: LIQUIDATOR_FEE_PRECISION" + ], + "type": "u32" + }, + { + "name": "ifLiquidationFee", + "docs": [ + "The fee the insurance fund receives from liquidation", + "precision: LIQUIDATOR_FEE_PRECISION" + ], + "type": "u32" + }, + { + "name": "marginRatioInitial", + "docs": [ + "The margin ratio which determines how much collateral is required to open a position", + "e.g. margin ratio of .1 means a user must have $100 of total collateral to open a $1000 position", + "precision: MARGIN_PRECISION" + ], + "type": "u32" + }, + { + "name": "marginRatioMaintenance", + "docs": [ + "The margin ratio which determines when a user will be liquidated", + "e.g. margin ratio of .05 means a user must have $50 of total collateral to maintain a $1000 position", + "else they will be liquidated", + "precision: MARGIN_PRECISION" + ], + "type": "u32" + }, + { + "name": "unrealizedPnlInitialAssetWeight", + "docs": [ + "The initial asset weight for positive pnl. Negative pnl always has an asset weight of 1", + "precision: SPOT_WEIGHT_PRECISION" + ], + "type": "u32" + }, + { + "name": "unrealizedPnlMaintenanceAssetWeight", + "docs": [ + "The maintenance asset weight for positive pnl. Negative pnl always has an asset weight of 1", + "precision: SPOT_WEIGHT_PRECISION" + ], + "type": "u32" + }, + { + "name": "numberOfUsersWithBase", + "docs": [ + "number of users in a position (base)" + ], + "type": "u32" + }, + { + "name": "numberOfUsers", + "docs": [ + "number of users in a position (pnl) or pnl (quote)" + ], + "type": "u32" + }, + { + "name": "marketIndex", + "type": "u16" + }, + { + "name": "status", + "docs": [ + "Whether a market is active, reduce only, expired, etc", + "Affects whether users can open/close positions" + ], + "type": { + "defined": "MarketStatus" + } + }, + { + "name": "contractType", + "docs": [ + "Currently only Perpetual markets are supported" + ], + "type": { + "defined": "ContractType" + } + }, + { + "name": "contractTier", + "docs": [ + "The contract tier determines how much insurance a market can receive, with more speculative markets receiving less insurance", + "It also influences the order perp markets can be liquidated, with less speculative markets being liquidated first" + ], + "type": { + "defined": "ContractTier" + } + }, + { + "name": "pausedOperations", + "type": "u8" + }, + { + "name": "quoteSpotMarketIndex", + "docs": [ + "The spot market that pnl is settled in" + ], + "type": "u16" + }, + { + "name": "feeAdjustment", + "docs": [ + "Between -100 and 100, represents what % to increase/decrease the fee by", + "E.g. if this is -50 and the fee is 5bps, the new fee will be 2.5bps", + "if this is 50 and the fee is 5bps, the new fee will be 7.5bps" + ], + "type": "i16" + }, + { + "name": "fuelBoostPosition", + "docs": [ + "fuel multiplier for perp funding", + "precision: 10" + ], + "type": "u8" + }, + { + "name": "fuelBoostTaker", + "docs": [ + "fuel multiplier for perp taker", + "precision: 10" + ], + "type": "u8" + }, + { + "name": "fuelBoostMaker", + "docs": [ + "fuel multiplier for perp maker", + "precision: 10" + ], + "type": "u8" + }, + { + "name": "padding", + "type": { + "array": [ + "u8", + 43 + ] + } + } + ] + } + }, + { + "name": "spotMarket", + "type": { + "kind": "struct", + "fields": [ + { + "name": "pubkey", + "docs": [ + "The address of the spot market. It is a pda of the market index" + ], + "type": "publicKey" + }, + { + "name": "oracle", + "docs": [ + "The oracle used to price the markets deposits/borrows" + ], + "type": "publicKey" + }, + { + "name": "mint", + "docs": [ + "The token mint of the market" + ], + "type": "publicKey" + }, + { + "name": "vault", + "docs": [ + "The vault used to store the market's deposits", + "The amount in the vault should be equal to or greater than deposits - borrows" + ], + "type": "publicKey" + }, + { + "name": "name", + "docs": [ + "The encoded display name for the market e.g. SOL" + ], + "type": { + "array": [ + "u8", + 32 + ] + } + }, + { + "name": "historicalOracleData", + "type": { + "defined": "HistoricalOracleData" + } + }, + { + "name": "historicalIndexData", + "type": { + "defined": "HistoricalIndexData" + } + }, + { + "name": "revenuePool", + "docs": [ + "Revenue the protocol has collected in this markets token", + "e.g. for SOL-PERP, funds can be settled in usdc and will flow into the USDC revenue pool" + ], + "type": { + "defined": "PoolBalance" + } + }, + { + "name": "spotFeePool", + "docs": [ + "The fees collected from swaps between this market and the quote market", + "Is settled to the quote markets revenue pool" + ], + "type": { + "defined": "PoolBalance" + } + }, + { + "name": "insuranceFund", + "docs": [ + "Details on the insurance fund covering bankruptcies in this markets token", + "Covers bankruptcies for borrows with this markets token and perps settling in this markets token" + ], + "type": { + "defined": "InsuranceFund" + } + }, + { + "name": "totalSpotFee", + "docs": [ + "The total spot fees collected for this market", + "precision: QUOTE_PRECISION" + ], + "type": "u128" + }, + { + "name": "depositBalance", + "docs": [ + "The sum of the scaled balances for deposits across users and pool balances", + "To convert to the deposit token amount, multiply by the cumulative deposit interest", + "precision: SPOT_BALANCE_PRECISION" + ], + "type": "u128" + }, + { + "name": "borrowBalance", + "docs": [ + "The sum of the scaled balances for borrows across users and pool balances", + "To convert to the borrow token amount, multiply by the cumulative borrow interest", + "precision: SPOT_BALANCE_PRECISION" + ], + "type": "u128" + }, + { + "name": "cumulativeDepositInterest", + "docs": [ + "The cumulative interest earned by depositors", + "Used to calculate the deposit token amount from the deposit balance", + "precision: SPOT_CUMULATIVE_INTEREST_PRECISION" + ], + "type": "u128" + }, + { + "name": "cumulativeBorrowInterest", + "docs": [ + "The cumulative interest earned by borrowers", + "Used to calculate the borrow token amount from the borrow balance", + "precision: SPOT_CUMULATIVE_INTEREST_PRECISION" + ], + "type": "u128" + }, + { + "name": "totalSocialLoss", + "docs": [ + "The total socialized loss from borrows, in the mint's token", + "precision: token mint precision" + ], + "type": "u128" + }, + { + "name": "totalQuoteSocialLoss", + "docs": [ + "The total socialized loss from borrows, in the quote market's token", + "preicision: QUOTE_PRECISION" + ], + "type": "u128" + }, + { + "name": "withdrawGuardThreshold", + "docs": [ + "no withdraw limits/guards when deposits below this threshold", + "precision: token mint precision" + ], + "type": "u64" + }, + { + "name": "maxTokenDeposits", + "docs": [ + "The max amount of token deposits in this market", + "0 if there is no limit", + "precision: token mint precision" + ], + "type": "u64" + }, + { + "name": "depositTokenTwap", + "docs": [ + "24hr average of deposit token amount", + "precision: token mint precision" + ], + "type": "u64" + }, + { + "name": "borrowTokenTwap", + "docs": [ + "24hr average of borrow token amount", + "precision: token mint precision" + ], + "type": "u64" + }, + { + "name": "utilizationTwap", + "docs": [ + "24hr average of utilization", + "which is borrow amount over token amount", + "precision: SPOT_UTILIZATION_PRECISION" + ], + "type": "u64" + }, + { + "name": "lastInterestTs", + "docs": [ + "Last time the cumulative deposit and borrow interest was updated" + ], + "type": "u64" + }, + { + "name": "lastTwapTs", + "docs": [ + "Last time the deposit/borrow/utilization averages were updated" + ], + "type": "u64" + }, + { + "name": "expiryTs", + "docs": [ + "The time the market is set to expire. Only set if market is in reduce only mode" + ], + "type": "i64" + }, + { + "name": "orderStepSize", + "docs": [ + "Spot orders must be a multiple of the step size", + "precision: token mint precision" + ], + "type": "u64" + }, + { + "name": "orderTickSize", + "docs": [ + "Spot orders must be a multiple of the tick size", + "precision: PRICE_PRECISION" + ], + "type": "u64" + }, + { + "name": "minOrderSize", + "docs": [ + "The minimum order size", + "precision: token mint precision" + ], + "type": "u64" + }, + { + "name": "maxPositionSize", + "docs": [ + "The maximum spot position size", + "if the limit is 0, there is no limit", + "precision: token mint precision" + ], + "type": "u64" + }, + { + "name": "nextFillRecordId", + "docs": [ + "Every spot trade has a fill record id. This is the next id to use" + ], + "type": "u64" + }, + { + "name": "nextDepositRecordId", + "docs": [ + "Every deposit has a deposit record id. This is the next id to use" + ], + "type": "u64" + }, + { + "name": "initialAssetWeight", + "docs": [ + "The initial asset weight used to calculate a deposits contribution to a users initial total collateral", + "e.g. if the asset weight is .8, $100 of deposits contributes $80 to the users initial total collateral", + "precision: SPOT_WEIGHT_PRECISION" + ], + "type": "u32" + }, + { + "name": "maintenanceAssetWeight", + "docs": [ + "The maintenance asset weight used to calculate a deposits contribution to a users maintenance total collateral", + "e.g. if the asset weight is .9, $100 of deposits contributes $90 to the users maintenance total collateral", + "precision: SPOT_WEIGHT_PRECISION" + ], + "type": "u32" + }, + { + "name": "initialLiabilityWeight", + "docs": [ + "The initial liability weight used to calculate a borrows contribution to a users initial margin requirement", + "e.g. if the liability weight is .9, $100 of borrows contributes $90 to the users initial margin requirement", + "precision: SPOT_WEIGHT_PRECISION" + ], + "type": "u32" + }, + { + "name": "maintenanceLiabilityWeight", + "docs": [ + "The maintenance liability weight used to calculate a borrows contribution to a users maintenance margin requirement", + "e.g. if the liability weight is .8, $100 of borrows contributes $80 to the users maintenance margin requirement", + "precision: SPOT_WEIGHT_PRECISION" + ], + "type": "u32" + }, + { + "name": "imfFactor", + "docs": [ + "The initial margin fraction factor. Used to increase liability weight/decrease asset weight for large positions", + "precision: MARGIN_PRECISION" + ], + "type": "u32" + }, + { + "name": "liquidatorFee", + "docs": [ + "The fee the liquidator is paid for taking over borrow/deposit", + "precision: LIQUIDATOR_FEE_PRECISION" + ], + "type": "u32" + }, + { + "name": "ifLiquidationFee", + "docs": [ + "The fee the insurance fund receives from liquidation", + "precision: LIQUIDATOR_FEE_PRECISION" + ], + "type": "u32" + }, + { + "name": "optimalUtilization", + "docs": [ + "The optimal utilization rate for this market.", + "Used to determine the markets borrow rate", + "precision: SPOT_UTILIZATION_PRECISION" + ], + "type": "u32" + }, + { + "name": "optimalBorrowRate", + "docs": [ + "The borrow rate for this market when the market has optimal utilization", + "precision: SPOT_RATE_PRECISION" + ], + "type": "u32" + }, + { + "name": "maxBorrowRate", + "docs": [ + "The borrow rate for this market when the market has 1000 utilization", + "precision: SPOT_RATE_PRECISION" + ], + "type": "u32" + }, + { + "name": "decimals", + "docs": [ + "The market's token mint's decimals. To from decimals to a precision, 10^decimals" + ], + "type": "u32" + }, + { + "name": "marketIndex", + "type": "u16" + }, + { + "name": "ordersEnabled", + "docs": [ + "Whether or not spot trading is enabled" + ], + "type": "bool" + }, + { + "name": "oracleSource", + "type": { + "defined": "OracleSource" + } + }, + { + "name": "status", + "type": { + "defined": "MarketStatus" + } + }, + { + "name": "assetTier", + "docs": [ + "The asset tier affects how a deposit can be used as collateral and the priority for a borrow being liquidated" + ], + "type": { + "defined": "AssetTier" + } + }, + { + "name": "pausedOperations", + "type": "u8" + }, + { + "name": "ifPausedOperations", + "type": "u8" + }, + { + "name": "feeAdjustment", + "type": "i16" + }, + { + "name": "maxTokenBorrowsFraction", + "docs": [ + "What fraction of max_token_deposits", + "disabled when 0, 1 => 1/10000 => .01% of max_token_deposits", + "precision: X/10000" + ], + "type": "u16" + }, + { + "name": "flashLoanAmount", + "docs": [ + "For swaps, the amount of token loaned out in the begin_swap ix", + "precision: token mint precision" + ], + "type": "u64" + }, + { + "name": "flashLoanInitialTokenAmount", + "docs": [ + "For swaps, the amount in the users token account in the begin_swap ix", + "Used to calculate how much of the token left the system in end_swap ix", + "precision: token mint precision" + ], + "type": "u64" + }, + { + "name": "totalSwapFee", + "docs": [ + "The total fees received from swaps", + "precision: token mint precision" + ], + "type": "u64" + }, + { + "name": "scaleInitialAssetWeightStart", + "docs": [ + "When to begin scaling down the initial asset weight", + "disabled when 0", + "precision: QUOTE_PRECISION" + ], + "type": "u64" + }, + { + "name": "minBorrowRate", + "docs": [ + "The min borrow rate for this market when the market regardless of utilization", + "1 => 1/200 => .5%", + "precision: X/200" + ], + "type": "u8" + }, + { + "name": "fuelBoostDeposits", + "docs": [ + "fuel multiplier for spot deposits", + "precision: 10" + ], + "type": "u8" + }, + { + "name": "fuelBoostBorrows", + "docs": [ + "fuel multiplier for spot borrows", + "precision: 10" + ], + "type": "u8" + }, + { + "name": "fuelBoostTaker", + "docs": [ + "fuel multiplier for spot taker", + "precision: 10" + ], + "type": "u8" + }, + { + "name": "fuelBoostMaker", + "docs": [ + "fuel multiplier for spot maker", + "precision: 10" + ], + "type": "u8" + }, + { + "name": "fuelBoostInsurance", + "docs": [ + "fuel multiplier for spot insurance stake", + "precision: 10" + ], + "type": "u8" + }, + { + "name": "tokenProgram", + "type": "u8" + }, + { + "name": "padding", + "type": { + "array": [ + "u8", + 41 + ] + } + } + ] + } + }, + { + "name": "State", + "type": { + "kind": "struct", + "fields": [ + { + "name": "admin", + "type": "publicKey" + }, + { + "name": "whitelistMint", + "type": "publicKey" + }, + { + "name": "discountMint", + "type": "publicKey" + }, + { + "name": "signer", + "type": "publicKey" + }, + { + "name": "srmVault", + "type": "publicKey" + }, + { + "name": "perpFeeStructure", + "type": { + "defined": "FeeStructure" + } + }, + { + "name": "spotFeeStructure", + "type": { + "defined": "FeeStructure" + } + }, + { + "name": "oracleGuardRails", + "type": { + "defined": "OracleGuardRails" + } + }, + { + "name": "numberOfAuthorities", + "type": "u64" + }, + { + "name": "numberOfSubAccounts", + "type": "u64" + }, + { + "name": "lpCooldownTime", + "type": "u64" + }, + { + "name": "liquidationMarginBufferRatio", + "type": "u32" + }, + { + "name": "settlementDuration", + "type": "u16" + }, + { + "name": "numberOfMarkets", + "type": "u16" + }, + { + "name": "numberOfSpotMarkets", + "type": "u16" + }, + { + "name": "signerNonce", + "type": "u8" + }, + { + "name": "minPerpAuctionDuration", + "type": "u8" + }, + { + "name": "defaultMarketOrderTimeInForce", + "type": "u8" + }, + { + "name": "defaultSpotAuctionDuration", + "type": "u8" + }, + { + "name": "exchangeStatus", + "type": "u8" + }, + { + "name": "liquidationDuration", + "type": "u8" + }, + { + "name": "initialPctToLiquidate", + "type": "u16" + }, + { + "name": "maxNumberOfSubAccounts", + "type": "u16" + }, + { + "name": "maxInitializeUserFee", + "type": "u16" + }, + { + "name": "padding", + "type": { + "array": [ + "u8", + 10 + ] + } + } + ] + } + }, + { + "name": "User", + "type": { + "kind": "struct", + "fields": [ + { + "name": "authority", + "docs": [ + "The owner/authority of the account" + ], + "type": "publicKey" + }, + { + "name": "delegate", + "docs": [ + "An addresses that can control the account on the authority's behalf. Has limited power, cant withdraw" + ], + "type": "publicKey" + }, + { + "name": "name", + "docs": [ + "Encoded display name e.g. \"toly\"" + ], + "type": { + "array": [ + "u8", + 32 + ] + } + }, + { + "name": "spotPositions", + "docs": [ + "The user's spot positions" + ], + "type": { + "array": [ + { + "defined": "SpotPosition" + }, + 8 + ] + } + }, + { + "name": "perpPositions", + "docs": [ + "The user's perp positions" + ], + "type": { + "array": [ + { + "defined": "PerpPosition" + }, + 8 + ] + } + }, + { + "name": "orders", + "docs": [ + "The user's orders" + ], + "type": { + "array": [ + { + "defined": "Order" + }, + 32 + ] + } + }, + { + "name": "lastAddPerpLpSharesTs", + "docs": [ + "The last time the user added perp lp positions" + ], + "type": "i64" + }, + { + "name": "totalDeposits", + "docs": [ + "The total values of deposits the user has made", + "precision: QUOTE_PRECISION" + ], + "type": "u64" + }, + { + "name": "totalWithdraws", + "docs": [ + "The total values of withdrawals the user has made", + "precision: QUOTE_PRECISION" + ], + "type": "u64" + }, + { + "name": "totalSocialLoss", + "docs": [ + "The total socialized loss the users has incurred upon the protocol", + "precision: QUOTE_PRECISION" + ], + "type": "u64" + }, + { + "name": "settledPerpPnl", + "docs": [ + "Fees (taker fees, maker rebate, referrer reward, filler reward) and pnl for perps", + "precision: QUOTE_PRECISION" + ], + "type": "i64" + }, + { + "name": "cumulativeSpotFees", + "docs": [ + "Fees (taker fees, maker rebate, filler reward) for spot", + "precision: QUOTE_PRECISION" + ], + "type": "i64" + }, + { + "name": "cumulativePerpFunding", + "docs": [ + "Cumulative funding paid/received for perps", + "precision: QUOTE_PRECISION" + ], + "type": "i64" + }, + { + "name": "liquidationMarginFreed", + "docs": [ + "The amount of margin freed during liquidation. Used to force the liquidation to occur over a period of time", + "Defaults to zero when not being liquidated", + "precision: QUOTE_PRECISION" + ], + "type": "u64" + }, + { + "name": "lastActiveSlot", + "docs": [ + "The last slot a user was active. Used to determine if a user is idle" + ], + "type": "u64" + }, + { + "name": "nextOrderId", + "docs": [ + "Every user order has an order id. This is the next order id to be used" + ], + "type": "u32" + }, + { + "name": "maxMarginRatio", + "docs": [ + "Custom max initial margin ratio for the user" + ], + "type": "u32" + }, + { + "name": "nextLiquidationId", + "docs": [ + "The next liquidation id to be used for user" + ], + "type": "u16" + }, + { + "name": "subAccountId", + "docs": [ + "The sub account id for this user" + ], + "type": "u16" + }, + { + "name": "status", + "docs": [ + "Whether the user is active, being liquidated or bankrupt" + ], + "type": "u8" + }, + { + "name": "isMarginTradingEnabled", + "docs": [ + "Whether the user has enabled margin trading" + ], + "type": "bool" + }, + { + "name": "idle", + "docs": [ + "User is idle if they haven't interacted with the protocol in 1 week and they have no orders, perp positions or borrows", + "Off-chain keeper bots can ignore users that are idle" + ], + "type": "bool" + }, + { + "name": "openOrders", + "docs": [ + "number of open orders" + ], + "type": "u8" + }, + { + "name": "hasOpenOrder", + "docs": [ + "Whether or not user has open order" + ], + "type": "bool" + }, + { + "name": "openAuctions", + "docs": [ + "number of open orders with auction" + ], + "type": "u8" + }, + { + "name": "hasOpenAuction", + "docs": [ + "Whether or not user has open order with auction" + ], + "type": "bool" + }, + { + "name": "padding1", + "type": { + "array": [ + "u8", + 5 + ] + } + }, + { + "name": "lastFuelBonusUpdateTs", + "type": "u32" + }, + { + "name": "padding", + "type": { + "array": [ + "u8", + 12 + ] + } + } + ] + } + }, + { + "name": "UserStats", + "type": { + "kind": "struct", + "fields": [ + { + "name": "authority", + "docs": [ + "The authority for all of a users sub accounts" + ], + "type": "publicKey" + }, + { + "name": "referrer", + "docs": [ + "The address that referred this user" + ], + "type": "publicKey" + }, + { + "name": "fees", + "docs": [ + "Stats on the fees paid by the user" + ], + "type": { + "defined": "UserFees" + } + }, + { + "name": "nextEpochTs", + "docs": [ + "The timestamp of the next epoch", + "Epoch is used to limit referrer rewards earned in single epoch" + ], + "type": "i64" + }, + { + "name": "makerVolume30d", + "docs": [ + "Rolling 30day maker volume for user", + "precision: QUOTE_PRECISION" + ], + "type": "u64" + }, + { + "name": "takerVolume30d", + "docs": [ + "Rolling 30day taker volume for user", + "precision: QUOTE_PRECISION" + ], + "type": "u64" + }, + { + "name": "fillerVolume30d", + "docs": [ + "Rolling 30day filler volume for user", + "precision: QUOTE_PRECISION" + ], + "type": "u64" + }, + { + "name": "lastMakerVolume30dTs", + "docs": [ + "last time the maker volume was updated" + ], + "type": "i64" + }, + { + "name": "lastTakerVolume30dTs", + "docs": [ + "last time the taker volume was updated" + ], + "type": "i64" + }, + { + "name": "lastFillerVolume30dTs", + "docs": [ + "last time the filler volume was updated" + ], + "type": "i64" + }, + { + "name": "ifStakedQuoteAssetAmount", + "docs": [ + "The amount of tokens staked in the quote spot markets if" + ], + "type": "u64" + }, + { + "name": "numberOfSubAccounts", + "docs": [ + "The current number of sub accounts" + ], + "type": "u16" + }, + { + "name": "numberOfSubAccountsCreated", + "docs": [ + "The number of sub accounts created. Can be greater than the number of sub accounts if user", + "has deleted sub accounts" + ], + "type": "u16" + }, + { + "name": "isReferrer", + "docs": [ + "Whether the user is a referrer. Sub account 0 can not be deleted if user is a referrer" + ], + "type": "bool" + }, + { + "name": "disableUpdatePerpBidAskTwap", + "type": "bool" + }, + { + "name": "padding1", + "type": { + "array": [ + "u8", + 2 + ] + } + }, + { + "name": "fuelInsurance", + "docs": [ + "accumulated fuel for token amounts of insurance" + ], + "type": "u32" + }, + { + "name": "fuelDeposits", + "docs": [ + "accumulated fuel for notional of deposits" + ], + "type": "u32" + }, + { + "name": "fuelBorrows", + "docs": [ + "accumulate fuel bonus for notional of borrows" + ], + "type": "u32" + }, + { + "name": "fuelPositions", + "docs": [ + "accumulated fuel for perp open interest" + ], + "type": "u32" + }, + { + "name": "fuelTaker", + "docs": [ + "accumulate fuel bonus for taker volume" + ], + "type": "u32" + }, + { + "name": "fuelMaker", + "docs": [ + "accumulate fuel bonus for maker volume" + ], + "type": "u32" + }, + { + "name": "ifStakedGovTokenAmount", + "docs": [ + "The amount of tokens staked in the governance spot markets if" + ], + "type": "u64" + }, + { + "name": "lastFuelIfBonusUpdateTs", + "docs": [ + "last unix ts user stats data was used to update if fuel (u32 to save space)" + ], + "type": "u32" + }, + { + "name": "padding", + "type": { + "array": [ + "u8", + 12 + ] + } + } + ] + } + }, + { + "name": "ReferrerName", + "type": { + "kind": "struct", + "fields": [ + { + "name": "authority", + "type": "publicKey" + }, + { + "name": "user", + "type": "publicKey" + }, + { + "name": "userStats", + "type": "publicKey" + }, + { + "name": "name", + "type": { + "array": [ + "u8", + 32 + ] + } + } + ] + } + } + ], + "types": [ + { + "name": "UpdatePerpMarketSummaryStatsParams", + "type": { + "kind": "struct", + "fields": [ + { + "name": "quoteAssetAmountWithUnsettledLp", + "type": { + "option": "i64" + } + }, + { + "name": "netUnsettledFundingPnl", + "type": { + "option": "i64" + } + }, + { + "name": "updateAmmSummaryStats", + "type": { + "option": "bool" + } + } + ] + } + }, + { + "name": "LiquidatePerpRecord", + "type": { + "kind": "struct", + "fields": [ + { + "name": "marketIndex", + "type": "u16" + }, + { + "name": "oraclePrice", + "type": "i64" + }, + { + "name": "baseAssetAmount", + "type": "i64" + }, + { + "name": "quoteAssetAmount", + "type": "i64" + }, + { + "name": "lpShares", + "docs": [ + "precision: AMM_RESERVE_PRECISION" + ], + "type": "u64" + }, + { + "name": "fillRecordId", + "type": "u64" + }, + { + "name": "userOrderId", + "type": "u32" + }, + { + "name": "liquidatorOrderId", + "type": "u32" + }, + { + "name": "liquidatorFee", + "docs": [ + "precision: QUOTE_PRECISION" + ], + "type": "u64" + }, + { + "name": "ifFee", + "docs": [ + "precision: QUOTE_PRECISION" + ], + "type": "u64" + } + ] + } + }, + { + "name": "LiquidateSpotRecord", + "type": { + "kind": "struct", + "fields": [ + { + "name": "assetMarketIndex", + "type": "u16" + }, + { + "name": "assetPrice", + "type": "i64" + }, + { + "name": "assetTransfer", + "type": "u128" + }, + { + "name": "liabilityMarketIndex", + "type": "u16" + }, + { + "name": "liabilityPrice", + "type": "i64" + }, + { + "name": "liabilityTransfer", + "docs": [ + "precision: token mint precision" + ], + "type": "u128" + }, + { + "name": "ifFee", + "docs": [ + "precision: token mint precision" + ], + "type": "u64" + } + ] + } + }, + { + "name": "LiquidateBorrowForPerpPnlRecord", + "type": { + "kind": "struct", + "fields": [ + { + "name": "perpMarketIndex", + "type": "u16" + }, + { + "name": "marketOraclePrice", + "type": "i64" + }, + { + "name": "pnlTransfer", + "type": "u128" + }, + { + "name": "liabilityMarketIndex", + "type": "u16" + }, + { + "name": "liabilityPrice", + "type": "i64" + }, + { + "name": "liabilityTransfer", + "type": "u128" + } + ] + } + }, + { + "name": "LiquidatePerpPnlForDepositRecord", + "type": { + "kind": "struct", + "fields": [ + { + "name": "perpMarketIndex", + "type": "u16" + }, + { + "name": "marketOraclePrice", + "type": "i64" + }, + { + "name": "pnlTransfer", + "type": "u128" + }, + { + "name": "assetMarketIndex", + "type": "u16" + }, + { + "name": "assetPrice", + "type": "i64" + }, + { + "name": "assetTransfer", + "type": "u128" + } + ] + } + }, + { + "name": "PerpBankruptcyRecord", + "type": { + "kind": "struct", + "fields": [ + { + "name": "marketIndex", + "type": "u16" + }, + { + "name": "pnl", + "type": "i128" + }, + { + "name": "ifPayment", + "type": "u128" + }, + { + "name": "clawbackUser", + "type": { + "option": "publicKey" + } + }, + { + "name": "clawbackUserPayment", + "type": { + "option": "u128" + } + }, + { + "name": "cumulativeFundingRateDelta", + "type": "i128" + } + ] + } + }, + { + "name": "SpotBankruptcyRecord", + "type": { + "kind": "struct", + "fields": [ + { + "name": "marketIndex", + "type": "u16" + }, + { + "name": "borrowAmount", + "type": "u128" + }, + { + "name": "ifPayment", + "type": "u128" + }, + { + "name": "cumulativeDepositInterestDelta", + "type": "u128" + } + ] + } + }, + { + "name": "MarketIdentifier", + "type": { + "kind": "struct", + "fields": [ + { + "name": "marketType", + "type": { + "defined": "MarketType" + } + }, + { + "name": "marketIndex", + "type": "u16" + } + ] + } + }, + { + "name": "HistoricalOracleData", + "type": { + "kind": "struct", + "fields": [ + { + "name": "lastOraclePrice", + "docs": [ + "precision: PRICE_PRECISION" + ], + "type": "i64" + }, + { + "name": "lastOracleConf", + "docs": [ + "precision: PRICE_PRECISION" + ], + "type": "u64" + }, + { + "name": "lastOracleDelay", + "docs": [ + "number of slots since last update" + ], + "type": "i64" + }, + { + "name": "lastOraclePriceTwap", + "docs": [ + "precision: PRICE_PRECISION" + ], + "type": "i64" + }, + { + "name": "lastOraclePriceTwap5min", + "docs": [ + "precision: PRICE_PRECISION" + ], + "type": "i64" + }, + { + "name": "lastOraclePriceTwapTs", + "docs": [ + "unix_timestamp of last snapshot" + ], + "type": "i64" + } + ] + } + }, + { + "name": "HistoricalIndexData", + "type": { + "kind": "struct", + "fields": [ + { + "name": "lastIndexBidPrice", + "docs": [ + "precision: PRICE_PRECISION" + ], + "type": "u64" + }, + { + "name": "lastIndexAskPrice", + "docs": [ + "precision: PRICE_PRECISION" + ], + "type": "u64" + }, + { + "name": "lastIndexPriceTwap", + "docs": [ + "precision: PRICE_PRECISION" + ], + "type": "u64" + }, + { + "name": "lastIndexPriceTwap5min", + "docs": [ + "precision: PRICE_PRECISION" + ], + "type": "u64" + }, + { + "name": "lastIndexPriceTwapTs", + "docs": [ + "unix_timestamp of last snapshot" + ], + "type": "i64" + } + ] + } + }, + { + "name": "PrelaunchOracleParams", + "type": { + "kind": "struct", + "fields": [ + { + "name": "perpMarketIndex", + "type": "u16" + }, + { + "name": "price", + "type": { + "option": "i64" + } + }, + { + "name": "maxPrice", + "type": { + "option": "i64" + } + } + ] + } + }, + { + "name": "OrderParams", + "type": { + "kind": "struct", + "fields": [ + { + "name": "orderType", + "type": { + "defined": "OrderType" + } + }, + { + "name": "marketType", + "type": { + "defined": "MarketType" + } + }, + { + "name": "direction", + "type": { + "defined": "PositionDirection" + } + }, + { + "name": "userOrderId", + "type": "u8" + }, + { + "name": "baseAssetAmount", + "type": "u64" + }, + { + "name": "price", + "type": "u64" + }, + { + "name": "marketIndex", + "type": "u16" + }, + { + "name": "reduceOnly", + "type": "bool" + }, + { + "name": "postOnly", + "type": { + "defined": "PostOnlyParam" + } + }, + { + "name": "immediateOrCancel", + "type": "bool" + }, + { + "name": "maxTs", + "type": { + "option": "i64" + } + }, + { + "name": "triggerPrice", + "type": { + "option": "u64" + } + }, + { + "name": "triggerCondition", + "type": { + "defined": "OrderTriggerCondition" + } + }, + { + "name": "oraclePriceOffset", + "type": { + "option": "i32" + } + }, + { + "name": "auctionDuration", + "type": { + "option": "u8" + } + }, + { + "name": "auctionStartPrice", + "type": { + "option": "i64" + } + }, + { + "name": "auctionEndPrice", + "type": { + "option": "i64" + } + } + ] + } + }, + { + "name": "ModifyOrderParams", + "type": { + "kind": "struct", + "fields": [ + { + "name": "direction", + "type": { + "option": { + "defined": "PositionDirection" + } + } + }, + { + "name": "baseAssetAmount", + "type": { + "option": "u64" + } + }, + { + "name": "price", + "type": { + "option": "u64" + } + }, + { + "name": "reduceOnly", + "type": { + "option": "bool" + } + }, + { + "name": "postOnly", + "type": { + "option": { + "defined": "PostOnlyParam" + } + } + }, + { + "name": "immediateOrCancel", + "type": { + "option": "bool" + } + }, + { + "name": "maxTs", + "type": { + "option": "i64" + } + }, + { + "name": "triggerPrice", + "type": { + "option": "u64" + } + }, + { + "name": "triggerCondition", + "type": { + "option": { + "defined": "OrderTriggerCondition" + } + } + }, + { + "name": "oraclePriceOffset", + "type": { + "option": "i32" + } + }, + { + "name": "auctionDuration", + "type": { + "option": "u8" + } + }, + { + "name": "auctionStartPrice", + "type": { + "option": "i64" + } + }, + { + "name": "auctionEndPrice", + "type": { + "option": "i64" + } + }, + { + "name": "policy", + "type": { + "option": { + "defined": "ModifyOrderPolicy" + } + } + } + ] + } + }, + { + "name": "InsuranceClaim", + "type": { + "kind": "struct", + "fields": [ + { + "name": "revenueWithdrawSinceLastSettle", + "docs": [ + "The amount of revenue last settled", + "Positive if funds left the perp market,", + "negative if funds were pulled into the perp market", + "precision: QUOTE_PRECISION" + ], + "type": "i64" + }, + { + "name": "maxRevenueWithdrawPerPeriod", + "docs": [ + "The max amount of revenue that can be withdrawn per period", + "precision: QUOTE_PRECISION" + ], + "type": "u64" + }, + { + "name": "quoteMaxInsurance", + "docs": [ + "The max amount of insurance that perp market can use to resolve bankruptcy and pnl deficits", + "precision: QUOTE_PRECISION" + ], + "type": "u64" + }, + { + "name": "quoteSettledInsurance", + "docs": [ + "The amount of insurance that has been used to resolve bankruptcy and pnl deficits", + "precision: QUOTE_PRECISION" + ], + "type": "u64" + }, + { + "name": "lastRevenueWithdrawTs", + "docs": [ + "The last time revenue was settled in/out of market" + ], + "type": "i64" + } + ] + } + }, + { + "name": "PoolBalance", + "type": { + "kind": "struct", + "fields": [ + { + "name": "scaledBalance", + "docs": [ + "To get the pool's token amount, you must multiply the scaled balance by the market's cumulative", + "deposit interest", + "precision: SPOT_BALANCE_PRECISION" + ], + "type": "u128" + }, + { + "name": "marketIndex", + "docs": [ + "The spot market the pool is for" + ], + "type": "u16" + }, + { + "name": "padding", + "type": { + "array": [ + "u8", + 6 + ] + } + } + ] + } + }, + { + "name": "AMM", + "type": { + "kind": "struct", + "fields": [ + { + "name": "oracle", + "docs": [ + "oracle price data public key" + ], + "type": "publicKey" + }, + { + "name": "historicalOracleData", + "docs": [ + "stores historically witnessed oracle data" + ], + "type": { + "defined": "HistoricalOracleData" + } + }, + { + "name": "baseAssetAmountPerLp", + "docs": [ + "accumulated base asset amount since inception per lp share", + "precision: QUOTE_PRECISION" + ], + "type": "i128" + }, + { + "name": "quoteAssetAmountPerLp", + "docs": [ + "accumulated quote asset amount since inception per lp share", + "precision: QUOTE_PRECISION" + ], + "type": "i128" + }, + { + "name": "feePool", + "docs": [ + "partition of fees from perp market trading moved from pnl settlements" + ], + "type": { + "defined": "PoolBalance" + } + }, + { + "name": "baseAssetReserve", + "docs": [ + "`x` reserves for constant product mm formula (x * y = k)", + "precision: AMM_RESERVE_PRECISION" + ], + "type": "u128" + }, + { + "name": "quoteAssetReserve", + "docs": [ + "`y` reserves for constant product mm formula (x * y = k)", + "precision: AMM_RESERVE_PRECISION" + ], + "type": "u128" + }, + { + "name": "concentrationCoef", + "docs": [ + "determines how close the min/max base asset reserve sit vs base reserves", + "allow for decreasing slippage without increasing liquidity and v.v.", + "precision: PERCENTAGE_PRECISION" + ], + "type": "u128" + }, + { + "name": "minBaseAssetReserve", + "docs": [ + "minimum base_asset_reserve allowed before AMM is unavailable", + "precision: AMM_RESERVE_PRECISION" + ], + "type": "u128" + }, + { + "name": "maxBaseAssetReserve", + "docs": [ + "maximum base_asset_reserve allowed before AMM is unavailable", + "precision: AMM_RESERVE_PRECISION" + ], + "type": "u128" + }, + { + "name": "sqrtK", + "docs": [ + "`sqrt(k)` in constant product mm formula (x * y = k). stored to avoid drift caused by integer math issues", + "precision: AMM_RESERVE_PRECISION" + ], + "type": "u128" + }, + { + "name": "pegMultiplier", + "docs": [ + "normalizing numerical factor for y, its use offers lowest slippage in cp-curve when market is balanced", + "precision: PEG_PRECISION" + ], + "type": "u128" + }, + { + "name": "terminalQuoteAssetReserve", + "docs": [ + "y when market is balanced. stored to save computation", + "precision: AMM_RESERVE_PRECISION" + ], + "type": "u128" + }, + { + "name": "baseAssetAmountLong", + "docs": [ + "always non-negative. tracks number of total longs in market (regardless of counterparty)", + "precision: BASE_PRECISION" + ], + "type": "i128" + }, + { + "name": "baseAssetAmountShort", + "docs": [ + "always non-positive. tracks number of total shorts in market (regardless of counterparty)", + "precision: BASE_PRECISION" + ], + "type": "i128" + }, + { + "name": "baseAssetAmountWithAmm", + "docs": [ + "tracks net position (longs-shorts) in market with AMM as counterparty", + "precision: BASE_PRECISION" + ], + "type": "i128" + }, + { + "name": "baseAssetAmountWithUnsettledLp", + "docs": [ + "tracks net position (longs-shorts) in market with LPs as counterparty", + "precision: BASE_PRECISION" + ], + "type": "i128" + }, + { + "name": "maxOpenInterest", + "docs": [ + "max allowed open interest, blocks trades that breach this value", + "precision: BASE_PRECISION" + ], + "type": "u128" + }, + { + "name": "quoteAssetAmount", + "docs": [ + "sum of all user's perp quote_asset_amount in market", + "precision: QUOTE_PRECISION" + ], + "type": "i128" + }, + { + "name": "quoteEntryAmountLong", + "docs": [ + "sum of all long user's quote_entry_amount in market", + "precision: QUOTE_PRECISION" + ], + "type": "i128" + }, + { + "name": "quoteEntryAmountShort", + "docs": [ + "sum of all short user's quote_entry_amount in market", + "precision: QUOTE_PRECISION" + ], + "type": "i128" + }, + { + "name": "quoteBreakEvenAmountLong", + "docs": [ + "sum of all long user's quote_break_even_amount in market", + "precision: QUOTE_PRECISION" + ], + "type": "i128" + }, + { + "name": "quoteBreakEvenAmountShort", + "docs": [ + "sum of all short user's quote_break_even_amount in market", + "precision: QUOTE_PRECISION" + ], + "type": "i128" + }, + { + "name": "userLpShares", + "docs": [ + "total user lp shares of sqrt_k (protocol owned liquidity = sqrt_k - last_funding_rate)", + "precision: AMM_RESERVE_PRECISION" + ], + "type": "u128" + }, + { + "name": "lastFundingRate", + "docs": [ + "last funding rate in this perp market (unit is quote per base)", + "precision: QUOTE_PRECISION" + ], + "type": "i64" + }, + { + "name": "lastFundingRateLong", + "docs": [ + "last funding rate for longs in this perp market (unit is quote per base)", + "precision: QUOTE_PRECISION" + ], + "type": "i64" + }, + { + "name": "lastFundingRateShort", + "docs": [ + "last funding rate for shorts in this perp market (unit is quote per base)", + "precision: QUOTE_PRECISION" + ], + "type": "i64" + }, + { + "name": "last24hAvgFundingRate", + "docs": [ + "estimate of last 24h of funding rate perp market (unit is quote per base)", + "precision: QUOTE_PRECISION" + ], + "type": "i64" + }, + { + "name": "totalFee", + "docs": [ + "total fees collected by this perp market", + "precision: QUOTE_PRECISION" + ], + "type": "i128" + }, + { + "name": "totalMmFee", + "docs": [ + "total fees collected by the vAMM's bid/ask spread", + "precision: QUOTE_PRECISION" + ], + "type": "i128" + }, + { + "name": "totalExchangeFee", + "docs": [ + "total fees collected by exchange fee schedule", + "precision: QUOTE_PRECISION" + ], + "type": "u128" + }, + { + "name": "totalFeeMinusDistributions", + "docs": [ + "total fees minus any recognized upnl and pool withdraws", + "precision: QUOTE_PRECISION" + ], + "type": "i128" + }, + { + "name": "totalFeeWithdrawn", + "docs": [ + "sum of all fees from fee pool withdrawn to revenue pool", + "precision: QUOTE_PRECISION" + ], + "type": "u128" + }, + { + "name": "totalLiquidationFee", + "docs": [ + "all fees collected by market for liquidations", + "precision: QUOTE_PRECISION" + ], + "type": "u128" + }, + { + "name": "cumulativeFundingRateLong", + "docs": [ + "accumulated funding rate for longs since inception in market" + ], + "type": "i128" + }, + { + "name": "cumulativeFundingRateShort", + "docs": [ + "accumulated funding rate for shorts since inception in market" + ], + "type": "i128" + }, + { + "name": "totalSocialLoss", + "docs": [ + "accumulated social loss paid by users since inception in market" + ], + "type": "u128" + }, + { + "name": "askBaseAssetReserve", + "docs": [ + "transformed base_asset_reserve for users going long", + "precision: AMM_RESERVE_PRECISION" + ], + "type": "u128" + }, + { + "name": "askQuoteAssetReserve", + "docs": [ + "transformed quote_asset_reserve for users going long", + "precision: AMM_RESERVE_PRECISION" + ], + "type": "u128" + }, + { + "name": "bidBaseAssetReserve", + "docs": [ + "transformed base_asset_reserve for users going short", + "precision: AMM_RESERVE_PRECISION" + ], + "type": "u128" + }, + { + "name": "bidQuoteAssetReserve", + "docs": [ + "transformed quote_asset_reserve for users going short", + "precision: AMM_RESERVE_PRECISION" + ], + "type": "u128" + }, + { + "name": "lastOracleNormalisedPrice", + "docs": [ + "the last seen oracle price partially shrunk toward the amm reserve price", + "precision: PRICE_PRECISION" + ], + "type": "i64" + }, + { + "name": "lastOracleReservePriceSpreadPct", + "docs": [ + "the gap between the oracle price and the reserve price = y * peg_multiplier / x" + ], + "type": "i64" + }, + { + "name": "lastBidPriceTwap", + "docs": [ + "average estimate of bid price over funding_period", + "precision: PRICE_PRECISION" + ], + "type": "u64" + }, + { + "name": "lastAskPriceTwap", + "docs": [ + "average estimate of ask price over funding_period", + "precision: PRICE_PRECISION" + ], + "type": "u64" + }, + { + "name": "lastMarkPriceTwap", + "docs": [ + "average estimate of (bid+ask)/2 price over funding_period", + "precision: PRICE_PRECISION" + ], + "type": "u64" + }, + { + "name": "lastMarkPriceTwap5min", + "docs": [ + "average estimate of (bid+ask)/2 price over FIVE_MINUTES" + ], + "type": "u64" + }, + { + "name": "lastUpdateSlot", + "docs": [ + "the last blockchain slot the amm was updated" + ], + "type": "u64" + }, + { + "name": "lastOracleConfPct", + "docs": [ + "the pct size of the oracle confidence interval", + "precision: PERCENTAGE_PRECISION" + ], + "type": "u64" + }, + { + "name": "netRevenueSinceLastFunding", + "docs": [ + "the total_fee_minus_distribution change since the last funding update", + "precision: QUOTE_PRECISION" + ], + "type": "i64" + }, + { + "name": "lastFundingRateTs", + "docs": [ + "the last funding rate update unix_timestamp" + ], + "type": "i64" + }, + { + "name": "fundingPeriod", + "docs": [ + "the peridocity of the funding rate updates" + ], + "type": "i64" + }, + { + "name": "orderStepSize", + "docs": [ + "the base step size (increment) of orders", + "precision: BASE_PRECISION" + ], + "type": "u64" + }, + { + "name": "orderTickSize", + "docs": [ + "the price tick size of orders", + "precision: PRICE_PRECISION" + ], + "type": "u64" + }, + { + "name": "minOrderSize", + "docs": [ + "the minimum base size of an order", + "precision: BASE_PRECISION" + ], + "type": "u64" + }, + { + "name": "maxPositionSize", + "docs": [ + "the max base size a single user can have", + "precision: BASE_PRECISION" + ], + "type": "u64" + }, + { + "name": "volume24h", + "docs": [ + "estimated total of volume in market", + "QUOTE_PRECISION" + ], + "type": "u64" + }, + { + "name": "longIntensityVolume", + "docs": [ + "the volume intensity of long fills against AMM" + ], + "type": "u64" + }, + { + "name": "shortIntensityVolume", + "docs": [ + "the volume intensity of short fills against AMM" + ], + "type": "u64" + }, + { + "name": "lastTradeTs", + "docs": [ + "the blockchain unix timestamp at the time of the last trade" + ], + "type": "i64" + }, + { + "name": "markStd", + "docs": [ + "estimate of standard deviation of the fill (mark) prices", + "precision: PRICE_PRECISION" + ], + "type": "u64" + }, + { + "name": "oracleStd", + "docs": [ + "estimate of standard deviation of the oracle price at each update", + "precision: PRICE_PRECISION" + ], + "type": "u64" + }, + { + "name": "lastMarkPriceTwapTs", + "docs": [ + "the last unix_timestamp the mark twap was updated" + ], + "type": "i64" + }, + { + "name": "baseSpread", + "docs": [ + "the minimum spread the AMM can quote. also used as step size for some spread logic increases." + ], + "type": "u32" + }, + { + "name": "maxSpread", + "docs": [ + "the maximum spread the AMM can quote" + ], + "type": "u32" + }, + { + "name": "longSpread", + "docs": [ + "the spread for asks vs the reserve price" + ], + "type": "u32" + }, + { + "name": "shortSpread", + "docs": [ + "the spread for bids vs the reserve price" + ], + "type": "u32" + }, + { + "name": "longIntensityCount", + "docs": [ + "the count intensity of long fills against AMM" + ], + "type": "u32" + }, + { + "name": "shortIntensityCount", + "docs": [ + "the count intensity of short fills against AMM" + ], + "type": "u32" + }, + { + "name": "maxFillReserveFraction", + "docs": [ + "the fraction of total available liquidity a single fill on the AMM can consume" + ], + "type": "u16" + }, + { + "name": "maxSlippageRatio", + "docs": [ + "the maximum slippage a single fill on the AMM can push" + ], + "type": "u16" + }, + { + "name": "curveUpdateIntensity", + "docs": [ + "the update intensity of AMM formulaic updates (adjusting k). 0-100" + ], + "type": "u8" + }, + { + "name": "ammJitIntensity", + "docs": [ + "the jit intensity of AMM. larger intensity means larger participation in jit. 0 means no jit participation.", + "(0, 100] is intensity for protocol-owned AMM. (100, 200] is intensity for user LP-owned AMM." + ], + "type": "u8" + }, + { + "name": "oracleSource", + "docs": [ + "the oracle provider information. used to decode/scale the oracle public key" + ], + "type": { + "defined": "OracleSource" + } + }, + { + "name": "lastOracleValid", + "docs": [ + "tracks whether the oracle was considered valid at the last AMM update" + ], + "type": "bool" + }, + { + "name": "targetBaseAssetAmountPerLp", + "docs": [ + "the target value for `base_asset_amount_per_lp`, used during AMM JIT with LP split", + "precision: BASE_PRECISION" + ], + "type": "i32" + }, + { + "name": "perLpBase", + "docs": [ + "expo for unit of per_lp, base 10 (if per_lp_base=X, then per_lp unit is 10^X)" + ], + "type": "i8" + }, + { + "name": "padding1", + "type": "u8" + }, + { + "name": "padding2", + "type": "u16" + }, + { + "name": "totalFeeEarnedPerLp", + "type": "u64" + }, + { + "name": "netUnsettledFundingPnl", + "type": "i64" + }, + { + "name": "quoteAssetAmountWithUnsettledLp", + "type": "i64" + }, + { + "name": "referencePriceOffset", + "type": "i32" + }, + { + "name": "padding", + "type": { + "array": [ + "u8", + 12 + ] + } + } + ] + } + }, + { + "name": "InsuranceFund", + "type": { + "kind": "struct", + "fields": [ + { + "name": "vault", + "type": "publicKey" + }, + { + "name": "totalShares", + "type": "u128" + }, + { + "name": "userShares", + "type": "u128" + }, + { + "name": "sharesBase", + "type": "u128" + }, + { + "name": "unstakingPeriod", + "type": "i64" + }, + { + "name": "lastRevenueSettleTs", + "type": "i64" + }, + { + "name": "revenueSettlePeriod", + "type": "i64" + }, + { + "name": "totalFactor", + "type": "u32" + }, + { + "name": "userFactor", + "type": "u32" + } + ] + } + }, + { + "name": "OracleGuardRails", + "type": { + "kind": "struct", + "fields": [ + { + "name": "priceDivergence", + "type": { + "defined": "PriceDivergenceGuardRails" + } + }, + { + "name": "validity", + "type": { + "defined": "ValidityGuardRails" + } + } + ] + } + }, + { + "name": "PriceDivergenceGuardRails", + "type": { + "kind": "struct", + "fields": [ + { + "name": "markOraclePercentDivergence", + "type": "u64" + }, + { + "name": "oracleTwap5minPercentDivergence", + "type": "u64" + } + ] + } + }, + { + "name": "ValidityGuardRails", + "type": { + "kind": "struct", + "fields": [ + { + "name": "slotsBeforeStaleForAmm", + "type": "i64" + }, + { + "name": "slotsBeforeStaleForMargin", + "type": "i64" + }, + { + "name": "confidenceIntervalMaxSize", + "type": "u64" + }, + { + "name": "tooVolatileRatio", + "type": "i64" + } + ] + } + }, + { + "name": "FeeStructure", + "type": { + "kind": "struct", + "fields": [ + { + "name": "feeTiers", + "type": { + "array": [ + { + "defined": "FeeTier" + }, + 10 + ] + } + }, + { + "name": "fillerRewardStructure", + "type": { + "defined": "OrderFillerRewardStructure" + } + }, + { + "name": "referrerRewardEpochUpperBound", + "type": "u64" + }, + { + "name": "flatFillerFee", + "type": "u64" + } + ] + } + }, + { + "name": "FeeTier", + "type": { + "kind": "struct", + "fields": [ + { + "name": "feeNumerator", + "type": "u32" + }, + { + "name": "feeDenominator", + "type": "u32" + }, + { + "name": "makerRebateNumerator", + "type": "u32" + }, + { + "name": "makerRebateDenominator", + "type": "u32" + }, + { + "name": "referrerRewardNumerator", + "type": "u32" + }, + { + "name": "referrerRewardDenominator", + "type": "u32" + }, + { + "name": "refereeFeeNumerator", + "type": "u32" + }, + { + "name": "refereeFeeDenominator", + "type": "u32" + } + ] + } + }, + { + "name": "OrderFillerRewardStructure", + "type": { + "kind": "struct", + "fields": [ + { + "name": "rewardNumerator", + "type": "u32" + }, + { + "name": "rewardDenominator", + "type": "u32" + }, + { + "name": "timeBasedRewardLowerBound", + "type": "u128" + } + ] + } + }, + { + "name": "UserFees", + "type": { + "kind": "struct", + "fields": [ + { + "name": "totalFeePaid", + "docs": [ + "Total taker fee paid", + "precision: QUOTE_PRECISION" + ], + "type": "u64" + }, + { + "name": "totalFeeRebate", + "docs": [ + "Total maker fee rebate", + "precision: QUOTE_PRECISION" + ], + "type": "u64" + }, + { + "name": "totalTokenDiscount", + "docs": [ + "Total discount from holding token", + "precision: QUOTE_PRECISION" + ], + "type": "u64" + }, + { + "name": "totalRefereeDiscount", + "docs": [ + "Total discount from being referred", + "precision: QUOTE_PRECISION" + ], + "type": "u64" + }, + { + "name": "totalReferrerReward", + "docs": [ + "Total reward to referrer", + "precision: QUOTE_PRECISION" + ], + "type": "u64" + }, + { + "name": "currentEpochReferrerReward", + "docs": [ + "Total reward to referrer this epoch", + "precision: QUOTE_PRECISION" + ], + "type": "u64" + } + ] + } + }, + { + "name": "SpotPosition", + "type": { + "kind": "struct", + "fields": [ + { + "name": "scaledBalance", + "docs": [ + "The scaled balance of the position. To get the token amount, multiply by the cumulative deposit/borrow", + "interest of corresponding market.", + "precision: SPOT_BALANCE_PRECISION" + ], + "type": "u64" + }, + { + "name": "openBids", + "docs": [ + "How many spot bids the user has open", + "precision: token mint precision" + ], + "type": "i64" + }, + { + "name": "openAsks", + "docs": [ + "How many spot asks the user has open", + "precision: token mint precision" + ], + "type": "i64" + }, + { + "name": "cumulativeDeposits", + "docs": [ + "The cumulative deposits/borrows a user has made into a market", + "precision: token mint precision" + ], + "type": "i64" + }, + { + "name": "marketIndex", + "docs": [ + "The market index of the corresponding spot market" + ], + "type": "u16" + }, + { + "name": "balanceType", + "docs": [ + "Whether the position is deposit or borrow" + ], + "type": { + "defined": "SpotBalanceType" + } + }, + { + "name": "openOrders", + "docs": [ + "Number of open orders" + ], + "type": "u8" + }, + { + "name": "padding", + "type": { + "array": [ + "u8", + 4 + ] + } + } + ] + } + }, + { + "name": "PerpPosition", + "type": { + "kind": "struct", + "fields": [ + { + "name": "lastCumulativeFundingRate", + "docs": [ + "The perp market's last cumulative funding rate. Used to calculate the funding payment owed to user", + "precision: FUNDING_RATE_PRECISION" + ], + "type": "i64" + }, + { + "name": "baseAssetAmount", + "docs": [ + "the size of the users perp position", + "precision: BASE_PRECISION" + ], + "type": "i64" + }, + { + "name": "quoteAssetAmount", + "docs": [ + "Used to calculate the users pnl. Upon entry, is equal to base_asset_amount * avg entry price - fees", + "Updated when the user open/closes position or settles pnl. Includes fees/funding", + "precision: QUOTE_PRECISION" + ], + "type": "i64" + }, + { + "name": "quoteBreakEvenAmount", + "docs": [ + "The amount of quote the user would need to exit their position at to break even", + "Updated when the user open/closes position or settles pnl. Includes fees/funding", + "precision: QUOTE_PRECISION" + ], + "type": "i64" + }, + { + "name": "quoteEntryAmount", + "docs": [ + "The amount quote the user entered the position with. Equal to base asset amount * avg entry price", + "Updated when the user open/closes position. Excludes fees/funding", + "precision: QUOTE_PRECISION" + ], + "type": "i64" + }, + { + "name": "openBids", + "docs": [ + "The amount of open bids the user has in this perp market", + "precision: BASE_PRECISION" + ], + "type": "i64" + }, + { + "name": "openAsks", + "docs": [ + "The amount of open asks the user has in this perp market", + "precision: BASE_PRECISION" + ], + "type": "i64" + }, + { + "name": "settledPnl", + "docs": [ + "The amount of pnl settled in this market since opening the position", + "precision: QUOTE_PRECISION" + ], + "type": "i64" + }, + { + "name": "lpShares", + "docs": [ + "The number of lp (liquidity provider) shares the user has in this perp market", + "LP shares allow users to provide liquidity via the AMM", + "precision: BASE_PRECISION" + ], + "type": "u64" + }, + { + "name": "lastBaseAssetAmountPerLp", + "docs": [ + "The last base asset amount per lp the amm had", + "Used to settle the users lp position", + "precision: BASE_PRECISION" + ], + "type": "i64" + }, + { + "name": "lastQuoteAssetAmountPerLp", + "docs": [ + "The last quote asset amount per lp the amm had", + "Used to settle the users lp position", + "precision: QUOTE_PRECISION" + ], + "type": "i64" + }, + { + "name": "remainderBaseAssetAmount", + "docs": [ + "Settling LP position can lead to a small amount of base asset being left over smaller than step size", + "This records that remainder so it can be settled later on", + "precision: BASE_PRECISION" + ], + "type": "i32" + }, + { + "name": "marketIndex", + "docs": [ + "The market index for the perp market" + ], + "type": "u16" + }, + { + "name": "openOrders", + "docs": [ + "The number of open orders" + ], + "type": "u8" + }, + { + "name": "perLpBase", + "type": "i8" + } + ] + } + }, + { + "name": "Order", + "type": { + "kind": "struct", + "fields": [ + { + "name": "slot", + "docs": [ + "The slot the order was placed" + ], + "type": "u64" + }, + { + "name": "price", + "docs": [ + "The limit price for the order (can be 0 for market orders)", + "For orders with an auction, this price isn't used until the auction is complete", + "precision: PRICE_PRECISION" + ], + "type": "u64" + }, + { + "name": "baseAssetAmount", + "docs": [ + "The size of the order", + "precision for perps: BASE_PRECISION", + "precision for spot: token mint precision" + ], + "type": "u64" + }, + { + "name": "baseAssetAmountFilled", + "docs": [ + "The amount of the order filled", + "precision for perps: BASE_PRECISION", + "precision for spot: token mint precision" + ], + "type": "u64" + }, + { + "name": "quoteAssetAmountFilled", + "docs": [ + "The amount of quote filled for the order", + "precision: QUOTE_PRECISION" + ], + "type": "u64" + }, + { + "name": "triggerPrice", + "docs": [ + "At what price the order will be triggered. Only relevant for trigger orders", + "precision: PRICE_PRECISION" + ], + "type": "u64" + }, + { + "name": "auctionStartPrice", + "docs": [ + "The start price for the auction. Only relevant for market/oracle orders", + "precision: PRICE_PRECISION" + ], + "type": "i64" + }, + { + "name": "auctionEndPrice", + "docs": [ + "The end price for the auction. Only relevant for market/oracle orders", + "precision: PRICE_PRECISION" + ], + "type": "i64" + }, + { + "name": "maxTs", + "docs": [ + "The time when the order will expire" + ], + "type": "i64" + }, + { + "name": "oraclePriceOffset", + "docs": [ + "If set, the order limit price is the oracle price + this offset", + "precision: PRICE_PRECISION" + ], + "type": "i32" + }, + { + "name": "orderId", + "docs": [ + "The id for the order. Each users has their own order id space" + ], + "type": "u32" + }, + { + "name": "marketIndex", + "docs": [ + "The perp/spot market index" + ], + "type": "u16" + }, + { + "name": "status", + "docs": [ + "Whether the order is open or unused" + ], + "type": { + "defined": "OrderStatus" + } + }, + { + "name": "orderType", + "docs": [ + "The type of order" + ], + "type": { + "defined": "OrderType" + } + }, + { + "name": "marketType", + "docs": [ + "Whether market is spot or perp" + ], + "type": { + "defined": "MarketType" + } + }, + { + "name": "userOrderId", + "docs": [ + "User generated order id. Can make it easier to place/cancel orders" + ], + "type": "u8" + }, + { + "name": "existingPositionDirection", + "docs": [ + "What the users position was when the order was placed" + ], + "type": { + "defined": "PositionDirection" + } + }, + { + "name": "direction", + "docs": [ + "Whether the user is going long or short. LONG = bid, SHORT = ask" + ], + "type": { + "defined": "PositionDirection" + } + }, + { + "name": "reduceOnly", + "docs": [ + "Whether the order is allowed to only reduce position size" + ], + "type": "bool" + }, + { + "name": "postOnly", + "docs": [ + "Whether the order must be a maker" + ], + "type": "bool" + }, + { + "name": "immediateOrCancel", + "docs": [ + "Whether the order must be canceled the same slot it is placed" + ], + "type": "bool" + }, + { + "name": "triggerCondition", + "docs": [ + "Whether the order is triggered above or below the trigger price. Only relevant for trigger orders" + ], + "type": { + "defined": "OrderTriggerCondition" + } + }, + { + "name": "auctionDuration", + "docs": [ + "How many slots the auction lasts" + ], + "type": "u8" + }, + { + "name": "padding", + "type": { + "array": [ + "u8", + 3 + ] + } + } + ] + } + }, + { + "name": "SwapDirection", + "type": { + "kind": "enum", + "variants": [ + { + "name": "Add" + }, + { + "name": "Remove" + } + ] + } + }, + { + "name": "ModifyOrderId", + "type": { + "kind": "enum", + "variants": [ + { + "name": "UserOrderId", + "fields": [ + "u8" + ] + }, + { + "name": "OrderId", + "fields": [ + "u32" + ] + } + ] + } + }, + { + "name": "PositionDirection", + "type": { + "kind": "enum", + "variants": [ + { + "name": "Long" + }, + { + "name": "Short" + } + ] + } + }, + { + "name": "SpotFulfillmentType", + "type": { + "kind": "enum", + "variants": [ + { + "name": "SerumV3" + }, + { + "name": "Match" + }, + { + "name": "PhoenixV1" + }, + { + "name": "OpenbookV2" + } + ] + } + }, + { + "name": "SwapReduceOnly", + "type": { + "kind": "enum", + "variants": [ + { + "name": "In" + }, + { + "name": "Out" + } + ] + } + }, + { + "name": "TwapPeriod", + "type": { + "kind": "enum", + "variants": [ + { + "name": "FundingPeriod" + }, + { + "name": "FiveMin" + } + ] + } + }, + { + "name": "LiquidationMultiplierType", + "type": { + "kind": "enum", + "variants": [ + { + "name": "Discount" + }, + { + "name": "Premium" + } + ] + } + }, + { + "name": "MarginRequirementType", + "type": { + "kind": "enum", + "variants": [ + { + "name": "Initial" + }, + { + "name": "Fill" + }, + { + "name": "Maintenance" + } + ] + } + }, + { + "name": "OracleValidity", + "type": { + "kind": "enum", + "variants": [ + { + "name": "NonPositive" + }, + { + "name": "TooVolatile" + }, + { + "name": "TooUncertain" + }, + { + "name": "StaleForMargin" + }, + { + "name": "InsufficientDataPoints" + }, + { + "name": "StaleForAMM" + }, + { + "name": "Valid" + } + ] + } + }, + { + "name": "DriftAction", + "type": { + "kind": "enum", + "variants": [ + { + "name": "UpdateFunding" + }, + { + "name": "SettlePnl" + }, + { + "name": "TriggerOrder" + }, + { + "name": "FillOrderMatch" + }, + { + "name": "FillOrderAmm" + }, + { + "name": "Liquidate" + }, + { + "name": "MarginCalc" + }, + { + "name": "UpdateTwap" + }, + { + "name": "UpdateAMMCurve" + }, + { + "name": "OracleOrderPrice" + } + ] + } + }, + { + "name": "PositionUpdateType", + "type": { + "kind": "enum", + "variants": [ + { + "name": "Open" + }, + { + "name": "Increase" + }, + { + "name": "Reduce" + }, + { + "name": "Close" + }, + { + "name": "Flip" + } + ] + } + }, + { + "name": "DepositExplanation", + "type": { + "kind": "enum", + "variants": [ + { + "name": "None" + }, + { + "name": "Transfer" + }, + { + "name": "Borrow" + }, + { + "name": "RepayBorrow" + } + ] + } + }, + { + "name": "DepositDirection", + "type": { + "kind": "enum", + "variants": [ + { + "name": "Deposit" + }, + { + "name": "Withdraw" + } + ] + } + }, + { + "name": "OrderAction", + "type": { + "kind": "enum", + "variants": [ + { + "name": "Place" + }, + { + "name": "Cancel" + }, + { + "name": "Fill" + }, + { + "name": "Trigger" + }, + { + "name": "Expire" + } + ] + } + }, + { + "name": "OrderActionExplanation", + "type": { + "kind": "enum", + "variants": [ + { + "name": "None" + }, + { + "name": "InsufficientFreeCollateral" + }, + { + "name": "OraclePriceBreachedLimitPrice" + }, + { + "name": "MarketOrderFilledToLimitPrice" + }, + { + "name": "OrderExpired" + }, + { + "name": "Liquidation" + }, + { + "name": "OrderFilledWithAMM" + }, + { + "name": "OrderFilledWithAMMJit" + }, + { + "name": "OrderFilledWithMatch" + }, + { + "name": "OrderFilledWithMatchJit" + }, + { + "name": "MarketExpired" + }, + { + "name": "RiskingIncreasingOrder" + }, + { + "name": "ReduceOnlyOrderIncreasedPosition" + }, + { + "name": "OrderFillWithSerum" + }, + { + "name": "NoBorrowLiquidity" + }, + { + "name": "OrderFillWithPhoenix" + }, + { + "name": "OrderFilledWithAMMJitLPSplit" + }, + { + "name": "OrderFilledWithLPJit" + }, + { + "name": "DeriskLp" + }, + { + "name": "OrderFilledWithOpenbookV2" + } + ] + } + }, + { + "name": "LPAction", + "type": { + "kind": "enum", + "variants": [ + { + "name": "AddLiquidity" + }, + { + "name": "RemoveLiquidity" + }, + { + "name": "SettleLiquidity" + }, + { + "name": "RemoveLiquidityDerisk" + } + ] + } + }, + { + "name": "LiquidationType", + "type": { + "kind": "enum", + "variants": [ + { + "name": "LiquidatePerp" + }, + { + "name": "LiquidateSpot" + }, + { + "name": "LiquidateBorrowForPerpPnl" + }, + { + "name": "LiquidatePerpPnlForDeposit" + }, + { + "name": "PerpBankruptcy" + }, + { + "name": "SpotBankruptcy" + } + ] + } + }, + { + "name": "SettlePnlExplanation", + "type": { + "kind": "enum", + "variants": [ + { + "name": "None" + }, + { + "name": "ExpiredPosition" + } + ] + } + }, + { + "name": "StakeAction", + "type": { + "kind": "enum", + "variants": [ + { + "name": "Stake" + }, + { + "name": "UnstakeRequest" + }, + { + "name": "UnstakeCancelRequest" + }, + { + "name": "Unstake" + }, + { + "name": "UnstakeTransfer" + }, + { + "name": "StakeTransfer" + } + ] + } + }, + { + "name": "FillMode", + "type": { + "kind": "enum", + "variants": [ + { + "name": "Fill" + }, + { + "name": "PlaceAndMake" + }, + { + "name": "PlaceAndTake" + }, + { + "name": "Liquidation" + } + ] + } + }, + { + "name": "PerpFulfillmentMethod", + "type": { + "kind": "enum", + "variants": [ + { + "name": "AMM", + "fields": [ + { + "option": "u64" + } + ] + }, + { + "name": "Match", + "fields": [ + "publicKey", + "u16" + ] + } + ] + } + }, + { + "name": "SpotFulfillmentMethod", + "type": { + "kind": "enum", + "variants": [ + { + "name": "ExternalMarket" + }, + { + "name": "Match", + "fields": [ + "publicKey", + "u16" + ] + } + ] + } + }, + { + "name": "MarginCalculationMode", + "type": { + "kind": "enum", + "variants": [ + { + "name": "Standard", + "fields": [ + { + "name": "trackOpenOrdersFraction", + "type": "bool" + } + ] + }, + { + "name": "Liquidation", + "fields": [ + { + "name": "marketToTrackMarginRequirement", + "type": { + "option": { + "defined": "MarketIdentifier" + } + } + } + ] + } + ] + } + }, + { + "name": "OracleSource", + "type": { + "kind": "enum", + "variants": [ + { + "name": "Pyth" + }, + { + "name": "Switchboard" + }, + { + "name": "QuoteAsset" + }, + { + "name": "Pyth1K" + }, + { + "name": "Pyth1M" + }, + { + "name": "PythStableCoin" + }, + { + "name": "Prelaunch" + }, + { + "name": "PythPull" + }, + { + "name": "Pyth1KPull" + }, + { + "name": "Pyth1MPull" + }, + { + "name": "PythStableCoinPull" + }, + { + "name": "SwitchboardOnDemand" + } + ] + } + }, + { + "name": "PostOnlyParam", + "type": { + "kind": "enum", + "variants": [ + { + "name": "None" + }, + { + "name": "MustPostOnly" + }, + { + "name": "TryPostOnly" + }, + { + "name": "Slide" + } + ] + } + }, + { + "name": "ModifyOrderPolicy", + "type": { + "kind": "enum", + "variants": [ + { + "name": "TryModify" + }, + { + "name": "MustModify" + } + ] + } + }, + { + "name": "PerpOperation", + "type": { + "kind": "enum", + "variants": [ + { + "name": "UpdateFunding" + }, + { + "name": "AmmFill" + }, + { + "name": "Fill" + }, + { + "name": "SettlePnl" + }, + { + "name": "SettlePnlWithPosition" + }, + { + "name": "Liquidation" + } + ] + } + }, + { + "name": "SpotOperation", + "type": { + "kind": "enum", + "variants": [ + { + "name": "UpdateCumulativeInterest" + }, + { + "name": "Fill" + }, + { + "name": "Deposit" + }, + { + "name": "Withdraw" + }, + { + "name": "Liquidation" + } + ] + } + }, + { + "name": "InsuranceFundOperation", + "type": { + "kind": "enum", + "variants": [ + { + "name": "Init" + }, + { + "name": "Add" + }, + { + "name": "RequestRemove" + }, + { + "name": "Remove" + } + ] + } + }, + { + "name": "MarketStatus", + "type": { + "kind": "enum", + "variants": [ + { + "name": "Initialized" + }, + { + "name": "Active" + }, + { + "name": "FundingPaused" + }, + { + "name": "AmmPaused" + }, + { + "name": "FillPaused" + }, + { + "name": "WithdrawPaused" + }, + { + "name": "ReduceOnly" + }, + { + "name": "Settlement" + }, + { + "name": "Delisted" + } + ] + } + }, + { + "name": "ContractType", + "type": { + "kind": "enum", + "variants": [ + { + "name": "Perpetual" + }, + { + "name": "Future" + }, + { + "name": "Prediction" + } + ] + } + }, + { + "name": "ContractTier", + "type": { + "kind": "enum", + "variants": [ + { + "name": "A" + }, + { + "name": "B" + }, + { + "name": "C" + }, + { + "name": "Speculative" + }, + { + "name": "HighlySpeculative" + }, + { + "name": "Isolated" + } + ] + } + }, + { + "name": "AMMLiquiditySplit", + "type": { + "kind": "enum", + "variants": [ + { + "name": "ProtocolOwned" + }, + { + "name": "LPOwned" + }, + { + "name": "Shared" + } + ] + } + }, + { + "name": "SettlePnlMode", + "type": { + "kind": "enum", + "variants": [ + { + "name": "MustSettle" + }, + { + "name": "TrySettle" + } + ] + } + }, + { + "name": "SpotBalanceType", + "type": { + "kind": "enum", + "variants": [ + { + "name": "Deposit" + }, + { + "name": "Borrow" + } + ] + } + }, + { + "name": "SpotFulfillmentConfigStatus", + "type": { + "kind": "enum", + "variants": [ + { + "name": "Enabled" + }, + { + "name": "Disabled" + } + ] + } + }, + { + "name": "AssetTier", + "type": { + "kind": "enum", + "variants": [ + { + "name": "Collateral" + }, + { + "name": "Protected" + }, + { + "name": "Cross" + }, + { + "name": "Isolated" + }, + { + "name": "Unlisted" + } + ] + } + }, + { + "name": "ExchangeStatus", + "type": { + "kind": "enum", + "variants": [ + { + "name": "DepositPaused" + }, + { + "name": "WithdrawPaused" + }, + { + "name": "AmmPaused" + }, + { + "name": "FillPaused" + }, + { + "name": "LiqPaused" + }, + { + "name": "FundingPaused" + }, + { + "name": "SettlePnlPaused" + } + ] + } + }, + { + "name": "UserStatus", + "type": { + "kind": "enum", + "variants": [ + { + "name": "BeingLiquidated" + }, + { + "name": "Bankrupt" + }, + { + "name": "ReduceOnly" + }, + { + "name": "AdvancedLp" + } + ] + } + }, + { + "name": "AssetType", + "type": { + "kind": "enum", + "variants": [ + { + "name": "Base" + }, + { + "name": "Quote" + } + ] + } + }, + { + "name": "OrderStatus", + "type": { + "kind": "enum", + "variants": [ + { + "name": "Init" + }, + { + "name": "Open" + }, + { + "name": "Filled" + }, + { + "name": "Canceled" + } + ] + } + }, + { + "name": "OrderType", + "type": { + "kind": "enum", + "variants": [ + { + "name": "Market" + }, + { + "name": "Limit" + }, + { + "name": "TriggerMarket" + }, + { + "name": "TriggerLimit" + }, + { + "name": "Oracle" + } + ] + } + }, + { + "name": "OrderTriggerCondition", + "type": { + "kind": "enum", + "variants": [ + { + "name": "Above" + }, + { + "name": "Below" + }, + { + "name": "TriggeredAbove" + }, + { + "name": "TriggeredBelow" + } + ] + } + }, + { + "name": "MarketType", + "type": { + "kind": "enum", + "variants": [ + { + "name": "Spot" + }, + { + "name": "Perp" + } + ] + } + } + ], + "events": [ + { + "name": "NewUserRecord", + "fields": [ + { + "name": "ts", + "type": "i64", + "index": false + }, + { + "name": "userAuthority", + "type": "publicKey", + "index": false + }, + { + "name": "user", + "type": "publicKey", + "index": false + }, + { + "name": "subAccountId", + "type": "u16", + "index": false + }, + { + "name": "name", + "type": { + "array": [ + "u8", + 32 + ] + }, + "index": false + }, + { + "name": "referrer", + "type": "publicKey", + "index": false + } + ] + }, + { + "name": "DepositRecord", + "fields": [ + { + "name": "ts", + "type": "i64", + "index": false + }, + { + "name": "userAuthority", + "type": "publicKey", + "index": false + }, + { + "name": "user", + "type": "publicKey", + "index": false + }, + { + "name": "direction", + "type": { + "defined": "DepositDirection" + }, + "index": false + }, + { + "name": "depositRecordId", + "type": "u64", + "index": false + }, + { + "name": "amount", + "type": "u64", + "index": false + }, + { + "name": "marketIndex", + "type": "u16", + "index": false + }, + { + "name": "oraclePrice", + "type": "i64", + "index": false + }, + { + "name": "marketDepositBalance", + "type": "u128", + "index": false + }, + { + "name": "marketWithdrawBalance", + "type": "u128", + "index": false + }, + { + "name": "marketCumulativeDepositInterest", + "type": "u128", + "index": false + }, + { + "name": "marketCumulativeBorrowInterest", + "type": "u128", + "index": false + }, + { + "name": "totalDepositsAfter", + "type": "u64", + "index": false + }, + { + "name": "totalWithdrawsAfter", + "type": "u64", + "index": false + }, + { + "name": "explanation", + "type": { + "defined": "DepositExplanation" + }, + "index": false + }, + { + "name": "transferUser", + "type": { + "option": "publicKey" + }, + "index": false + } + ] + }, + { + "name": "SpotInterestRecord", + "fields": [ + { + "name": "ts", + "type": "i64", + "index": false + }, + { + "name": "marketIndex", + "type": "u16", + "index": false + }, + { + "name": "depositBalance", + "type": "u128", + "index": false + }, + { + "name": "cumulativeDepositInterest", + "type": "u128", + "index": false + }, + { + "name": "borrowBalance", + "type": "u128", + "index": false + }, + { + "name": "cumulativeBorrowInterest", + "type": "u128", + "index": false + }, + { + "name": "optimalUtilization", + "type": "u32", + "index": false + }, + { + "name": "optimalBorrowRate", + "type": "u32", + "index": false + }, + { + "name": "maxBorrowRate", + "type": "u32", + "index": false + } + ] + }, + { + "name": "FundingPaymentRecord", + "fields": [ + { + "name": "ts", + "type": "i64", + "index": false + }, + { + "name": "userAuthority", + "type": "publicKey", + "index": false + }, + { + "name": "user", + "type": "publicKey", + "index": false + }, + { + "name": "marketIndex", + "type": "u16", + "index": false + }, + { + "name": "fundingPayment", + "type": "i64", + "index": false + }, + { + "name": "baseAssetAmount", + "type": "i64", + "index": false + }, + { + "name": "userLastCumulativeFunding", + "type": "i64", + "index": false + }, + { + "name": "ammCumulativeFundingLong", + "type": "i128", + "index": false + }, + { + "name": "ammCumulativeFundingShort", + "type": "i128", + "index": false + } + ] + }, + { + "name": "FundingRateRecord", + "fields": [ + { + "name": "ts", + "type": "i64", + "index": false + }, + { + "name": "recordId", + "type": "u64", + "index": false + }, + { + "name": "marketIndex", + "type": "u16", + "index": false + }, + { + "name": "fundingRate", + "type": "i64", + "index": false + }, + { + "name": "fundingRateLong", + "type": "i128", + "index": false + }, + { + "name": "fundingRateShort", + "type": "i128", + "index": false + }, + { + "name": "cumulativeFundingRateLong", + "type": "i128", + "index": false + }, + { + "name": "cumulativeFundingRateShort", + "type": "i128", + "index": false + }, + { + "name": "oraclePriceTwap", + "type": "i64", + "index": false + }, + { + "name": "markPriceTwap", + "type": "u64", + "index": false + }, + { + "name": "periodRevenue", + "type": "i64", + "index": false + }, + { + "name": "baseAssetAmountWithAmm", + "type": "i128", + "index": false + }, + { + "name": "baseAssetAmountWithUnsettledLp", + "type": "i128", + "index": false + } + ] + }, + { + "name": "CurveRecord", + "fields": [ + { + "name": "ts", + "type": "i64", + "index": false + }, + { + "name": "recordId", + "type": "u64", + "index": false + }, + { + "name": "pegMultiplierBefore", + "type": "u128", + "index": false + }, + { + "name": "baseAssetReserveBefore", + "type": "u128", + "index": false + }, + { + "name": "quoteAssetReserveBefore", + "type": "u128", + "index": false + }, + { + "name": "sqrtKBefore", + "type": "u128", + "index": false + }, + { + "name": "pegMultiplierAfter", + "type": "u128", + "index": false + }, + { + "name": "baseAssetReserveAfter", + "type": "u128", + "index": false + }, + { + "name": "quoteAssetReserveAfter", + "type": "u128", + "index": false + }, + { + "name": "sqrtKAfter", + "type": "u128", + "index": false + }, + { + "name": "baseAssetAmountLong", + "type": "u128", + "index": false + }, + { + "name": "baseAssetAmountShort", + "type": "u128", + "index": false + }, + { + "name": "baseAssetAmountWithAmm", + "type": "i128", + "index": false + }, + { + "name": "totalFee", + "type": "i128", + "index": false + }, + { + "name": "totalFeeMinusDistributions", + "type": "i128", + "index": false + }, + { + "name": "adjustmentCost", + "type": "i128", + "index": false + }, + { + "name": "oraclePrice", + "type": "i64", + "index": false + }, + { + "name": "fillRecord", + "type": "u128", + "index": false + }, + { + "name": "numberOfUsers", + "type": "u32", + "index": false + }, + { + "name": "marketIndex", + "type": "u16", + "index": false + } + ] + }, + { + "name": "OrderRecord", + "fields": [ + { + "name": "ts", + "type": "i64", + "index": false + }, + { + "name": "user", + "type": "publicKey", + "index": false + }, + { + "name": "order", + "type": { + "defined": "Order" + }, + "index": false + } + ] + }, + { + "name": "OrderActionRecord", + "fields": [ + { + "name": "ts", + "type": "i64", + "index": false + }, + { + "name": "action", + "type": { + "defined": "OrderAction" + }, + "index": false + }, + { + "name": "actionExplanation", + "type": { + "defined": "OrderActionExplanation" + }, + "index": false + }, + { + "name": "marketIndex", + "type": "u16", + "index": false + }, + { + "name": "marketType", + "type": { + "defined": "MarketType" + }, + "index": false + }, + { + "name": "filler", + "type": { + "option": "publicKey" + }, + "index": false + }, + { + "name": "fillerReward", + "type": { + "option": "u64" + }, + "index": false + }, + { + "name": "fillRecordId", + "type": { + "option": "u64" + }, + "index": false + }, + { + "name": "baseAssetAmountFilled", + "type": { + "option": "u64" + }, + "index": false + }, + { + "name": "quoteAssetAmountFilled", + "type": { + "option": "u64" + }, + "index": false + }, + { + "name": "takerFee", + "type": { + "option": "u64" + }, + "index": false + }, + { + "name": "makerFee", + "type": { + "option": "i64" + }, + "index": false + }, + { + "name": "referrerReward", + "type": { + "option": "u32" + }, + "index": false + }, + { + "name": "quoteAssetAmountSurplus", + "type": { + "option": "i64" + }, + "index": false + }, + { + "name": "spotFulfillmentMethodFee", + "type": { + "option": "u64" + }, + "index": false + }, + { + "name": "taker", + "type": { + "option": "publicKey" + }, + "index": false + }, + { + "name": "takerOrderId", + "type": { + "option": "u32" + }, + "index": false + }, + { + "name": "takerOrderDirection", + "type": { + "option": { + "defined": "PositionDirection" + } + }, + "index": false + }, + { + "name": "takerOrderBaseAssetAmount", + "type": { + "option": "u64" + }, + "index": false + }, + { + "name": "takerOrderCumulativeBaseAssetAmountFilled", + "type": { + "option": "u64" + }, + "index": false + }, + { + "name": "takerOrderCumulativeQuoteAssetAmountFilled", + "type": { + "option": "u64" + }, + "index": false + }, + { + "name": "maker", + "type": { + "option": "publicKey" + }, + "index": false + }, + { + "name": "makerOrderId", + "type": { + "option": "u32" + }, + "index": false + }, + { + "name": "makerOrderDirection", + "type": { + "option": { + "defined": "PositionDirection" + } + }, + "index": false + }, + { + "name": "makerOrderBaseAssetAmount", + "type": { + "option": "u64" + }, + "index": false + }, + { + "name": "makerOrderCumulativeBaseAssetAmountFilled", + "type": { + "option": "u64" + }, + "index": false + }, + { + "name": "makerOrderCumulativeQuoteAssetAmountFilled", + "type": { + "option": "u64" + }, + "index": false + }, + { + "name": "oraclePrice", + "type": "i64", + "index": false + } + ] + }, + { + "name": "LPRecord", + "fields": [ + { + "name": "ts", + "type": "i64", + "index": false + }, + { + "name": "user", + "type": "publicKey", + "index": false + }, + { + "name": "action", + "type": { + "defined": "LPAction" + }, + "index": false + }, + { + "name": "nShares", + "type": "u64", + "index": false + }, + { + "name": "marketIndex", + "type": "u16", + "index": false + }, + { + "name": "deltaBaseAssetAmount", + "type": "i64", + "index": false + }, + { + "name": "deltaQuoteAssetAmount", + "type": "i64", + "index": false + }, + { + "name": "pnl", + "type": "i64", + "index": false + } + ] + }, + { + "name": "LiquidationRecord", + "fields": [ + { + "name": "ts", + "type": "i64", + "index": false + }, + { + "name": "liquidationType", + "type": { + "defined": "LiquidationType" + }, + "index": false + }, + { + "name": "user", + "type": "publicKey", + "index": false + }, + { + "name": "liquidator", + "type": "publicKey", + "index": false + }, + { + "name": "marginRequirement", + "type": "u128", + "index": false + }, + { + "name": "totalCollateral", + "type": "i128", + "index": false + }, + { + "name": "marginFreed", + "type": "u64", + "index": false + }, + { + "name": "liquidationId", + "type": "u16", + "index": false + }, + { + "name": "bankrupt", + "type": "bool", + "index": false + }, + { + "name": "canceledOrderIds", + "type": { + "vec": "u32" + }, + "index": false + }, + { + "name": "liquidatePerp", + "type": { + "defined": "LiquidatePerpRecord" + }, + "index": false + }, + { + "name": "liquidateSpot", + "type": { + "defined": "LiquidateSpotRecord" + }, + "index": false + }, + { + "name": "liquidateBorrowForPerpPnl", + "type": { + "defined": "LiquidateBorrowForPerpPnlRecord" + }, + "index": false + }, + { + "name": "liquidatePerpPnlForDeposit", + "type": { + "defined": "LiquidatePerpPnlForDepositRecord" + }, + "index": false + }, + { + "name": "perpBankruptcy", + "type": { + "defined": "PerpBankruptcyRecord" + }, + "index": false + }, + { + "name": "spotBankruptcy", + "type": { + "defined": "SpotBankruptcyRecord" + }, + "index": false + } + ] + }, + { + "name": "SettlePnlRecord", + "fields": [ + { + "name": "ts", + "type": "i64", + "index": false + }, + { + "name": "user", + "type": "publicKey", + "index": false + }, + { + "name": "marketIndex", + "type": "u16", + "index": false + }, + { + "name": "pnl", + "type": "i128", + "index": false + }, + { + "name": "baseAssetAmount", + "type": "i64", + "index": false + }, + { + "name": "quoteAssetAmountAfter", + "type": "i64", + "index": false + }, + { + "name": "quoteEntryAmount", + "type": "i64", + "index": false + }, + { + "name": "settlePrice", + "type": "i64", + "index": false + }, + { + "name": "explanation", + "type": { + "defined": "SettlePnlExplanation" + }, + "index": false + } + ] + }, + { + "name": "InsuranceFundRecord", + "fields": [ + { + "name": "ts", + "type": "i64", + "index": false + }, + { + "name": "spotMarketIndex", + "type": "u16", + "index": false + }, + { + "name": "perpMarketIndex", + "type": "u16", + "index": false + }, + { + "name": "userIfFactor", + "type": "u32", + "index": false + }, + { + "name": "totalIfFactor", + "type": "u32", + "index": false + }, + { + "name": "vaultAmountBefore", + "type": "u64", + "index": false + }, + { + "name": "insuranceVaultAmountBefore", + "type": "u64", + "index": false + }, + { + "name": "totalIfSharesBefore", + "type": "u128", + "index": false + }, + { + "name": "totalIfSharesAfter", + "type": "u128", + "index": false + }, + { + "name": "amount", + "type": "i64", + "index": false + } + ] + }, + { + "name": "InsuranceFundStakeRecord", + "fields": [ + { + "name": "ts", + "type": "i64", + "index": false + }, + { + "name": "userAuthority", + "type": "publicKey", + "index": false + }, + { + "name": "action", + "type": { + "defined": "StakeAction" + }, + "index": false + }, + { + "name": "amount", + "type": "u64", + "index": false + }, + { + "name": "marketIndex", + "type": "u16", + "index": false + }, + { + "name": "insuranceVaultAmountBefore", + "type": "u64", + "index": false + }, + { + "name": "ifSharesBefore", + "type": "u128", + "index": false + }, + { + "name": "userIfSharesBefore", + "type": "u128", + "index": false + }, + { + "name": "totalIfSharesBefore", + "type": "u128", + "index": false + }, + { + "name": "ifSharesAfter", + "type": "u128", + "index": false + }, + { + "name": "userIfSharesAfter", + "type": "u128", + "index": false + }, + { + "name": "totalIfSharesAfter", + "type": "u128", + "index": false + } + ] + }, + { + "name": "SwapRecord", + "fields": [ + { + "name": "ts", + "type": "i64", + "index": false + }, + { + "name": "user", + "type": "publicKey", + "index": false + }, + { + "name": "amountOut", + "type": "u64", + "index": false + }, + { + "name": "amountIn", + "type": "u64", + "index": false + }, + { + "name": "outMarketIndex", + "type": "u16", + "index": false + }, + { + "name": "inMarketIndex", + "type": "u16", + "index": false + }, + { + "name": "outOraclePrice", + "type": "i64", + "index": false + }, + { + "name": "inOraclePrice", + "type": "i64", + "index": false + }, + { + "name": "fee", + "type": "u64", + "index": false + } + ] + }, + { + "name": "SpotMarketVaultDepositRecord", + "fields": [ + { + "name": "ts", + "type": "i64", + "index": false + }, + { + "name": "marketIndex", + "type": "u16", + "index": false + }, + { + "name": "depositBalance", + "type": "u128", + "index": false + }, + { + "name": "cumulativeDepositInterestBefore", + "type": "u128", + "index": false + }, + { + "name": "cumulativeDepositInterestAfter", + "type": "u128", + "index": false + }, + { + "name": "depositTokenAmountBefore", + "type": "u64", + "index": false + }, + { + "name": "amount", + "type": "u64", + "index": false + } + ] + } + ], + "errors": [ + { + "code": 6000, + "name": "InvalidSpotMarketAuthority", + "msg": "Invalid Spot Market Authority" + }, + { + "code": 6001, + "name": "InvalidInsuranceFundAuthority", + "msg": "Clearing house not insurance fund authority" + }, + { + "code": 6002, + "name": "InsufficientDeposit", + "msg": "Insufficient deposit" + }, + { + "code": 6003, + "name": "InsufficientCollateral", + "msg": "Insufficient collateral" + }, + { + "code": 6004, + "name": "SufficientCollateral", + "msg": "Sufficient collateral" + }, + { + "code": 6005, + "name": "MaxNumberOfPositions", + "msg": "Max number of positions taken" + }, + { + "code": 6006, + "name": "AdminControlsPricesDisabled", + "msg": "Admin Controls Prices Disabled" + }, + { + "code": 6007, + "name": "MarketDelisted", + "msg": "Market Delisted" + }, + { + "code": 6008, + "name": "MarketIndexAlreadyInitialized", + "msg": "Market Index Already Initialized" + }, + { + "code": 6009, + "name": "UserAccountAndUserPositionsAccountMismatch", + "msg": "User Account And User Positions Account Mismatch" + }, + { + "code": 6010, + "name": "UserHasNoPositionInMarket", + "msg": "User Has No Position In Market" + }, + { + "code": 6011, + "name": "InvalidInitialPeg", + "msg": "Invalid Initial Peg" + }, + { + "code": 6012, + "name": "InvalidRepegRedundant", + "msg": "AMM repeg already configured with amt given" + }, + { + "code": 6013, + "name": "InvalidRepegDirection", + "msg": "AMM repeg incorrect repeg direction" + }, + { + "code": 6014, + "name": "InvalidRepegProfitability", + "msg": "AMM repeg out of bounds pnl" + }, + { + "code": 6015, + "name": "SlippageOutsideLimit", + "msg": "Slippage Outside Limit Price" + }, + { + "code": 6016, + "name": "OrderSizeTooSmall", + "msg": "Order Size Too Small" + }, + { + "code": 6017, + "name": "InvalidUpdateK", + "msg": "Price change too large when updating K" + }, + { + "code": 6018, + "name": "AdminWithdrawTooLarge", + "msg": "Admin tried to withdraw amount larger than fees collected" + }, + { + "code": 6019, + "name": "MathError", + "msg": "Math Error" + }, + { + "code": 6020, + "name": "BnConversionError", + "msg": "Conversion to u128/u64 failed with an overflow or underflow" + }, + { + "code": 6021, + "name": "ClockUnavailable", + "msg": "Clock unavailable" + }, + { + "code": 6022, + "name": "UnableToLoadOracle", + "msg": "Unable To Load Oracles" + }, + { + "code": 6023, + "name": "PriceBandsBreached", + "msg": "Price Bands Breached" + }, + { + "code": 6024, + "name": "ExchangePaused", + "msg": "Exchange is paused" + }, + { + "code": 6025, + "name": "InvalidWhitelistToken", + "msg": "Invalid whitelist token" + }, + { + "code": 6026, + "name": "WhitelistTokenNotFound", + "msg": "Whitelist token not found" + }, + { + "code": 6027, + "name": "InvalidDiscountToken", + "msg": "Invalid discount token" + }, + { + "code": 6028, + "name": "DiscountTokenNotFound", + "msg": "Discount token not found" + }, + { + "code": 6029, + "name": "ReferrerNotFound", + "msg": "Referrer not found" + }, + { + "code": 6030, + "name": "ReferrerStatsNotFound", + "msg": "ReferrerNotFound" + }, + { + "code": 6031, + "name": "ReferrerMustBeWritable", + "msg": "ReferrerMustBeWritable" + }, + { + "code": 6032, + "name": "ReferrerStatsMustBeWritable", + "msg": "ReferrerMustBeWritable" + }, + { + "code": 6033, + "name": "ReferrerAndReferrerStatsAuthorityUnequal", + "msg": "ReferrerAndReferrerStatsAuthorityUnequal" + }, + { + "code": 6034, + "name": "InvalidReferrer", + "msg": "InvalidReferrer" + }, + { + "code": 6035, + "name": "InvalidOracle", + "msg": "InvalidOracle" + }, + { + "code": 6036, + "name": "OracleNotFound", + "msg": "OracleNotFound" + }, + { + "code": 6037, + "name": "LiquidationsBlockedByOracle", + "msg": "Liquidations Blocked By Oracle" + }, + { + "code": 6038, + "name": "MaxDeposit", + "msg": "Can not deposit more than max deposit" + }, + { + "code": 6039, + "name": "CantDeleteUserWithCollateral", + "msg": "Can not delete user that still has collateral" + }, + { + "code": 6040, + "name": "InvalidFundingProfitability", + "msg": "AMM funding out of bounds pnl" + }, + { + "code": 6041, + "name": "CastingFailure", + "msg": "Casting Failure" + }, + { + "code": 6042, + "name": "InvalidOrder", + "msg": "InvalidOrder" + }, + { + "code": 6043, + "name": "InvalidOrderMaxTs", + "msg": "InvalidOrderMaxTs" + }, + { + "code": 6044, + "name": "InvalidOrderMarketType", + "msg": "InvalidOrderMarketType" + }, + { + "code": 6045, + "name": "InvalidOrderForInitialMarginReq", + "msg": "InvalidOrderForInitialMarginReq" + }, + { + "code": 6046, + "name": "InvalidOrderNotRiskReducing", + "msg": "InvalidOrderNotRiskReducing" + }, + { + "code": 6047, + "name": "InvalidOrderSizeTooSmall", + "msg": "InvalidOrderSizeTooSmall" + }, + { + "code": 6048, + "name": "InvalidOrderNotStepSizeMultiple", + "msg": "InvalidOrderNotStepSizeMultiple" + }, + { + "code": 6049, + "name": "InvalidOrderBaseQuoteAsset", + "msg": "InvalidOrderBaseQuoteAsset" + }, + { + "code": 6050, + "name": "InvalidOrderIOC", + "msg": "InvalidOrderIOC" + }, + { + "code": 6051, + "name": "InvalidOrderPostOnly", + "msg": "InvalidOrderPostOnly" + }, + { + "code": 6052, + "name": "InvalidOrderIOCPostOnly", + "msg": "InvalidOrderIOCPostOnly" + }, + { + "code": 6053, + "name": "InvalidOrderTrigger", + "msg": "InvalidOrderTrigger" + }, + { + "code": 6054, + "name": "InvalidOrderAuction", + "msg": "InvalidOrderAuction" + }, + { + "code": 6055, + "name": "InvalidOrderOracleOffset", + "msg": "InvalidOrderOracleOffset" + }, + { + "code": 6056, + "name": "InvalidOrderMinOrderSize", + "msg": "InvalidOrderMinOrderSize" + }, + { + "code": 6057, + "name": "PlacePostOnlyLimitFailure", + "msg": "Failed to Place Post-Only Limit Order" + }, + { + "code": 6058, + "name": "UserHasNoOrder", + "msg": "User has no order" + }, + { + "code": 6059, + "name": "OrderAmountTooSmall", + "msg": "Order Amount Too Small" + }, + { + "code": 6060, + "name": "MaxNumberOfOrders", + "msg": "Max number of orders taken" + }, + { + "code": 6061, + "name": "OrderDoesNotExist", + "msg": "Order does not exist" + }, + { + "code": 6062, + "name": "OrderNotOpen", + "msg": "Order not open" + }, + { + "code": 6063, + "name": "FillOrderDidNotUpdateState", + "msg": "FillOrderDidNotUpdateState" + }, + { + "code": 6064, + "name": "ReduceOnlyOrderIncreasedRisk", + "msg": "Reduce only order increased risk" + }, + { + "code": 6065, + "name": "UnableToLoadAccountLoader", + "msg": "Unable to load AccountLoader" + }, + { + "code": 6066, + "name": "TradeSizeTooLarge", + "msg": "Trade Size Too Large" + }, + { + "code": 6067, + "name": "UserCantReferThemselves", + "msg": "User cant refer themselves" + }, + { + "code": 6068, + "name": "DidNotReceiveExpectedReferrer", + "msg": "Did not receive expected referrer" + }, + { + "code": 6069, + "name": "CouldNotDeserializeReferrer", + "msg": "Could not deserialize referrer" + }, + { + "code": 6070, + "name": "CouldNotDeserializeReferrerStats", + "msg": "Could not deserialize referrer stats" + }, + { + "code": 6071, + "name": "UserOrderIdAlreadyInUse", + "msg": "User Order Id Already In Use" + }, + { + "code": 6072, + "name": "NoPositionsLiquidatable", + "msg": "No positions liquidatable" + }, + { + "code": 6073, + "name": "InvalidMarginRatio", + "msg": "Invalid Margin Ratio" + }, + { + "code": 6074, + "name": "CantCancelPostOnlyOrder", + "msg": "Cant Cancel Post Only Order" + }, + { + "code": 6075, + "name": "InvalidOracleOffset", + "msg": "InvalidOracleOffset" + }, + { + "code": 6076, + "name": "CantExpireOrders", + "msg": "CantExpireOrders" + }, + { + "code": 6077, + "name": "CouldNotLoadMarketData", + "msg": "CouldNotLoadMarketData" + }, + { + "code": 6078, + "name": "PerpMarketNotFound", + "msg": "PerpMarketNotFound" + }, + { + "code": 6079, + "name": "InvalidMarketAccount", + "msg": "InvalidMarketAccount" + }, + { + "code": 6080, + "name": "UnableToLoadPerpMarketAccount", + "msg": "UnableToLoadMarketAccount" + }, + { + "code": 6081, + "name": "MarketWrongMutability", + "msg": "MarketWrongMutability" + }, + { + "code": 6082, + "name": "UnableToCastUnixTime", + "msg": "UnableToCastUnixTime" + }, + { + "code": 6083, + "name": "CouldNotFindSpotPosition", + "msg": "CouldNotFindSpotPosition" + }, + { + "code": 6084, + "name": "NoSpotPositionAvailable", + "msg": "NoSpotPositionAvailable" + }, + { + "code": 6085, + "name": "InvalidSpotMarketInitialization", + "msg": "InvalidSpotMarketInitialization" + }, + { + "code": 6086, + "name": "CouldNotLoadSpotMarketData", + "msg": "CouldNotLoadSpotMarketData" + }, + { + "code": 6087, + "name": "SpotMarketNotFound", + "msg": "SpotMarketNotFound" + }, + { + "code": 6088, + "name": "InvalidSpotMarketAccount", + "msg": "InvalidSpotMarketAccount" + }, + { + "code": 6089, + "name": "UnableToLoadSpotMarketAccount", + "msg": "UnableToLoadSpotMarketAccount" + }, + { + "code": 6090, + "name": "SpotMarketWrongMutability", + "msg": "SpotMarketWrongMutability" + }, + { + "code": 6091, + "name": "SpotMarketInterestNotUpToDate", + "msg": "SpotInterestNotUpToDate" + }, + { + "code": 6092, + "name": "SpotMarketInsufficientDeposits", + "msg": "SpotMarketInsufficientDeposits" + }, + { + "code": 6093, + "name": "UserMustSettleTheirOwnPositiveUnsettledPNL", + "msg": "UserMustSettleTheirOwnPositiveUnsettledPNL" + }, + { + "code": 6094, + "name": "CantUpdatePoolBalanceType", + "msg": "CantUpdatePoolBalanceType" + }, + { + "code": 6095, + "name": "InsufficientCollateralForSettlingPNL", + "msg": "InsufficientCollateralForSettlingPNL" + }, + { + "code": 6096, + "name": "AMMNotUpdatedInSameSlot", + "msg": "AMMNotUpdatedInSameSlot" + }, + { + "code": 6097, + "name": "AuctionNotComplete", + "msg": "AuctionNotComplete" + }, + { + "code": 6098, + "name": "MakerNotFound", + "msg": "MakerNotFound" + }, + { + "code": 6099, + "name": "MakerStatsNotFound", + "msg": "MakerNotFound" + }, + { + "code": 6100, + "name": "MakerMustBeWritable", + "msg": "MakerMustBeWritable" + }, + { + "code": 6101, + "name": "MakerStatsMustBeWritable", + "msg": "MakerMustBeWritable" + }, + { + "code": 6102, + "name": "MakerOrderNotFound", + "msg": "MakerOrderNotFound" + }, + { + "code": 6103, + "name": "CouldNotDeserializeMaker", + "msg": "CouldNotDeserializeMaker" + }, + { + "code": 6104, + "name": "CouldNotDeserializeMakerStats", + "msg": "CouldNotDeserializeMaker" + }, + { + "code": 6105, + "name": "AuctionPriceDoesNotSatisfyMaker", + "msg": "AuctionPriceDoesNotSatisfyMaker" + }, + { + "code": 6106, + "name": "MakerCantFulfillOwnOrder", + "msg": "MakerCantFulfillOwnOrder" + }, + { + "code": 6107, + "name": "MakerOrderMustBePostOnly", + "msg": "MakerOrderMustBePostOnly" + }, + { + "code": 6108, + "name": "CantMatchTwoPostOnlys", + "msg": "CantMatchTwoPostOnlys" + }, + { + "code": 6109, + "name": "OrderBreachesOraclePriceLimits", + "msg": "OrderBreachesOraclePriceLimits" + }, + { + "code": 6110, + "name": "OrderMustBeTriggeredFirst", + "msg": "OrderMustBeTriggeredFirst" + }, + { + "code": 6111, + "name": "OrderNotTriggerable", + "msg": "OrderNotTriggerable" + }, + { + "code": 6112, + "name": "OrderDidNotSatisfyTriggerCondition", + "msg": "OrderDidNotSatisfyTriggerCondition" + }, + { + "code": 6113, + "name": "PositionAlreadyBeingLiquidated", + "msg": "PositionAlreadyBeingLiquidated" + }, + { + "code": 6114, + "name": "PositionDoesntHaveOpenPositionOrOrders", + "msg": "PositionDoesntHaveOpenPositionOrOrders" + }, + { + "code": 6115, + "name": "AllOrdersAreAlreadyLiquidations", + "msg": "AllOrdersAreAlreadyLiquidations" + }, + { + "code": 6116, + "name": "CantCancelLiquidationOrder", + "msg": "CantCancelLiquidationOrder" + }, + { + "code": 6117, + "name": "UserIsBeingLiquidated", + "msg": "UserIsBeingLiquidated" + }, + { + "code": 6118, + "name": "LiquidationsOngoing", + "msg": "LiquidationsOngoing" + }, + { + "code": 6119, + "name": "WrongSpotBalanceType", + "msg": "WrongSpotBalanceType" + }, + { + "code": 6120, + "name": "UserCantLiquidateThemself", + "msg": "UserCantLiquidateThemself" + }, + { + "code": 6121, + "name": "InvalidPerpPositionToLiquidate", + "msg": "InvalidPerpPositionToLiquidate" + }, + { + "code": 6122, + "name": "InvalidBaseAssetAmountForLiquidatePerp", + "msg": "InvalidBaseAssetAmountForLiquidatePerp" + }, + { + "code": 6123, + "name": "InvalidPositionLastFundingRate", + "msg": "InvalidPositionLastFundingRate" + }, + { + "code": 6124, + "name": "InvalidPositionDelta", + "msg": "InvalidPositionDelta" + }, + { + "code": 6125, + "name": "UserBankrupt", + "msg": "UserBankrupt" + }, + { + "code": 6126, + "name": "UserNotBankrupt", + "msg": "UserNotBankrupt" + }, + { + "code": 6127, + "name": "UserHasInvalidBorrow", + "msg": "UserHasInvalidBorrow" + }, + { + "code": 6128, + "name": "DailyWithdrawLimit", + "msg": "DailyWithdrawLimit" + }, + { + "code": 6129, + "name": "DefaultError", + "msg": "DefaultError" + }, + { + "code": 6130, + "name": "InsufficientLPTokens", + "msg": "Insufficient LP tokens" + }, + { + "code": 6131, + "name": "CantLPWithPerpPosition", + "msg": "Cant LP with a market position" + }, + { + "code": 6132, + "name": "UnableToBurnLPTokens", + "msg": "Unable to burn LP tokens" + }, + { + "code": 6133, + "name": "TryingToRemoveLiquidityTooFast", + "msg": "Trying to remove liqudity too fast after adding it" + }, + { + "code": 6134, + "name": "InvalidSpotMarketVault", + "msg": "Invalid Spot Market Vault" + }, + { + "code": 6135, + "name": "InvalidSpotMarketState", + "msg": "Invalid Spot Market State" + }, + { + "code": 6136, + "name": "InvalidSerumProgram", + "msg": "InvalidSerumProgram" + }, + { + "code": 6137, + "name": "InvalidSerumMarket", + "msg": "InvalidSerumMarket" + }, + { + "code": 6138, + "name": "InvalidSerumBids", + "msg": "InvalidSerumBids" + }, + { + "code": 6139, + "name": "InvalidSerumAsks", + "msg": "InvalidSerumAsks" + }, + { + "code": 6140, + "name": "InvalidSerumOpenOrders", + "msg": "InvalidSerumOpenOrders" + }, + { + "code": 6141, + "name": "FailedSerumCPI", + "msg": "FailedSerumCPI" + }, + { + "code": 6142, + "name": "FailedToFillOnExternalMarket", + "msg": "FailedToFillOnExternalMarket" + }, + { + "code": 6143, + "name": "InvalidFulfillmentConfig", + "msg": "InvalidFulfillmentConfig" + }, + { + "code": 6144, + "name": "InvalidFeeStructure", + "msg": "InvalidFeeStructure" + }, + { + "code": 6145, + "name": "InsufficientIFShares", + "msg": "Insufficient IF shares" + }, + { + "code": 6146, + "name": "MarketActionPaused", + "msg": "the Market has paused this action" + }, + { + "code": 6147, + "name": "MarketPlaceOrderPaused", + "msg": "the Market status doesnt allow placing orders" + }, + { + "code": 6148, + "name": "MarketFillOrderPaused", + "msg": "the Market status doesnt allow filling orders" + }, + { + "code": 6149, + "name": "MarketWithdrawPaused", + "msg": "the Market status doesnt allow withdraws" + }, + { + "code": 6150, + "name": "ProtectedAssetTierViolation", + "msg": "Action violates the Protected Asset Tier rules" + }, + { + "code": 6151, + "name": "IsolatedAssetTierViolation", + "msg": "Action violates the Isolated Asset Tier rules" + }, + { + "code": 6152, + "name": "UserCantBeDeleted", + "msg": "User Cant Be Deleted" + }, + { + "code": 6153, + "name": "ReduceOnlyWithdrawIncreasedRisk", + "msg": "Reduce Only Withdraw Increased Risk" + }, + { + "code": 6154, + "name": "MaxOpenInterest", + "msg": "Max Open Interest" + }, + { + "code": 6155, + "name": "CantResolvePerpBankruptcy", + "msg": "Cant Resolve Perp Bankruptcy" + }, + { + "code": 6156, + "name": "LiquidationDoesntSatisfyLimitPrice", + "msg": "Liquidation Doesnt Satisfy Limit Price" + }, + { + "code": 6157, + "name": "MarginTradingDisabled", + "msg": "Margin Trading Disabled" + }, + { + "code": 6158, + "name": "InvalidMarketStatusToSettlePnl", + "msg": "Invalid Market Status to Settle Perp Pnl" + }, + { + "code": 6159, + "name": "PerpMarketNotInSettlement", + "msg": "PerpMarketNotInSettlement" + }, + { + "code": 6160, + "name": "PerpMarketNotInReduceOnly", + "msg": "PerpMarketNotInReduceOnly" + }, + { + "code": 6161, + "name": "PerpMarketSettlementBufferNotReached", + "msg": "PerpMarketSettlementBufferNotReached" + }, + { + "code": 6162, + "name": "PerpMarketSettlementUserHasOpenOrders", + "msg": "PerpMarketSettlementUserHasOpenOrders" + }, + { + "code": 6163, + "name": "PerpMarketSettlementUserHasActiveLP", + "msg": "PerpMarketSettlementUserHasActiveLP" + }, + { + "code": 6164, + "name": "UnableToSettleExpiredUserPosition", + "msg": "UnableToSettleExpiredUserPosition" + }, + { + "code": 6165, + "name": "UnequalMarketIndexForSpotTransfer", + "msg": "UnequalMarketIndexForSpotTransfer" + }, + { + "code": 6166, + "name": "InvalidPerpPositionDetected", + "msg": "InvalidPerpPositionDetected" + }, + { + "code": 6167, + "name": "InvalidSpotPositionDetected", + "msg": "InvalidSpotPositionDetected" + }, + { + "code": 6168, + "name": "InvalidAmmDetected", + "msg": "InvalidAmmDetected" + }, + { + "code": 6169, + "name": "InvalidAmmForFillDetected", + "msg": "InvalidAmmForFillDetected" + }, + { + "code": 6170, + "name": "InvalidAmmLimitPriceOverride", + "msg": "InvalidAmmLimitPriceOverride" + }, + { + "code": 6171, + "name": "InvalidOrderFillPrice", + "msg": "InvalidOrderFillPrice" + }, + { + "code": 6172, + "name": "SpotMarketBalanceInvariantViolated", + "msg": "SpotMarketBalanceInvariantViolated" + }, + { + "code": 6173, + "name": "SpotMarketVaultInvariantViolated", + "msg": "SpotMarketVaultInvariantViolated" + }, + { + "code": 6174, + "name": "InvalidPDA", + "msg": "InvalidPDA" + }, + { + "code": 6175, + "name": "InvalidPDASigner", + "msg": "InvalidPDASigner" + }, + { + "code": 6176, + "name": "RevenueSettingsCannotSettleToIF", + "msg": "RevenueSettingsCannotSettleToIF" + }, + { + "code": 6177, + "name": "NoRevenueToSettleToIF", + "msg": "NoRevenueToSettleToIF" + }, + { + "code": 6178, + "name": "NoAmmPerpPnlDeficit", + "msg": "NoAmmPerpPnlDeficit" + }, + { + "code": 6179, + "name": "SufficientPerpPnlPool", + "msg": "SufficientPerpPnlPool" + }, + { + "code": 6180, + "name": "InsufficientPerpPnlPool", + "msg": "InsufficientPerpPnlPool" + }, + { + "code": 6181, + "name": "PerpPnlDeficitBelowThreshold", + "msg": "PerpPnlDeficitBelowThreshold" + }, + { + "code": 6182, + "name": "MaxRevenueWithdrawPerPeriodReached", + "msg": "MaxRevenueWithdrawPerPeriodReached" + }, + { + "code": 6183, + "name": "MaxIFWithdrawReached", + "msg": "InvalidSpotPositionDetected" + }, + { + "code": 6184, + "name": "NoIFWithdrawAvailable", + "msg": "NoIFWithdrawAvailable" + }, + { + "code": 6185, + "name": "InvalidIFUnstake", + "msg": "InvalidIFUnstake" + }, + { + "code": 6186, + "name": "InvalidIFUnstakeSize", + "msg": "InvalidIFUnstakeSize" + }, + { + "code": 6187, + "name": "InvalidIFUnstakeCancel", + "msg": "InvalidIFUnstakeCancel" + }, + { + "code": 6188, + "name": "InvalidIFForNewStakes", + "msg": "InvalidIFForNewStakes" + }, + { + "code": 6189, + "name": "InvalidIFRebase", + "msg": "InvalidIFRebase" + }, + { + "code": 6190, + "name": "InvalidInsuranceUnstakeSize", + "msg": "InvalidInsuranceUnstakeSize" + }, + { + "code": 6191, + "name": "InvalidOrderLimitPrice", + "msg": "InvalidOrderLimitPrice" + }, + { + "code": 6192, + "name": "InvalidIFDetected", + "msg": "InvalidIFDetected" + }, + { + "code": 6193, + "name": "InvalidAmmMaxSpreadDetected", + "msg": "InvalidAmmMaxSpreadDetected" + }, + { + "code": 6194, + "name": "InvalidConcentrationCoef", + "msg": "InvalidConcentrationCoef" + }, + { + "code": 6195, + "name": "InvalidSrmVault", + "msg": "InvalidSrmVault" + }, + { + "code": 6196, + "name": "InvalidVaultOwner", + "msg": "InvalidVaultOwner" + }, + { + "code": 6197, + "name": "InvalidMarketStatusForFills", + "msg": "InvalidMarketStatusForFills" + }, + { + "code": 6198, + "name": "IFWithdrawRequestInProgress", + "msg": "IFWithdrawRequestInProgress" + }, + { + "code": 6199, + "name": "NoIFWithdrawRequestInProgress", + "msg": "NoIFWithdrawRequestInProgress" + }, + { + "code": 6200, + "name": "IFWithdrawRequestTooSmall", + "msg": "IFWithdrawRequestTooSmall" + }, + { + "code": 6201, + "name": "IncorrectSpotMarketAccountPassed", + "msg": "IncorrectSpotMarketAccountPassed" + }, + { + "code": 6202, + "name": "BlockchainClockInconsistency", + "msg": "BlockchainClockInconsistency" + }, + { + "code": 6203, + "name": "InvalidIFSharesDetected", + "msg": "InvalidIFSharesDetected" + }, + { + "code": 6204, + "name": "NewLPSizeTooSmall", + "msg": "NewLPSizeTooSmall" + }, + { + "code": 6205, + "name": "MarketStatusInvalidForNewLP", + "msg": "MarketStatusInvalidForNewLP" + }, + { + "code": 6206, + "name": "InvalidMarkTwapUpdateDetected", + "msg": "InvalidMarkTwapUpdateDetected" + }, + { + "code": 6207, + "name": "MarketSettlementAttemptOnActiveMarket", + "msg": "MarketSettlementAttemptOnActiveMarket" + }, + { + "code": 6208, + "name": "MarketSettlementRequiresSettledLP", + "msg": "MarketSettlementRequiresSettledLP" + }, + { + "code": 6209, + "name": "MarketSettlementAttemptTooEarly", + "msg": "MarketSettlementAttemptTooEarly" + }, + { + "code": 6210, + "name": "MarketSettlementTargetPriceInvalid", + "msg": "MarketSettlementTargetPriceInvalid" + }, + { + "code": 6211, + "name": "UnsupportedSpotMarket", + "msg": "UnsupportedSpotMarket" + }, + { + "code": 6212, + "name": "SpotOrdersDisabled", + "msg": "SpotOrdersDisabled" + }, + { + "code": 6213, + "name": "MarketBeingInitialized", + "msg": "Market Being Initialized" + }, + { + "code": 6214, + "name": "InvalidUserSubAccountId", + "msg": "Invalid Sub Account Id" + }, + { + "code": 6215, + "name": "InvalidTriggerOrderCondition", + "msg": "Invalid Trigger Order Condition" + }, + { + "code": 6216, + "name": "InvalidSpotPosition", + "msg": "Invalid Spot Position" + }, + { + "code": 6217, + "name": "CantTransferBetweenSameUserAccount", + "msg": "Cant transfer between same user account" + }, + { + "code": 6218, + "name": "InvalidPerpPosition", + "msg": "Invalid Perp Position" + }, + { + "code": 6219, + "name": "UnableToGetLimitPrice", + "msg": "Unable To Get Limit Price" + }, + { + "code": 6220, + "name": "InvalidLiquidation", + "msg": "Invalid Liquidation" + }, + { + "code": 6221, + "name": "SpotFulfillmentConfigDisabled", + "msg": "Spot Fulfillment Config Disabled" + }, + { + "code": 6222, + "name": "InvalidMaker", + "msg": "Invalid Maker" + }, + { + "code": 6223, + "name": "FailedUnwrap", + "msg": "Failed Unwrap" + }, + { + "code": 6224, + "name": "MaxNumberOfUsers", + "msg": "Max Number Of Users" + }, + { + "code": 6225, + "name": "InvalidOracleForSettlePnl", + "msg": "InvalidOracleForSettlePnl" + }, + { + "code": 6226, + "name": "MarginOrdersOpen", + "msg": "MarginOrdersOpen" + }, + { + "code": 6227, + "name": "TierViolationLiquidatingPerpPnl", + "msg": "TierViolationLiquidatingPerpPnl" + }, + { + "code": 6228, + "name": "CouldNotLoadUserData", + "msg": "CouldNotLoadUserData" + }, + { + "code": 6229, + "name": "UserWrongMutability", + "msg": "UserWrongMutability" + }, + { + "code": 6230, + "name": "InvalidUserAccount", + "msg": "InvalidUserAccount" + }, + { + "code": 6231, + "name": "CouldNotLoadUserStatsData", + "msg": "CouldNotLoadUserData" + }, + { + "code": 6232, + "name": "UserStatsWrongMutability", + "msg": "UserWrongMutability" + }, + { + "code": 6233, + "name": "InvalidUserStatsAccount", + "msg": "InvalidUserAccount" + }, + { + "code": 6234, + "name": "UserNotFound", + "msg": "UserNotFound" + }, + { + "code": 6235, + "name": "UnableToLoadUserAccount", + "msg": "UnableToLoadUserAccount" + }, + { + "code": 6236, + "name": "UserStatsNotFound", + "msg": "UserStatsNotFound" + }, + { + "code": 6237, + "name": "UnableToLoadUserStatsAccount", + "msg": "UnableToLoadUserStatsAccount" + }, + { + "code": 6238, + "name": "UserNotInactive", + "msg": "User Not Inactive" + }, + { + "code": 6239, + "name": "RevertFill", + "msg": "RevertFill" + }, + { + "code": 6240, + "name": "InvalidMarketAccountforDeletion", + "msg": "Invalid MarketAccount for Deletion" + }, + { + "code": 6241, + "name": "InvalidSpotFulfillmentParams", + "msg": "Invalid Spot Fulfillment Params" + }, + { + "code": 6242, + "name": "FailedToGetMint", + "msg": "Failed to Get Mint" + }, + { + "code": 6243, + "name": "FailedPhoenixCPI", + "msg": "FailedPhoenixCPI" + }, + { + "code": 6244, + "name": "FailedToDeserializePhoenixMarket", + "msg": "FailedToDeserializePhoenixMarket" + }, + { + "code": 6245, + "name": "InvalidPricePrecision", + "msg": "InvalidPricePrecision" + }, + { + "code": 6246, + "name": "InvalidPhoenixProgram", + "msg": "InvalidPhoenixProgram" + }, + { + "code": 6247, + "name": "InvalidPhoenixMarket", + "msg": "InvalidPhoenixMarket" + }, + { + "code": 6248, + "name": "InvalidSwap", + "msg": "InvalidSwap" + }, + { + "code": 6249, + "name": "SwapLimitPriceBreached", + "msg": "SwapLimitPriceBreached" + }, + { + "code": 6250, + "name": "SpotMarketReduceOnly", + "msg": "SpotMarketReduceOnly" + }, + { + "code": 6251, + "name": "FundingWasNotUpdated", + "msg": "FundingWasNotUpdated" + }, + { + "code": 6252, + "name": "ImpossibleFill", + "msg": "ImpossibleFill" + }, + { + "code": 6253, + "name": "CantUpdatePerpBidAskTwap", + "msg": "CantUpdatePerpBidAskTwap" + }, + { + "code": 6254, + "name": "UserReduceOnly", + "msg": "UserReduceOnly" + }, + { + "code": 6255, + "name": "InvalidMarginCalculation", + "msg": "InvalidMarginCalculation" + }, + { + "code": 6256, + "name": "CantPayUserInitFee", + "msg": "CantPayUserInitFee" + }, + { + "code": 6257, + "name": "CantReclaimRent", + "msg": "CantReclaimRent" + }, + { + "code": 6258, + "name": "InsuranceFundOperationPaused", + "msg": "InsuranceFundOperationPaused" + }, + { + "code": 6259, + "name": "NoUnsettledPnl", + "msg": "NoUnsettledPnl" + }, + { + "code": 6260, + "name": "PnlPoolCantSettleUser", + "msg": "PnlPoolCantSettleUser" + }, + { + "code": 6261, + "name": "OracleNonPositive", + "msg": "OracleInvalid" + }, + { + "code": 6262, + "name": "OracleTooVolatile", + "msg": "OracleTooVolatile" + }, + { + "code": 6263, + "name": "OracleTooUncertain", + "msg": "OracleTooUncertain" + }, + { + "code": 6264, + "name": "OracleStaleForMargin", + "msg": "OracleStaleForMargin" + }, + { + "code": 6265, + "name": "OracleInsufficientDataPoints", + "msg": "OracleInsufficientDataPoints" + }, + { + "code": 6266, + "name": "OracleStaleForAMM", + "msg": "OracleStaleForAMM" + }, + { + "code": 6267, + "name": "UnableToParsePullOracleMessage", + "msg": "Unable to parse pull oracle message" + }, + { + "code": 6268, + "name": "MaxBorrows", + "msg": "Can not borow more than max borrows" + }, + { + "code": 6269, + "name": "OracleUpdatesNotMonotonic", + "msg": "Updates must be monotonically increasing" + }, + { + "code": 6270, + "name": "OraclePriceFeedMessageMismatch", + "msg": "Trying to update price feed with the wrong feed id" + }, + { + "code": 6271, + "name": "OracleUnsupportedMessageType", + "msg": "The message in the update must be a PriceFeedMessage" + }, + { + "code": 6272, + "name": "OracleDeserializeMessageFailed", + "msg": "Could not deserialize the message in the update" + }, + { + "code": 6273, + "name": "OracleWrongGuardianSetOwner", + "msg": "Wrong guardian set owner in update price atomic" + }, + { + "code": 6274, + "name": "OracleWrongWriteAuthority", + "msg": "Oracle post update atomic price feed account must be drift program" + }, + { + "code": 6275, + "name": "OracleWrongVaaOwner", + "msg": "Oracle vaa owner must be wormhole program" + }, + { + "code": 6276, + "name": "OracleTooManyPriceAccountUpdates", + "msg": "Multi updates must have 2 or fewer accounts passed in remaining accounts" + }, + { + "code": 6277, + "name": "OracleMismatchedVaaAndPriceUpdates", + "msg": "Don't have the same remaining accounts number and merkle price updates left" + }, + { + "code": 6278, + "name": "OracleBadRemainingAccountPublicKey", + "msg": "Remaining account passed is not a valid pda" + }, + { + "code": 6279, + "name": "FailedOpenbookV2CPI", + "msg": "FailedOpenbookV2CPI" + }, + { + "code": 6280, + "name": "InvalidOpenbookV2Program", + "msg": "InvalidOpenbookV2Program" + }, + { + "code": 6281, + "name": "InvalidOpenbookV2Market", + "msg": "InvalidOpenbookV2Market" + }, + { + "code": 6282, + "name": "NonZeroTransferFee", + "msg": "Non zero transfer fee" + }, + { + "code": 6283, + "name": "LiquidationOrderFailedToFill", + "msg": "Liquidation order failed to fill" + }, + { + "code": 6284, + "name": "InvalidPredictionMarketOrder", + "msg": "Invalid prediction market order" + } + ] +} \ No newline at end of file diff --git a/DriftStakeVoterPlugin/idl/driftStakeVoter.ts b/DriftStakeVoterPlugin/idl/driftStakeVoter.ts new file mode 100644 index 0000000000..61496132c7 --- /dev/null +++ b/DriftStakeVoterPlugin/idl/driftStakeVoter.ts @@ -0,0 +1,745 @@ +export type DriftStakeVoter = { + version: '0.0.1' + name: 'drift_stake_voter' + instructions: [ + { + name: 'createRegistrar' + accounts: [ + { + name: 'registrar' + isMut: true + isSigner: false + docs: [ + 'There can only be a single registrar per Realm and governing mint of the Realm' + ] + }, + { + name: 'governanceProgramId' + isMut: false + isSigner: false + docs: [ + 'The program id of the spl-governance program the realm belongs to' + ] + }, + { + name: 'driftProgramId' + isMut: false + isSigner: false + }, + { + name: 'realm' + isMut: false + isSigner: false + docs: [ + 'An spl-governance Realm', + '', + 'Realm is validated in the instruction:', + '- Realm is owned by the governance_program_id', + '- governing_token_mint must be the community or council mint', + '- realm_authority is realm.authority' + ] + }, + { + name: 'governingTokenMint' + isMut: false + isSigner: false + docs: [ + 'Either the realm community mint or the council mint.', + 'It must match Realm.community_mint or Realm.config.council_mint', + '', + 'Note: Once the Realm voter plugin is enabled the governing_token_mint is used only as identity', + 'for the voting population and the tokens of that are no longer used' + ] + }, + { + name: 'realmAuthority' + isMut: false + isSigner: true + docs: ['realm_authority must sign and match Realm.authority'] + }, + { + name: 'payer' + isMut: true + isSigner: true + }, + { + name: 'systemProgram' + isMut: false + isSigner: false + } + ] + args: [ + { + name: 'spotMarketIndex' + type: 'u16' + } + ] + }, + { + name: 'createVoterWeightRecord' + accounts: [ + { + name: 'registrar' + isMut: false + isSigner: false + }, + { + name: 'voterWeightRecord' + isMut: true + isSigner: false + }, + { + name: 'payer' + isMut: true + isSigner: true + }, + { + name: 'systemProgram' + isMut: false + isSigner: false + } + ] + args: [ + { + name: 'governingTokenOwner' + type: 'publicKey' + } + ] + }, + { + name: 'updateVoterWeightRecord' + accounts: [ + { + name: 'registrar' + isMut: false + isSigner: false + }, + { + name: 'voterWeightRecord' + isMut: true + isSigner: false + }, + { + name: 'tokenOwnerRecord' + isMut: false + isSigner: false + docs: [ + 'TokenOwnerRecord for any of the configured spl-governance instances' + ] + }, + { + name: 'spotMarket' + isMut: false + isSigner: false + isOptional: true + }, + { + name: 'insuranceFundVault' + isMut: false + isSigner: false + isOptional: true + }, + { + name: 'insuranceFundStake' + isMut: false + isSigner: false + isOptional: true + }, + { + name: 'driftProgram' + isMut: false + isSigner: false + } + ] + args: [] + } + ] + accounts: [ + { + name: 'registrar' + docs: [ + 'Registrar which stores spl-governance configurations for the given Realm' + ] + type: { + kind: 'struct' + fields: [ + { + name: 'governanceProgramId' + docs: ['spl-governance program the Realm belongs to'] + type: 'publicKey' + }, + { + name: 'realm' + docs: ['Realm of the Registrar'] + type: 'publicKey' + }, + { + name: 'governingTokenMint' + docs: [ + 'Governing token mint the Registrar is for', + 'It can either be the Community or the Council mint of the Realm', + 'When the plugin is enabled the mint is only used as the identity of the governing power (voting population)', + 'and the actual token of the mint is not used' + ] + type: 'publicKey' + }, + { + name: 'driftProgramId' + type: 'publicKey' + }, + { + name: 'spotMarketIndex' + type: 'u16' + } + ] + } + }, + { + name: 'voterWeightRecord' + docs: [ + 'VoterWeightRecord account as defined in spl-governance-addin-api', + "It's redefined here without account_discriminator for Anchor to treat it as native account", + '', + 'The account is used as an api interface to provide voting power to the governance program from external addin contracts' + ] + type: { + kind: 'struct' + fields: [ + { + name: 'realm' + docs: ['The Realm the VoterWeightRecord belongs to'] + type: 'publicKey' + }, + { + name: 'governingTokenMint' + docs: [ + 'Governing Token Mint the VoterWeightRecord is associated with', + 'Note: The addin can take deposits of any tokens and is not restricted to the community or council tokens only' + ] + type: 'publicKey' + }, + { + name: 'governingTokenOwner' + docs: [ + 'The owner of the governing token and voter', + 'This is the actual owner (voter) and corresponds to TokenOwnerRecord.governing_token_owner' + ] + type: 'publicKey' + }, + { + name: 'voterWeight' + docs: [ + "Voter's weight", + 'The weight of the voter provided by the addin for the given realm, governing_token_mint and governing_token_owner (voter)' + ] + type: 'u64' + }, + { + name: 'voterWeightExpiry' + docs: [ + 'The slot when the voting weight expires', + 'It should be set to None if the weight never expires', + 'If the voter weight decays with time, for example for time locked based weights, then the expiry must be set', + 'As a common pattern Revise instruction to update the weight should be invoked before governance instruction within the same transaction', + 'and the expiry set to the current slot to provide up to date weight' + ] + type: { + option: 'u64' + } + }, + { + name: 'weightAction' + docs: [ + "The governance action the voter's weight pertains to", + "It allows to provided voter's weight specific to the particular action the weight is evaluated for", + 'When the action is provided then the governance program asserts the executing action is the same as specified by the addin' + ] + type: { + option: { + defined: 'VoterWeightAction' + } + } + }, + { + name: 'weightActionTarget' + docs: [ + "The target the voter's weight action pertains to", + "It allows to provided voter's weight specific to the target the weight is evaluated for", + 'For example when addin supplies weight to vote on a particular proposal then it must specify the proposal as the action target', + 'When the target is provided then the governance program asserts the target is the same as specified by the addin' + ] + type: { + option: 'publicKey' + } + }, + { + name: 'reserved' + docs: ['Reserved space for future versions'] + type: { + array: ['u8', 8] + } + } + ] + } + } + ] + types: [ + { + name: 'CollectionItemChangeType' + docs: ['Enum defining collection item change type'] + type: { + kind: 'enum' + variants: [ + { + name: 'Upsert' + }, + { + name: 'Remove' + } + ] + } + }, + { + name: 'VoterWeightAction' + docs: [ + 'VoterWeightAction enum as defined in spl-governance-addin-api', + "It's redefined here for Anchor to export it to IDL" + ] + type: { + kind: 'enum' + variants: [ + { + name: 'CastVote' + }, + { + name: 'CommentProposal' + }, + { + name: 'CreateGovernance' + }, + { + name: 'CreateProposal' + }, + { + name: 'SignOffProposal' + } + ] + } + } + ] + errors: [ + { + code: 6000 + name: 'InvalidRealmAuthority' + msg: 'Invalid Realm Authority' + }, + { + code: 6001 + name: 'InvalidRealmForRegistrar' + msg: 'Invalid Realm for Registrar' + }, + { + code: 6002 + name: 'InvalidVoterWeightRecordRealm' + msg: 'Invalid VoterWeightRecord Realm' + }, + { + code: 6003 + name: 'InvalidVoterWeightRecordMint' + msg: 'Invalid VoterWeightRecord Mint' + }, + { + code: 6004 + name: 'TokenOwnerRecordFromOwnRealmNotAllowed' + msg: 'TokenOwnerRecord from own realm is not allowed' + }, + { + code: 6005 + name: 'GovernanceProgramNotConfigured' + msg: 'Governance program not configured' + }, + { + code: 6006 + name: 'GoverningTokenOwnerMustMatch' + msg: 'Governing TokenOwner must match' + }, + { + code: 6007 + name: 'DriftError' + msg: 'DriftError' + } + ] +} + +export const IDL: DriftStakeVoter = { + version: '0.0.1', + name: 'drift_stake_voter', + instructions: [ + { + name: 'createRegistrar', + accounts: [ + { + name: 'registrar', + isMut: true, + isSigner: false, + docs: [ + 'There can only be a single registrar per Realm and governing mint of the Realm', + ], + }, + { + name: 'governanceProgramId', + isMut: false, + isSigner: false, + docs: [ + 'The program id of the spl-governance program the realm belongs to', + ], + }, + { + name: 'driftProgramId', + isMut: false, + isSigner: false, + }, + { + name: 'realm', + isMut: false, + isSigner: false, + docs: [ + 'An spl-governance Realm', + '', + 'Realm is validated in the instruction:', + '- Realm is owned by the governance_program_id', + '- governing_token_mint must be the community or council mint', + '- realm_authority is realm.authority', + ], + }, + { + name: 'governingTokenMint', + isMut: false, + isSigner: false, + docs: [ + 'Either the realm community mint or the council mint.', + 'It must match Realm.community_mint or Realm.config.council_mint', + '', + 'Note: Once the Realm voter plugin is enabled the governing_token_mint is used only as identity', + 'for the voting population and the tokens of that are no longer used', + ], + }, + { + name: 'realmAuthority', + isMut: false, + isSigner: true, + docs: ['realm_authority must sign and match Realm.authority'], + }, + { + name: 'payer', + isMut: true, + isSigner: true, + }, + { + name: 'systemProgram', + isMut: false, + isSigner: false, + }, + ], + args: [ + { + name: 'spotMarketIndex', + type: 'u16', + }, + ], + }, + { + name: 'createVoterWeightRecord', + accounts: [ + { + name: 'registrar', + isMut: false, + isSigner: false, + }, + { + name: 'voterWeightRecord', + isMut: true, + isSigner: false, + }, + { + name: 'payer', + isMut: true, + isSigner: true, + }, + { + name: 'systemProgram', + isMut: false, + isSigner: false, + }, + ], + args: [ + { + name: 'governingTokenOwner', + type: 'publicKey', + }, + ], + }, + { + name: 'updateVoterWeightRecord', + accounts: [ + { + name: 'registrar', + isMut: false, + isSigner: false, + }, + { + name: 'voterWeightRecord', + isMut: true, + isSigner: false, + }, + { + name: 'tokenOwnerRecord', + isMut: false, + isSigner: false, + docs: [ + 'TokenOwnerRecord for any of the configured spl-governance instances', + ], + }, + { + name: 'spotMarket', + isMut: false, + isSigner: false, + isOptional: true, + }, + { + name: 'insuranceFundVault', + isMut: false, + isSigner: false, + isOptional: true, + }, + { + name: 'insuranceFundStake', + isMut: false, + isSigner: false, + isOptional: true, + }, + { + name: 'driftProgram', + isMut: false, + isSigner: false, + }, + ], + args: [], + }, + ], + accounts: [ + { + name: 'registrar', + docs: [ + 'Registrar which stores spl-governance configurations for the given Realm', + ], + type: { + kind: 'struct', + fields: [ + { + name: 'governanceProgramId', + docs: ['spl-governance program the Realm belongs to'], + type: 'publicKey', + }, + { + name: 'realm', + docs: ['Realm of the Registrar'], + type: 'publicKey', + }, + { + name: 'governingTokenMint', + docs: [ + 'Governing token mint the Registrar is for', + 'It can either be the Community or the Council mint of the Realm', + 'When the plugin is enabled the mint is only used as the identity of the governing power (voting population)', + 'and the actual token of the mint is not used', + ], + type: 'publicKey', + }, + { + name: 'driftProgramId', + type: 'publicKey', + }, + { + name: 'spotMarketIndex', + type: 'u16', + }, + ], + }, + }, + { + name: 'voterWeightRecord', + docs: [ + 'VoterWeightRecord account as defined in spl-governance-addin-api', + "It's redefined here without account_discriminator for Anchor to treat it as native account", + '', + 'The account is used as an api interface to provide voting power to the governance program from external addin contracts', + ], + type: { + kind: 'struct', + fields: [ + { + name: 'realm', + docs: ['The Realm the VoterWeightRecord belongs to'], + type: 'publicKey', + }, + { + name: 'governingTokenMint', + docs: [ + 'Governing Token Mint the VoterWeightRecord is associated with', + 'Note: The addin can take deposits of any tokens and is not restricted to the community or council tokens only', + ], + type: 'publicKey', + }, + { + name: 'governingTokenOwner', + docs: [ + 'The owner of the governing token and voter', + 'This is the actual owner (voter) and corresponds to TokenOwnerRecord.governing_token_owner', + ], + type: 'publicKey', + }, + { + name: 'voterWeight', + docs: [ + "Voter's weight", + 'The weight of the voter provided by the addin for the given realm, governing_token_mint and governing_token_owner (voter)', + ], + type: 'u64', + }, + { + name: 'voterWeightExpiry', + docs: [ + 'The slot when the voting weight expires', + 'It should be set to None if the weight never expires', + 'If the voter weight decays with time, for example for time locked based weights, then the expiry must be set', + 'As a common pattern Revise instruction to update the weight should be invoked before governance instruction within the same transaction', + 'and the expiry set to the current slot to provide up to date weight', + ], + type: { + option: 'u64', + }, + }, + { + name: 'weightAction', + docs: [ + "The governance action the voter's weight pertains to", + "It allows to provided voter's weight specific to the particular action the weight is evaluated for", + 'When the action is provided then the governance program asserts the executing action is the same as specified by the addin', + ], + type: { + option: { + defined: 'VoterWeightAction', + }, + }, + }, + { + name: 'weightActionTarget', + docs: [ + "The target the voter's weight action pertains to", + "It allows to provided voter's weight specific to the target the weight is evaluated for", + 'For example when addin supplies weight to vote on a particular proposal then it must specify the proposal as the action target', + 'When the target is provided then the governance program asserts the target is the same as specified by the addin', + ], + type: { + option: 'publicKey', + }, + }, + { + name: 'reserved', + docs: ['Reserved space for future versions'], + type: { + array: ['u8', 8], + }, + }, + ], + }, + }, + ], + types: [ + { + name: 'CollectionItemChangeType', + docs: ['Enum defining collection item change type'], + type: { + kind: 'enum', + variants: [ + { + name: 'Upsert', + }, + { + name: 'Remove', + }, + ], + }, + }, + { + name: 'VoterWeightAction', + docs: [ + 'VoterWeightAction enum as defined in spl-governance-addin-api', + "It's redefined here for Anchor to export it to IDL", + ], + type: { + kind: 'enum', + variants: [ + { + name: 'CastVote', + }, + { + name: 'CommentProposal', + }, + { + name: 'CreateGovernance', + }, + { + name: 'CreateProposal', + }, + { + name: 'SignOffProposal', + }, + ], + }, + }, + ], + errors: [ + { + code: 6000, + name: 'InvalidRealmAuthority', + msg: 'Invalid Realm Authority', + }, + { + code: 6001, + name: 'InvalidRealmForRegistrar', + msg: 'Invalid Realm for Registrar', + }, + { + code: 6002, + name: 'InvalidVoterWeightRecordRealm', + msg: 'Invalid VoterWeightRecord Realm', + }, + { + code: 6003, + name: 'InvalidVoterWeightRecordMint', + msg: 'Invalid VoterWeightRecord Mint', + }, + { + code: 6004, + name: 'TokenOwnerRecordFromOwnRealmNotAllowed', + msg: 'TokenOwnerRecord from own realm is not allowed', + }, + { + code: 6005, + name: 'GovernanceProgramNotConfigured', + msg: 'Governance program not configured', + }, + { + code: 6006, + name: 'GoverningTokenOwnerMustMatch', + msg: 'Governing TokenOwner must match', + }, + { + code: 6007, + name: 'DriftError', + msg: 'DriftError', + }, + ], +} diff --git a/GatewayPlugin/config.ts b/GatewayPlugin/config.ts new file mode 100644 index 0000000000..9c2b1df115 --- /dev/null +++ b/GatewayPlugin/config.ts @@ -0,0 +1,48 @@ +// A list of "passes" offered by Civic to verify or gate access to a DAO. +export const availablePasses: { + name: string + value: string + description: string + isSybilResistance: boolean +}[] = [ + // Default + { + name: 'Uniqueness', + value: 'uniqobk8oGh4XBLMqM68K8M2zNu3CdYX7q5go7whQiv', + description: + 'A biometric proof of personhood, preventing Sybil attacks while retaining privacy', + isSybilResistance: true, + }, + { + name: 'ID Verification', + value: 'bni1ewus6aMxTxBi5SAfzEmmXLf8KcVFRmTfproJuKw', + description: + 'A KYC process for your DAO, allowing users to prove their identity by presenting a government-issued ID', + isSybilResistance: false, + }, + { + name: 'Bot Resistance', + value: 'ignREusXmGrscGNUesoU9mxfds9AiYTezUKex2PsZV6', + description: 'A simple CAPTCHA to prevent bots from spamming your DAO', + isSybilResistance: false, + }, + { + name: 'Other', + value: '', + description: + 'Set up your own custom verification (contact Civic.com for options)', + isSybilResistance: false, + }, +] + +// Infer the types from the available passes, giving type safety on the `other` and `default` pass types +type ArrayElement< + ArrayType extends readonly unknown[] +> = ArrayType extends readonly (infer ElementType)[] ? ElementType : never +export type CivicPass = ArrayElement + +// Use this when populating a dropdown +export const defaultPass: CivicPass = availablePasses[0] +// Use this in cases where you are implicitly adding sybil resistance to a DAO (e.g. QV DAO creation), rather than +// offering a choice - this allows defaultPass to be something *other than* sybil resistance without breaking things. +export const defaultSybilResistancePass = availablePasses[0] diff --git a/GatewayPlugin/sdk/accounts.tsx b/GatewayPlugin/sdk/accounts.tsx deleted file mode 100644 index 7a8bf84f1c..0000000000 --- a/GatewayPlugin/sdk/accounts.tsx +++ /dev/null @@ -1,124 +0,0 @@ -import { PublicKey } from '@solana/web3.js' -import { - getTokenOwnerRecordAddress, - ProgramAccount, - Realm, -} from '@solana/spl-governance' -import { GatewayClient } from '@solana/governance-program-library/dist' -import { getRegistrarPDA, getVoterWeightRecord } from '@utils/plugin/accounts' -import { notify } from '@utils/notifications' - -export const getGatekeeperNetwork = async ( - client: GatewayClient, - realm: ProgramAccount -): Promise => { - // Get the registrar for the realm - const { registrar } = await getRegistrarPDA( - realm.pubkey, - realm.account.communityMint, - client.program.programId - ) - const registrarObject = await client.program.account.registrar.fetch( - registrar - ) - - // Find the gatekeeper network from the registrar - return registrarObject.gatekeeperNetwork -} - -const getPredecessorProgramId = async ( - client: GatewayClient, - realm: ProgramAccount -): Promise => { - // Get the registrar for the realm - const { registrar } = await getRegistrarPDA( - realm.pubkey, - realm.account.communityMint, - client.program.programId - ) - const registrarObject = await client.program.account.registrar.fetch( - registrar - ) - - // Find the gatekeeper network from the registrar - return registrarObject.previousVoterWeightPluginProgramId -} - -export const getPreviousVotingWeightRecord = async ( - client: GatewayClient, - realm: ProgramAccount, - walletPk: PublicKey -): Promise => { - // TODO cache this to avoid lookup every time someone votes - const predecessorProgramId = await getPredecessorProgramId(client, realm) - - if (predecessorProgramId) { - // this gateway plugin registrar has a predecessor plugin - get its voting weight record - const { voterWeightPk } = await getVoterWeightRecord( - realm.pubkey, - realm.account.communityMint, - walletPk, - predecessorProgramId - ) - return voterWeightPk - } - - // this gateway plugin registrar has no predecessor plugin. - // The previous voting weight record is the token owner record - return getTokenOwnerRecordAddress( - realm.owner, - realm.pubkey, - realm.account.communityMint, - walletPk - ) -} - -export const getVoteInstruction = async ( - client: GatewayClient, - gatewayToken: PublicKey, - realm: ProgramAccount, - walletPk: PublicKey -) => { - // Throw if the user has no gateway token (TODO handle this later) - if (!gatewayToken) { - const error = new Error( - `Unable to execute transaction: No Civic Pass found` - ) - notify({ type: 'error', message: `${error}` }) - throw error - } - - // get the user's voter weight account address - const { voterWeightPk } = await getVoterWeightRecord( - realm.pubkey, - realm.account.communityMint, - walletPk, - client.program.programId - ) - - // Get the registrar for the realm - const { registrar } = await getRegistrarPDA( - realm.pubkey, - realm.account.communityMint, - client.program.programId - ) - - // the previous voting weight record in the chain of plugins, - // or the token owner record if this is the first plugin in the chain - const inputVoterWeight = await getPreviousVotingWeightRecord( - client, - realm, - walletPk - ) - - // call updateVotingWeightRecord on the plugin - return client.program.methods - .updateVoterWeightRecord() - .accounts({ - registrar, - voterWeightRecord: voterWeightPk, - inputVoterWeight, - gatewayToken, - }) - .instruction() -} diff --git a/GatewayPlugin/sdk/api.ts b/GatewayPlugin/sdk/api.ts index ecce0caab5..d95e6d92e6 100644 --- a/GatewayPlugin/sdk/api.ts +++ b/GatewayPlugin/sdk/api.ts @@ -1,16 +1,65 @@ -import { PublicKey } from '@solana/web3.js' -import { GatewayClient } from '@solana/governance-program-library' +import {PublicKey} from '@solana/web3.js' +import {GatewayClient} from '@solana/governance-program-library' +import {ProgramAccount, Realm, SYSTEM_PROGRAM_ID} from "@solana/spl-governance"; +import {getRegistrarPDA} from "@utils/plugin/accounts"; -export const tryGetGatewayRegistrar = async ( - registrarPk: PublicKey, - client: GatewayClient + +// Create an instruction to create a registrar account for a given realm +export const createCivicRegistrarIx = async ( + realm: ProgramAccount, + payer: PublicKey, + gatewayClient: GatewayClient, + gatekeeperNetwork: PublicKey, + predecessor?: PublicKey ) => { - try { - const existingRegistrar = await client.program.account.registrar.fetch( - registrarPk - ) - return existingRegistrar - } catch (e) { - return null - } + const {registrar} = getRegistrarPDA( + realm.pubkey, + realm.account.communityMint, + gatewayClient.program.programId + ) + + const remainingAccounts = predecessor + ? [{pubkey: predecessor, isSigner: false, isWritable: false}] + : [] + + return gatewayClient!.program.methods + .createRegistrar(!!predecessor) + .accounts({ + registrar, + realm: realm.pubkey, + governanceProgramId: realm.owner, + realmAuthority: realm.account.authority!, + governingTokenMint: realm.account.communityMint!, + gatekeeperNetwork, + payer, + systemProgram: SYSTEM_PROGRAM_ID, + }) + .remainingAccounts(remainingAccounts) + .instruction() } +// Create an instruction to configure a registrar account for a given realm +export const configureCivicRegistrarIx = async ( + realm: ProgramAccount, + gatewayClient: GatewayClient, + gatekeeperNetwork: PublicKey, + predecessor?: PublicKey +) => { + const {registrar} = getRegistrarPDA( + realm.pubkey, + realm.account.communityMint, + gatewayClient.program.programId + ) + const remainingAccounts = predecessor + ? [{pubkey: predecessor, isSigner: false, isWritable: false}] + : [] + return gatewayClient.program.methods + .configureRegistrar(false) + .accounts({ + registrar, + realm: realm.pubkey, + realmAuthority: realm.account.authority!, + gatekeeperNetwork: gatekeeperNetwork, + }) + .remainingAccounts(remainingAccounts) + .instruction() +} \ No newline at end of file diff --git a/GatewayPlugin/store/gatewayPluginStore.ts b/GatewayPlugin/store/gatewayPluginStore.ts deleted file mode 100644 index 33020c28aa..0000000000 --- a/GatewayPlugin/store/gatewayPluginStore.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { BN } from '@coral-xyz/anchor' -import { MaxVoterWeightRecord, ProgramAccount } from '@solana/spl-governance' -import { VotingClient } from '@utils/uiTypes/VotePlugin' -import create, { State } from 'zustand' -import { PublicKey } from '@solana/web3.js' - -interface gatewayPluginStore extends State { - state: { - gatewayToken: PublicKey | null - gatekeeperNetwork: PublicKey | null - votingPower: BN - maxVoteRecord: ProgramAccount | null - isLoadingGatewayToken: boolean - } - setGatewayToken: (gatewayToken: PublicKey, votingClient: VotingClient) => void - setGatekeeperNetwork: (gatekeeperNetwork: PublicKey) => void - setVotingPower: (gatewayToken: PublicKey) => void - setMaxVoterWeight: ( - maxVoterRecord: ProgramAccount | null - ) => void - setIsLoadingGatewayToken: (val: boolean) => void -} - -const defaultState = { - gatewayToken: null, - gatekeeperNetwork: null, - votingPower: new BN(0), - maxVoteRecord: null, - isLoadingGatewayToken: false, -} - -const useGatewayPluginStore = create((set, _get) => ({ - state: { - ...defaultState, - }, - setIsLoadingGatewayToken: (val) => { - set((s) => { - s.state.isLoadingGatewayToken = val - }) - }, - setGatewayToken: (gatewayToken, votingClient) => { - votingClient._setCurrentVoterGatewayToken(gatewayToken) - set((s) => { - s.state.gatewayToken = gatewayToken - }) - _get().setVotingPower(gatewayToken) - }, - setGatekeeperNetwork: (gatekeeperNetwork) => { - set((s) => { - s.state.gatekeeperNetwork = gatekeeperNetwork - }) - }, - - setVotingPower: () => { - set((s) => { - s.state.votingPower = new BN(1) - }) - }, - setMaxVoterWeight: (maxVoterRecord) => { - set((s) => { - s.state.maxVoteRecord = maxVoterRecord - }) - }, -})) - -export default useGatewayPluginStore diff --git a/HeliumVotePlugin/components/ClaimUnreleasedPositions.tsx b/HeliumVotePlugin/components/ClaimUnreleasedPositions.tsx index 15fad243a2..db761c946d 100644 --- a/HeliumVotePlugin/components/ClaimUnreleasedPositions.tsx +++ b/HeliumVotePlugin/components/ClaimUnreleasedPositions.tsx @@ -1,8 +1,6 @@ import { useEffect, useState } from 'react' import { TransactionInstruction } from '@solana/web3.js' import { SecondaryButton } from '@components/Button' -import useVotePluginsClientStore from 'stores/useVotePluginsClientStore' -import { HeliumVsrClient } from 'HeliumVotePlugin/sdk/client' import { chunks } from '@utils/helpers' import { registrarKey, @@ -20,6 +18,7 @@ import { useAddressQuery_CommunityTokenOwner } from '@hooks/queries/addresses/to import { useConnection } from '@solana/wallet-adapter-react' import { useRealmQuery } from '@hooks/queries/realm' import { useRealmProposalsQuery } from '@hooks/queries/proposal' +import {useHeliumClient} from "../../VoterWeightPlugins/useHeliumClient"; const NFT_SOL_BALANCE = 0.0014616 @@ -34,30 +33,29 @@ const ClaimUnreleasedPositions = ({ const [ownVoteRecords, setOwnVoteRecords] = useState([]) const [solToBeClaimed, setSolToBeClaimed] = useState(0) const realm = useRealmQuery().data?.result - const votingPlugin = useVotePluginsClientStore( - (s) => s.state.currentRealmVotingClient - ) + const { heliumClient } = useHeliumClient(); const { data: proposalsArray } = useRealmProposalsQuery() const { data: tokenOwnerRecord } = useAddressQuery_CommunityTokenOwner() - const isHeliumVsr = votingPlugin.client instanceof HeliumVsrClient + const isHeliumVsr = !!heliumClient; const releasePositions = async () => { if (!wallet?.publicKey) throw new Error('no wallet') if (!realm) throw new Error() if (!tokenOwnerRecord) throw new Error() + if (!heliumClient) throw new Error("No Helium client") setIsLoading(true) const instructions: TransactionInstruction[] = [] const [registrar] = registrarKey( realm.pubkey, realm.account.communityMint, - votingPlugin.client!.program.programId + heliumClient!.program.programId ) const [voterWeightPk] = voterWeightRecordKey( registrar, wallet.publicKey, - votingPlugin.client!.program.programId + heliumClient!.program.programId ) const voteRecords = ownVoteRecords @@ -67,7 +65,7 @@ const ClaimUnreleasedPositions = ({ ) const [posKey] = positionKey( i.account.nftMint, - votingPlugin.client!.program.programId + heliumClient.program.programId ) if ( proposal === undefined || @@ -77,7 +75,7 @@ const ClaimUnreleasedPositions = ({ continue } - const relinquishVoteIx = await (votingPlugin.client as HeliumVsrClient).program.methods + const relinquishVoteIx = await heliumClient.program.methods .relinquishVoteV0() .accounts({ registrar, @@ -120,7 +118,8 @@ const ClaimUnreleasedPositions = ({ } } const getVoteRecords = async () => { - const currentClient = votingPlugin.client as HeliumVsrClient + const currentClient = heliumClient; + if (!currentClient) return const voteRecords = (await currentClient.program.account['nftVoteRecord']?.all([ { @@ -148,11 +147,11 @@ const ClaimUnreleasedPositions = ({ } useEffect(() => { - if (wallet?.publicKey && isHeliumVsr && votingPlugin.client) { + if (wallet?.publicKey && isHeliumVsr && heliumClient) { getVoteRecords() } // eslint-disable-next-line react-hooks/exhaustive-deps -- TODO please fix, it can cause difficult bugs. You might wanna check out https://bobbyhadz.com/blog/react-hooks-exhaustive-deps for info. -@asktree - }, [votingPlugin.clientType, isHeliumVsr, wallet?.publicKey?.toBase58()]) + }, [heliumClient, isHeliumVsr, wallet?.publicKey?.toBase58()]) if (isHeliumVsr) { return ( diff --git a/HeliumVotePlugin/components/LockTokensAccount.tsx b/HeliumVotePlugin/components/LockTokensAccount.tsx index c3bb1450f6..ad667c8752 100644 --- a/HeliumVotePlugin/components/LockTokensAccount.tsx +++ b/HeliumVotePlugin/components/LockTokensAccount.tsx @@ -22,7 +22,6 @@ import { getMintMetadata } from '@components/instructions/programs/splToken' import { abbreviateAddress } from '@utils/formatting' import Button from '@components/Button' import { daysToSecs } from '@utils/dateTools' -import useVotePluginsClientStore from 'stores/useVotePluginsClientStore' import { LockCommunityTokensBtn } from './LockCommunityTokensBtn' import { LockTokensModal, LockTokensModalFormValues } from './LockTokensModal' import { useCreatePosition } from '../hooks/useCreatePosition' @@ -45,6 +44,9 @@ import { } from '@hooks/queries/mintInfo' import useLegacyConnectionContext from '@hooks/useLegacyConnectionContext' import { fetchJupiterPrice } from '@hooks/queries/jupiterPrice' +import {useHeliumClient} from "../../VoterWeightPlugins/useHeliumClient"; +import {Registrar} from "../sdk/types"; +import {useVotingClients} from "@hooks/useVotingClients"; export const LockTokensAccount: React.FC<{ // tokenOwnerRecordPk: string | string[] | undefined // @asktree: this was unused @@ -74,15 +76,18 @@ export const LockTokensAccount: React.FC<{ : // I wanted to eliminate `null` as a possible type wallet?.publicKey ?? undefined - const [ - currentClient, - vsrClient, - vsrRegistrar, - ] = useVotePluginsClientStore((s) => [ - s.state.currentRealmVotingClient, - s.state.heliumVsrClient, - s.state.heliumVsrRegistrar, - ]) + const { heliumClient: vsrClient } = useHeliumClient(); + const votingClients = useVotingClients(); + + const vsrRegistrar = useAsync( + async () => { + if (realm && vsrClient) { + return vsrClient.getRegistrarAccount(realm?.pubkey, realm?.account.communityMint) as Promise + } + }, + [realm, vsrClient] + ) + const { loading: loadingSubDaos, error: subDaosError, @@ -136,7 +141,7 @@ export const LockTokensAccount: React.FC<{ vsrClient ) { await getPositions({ - votingClient: currentClient, + votingClient: votingClients('community'), // community mint is hardcoded for getPositions realmPk: realm.pubkey, communityMintPk: realm.account.communityMint, walletPk: tokenOwnerRecordWalletPk @@ -208,10 +213,10 @@ export const LockTokensAccount: React.FC<{ (lockupPeriodInDays: number) => calcLockupMultiplier({ lockupSecs: daysToSecs(lockupPeriodInDays), - registrar: vsrRegistrar, + registrar: vsrRegistrar.result ?? null, realm, }), - [realm, vsrRegistrar] + [realm, vsrRegistrar.result] ) const handleLockTokens = async (values: LockTokensModalFormValues) => { @@ -229,7 +234,7 @@ export const LockTokensAccount: React.FC<{ if (!error) { await getPositions({ - votingClient: currentClient, + votingClient: votingClients('community'), // community mint is hardcoded for getPositions realmPk: realm!.pubkey, communityMintPk: realm!.account.communityMint, walletPk: wallet!.publicKey!, @@ -401,7 +406,6 @@ export const LockTokensAccount: React.FC<{
diff --git a/HeliumVotePlugin/components/PositionCard.tsx b/HeliumVotePlugin/components/PositionCard.tsx index 1ee16ac0bf..7a3c0adf21 100644 --- a/HeliumVotePlugin/components/PositionCard.tsx +++ b/HeliumVotePlugin/components/PositionCard.tsx @@ -1,5 +1,4 @@ import React, { useCallback, useState, useMemo } from 'react' -import useVotePluginsClientStore from 'stores/useVotePluginsClientStore' import { fmtMintAmount, getMintDecimalAmount } from '@tools/sdk/units' import tokenPriceService from '@utils/services/tokenPrice' import { abbreviateAddress } from '@utils/formatting' @@ -14,7 +13,7 @@ import { getMinDurationFmt, getTimeLeftFromNowFmt, } from '@utils/dateTools' -import { PositionWithMeta, SubDaoWithMeta } from '../sdk/types' +import {PositionWithMeta, Registrar, SubDaoWithMeta} from '../sdk/types' import useHeliumVsrStore from '../hooks/useHeliumVsrStore' import { LockTokensModal, @@ -39,6 +38,9 @@ import { useRealmCommunityMintInfoQuery } from '@hooks/queries/mintInfo' import queryClient from '@hooks/queries/queryClient' import { tokenAccountQueryKeys } from '@hooks/queries/tokenAccount' import useLegacyConnectionContext from '@hooks/useLegacyConnectionContext' +import {useHeliumClient} from "../../VoterWeightPlugins/useHeliumClient"; +import {useVotingClients} from "@hooks/useVotingClients"; +import {useAsync} from "react-async-hook"; interface PositionCardProps { subDaos?: SubDaoWithMeta[] @@ -65,15 +67,17 @@ export const PositionCard: React.FC = ({ s.state.positions, s.getPositions, ]) - const [ - currentClient, - vsrClient, - vsrRegistrar, - ] = useVotePluginsClientStore((s) => [ - s.state.currentRealmVotingClient, - s.state.heliumVsrClient, - s.state.heliumVsrRegistrar, - ]) + const { heliumClient: vsrClient } = useHeliumClient(); + const votingClients = useVotingClients(); + + const vsrRegistrar = useAsync( + async () => { + if (realm && vsrClient) { + return vsrClient.getRegistrarAccount(realm?.pubkey, realm?.account.communityMint) as Promise + } + }, + [realm, vsrClient] + ) const transferablePositions: PositionWithMeta[] = useMemo(() => { if (!unixNow || !positions.length) { @@ -189,10 +193,10 @@ export const PositionCard: React.FC = ({ (lockupPeriodInDays: number) => calcLockupMultiplier({ lockupSecs: daysToSecs(lockupPeriodInDays), - registrar: vsrRegistrar, + registrar: vsrRegistrar.result ?? null, realm, }), - [realm, vsrRegistrar] + [realm, vsrRegistrar.result] ) const refetchState = async () => { @@ -200,7 +204,7 @@ export const PositionCard: React.FC = ({ queryKey: tokenAccountQueryKeys.all(connection.endpoint), }) await getPositions({ - votingClient: currentClient, + votingClient: votingClients('community'), // community mint is hardcoded for getPositions realmPk: realm!.pubkey, communityMintPk: realm!.account.communityMint, walletPk: wallet!.publicKey!, diff --git a/HeliumVotePlugin/components/VotingPowerCard.tsx b/HeliumVotePlugin/components/VotingPowerCard.tsx index 3b20531b05..78fc7167e8 100644 --- a/HeliumVotePlugin/components/VotingPowerCard.tsx +++ b/HeliumVotePlugin/components/VotingPowerCard.tsx @@ -63,7 +63,6 @@ export const VotingPowerCard: React.FC<{ diff --git a/HeliumVotePlugin/hooks/useClosePosition.ts b/HeliumVotePlugin/hooks/useClosePosition.ts index 7088a69eed..cacbb2ed2b 100644 --- a/HeliumVotePlugin/hooks/useClosePosition.ts +++ b/HeliumVotePlugin/hooks/useClosePosition.ts @@ -4,8 +4,6 @@ import { PublicKey, TransactionInstruction } from '@solana/web3.js' import { useAsyncCallback } from 'react-async-hook' import { PositionWithMeta } from '../sdk/types' import useRealm from '@hooks/useRealm' -import useVotePluginsClientStore from 'stores/useVotePluginsClientStore' -import { HeliumVsrClient } from 'HeliumVotePlugin/sdk/client' import { useSolanaUnixNow } from '@hooks/useSolanaUnixNow' import { SequenceType } from '@blockworks-foundation/mangolana/lib/globalTypes' import { notify } from '@utils/notifications' @@ -15,15 +13,14 @@ import { } from '@utils/sendTransactions' import { withCreateTokenOwnerRecord } from '@solana/spl-governance' import { useRealmQuery } from '@hooks/queries/realm' +import {useHeliumClient} from "../../VoterWeightPlugins/useHeliumClient"; export const useClosePosition = () => { const { unixNow } = useSolanaUnixNow() const { connection, wallet } = useWalletDeprecated() const realm = useRealmQuery().data?.result const { realmInfo } = useRealm() - const [{ client }] = useVotePluginsClientStore((s) => [ - s.state.currentRealmVotingClient, - ]) + const { heliumClient } = useHeliumClient(); const { error, loading, execute } = useAsyncCallback( async ({ position, @@ -39,8 +36,7 @@ export const useClosePosition = () => { !connection.current || !realm || !realmInfo || - !client || - !(client instanceof HeliumVsrClient) || + !heliumClient || !wallet || position.numActiveVotes > 0 || // lockupExpired @@ -69,7 +65,7 @@ export const useClosePosition = () => { } instructions.push( - await client.program.methods + await heliumClient.program.methods .withdrawV0({ amount: position.amountDepositedNative, }) @@ -81,7 +77,7 @@ export const useClosePosition = () => { ) instructions.push( - await client.program.methods + await heliumClient.program.methods .closePositionV0() .accounts({ position: position.pubkey, diff --git a/HeliumVotePlugin/hooks/useCreatePosition.ts b/HeliumVotePlugin/hooks/useCreatePosition.ts index 7f7898a775..1783a64081 100644 --- a/HeliumVotePlugin/hooks/useCreatePosition.ts +++ b/HeliumVotePlugin/hooks/useCreatePosition.ts @@ -12,8 +12,6 @@ import { useAsyncCallback } from 'react-async-hook' import { positionKey } from '@helium/voter-stake-registry-sdk' import useRealm from '@hooks/useRealm' import { LockupKind } from 'HeliumVotePlugin/components/LockTokensModal' -import useVotePluginsClientStore from 'stores/useVotePluginsClientStore' -import { HeliumVsrClient } from 'HeliumVotePlugin/sdk/client' import { notify } from '@utils/notifications' import { sendTransactionsV3, @@ -21,15 +19,16 @@ import { txBatchesToInstructionSetWithSigners, } from '@utils/sendTransactions' import { useRealmQuery } from '@hooks/queries/realm' +import {useHeliumClient} from "../../VoterWeightPlugins/useHeliumClient"; export const useCreatePosition = () => { const { connection, wallet } = useWalletDeprecated() const realm = useRealmQuery().data?.result const { realmInfo } = useRealm() - const [{ client }, registrarPk] = useVotePluginsClientStore((s) => [ - s.state.currentRealmVotingClient, - s.state.voteStakeRegistryRegistrarPk, - ]) + const {heliumClient} = useHeliumClient(); + const registrarPk = realm && heliumClient ? + heliumClient.getRegistrarPDA(realm.pubkey, realm.account.communityMint).registrar : undefined; + const { error, loading, execute } = useAsyncCallback( async ({ amount, @@ -47,8 +46,7 @@ export const useCreatePosition = () => { !connection.current || !registrarPk || !realm || - !client || - !(client instanceof HeliumVsrClient) || + !heliumClient || !wallet || !realmInfo || !realmInfo.programVersion @@ -98,7 +96,7 @@ export const useCreatePosition = () => { } instructions.push( - await client.program.methods + await heliumClient.program.methods .initializePositionV0({ kind: { [lockupKind]: {} }, periods: lockupPeriodsInDays, @@ -113,7 +111,7 @@ export const useCreatePosition = () => { ) instructions.push( - await client.program.methods + await heliumClient.program.methods .depositV0({ amount, }) diff --git a/HeliumVotePlugin/hooks/useExtendPosition.ts b/HeliumVotePlugin/hooks/useExtendPosition.ts index f95a0df691..22d1fe7e8a 100644 --- a/HeliumVotePlugin/hooks/useExtendPosition.ts +++ b/HeliumVotePlugin/hooks/useExtendPosition.ts @@ -11,18 +11,15 @@ import { sendTransactionsV3, txBatchesToInstructionSetWithSigners, } from '@utils/sendTransactions' -import { HeliumVsrClient } from 'HeliumVotePlugin/sdk/client' -import useVotePluginsClientStore from 'stores/useVotePluginsClientStore' import { withCreateTokenOwnerRecord } from '@solana/spl-governance' import { useRealmQuery } from '@hooks/queries/realm' +import {useHeliumClient} from "../../VoterWeightPlugins/useHeliumClient"; export const useExtendPosition = () => { const { connection, wallet, anchorProvider: provider } = useWalletDeprecated() const realm = useRealmQuery().data?.result const { realmInfo } = useRealm() - const [{ client }] = useVotePluginsClientStore((s) => [ - s.state.currentRealmVotingClient, - ]) + const {heliumClient} = useHeliumClient(); const { error, loading, execute } = useAsyncCallback( async ({ position, @@ -41,9 +38,8 @@ export const useExtendPosition = () => { !provider || !realm || !wallet || - !client || - !realmInfo || - !(client instanceof HeliumVsrClient) + !heliumClient || + !realmInfo; const idl = await Program.fetchIdl(programId, provider) const hsdProgram = await init(provider as any, programId, idl) @@ -84,7 +80,7 @@ export const useExtendPosition = () => { ) } else { instructions.push( - await client.program.methods + await heliumClient.program.methods .resetLockupV0({ kind: position.lockup.kind, periods: lockupPeriodsInDays, diff --git a/HeliumVotePlugin/hooks/useFlipPositionLockupKind.ts b/HeliumVotePlugin/hooks/useFlipPositionLockupKind.ts index 12648cbc2c..7b597887cf 100644 --- a/HeliumVotePlugin/hooks/useFlipPositionLockupKind.ts +++ b/HeliumVotePlugin/hooks/useFlipPositionLockupKind.ts @@ -12,20 +12,17 @@ import { sendTransactionsV3, txBatchesToInstructionSetWithSigners, } from '@utils/sendTransactions' -import useVotePluginsClientStore from 'stores/useVotePluginsClientStore' -import { HeliumVsrClient } from 'HeliumVotePlugin/sdk/client' import { useSolanaUnixNow } from '@hooks/useSolanaUnixNow' import { withCreateTokenOwnerRecord } from '@solana/spl-governance' import { useRealmQuery } from '@hooks/queries/realm' +import {useHeliumClient} from "../../VoterWeightPlugins/useHeliumClient"; export const useFlipPositionLockupKind = () => { const { unixNow } = useSolanaUnixNow() const { connection, wallet, anchorProvider: provider } = useWalletDeprecated() const realm = useRealmQuery().data?.result const { realmInfo } = useRealm() - const [{ client }] = useVotePluginsClientStore((s) => [ - s.state.currentRealmVotingClient, - ]) + const {heliumClient} = useHeliumClient(); const { error, loading, execute } = useAsyncCallback( async ({ position, @@ -41,10 +38,9 @@ export const useFlipPositionLockupKind = () => { !connection.current || !realm || !wallet || - !client || + !heliumClient || !unixNow || !realmInfo || - !(client instanceof HeliumVsrClient) || position.numActiveVotes > 0 const lockupKind = Object.keys(position.lockup.kind)[0] as string @@ -100,7 +96,7 @@ export const useFlipPositionLockupKind = () => { ) } else { instructions.push( - await client.program.methods + await heliumClient.program.methods .resetLockupV0({ kind, periods: positionLockupPeriodInDays, diff --git a/HeliumVotePlugin/hooks/useSplitPosition.ts b/HeliumVotePlugin/hooks/useSplitPosition.ts index ab6307fae0..b478eabf3f 100644 --- a/HeliumVotePlugin/hooks/useSplitPosition.ts +++ b/HeliumVotePlugin/hooks/useSplitPosition.ts @@ -13,8 +13,6 @@ import { PositionWithMeta } from '../sdk/types' import { PROGRAM_ID, init, daoKey } from '@helium/helium-sub-daos-sdk' import useRealm from '@hooks/useRealm' import { LockupKind } from 'HeliumVotePlugin/components/LockTokensModal' -import useVotePluginsClientStore from 'stores/useVotePluginsClientStore' -import { HeliumVsrClient } from 'HeliumVotePlugin/sdk/client' import { getMintNaturalAmountFromDecimalAsBN } from '@tools/sdk/units' import { positionKey } from '@helium/voter-stake-registry-sdk' import { notify } from '@utils/notifications' @@ -26,16 +24,16 @@ import { import { chunks } from '@utils/helpers' import { useRealmQuery } from '@hooks/queries/realm' import { useRealmCommunityMintInfoQuery } from '@hooks/queries/mintInfo' +import {useHeliumClient} from "../../VoterWeightPlugins/useHeliumClient"; export const useSplitPosition = () => { const { connection, wallet, anchorProvider: provider } = useWalletDeprecated() const realm = useRealmQuery().data?.result const mint = useRealmCommunityMintInfoQuery().data?.result const { realmInfo } = useRealm() - const [{ client }, registrarPk] = useVotePluginsClientStore((s) => [ - s.state.currentRealmVotingClient, - s.state.voteStakeRegistryRegistrarPk, - ]) + const {heliumClient} = useHeliumClient(); + const registrarPk = realm && heliumClient ? + heliumClient.getRegistrarPDA(realm.pubkey, realm.account.communityMint).registrar : undefined; const { error, loading, execute } = useAsyncCallback( async ({ sourcePosition, @@ -59,8 +57,7 @@ export const useSplitPosition = () => { !realm || !registrarPk || !realm || - !client || - !(client instanceof HeliumVsrClient) || + !heliumClient || !wallet || !realmInfo || !realmInfo.programVersion || @@ -121,7 +118,7 @@ export const useSplitPosition = () => { } instructions.push( - await client.program.methods + await heliumClient.program.methods .initializePositionV0({ kind: { [lockupKind]: {} }, periods: lockupPeriodsInDays, @@ -151,7 +148,7 @@ export const useSplitPosition = () => { ) } else { instructions.push( - await client.program.methods + await heliumClient.program.methods .transferV0({ amount: amountToTransfer, }) @@ -166,7 +163,7 @@ export const useSplitPosition = () => { if (amountToTransfer.eq(sourcePosition.amountDepositedNative)) { instructions.push( - await client.program.methods + await heliumClient.program.methods .closePositionV0() .accounts({ position: sourcePosition.pubkey, @@ -186,7 +183,7 @@ export const useSplitPosition = () => { sequenceType: SequenceType.Sequential, })) - notify({ message: 'Spliting Position' }) + notify({ message: 'Splitting Position' }) await sendTransactionsV3({ transactionInstructions: txsChunks, wallet, diff --git a/HeliumVotePlugin/hooks/useTransferPosition.ts b/HeliumVotePlugin/hooks/useTransferPosition.ts index d7e68e5105..d45f9d8b8e 100644 --- a/HeliumVotePlugin/hooks/useTransferPosition.ts +++ b/HeliumVotePlugin/hooks/useTransferPosition.ts @@ -4,8 +4,6 @@ import { PublicKey, TransactionInstruction } from '@solana/web3.js' import { useAsyncCallback } from 'react-async-hook' import { PositionWithMeta } from '../sdk/types' import { PROGRAM_ID, init, daoKey } from '@helium/helium-sub-daos-sdk' -import useVotePluginsClientStore from 'stores/useVotePluginsClientStore' -import { HeliumVsrClient } from 'HeliumVotePlugin/sdk/client' import { getMintNaturalAmountFromDecimalAsBN } from '@tools/sdk/units' import { notify } from '@utils/notifications' import { @@ -15,15 +13,13 @@ import { } from '@utils/sendTransactions' import { useRealmQuery } from '@hooks/queries/realm' import { useRealmCommunityMintInfoQuery } from '@hooks/queries/mintInfo' +import {useHeliumClient} from "../../VoterWeightPlugins/useHeliumClient"; export const useTransferPosition = () => { const { connection, wallet, anchorProvider: provider } = useWalletDeprecated() const realm = useRealmQuery().data?.result const mint = useRealmCommunityMintInfoQuery().data?.result - const [{ client }] = useVotePluginsClientStore((s) => [ - s.state.currentRealmVotingClient, - s.state.voteStakeRegistryRegistrarPk, - ]) + const {heliumClient} = useHeliumClient(); const { error, loading, execute } = useAsyncCallback( async ({ sourcePosition, @@ -43,8 +39,7 @@ export const useTransferPosition = () => { !realm || !mint || !wallet || - !client || - !(client instanceof HeliumVsrClient) || + !heliumClient || sourcePosition.numActiveVotes > 0 || targetPosition.numActiveVotes > 0 @@ -80,7 +75,7 @@ export const useTransferPosition = () => { ) } else { instructions.push( - await client.program.methods + await heliumClient.program.methods .transferV0({ amount: amountToTransfer, }) @@ -95,7 +90,7 @@ export const useTransferPosition = () => { if (amountToTransfer.eq(sourcePosition.amountDepositedNative)) { instructions.push( - await client.program.methods + await heliumClient.program.methods .closePositionV0() .accounts({ position: sourcePosition.pubkey, @@ -104,7 +99,7 @@ export const useTransferPosition = () => { ) } - notify({ message: 'Transfering' }) + notify({ message: 'Transferring' }) await sendTransactionsV3({ transactionInstructions: [ { diff --git a/HeliumVotePlugin/sdk/client.ts b/HeliumVotePlugin/sdk/client.ts index 3b35645781..b6341f95fc 100644 --- a/HeliumVotePlugin/sdk/client.ts +++ b/HeliumVotePlugin/sdk/client.ts @@ -1,21 +1,111 @@ -import { Program, Provider, web3 } from '@coral-xyz/anchor' -import { VoterStakeRegistry } from '@helium/idls/lib/types/voter_stake_registry' -import { PROGRAM_ID, init } from '@helium/voter-stake-registry-sdk' +import {Program, Provider, web3 } from '@coral-xyz/anchor' +import { VoterStakeRegistry, IDL } from '@helium/idls/lib/types/voter_stake_registry' +import {PROGRAM_ID, init, registrarKey, voterWeightRecordKey} from '@helium/voter-stake-registry-sdk' +import {Client, DEFAULT_GOVERNANCE_PROGRAM_ID} from "@solana/governance-program-library"; +import {PublicKey, TransactionInstruction} from "@solana/web3.js"; +import {getTokenOwnerRecordAddress, VoterWeightAction} from "@solana/spl-governance"; +import {AccountData} from "@utils/uiTypes/VotePlugin"; +import {getAssociatedTokenAddress} from "@blockworks-foundation/mango-v4"; +import BN from "bn.js"; +import {getPositions, GetPositionsReturn} from "../utils/getPositions"; -export class HeliumVsrClient { +export class HeliumVsrClient extends Client { constructor( public program: Program, - public devent?: boolean - ) {} + public devnet: boolean, + readonly governanceProgramId: PublicKey + ) { + super(program, devnet) + } + + readonly requiresInputVoterWeight = false; + + // NO-OP TODO: Double-check + async createVoterWeightRecord(): Promise { + return null; + } + + // NO-OP + async createMaxVoterWeightRecord(): Promise { + return null; + } + + async updateVoterWeightRecord(voter: PublicKey, realm: PublicKey, mint: PublicKey, action: VoterWeightAction) { + const { positions } = await this.getPositions(voter, realm, mint); + const tokenOwnerRecord = await getTokenOwnerRecordAddress(this.governanceProgramId, realm, mint, voter); + + const remainingAccounts: AccountData[] = [] + const [registrar] = registrarKey( + realm, + mint, + this.program.programId + ) + + for (const pos of positions) { + const tokenAccount = await getAssociatedTokenAddress( + pos.mint, + voter, + true + ) + + remainingAccounts.push( + new AccountData(tokenAccount), + new AccountData(pos.pubkey) + ) + } + + const [voterWeightPk] = voterWeightRecordKey( + registrar, + voter, + this.program.programId + ) + + const ix = await this.program.methods + .updateVoterWeightRecordV0({ + owner: voter, + voterWeightAction: { + [action]: {}, + }, + } as any) + .accounts({ + registrar, + voterWeightRecord: voterWeightPk, + voterTokenOwnerRecord: tokenOwnerRecord, + }) + .remainingAccounts(remainingAccounts.slice(0, 10)) + .instruction(); + + return { pre: [ix] } + } + // NO-OP + async updateMaxVoterWeightRecord(): Promise { + return null; + } + async calculateVoterWeight(voter: PublicKey, realm: PublicKey, mint: PublicKey): Promise { + const positionDetails = await this.getPositions(voter, realm, mint); + return positionDetails.votingPower + } + + private async getPositions(voter: PublicKey, realm: PublicKey, mint: PublicKey): Promise { + return getPositions({ + realmPk: realm, + walletPk: voter, + communityMintPk: mint, + client: this, + connection: this.program.provider.connection + }) + } static async connect( provider: Provider, programId: web3.PublicKey = PROGRAM_ID, - devnet?: boolean + devnet = false, + governanceProgramId = DEFAULT_GOVERNANCE_PROGRAM_ID ): Promise { return new HeliumVsrClient( (await init(provider as any, programId)) as any, - devnet + devnet, + governanceProgramId, ) } } diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000000..f49a4e16e6 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. \ No newline at end of file diff --git a/NftVotePlugin/NftProposalVoteState.tsx b/NftVotePlugin/NftProposalVoteState.tsx index 65c849c7bc..c087e8c524 100644 --- a/NftVotePlugin/NftProposalVoteState.tsx +++ b/NftVotePlugin/NftProposalVoteState.tsx @@ -2,12 +2,12 @@ import { NFT_PLUGINS_PKS } from '@constants/plugins' import useWalletOnePointOh from '@hooks/useWalletOnePointOh' import { ProgramAccount, Proposal, ProposalState } from '@solana/spl-governance' import { useEffect } from 'react' -import useVotePluginsClientStore from 'stores/useVotePluginsClientStore' import useNftProposalStore from './NftProposalStore' import { useRealmConfigQuery } from '@hooks/queries/realmConfig' import { useProposalVoteRecordQuery } from '@hooks/queries/voteRecord' -import { useGovernancePowerAsync } from '@hooks/queries/governancePower' import { useVotingPop } from '@components/VotePanel/hooks' +import { useRealmVoterWeightPlugins } from '@hooks/useRealmVoterWeightPlugins' +import {useNftClient} from "../VoterWeightPlugins/useNftClient"; const NftProposalVoteState = ({ proposal, @@ -16,12 +16,14 @@ const NftProposalVoteState = ({ }) => { const config = useRealmConfigQuery().data?.result - const plugin = useVotePluginsClientStore((s) => s.state.nftClient) + const { nftClient } = useNftClient(); const getCountedNfts = useNftProposalStore((s) => s.getCountedNfts) const countedNfts = useNftProposalStore((s) => s.countedNftsForProposal) const wallet = useWalletOnePointOh() const votingPop = useVotingPop() - const { result: votingPower } = useGovernancePowerAsync(votingPop) + const { totalCalculatedVoterWeight, isReady } = useRealmVoterWeightPlugins( + votingPop + ) const isNftPlugin = config?.account.communityTokenConfig.voterWeightAddin && NFT_PLUGINS_PKS.includes( @@ -31,13 +33,14 @@ const NftProposalVoteState = ({ const ownVoteRecord = useProposalVoteRecordQuery('electoral').data?.result const showVoteRecords = - votingPower && + isReady && + totalCalculatedVoterWeight?.value && countedNfts.length > 0 && - countedNfts.length < votingPower.toNumber() && + countedNfts.length < totalCalculatedVoterWeight.value.toNumber() && !ownVoteRecord const useComponent = - plugin && + nftClient && proposal && wallet?.connected && isNftPlugin && @@ -46,7 +49,7 @@ const NftProposalVoteState = ({ useEffect(() => { if (useComponent) { - getCountedNfts(plugin, proposal, wallet.publicKey!) + getCountedNfts(nftClient, proposal, wallet.publicKey!) } // eslint-disable-next-line react-hooks/exhaustive-deps -- TODO please fix, it can cause difficult bugs. You might wanna check out https://bobbyhadz.com/blog/react-hooks-exhaustive-deps for info. -@asktree }, [useComponent]) diff --git a/NftVotePlugin/store/nftPluginStore.ts b/NftVotePlugin/store/nftPluginStore.ts index 0332cfe8b9..3035058650 100644 --- a/NftVotePlugin/store/nftPluginStore.ts +++ b/NftVotePlugin/store/nftPluginStore.ts @@ -1,29 +1,17 @@ import { MaxVoterWeightRecord, ProgramAccount } from '@solana/spl-governance' -import { VotingClient } from '@utils/uiTypes/VotePlugin' -import { DasNftObject } from '@hooks/queries/digitalAssets' import create, { State } from 'zustand' interface nftPluginStore extends State { state: { - votingNfts: DasNftObject[] maxVoteRecord: ProgramAccount | null - isLoadingNfts: boolean } - setVotingNfts: ( - nfts: DasNftObject[], - votingClient: VotingClient, - nftMintRegistrar: any - ) => void setMaxVoterWeight: ( maxVoterRecord: ProgramAccount | null ) => void - setIsLoadingNfts: (val: boolean) => void } const defaultState = { - votingNfts: [], maxVoteRecord: null, - isLoadingNfts: false, } /** @@ -34,18 +22,6 @@ const useNftPluginStore = create((set, _get) => ({ state: { ...defaultState, }, - setIsLoadingNfts: (val) => { - set((s) => { - s.state.isLoadingNfts = val - }) - }, - setVotingNfts: (nfts, votingClient, _nftMintRegistrar) => { - votingClient._setCurrentVoterNfts(nfts) - set((s) => { - s.state.votingNfts = nfts - }) - }, - setMaxVoterWeight: (maxVoterRecord) => { set((s) => { s.state.maxVoteRecord = maxVoterRecord diff --git a/ParclVotePlugin/ParclVoterWeightPluginClient.ts b/ParclVotePlugin/ParclVoterWeightPluginClient.ts new file mode 100644 index 0000000000..b196eddd61 --- /dev/null +++ b/ParclVotePlugin/ParclVoterWeightPluginClient.ts @@ -0,0 +1,101 @@ +import {Client} from "@solana/governance-program-library"; +import {PublicKey, TransactionInstruction} from "@solana/web3.js"; +import BN from "bn.js"; +import {StakeAccount, StakeConnection} from "@parcl-oss/staking"; +import {Provider, Wallet} from "@coral-xyz/anchor"; +import {VoterWeightAction} from "@solana/spl-governance"; +import {convertVoterWeightActionToType} from "../VoterWeightPlugins/lib/utils"; +import queryClient from "@hooks/queries/queryClient"; + +// A wrapper for the StakeConnection from @parcl-oss/staking, that implements the generic plugin client interface +export class ParclVoterWeightPluginClient extends Client { + readonly requiresInputVoterWeight = false; + // The parcl plugin does not have a registrar account + async getRegistrarAccount(): Promise { + return null; + } + + async getMaxVoterWeightRecordPDA() { + const maxVoterWeightPk = (await this.client.program.methods.updateMaxVoterWeight().pubkeys()).maxVoterRecord + + if (!maxVoterWeightPk) return null; + + return { + maxVoterWeightPk, + maxVoterWeightRecordBump: 0 // This is wrong for Parcl - but it doesn't matter as it is not used + } + } + + async getVoterWeightRecordPDA(realm: PublicKey, mint: PublicKey, voter: PublicKey) { + const { voterWeightAccount } = await this.getUpdateVoterWeightPks([], voter, VoterWeightAction.CastVote, PublicKey.default); + + return { + voterWeightPk: voterWeightAccount, + voterWeightRecordBump: 0 // This is wrong for Parcl - but it doesn't matter as it is not used + }; + } + + // NO-OP Parcl records are created through the Parcl dApp. + async createVoterWeightRecord(): Promise { + return null; + } + + // NO-OP + async createMaxVoterWeightRecord(): Promise { + return null; + } + + private async getStakeAccount(voter: PublicKey): Promise { + return queryClient.fetchQuery({ + queryKey: ['parcl getStakeAccount', voter], + queryFn: async() => { + const account = await this.client.getMainAccount(voter) + return account !== undefined ? account : null + }, + }) + } + + private async getUpdateVoterWeightPks(instructions: TransactionInstruction[], voter: PublicKey, action: VoterWeightAction, target?: PublicKey) { + const stakeAccount = await this.getStakeAccount(voter) + + if (!stakeAccount) throw new Error("Stake account not found for voter " + voter.toString()); + return this.client.withUpdateVoterWeight( + instructions, + stakeAccount, + { [convertVoterWeightActionToType(action)]: {} } as any, + target + ); + } + + async updateVoterWeightRecord(voter: PublicKey, realm: PublicKey, mint: PublicKey, action: VoterWeightAction, inputRecordCallback?: () => Promise, target?: PublicKey) { + const instructions: TransactionInstruction[] = []; + await this.getUpdateVoterWeightPks(instructions, voter, action, target); + + return { pre: instructions }; + } + // NO-OP + async updateMaxVoterWeightRecord(): Promise { + return null; + } + async calculateVoterWeight(voter: PublicKey): Promise { + const stakeAccount = await this.getStakeAccount(voter) + + if (stakeAccount) { + return stakeAccount.getVoterWeight(await this.client.getTime()).toBN() + } else { + return new BN(0) + } + } + constructor(program: typeof StakeConnection.prototype.program, private client: StakeConnection, devnet:boolean) { + super(program, devnet); + } + + static async connect(provider: Provider, devnet = false, wallet: Wallet): Promise { + const parclClient = await StakeConnection.connect( + provider.connection, + wallet + ) + + return new ParclVoterWeightPluginClient(parclClient.program, parclClient, devnet); + } +} \ No newline at end of file diff --git a/ParclVotePlugin/components/ParclAccountDetails.tsx b/ParclVotePlugin/components/ParclAccountDetails.tsx new file mode 100644 index 0000000000..16fa342857 --- /dev/null +++ b/ParclVotePlugin/components/ParclAccountDetails.tsx @@ -0,0 +1,163 @@ +import { MintInfo } from '@solana/spl-token' +import BN from 'bn.js' +import { GoverningTokenType } from '@solana/spl-governance' +import { fmtMintAmount } from '@tools/sdk/units' +import { useEffect } from 'react' +import useWalletOnePointOh from '@hooks/useWalletOnePointOh' +import { useUserCommunityTokenOwnerRecord } from '@hooks/queries/tokenOwnerRecord' +import { useRealmConfigQuery } from '@hooks/queries/realmConfig' +import { useRealmCommunityMintInfoQuery, useRealmCouncilMintInfoQuery } from '@hooks/queries/mintInfo' +import ParclVotingPower from './ParclVotingPower' +import { useUserTokenAccountsQuery } from '@hooks/queries/tokenAccount' +import { useRealmQuery } from '@hooks/queries/realm' +import { PublicKey } from '@solana/web3.js' +import VanillaWithdrawTokensButton from '@components/TokenBalance/VanillaWithdrawTokensButton' +import { Deposit } from '@components/GovernancePower/Power/Vanilla/Deposit' + +export const PARCL_INSTRUCTIONS = + 'You can deposit PRCL tokens at https://app.parcl.co/staking' + +const TokenDeposit = ({ + mintInfo, + mintAddress, + inAccountDetails, + setHasGovPower, + role +}: { + mintInfo: MintInfo | undefined, + mintAddress: PublicKey, + inAccountDetails?: boolean, + role: 'council' | 'community', + setHasGovPower?: (hasGovPower: boolean) => void +}) => { + const wallet = useWalletOnePointOh() + const { data: tokenAccounts } = useUserTokenAccountsQuery() + const connected = !!wallet?.connected + + const ownTokenRecord = useUserCommunityTokenOwnerRecord().data?.result + const config = useRealmConfigQuery().data?.result + + const relevantTokenConfig = role === "community" + ? config?.account.communityTokenConfig + : config?.account.councilTokenConfig; + + const isMembership = + relevantTokenConfig?.tokenType === GoverningTokenType.Membership + const isDormant = + relevantTokenConfig?.tokenType === GoverningTokenType.Dormant; + + const depositTokenRecord = ownTokenRecord + const depositTokenAccount = tokenAccounts?.find((a) => + a.account.mint.equals(mintAddress) + ); + + const hasTokensInWallet = + depositTokenAccount && depositTokenAccount.account.amount.gt(new BN(0)) + + const hasTokensDeposited = + depositTokenRecord && + depositTokenRecord.account.governingTokenDepositAmount.gt(new BN(0)) + + const availableTokens = + depositTokenRecord && mintInfo + ? fmtMintAmount( + mintInfo, + depositTokenRecord.account.governingTokenDepositAmount + ) + : '0' + + useEffect(() => { + if (availableTokens != '0' || hasTokensDeposited || hasTokensInWallet) { + if (setHasGovPower) setHasGovPower(true) + } + }, [availableTokens, hasTokensDeposited, hasTokensInWallet, setHasGovPower]) + + const canShowAvailableTokensMessage = hasTokensInWallet && connected + const tokensToShow = + hasTokensInWallet && depositTokenAccount + ? fmtMintAmount(mintInfo, depositTokenAccount.account.amount) + : hasTokensInWallet + ? availableTokens + : 0 + + // Do not show deposits for mints with zero supply because nobody can deposit anyway + if (!mintInfo || mintInfo.supply.isZero()) { + return null + } + + return ( +
+ {(availableTokens != '0' || inAccountDetails) && ( +
+ +
+ )} +
+ You have {tokensToShow} {hasTokensDeposited ? `more ` : ``} tokens + available to deposit. +
+ { + role === "community" + ?
{PARCL_INSTRUCTIONS}
+ : null + } + { + !isDormant + ? + : null + } +
+ {!isMembership && // Membership tokens can't be withdrawn (that is their whole point, actually) + !isDormant && + inAccountDetails && ( + + )} +
+
+ ) +} + +const ParclAccountDetails = () => { + const realm = useRealmQuery().data?.result + const communityMint = useRealmCommunityMintInfoQuery().data?.result + const councilMint = useRealmCouncilMintInfoQuery().data?.result; + const wallet = useWalletOnePointOh() + const connected = !!wallet?.connected + const councilMintAddress = realm?.account.config.councilMint; + const hasLoaded = communityMint && councilMint && realm && councilMintAddress; + + return ( + <> + {hasLoaded ? ( +
+ {!connected ? ( +
+ Connect your wallet to see governance power +
+ ) : + ( + <> + + + + )} +
+ ) : ( + <> +
+
+ + )} + + ) +} + +export default ParclAccountDetails diff --git a/ParclVotePlugin/components/ParclVotingPower.tsx b/ParclVotePlugin/components/ParclVotingPower.tsx new file mode 100644 index 0000000000..7d9649f2e8 --- /dev/null +++ b/ParclVotePlugin/components/ParclVotingPower.tsx @@ -0,0 +1,113 @@ +import { BigNumber } from 'bignumber.js' +import { useMemo } from 'react' +import { useRealmQuery } from '@hooks/queries/realm' +import { useMintInfoByPubkeyQuery } from '@hooks/queries/mintInfo' +import { useConnection } from '@solana/wallet-adapter-react' +import { getParclGovPower } from '@hooks/queries/governancePower' +import { useAsync } from 'react-async-hook' +import BN from 'bn.js' +import { getMintMetadata } from '@components/instructions/programs/splToken' +import VotingPowerPct from '@components/ProposalVotingPower/VotingPowerPct' +import clsx from 'clsx' +import { useRealmConfigQuery } from '@hooks/queries/realmConfig' +import { GoverningTokenType } from '@solana/spl-governance' +import useParclScalingFactor from '@hooks/parcl/useScalingFactor' +import useWalletOnePointOh from '@hooks/useWalletOnePointOh' +import { useUserTokenAccountsQuery } from '@hooks/queries/tokenAccount' + +interface Props { + className?: string + role: 'community' | 'council' + hideIfZero?: boolean + children?: React.ReactNode +} + +export default function ParclVotingPower({ + role, + hideIfZero, + children, + ...props +}: Props) { + const realm = useRealmQuery().data?.result + const realmConfig = useRealmConfigQuery().data?.result + const { data: tokenAccounts } = useUserTokenAccountsQuery() + const wallet = useWalletOnePointOh() + + const { connection } = useConnection() + + const relevantMint = + role === 'community' + ? realm?.account.communityMint + : realm?.account.config.councilMint + + const mintInfo = useMintInfoByPubkeyQuery(relevantMint).data?.result + + const { result: personalAmount } = useAsync( + async () => + wallet?.publicKey && role === 'community' + ? getParclGovPower(connection, wallet.publicKey) + : tokenAccounts?.find( + (a) => relevantMint && a.account.mint.equals(relevantMint) + )?.account.amount as BN, + [connection, wallet, role, relevantMint, tokenAccounts] + ) + + const parclScalingFactor = useParclScalingFactor() ?? 1; + + const totalAmount = personalAmount ?? new BN(0) + + const formattedTotal = useMemo( + () => + mintInfo && totalAmount !== undefined + ? new BigNumber(totalAmount.toString()) + .multipliedBy( + role === 'community' + ? parclScalingFactor + : 1 + ) + .shiftedBy(-mintInfo.decimals) + .integerValue() + .toString() + : undefined, + [totalAmount, mintInfo] + ) + + const tokenName = + getMintMetadata(relevantMint)?.name ?? realm?.account.name ?? '' + + const disabled = + realmConfig?.account.councilTokenConfig.tokenType === + GoverningTokenType.Dormant + + return ( +
+
+
+ {tokenName} + {role === 'council' ? ' Council' : ''} Votes +
+
+
+
+ {formattedTotal ?? 0} +
+
+ + {mintInfo && ( + + )} +
+
+ {children} +
+ ) +} diff --git a/PythVotePlugin/components/PythAccountDetails.tsx b/PythVotePlugin/components/PythAccountDetails.tsx new file mode 100644 index 0000000000..09c81443b1 --- /dev/null +++ b/PythVotePlugin/components/PythAccountDetails.tsx @@ -0,0 +1,142 @@ +import { MintInfo } from '@solana/spl-token' +import BN from 'bn.js' +import useRealm from '@hooks/useRealm' +import { GoverningTokenType } from '@solana/spl-governance' +import { fmtMintAmount } from '@tools/sdk/units' +import { useEffect } from 'react' +import useWalletOnePointOh from '@hooks/useWalletOnePointOh' +import { + useUserCommunityTokenOwnerRecord, +} from '@hooks/queries/tokenOwnerRecord' +import { useRealmConfigQuery } from '@hooks/queries/realmConfig' +import VanillaWithdrawTokensButton from '@components/TokenBalance/VanillaWithdrawTokensButton' +import { useRealmCommunityMintInfoQuery } from '@hooks/queries/mintInfo' +import PythVotingPower from './PythVotingPower' + +export const PYTH_INSTRUCTIONS = "You can deposit Pyth tokens at https://staking.pyth.network/. If you previously deposited tokens on https://app.realms.today/dao/PYTH, use the button below to withdraw them immediately. Those tokens have no voting power." + +const TokenDeposit = ({ + mint, + inAccountDetails, + setHasGovPower, +}: { + mint: MintInfo | undefined + inAccountDetails?: boolean + setHasGovPower?: (hasGovPower: boolean) => void +}) => { + const wallet = useWalletOnePointOh() + const connected = !!wallet?.connected + + const ownTokenRecord = useUserCommunityTokenOwnerRecord().data?.result + const config = useRealmConfigQuery().data?.result + + const relevantTokenConfig = config?.account.communityTokenConfig + const isMembership = + relevantTokenConfig?.tokenType === GoverningTokenType.Membership + + const { realmTokenAccount } = useRealm() + + const depositTokenRecord = ownTokenRecord + const depositTokenAccount = realmTokenAccount + + const hasTokensInWallet = + depositTokenAccount && depositTokenAccount.account.amount.gt(new BN(0)) + + const hasTokensDeposited = + depositTokenRecord && + depositTokenRecord.account.governingTokenDepositAmount.gt(new BN(0)) + + const availableTokens = + depositTokenRecord && mint + ? fmtMintAmount( + mint, + depositTokenRecord.account.governingTokenDepositAmount + ) + : '0' + + useEffect(() => { + if (availableTokens != '0' || hasTokensDeposited || hasTokensInWallet) { + if (setHasGovPower) setHasGovPower(true) + } + }, [availableTokens, hasTokensDeposited, hasTokensInWallet, setHasGovPower]) + + const canShowAvailableTokensMessage = hasTokensInWallet && connected + const tokensToShow = + hasTokensInWallet && depositTokenAccount + ? fmtMintAmount(mint, depositTokenAccount.account.amount) + : hasTokensInWallet + ? availableTokens + : 0 + + // Do not show deposits for mints with zero supply because nobody can deposit anyway + if (!mint || mint.supply.isZero()) { + return null + } + + return ( +
+ {(availableTokens != '0' || inAccountDetails) && ( +
+ +
+ )} + +
+ You have {tokensToShow} {hasTokensDeposited ? `more ` : ``} tokens available to deposit. +
+
+ {PYTH_INSTRUCTIONS} +
+ +
+ {!isMembership && // Membership tokens can't be withdrawn (that is their whole point, actually) + inAccountDetails && ( + + )} +
+
+ ) +} + +const PythAccountDetails = () => { + const mint = useRealmCommunityMintInfoQuery().data?.result + const wallet = useWalletOnePointOh() + const connected = !!wallet?.connected + const hasLoaded = mint + + return ( + <> + {hasLoaded ? ( +
+ {!connected && ( +
+ Connect your wallet to see governance power +
+ )} + {( + + )} +
+ ) : ( + <> +
+
+ + )} + + ) +} + +export default PythAccountDetails diff --git a/PythVotePlugin/components/PythVotingPower.tsx b/PythVotePlugin/components/PythVotingPower.tsx new file mode 100644 index 0000000000..39bc29442c --- /dev/null +++ b/PythVotePlugin/components/PythVotingPower.tsx @@ -0,0 +1,109 @@ +import { BigNumber } from 'bignumber.js' +import { useMemo } from 'react' +import { useRealmQuery } from '@hooks/queries/realm' +import { useMintInfoByPubkeyQuery } from '@hooks/queries/mintInfo' +import { useConnection } from '@solana/wallet-adapter-react' +import { getPythGovPower } from '@hooks/queries/governancePower' +import { useAsync } from 'react-async-hook' +import BN from 'bn.js' +import { getMintMetadata } from '@components/instructions/programs/splToken' +import VotingPowerPct from '@components/ProposalVotingPower/VotingPowerPct' +import clsx from 'clsx' +import { useRealmConfigQuery } from '@hooks/queries/realmConfig' +import { GoverningTokenType } from '@solana/spl-governance' +import usePythScalingFactor from '@hooks/PythNetwork/useScalingFactor' +import useWalletOnePointOh from '@hooks/useWalletOnePointOh' + +export const PYTH_INSTRUCTIONS = "You can deposit Pyth tokens at https://staking.pyth.network/. If you previously deposited tokens on https://app.realms.today/dao/PYTH, use the button below to withdraw them immediately. Those tokens have no voting power." + +interface Props { + className?: string + role: 'community' | 'council' + hideIfZero?: boolean + children?: React.ReactNode +} + +export default function PythVotingPower({ + role, + hideIfZero, + children, + ...props +}: Props) { + const realm = useRealmQuery().data?.result + const realmConfig = useRealmConfigQuery().data?.result + + const wallet = useWalletOnePointOh(); + + + const { connection } = useConnection() + + const relevantMint = + role === 'community' + ? realm?.account.communityMint + : realm?.account.config.councilMint + + const mintInfo = useMintInfoByPubkeyQuery(relevantMint).data?.result + + const { result: personalAmount } = useAsync( + async () => wallet?.publicKey && getPythGovPower(connection, wallet?.publicKey), + [connection, wallet] + ) + + const pythScalingFactor: number | undefined = usePythScalingFactor(); + + const totalAmount = personalAmount ?? new BN(0) + + const formattedTotal = useMemo( + () => + mintInfo && totalAmount !== undefined + ? new BigNumber(totalAmount.toString()) + .multipliedBy(pythScalingFactor ?? 1) + .shiftedBy(-mintInfo.decimals) + .integerValue() + .toString() + : undefined, + [totalAmount, mintInfo] + ) + + const tokenName = + getMintMetadata(relevantMint)?.name ?? realm?.account.name ?? '' + + const disabled = + role === 'community' + ? realmConfig?.account.communityTokenConfig.tokenType === + GoverningTokenType.Dormant + : realmConfig?.account.councilTokenConfig.tokenType === + GoverningTokenType.Dormant + + return ( +
+
+
+ {tokenName} + {role === 'council' ? ' Council' : ''} Votes +
+
+
+
+ {formattedTotal ?? 0} +
+
+ + {mintInfo && ( + + )} +
+
+ {children} +
+ ) +} diff --git a/QuadraticPlugin/sdk/api.ts b/QuadraticPlugin/sdk/api.ts new file mode 100644 index 0000000000..1edbff051f --- /dev/null +++ b/QuadraticPlugin/sdk/api.ts @@ -0,0 +1,100 @@ +import { PublicKey } from '@solana/web3.js' +import {Coefficients, QuadraticClient } from '@solana/governance-program-library' +import { + ProgramAccount, + Realm, + SYSTEM_PROGRAM_ID, +} from '@solana/spl-governance' +import { getRegistrarPDA } from '@utils/plugin/accounts' + +// By default, the quadratic plugin will use a function ax-2 + bx - c +// resulting in a vote weight that is the square root of the token balance +// The `a` coefficient is set to 1000, which, assuming the governance token has 6 decimals, +// will result in a vote weight that is the square root of the token balance in major denomination. +// For example, if the token balance is 100, then the vote weight will be: +// sqrt(100 * 10^6) = 10,000 * 10^3 = 10,000,000 = 10 votes +// This should be handled dynamically by the UI in the future. +export const DEFAULT_COEFFICIENTS: Coefficients = [1000, 0, 0] + +export const toAnchorType = (coefficients: Coefficients) => ({ + a: coefficients[0], + b: coefficients[1], + c: coefficients[2], +}) + +export type AnchorParams = { + quadraticCoefficients: { + a: number; + b: number; + c: number; + } +} + +// Create an instruction to create a registrar account for a given realm +export const createQuadraticRegistrarIx = async ( + realm: ProgramAccount, + payer: PublicKey, + quadraticClient: QuadraticClient, + coefficients?: Coefficients, + predecessor?: PublicKey +) => { + const { registrar } = getRegistrarPDA( + realm.pubkey, + realm.account.communityMint, + quadraticClient.program.programId + ) + + const remainingAccounts = predecessor + ? [{ pubkey: predecessor, isSigner: false, isWritable: false }] + : [] + + return quadraticClient!.program.methods + .createRegistrar( + toAnchorType(coefficients || DEFAULT_COEFFICIENTS), + !!predecessor + ) + .accounts({ + registrar, + realm: realm.pubkey, + governanceProgramId: realm.owner, + realmAuthority: realm.account.authority!, + governingTokenMint: realm.account.communityMint!, + payer, + systemProgram: SYSTEM_PROGRAM_ID, + }) + .remainingAccounts(remainingAccounts) + .instruction() +} +// Create an instruction to configure a registrar account for a given realm +export const configureQuadraticRegistrarIx = async ( + realm: ProgramAccount, + quadraticClient: QuadraticClient, + coefficients?: Coefficients, + predecessor?: PublicKey +) => { + const { registrar } = getRegistrarPDA( + realm.pubkey, + realm.account.communityMint, + quadraticClient.program.programId + ) + const remainingAccounts = predecessor + ? [{ pubkey: predecessor, isSigner: false, isWritable: false }] + : [] + return quadraticClient.program.methods + .configureRegistrar( + toAnchorType(coefficients || DEFAULT_COEFFICIENTS), + !!predecessor + ) + .accounts({ + registrar, + realm: realm.pubkey, + realmAuthority: realm.account.authority!, + }) + .remainingAccounts(remainingAccounts) + .instruction() +} + +export const coefficientsEqual = (x: Coefficients, y: Coefficients | undefined): boolean => { + if (!y) return false + return x[0] === y[0] && x[1] === y[1] && x[2] === y[2] +} \ No newline at end of file diff --git a/README.md b/README.md index e69de29bb2..34a1477118 100644 --- a/README.md +++ b/README.md @@ -0,0 +1,7 @@ +### Using custom Swap API endpoints + +You can set custom URLs via the configuration for any self-hosted Jupiter APIs, like the [V6 Swap API](https://station.jup.ag/docs/apis/self-hosted) or [Paid Hosted APIs](https://station.jup.ag/docs/apis/self-hosted#paid-hosted-apis) Here is an example: + +``` +NEXT_PUBLIC_JUPTER_SWAP_API_ENDPOINT=https://quote-api.jup.ag/v6 +``` \ No newline at end of file diff --git a/Strategies/components/psyfi/Deposit.tsx b/Strategies/components/psyfi/Deposit.tsx index 08f3338e02..7577d0c54a 100644 --- a/Strategies/components/psyfi/Deposit.tsx +++ b/Strategies/components/psyfi/Deposit.tsx @@ -25,7 +25,6 @@ import BigNumber from 'bignumber.js' import { useRouter } from 'next/router' import { pdas } from 'psyfi-euros-test' import React, { useCallback, useEffect, useState } from 'react' -import useVotePluginsClientStore from 'stores/useVotePluginsClientStore' import { Action, CreatePsyFiStrategy, @@ -45,6 +44,8 @@ import { } from '@hooks/queries/mintInfo' import useLegacyConnectionContext from '@hooks/useLegacyConnectionContext' import { useLegacyVoterWeight } from '@hooks/queries/governancePower' +import {useVotingClients} from "@hooks/useVotingClients"; +import {useVoteByCouncilToggle} from "@hooks/useVoteByCouncilToggle"; const SOL_BUFFER = 0.02 @@ -73,9 +74,7 @@ export const Deposit: React.FC<{ canUseTransferInstruction, governedTokenAccountsWithoutNfts, } = useGovernanceAssets() - const client = useVotePluginsClientStore( - (s) => s.state.currentRealmVotingClient - ) + const votingClients = useVotingClients(); const connection = useLegacyConnectionContext() const wallet = useWalletOnePointOh() const [ownedStrategyTokenAccount, setOwnedStrategyTokenAccount] = useState< @@ -89,7 +88,7 @@ export const Deposit: React.FC<{ >() const [depositReceiptPubkey, setDepositReceiptPubkey] = useState() const [isDepositing, setIsDepositing] = useState(false) - const [voteByCouncil, setVoteByCouncil] = useState(false) + const { voteByCouncil, setVoteByCouncil } = useVoteByCouncilToggle(); const [form, setForm] = useState({ strategy: proposedInvestment, title: '', @@ -254,7 +253,7 @@ export const Deposit: React.FC<{ governedTokenAccount!.governance!.account!.proposalCount, false, connection, - client + votingClients(voteByCouncil? 'council' : 'community') ) const url = fmtUrlWithCluster( `/dao/${symbol}/proposal/${proposalAddress}` @@ -268,7 +267,7 @@ export const Deposit: React.FC<{ } // eslint-disable-next-line react-hooks/exhaustive-deps -- TODO please fix, it can cause difficult bugs. You might wanna check out https://bobbyhadz.com/blog/react-hooks-exhaustive-deps for info. -@asktree }, [ - client, + votingClients, config, connection, councilMint, diff --git a/Strategies/components/solend/SolendDeposit.tsx b/Strategies/components/solend/SolendDeposit.tsx index f9edc05517..765aa20904 100644 --- a/Strategies/components/solend/SolendDeposit.tsx +++ b/Strategies/components/solend/SolendDeposit.tsx @@ -30,7 +30,6 @@ import { getReserveData, SolendSubStrategy, } from 'Strategies/protocols/solend' -import useVotePluginsClientStore from 'stores/useVotePluginsClientStore' import { PublicKey } from '@solana/web3.js' import useWalletOnePointOh from '@hooks/useWalletOnePointOh' import { useRealmQuery } from '@hooks/queries/realm' @@ -42,6 +41,8 @@ import { import useLegacyConnectionContext from '@hooks/useLegacyConnectionContext' import { useRealmProposalsQuery } from '@hooks/queries/proposal' import { useLegacyVoterWeight } from '@hooks/queries/governancePower' +import {useVotingClients} from "@hooks/useVotingClients"; +import {useVoteByCouncilToggle} from "@hooks/useVoteByCouncilToggle"; const SOL_BUFFER = 0.02 @@ -70,10 +71,8 @@ const SolendDeposit = ({ const [deposits, setDeposits] = useState<{ [reserveAddress: string]: number }>({}) - const [voteByCouncil, setVoteByCouncil] = useState(false) - const client = useVotePluginsClientStore( - (s) => s.state.currentRealmVotingClient - ) + const { voteByCouncil, setVoteByCouncil } = useVoteByCouncilToggle(); + const votingClients = useVotingClients(); const connection = useLegacyConnectionContext() const wallet = useWalletOnePointOh() const tokenInfo = tokenPriceService.getTokenInfo(handledMint) @@ -249,7 +248,7 @@ const SolendDeposit = ({ governedTokenAccount!.governance!.account!.proposalCount, false, connection, - client + votingClients(voteByCouncil? 'council' : 'community') ) const url = fmtUrlWithCluster( `/dao/${symbol}/proposal/${proposalAddress}` diff --git a/Strategies/components/solend/SolendWithdraw.tsx b/Strategies/components/solend/SolendWithdraw.tsx index 79afbfe3f1..8733c75ead 100644 --- a/Strategies/components/solend/SolendWithdraw.tsx +++ b/Strategies/components/solend/SolendWithdraw.tsx @@ -18,7 +18,6 @@ import BigNumber from 'bignumber.js' import { useRouter } from 'next/router' import { useEffect, useState } from 'react' import { SolendStrategy } from 'Strategies/types/types' -import useVotePluginsClientStore from 'stores/useVotePluginsClientStore' import AdditionalProposalOptions from '@components/AdditionalProposalOptions' import { validateInstruction } from '@utils/instructionTools' import * as yup from 'yup' @@ -41,6 +40,8 @@ import { import useLegacyConnectionContext from '@hooks/useLegacyConnectionContext' import { useRealmProposalsQuery } from '@hooks/queries/proposal' import { useLegacyVoterWeight } from '@hooks/queries/governancePower' +import {useVotingClients} from "@hooks/useVotingClients"; +import {useVoteByCouncilToggle} from "@hooks/useVoteByCouncilToggle"; const SolendWithdraw = ({ proposedInvestment, @@ -68,13 +69,11 @@ const SolendWithdraw = ({ const { result: ownVoterWeight } = useLegacyVoterWeight() const { realmInfo } = useRealm() const [isWithdrawing, setIsWithdrawing] = useState(false) - const [voteByCouncil, setVoteByCouncil] = useState(false) + const { voteByCouncil, setVoteByCouncil } = useVoteByCouncilToggle(); const [deposits, setDeposits] = useState<{ [reserveAddress: string]: { amount: number; amountExact: number } }>({}) - const client = useVotePluginsClientStore( - (s) => s.state.currentRealmVotingClient - ) + const votingClients = useVotingClients(); const proposals = useRealmProposalsQuery().data const connection = useLegacyConnectionContext() const wallet = useWalletOnePointOh() @@ -259,7 +258,7 @@ const SolendWithdraw = ({ governedTokenAccount!.governance!.account!.proposalCount, false, connection, - client + votingClients(voteByCouncil? 'council' : 'community'), ) const url = fmtUrlWithCluster( `/dao/${symbol}/proposal/${proposalAddress}` diff --git a/Strategies/protocols/foresight/tools.ts b/Strategies/protocols/foresight/tools.ts deleted file mode 100644 index db42d1ef84..0000000000 --- a/Strategies/protocols/foresight/tools.ts +++ /dev/null @@ -1,2 +0,0 @@ -export const FORESIGHT_MINT_DEVNET = - 'H7uqouPsJkeEiLpCEoC1qYVVquDrZan6ZfdPK2gS44zm' diff --git a/TokenHaverPlugin/TokenHaverClient.ts b/TokenHaverPlugin/TokenHaverClient.ts new file mode 100644 index 0000000000..7b575a6c8e --- /dev/null +++ b/TokenHaverPlugin/TokenHaverClient.ts @@ -0,0 +1,151 @@ +import { BN, Program, Provider } from '@coral-xyz/anchor' +import { Client } from '@solana/governance-program-library' +import { SYSTEM_PROGRAM_ID } from '@solana/spl-governance' +import { PublicKey, TransactionInstruction } from '@solana/web3.js' + +import { fetchTokenAccountByPubkey } from '@hooks/queries/tokenAccount' +import { fetchRealmByPubkey } from '@hooks/queries/realm' +import { IDL, TokenHaver } from './idl/token_haver' +import { getAssociatedTokenAddress } from '@blockworks-foundation/mango-v4' +import { TOKEN_HAVER_PLUGIN } from './constants' + +export class TokenHaverClient extends Client { + readonly requiresInputVoterWeight = true + + constructor(public program: Program, public devnet: boolean) { + super(program, devnet) + } + + async calculateMaxVoterWeight( + _realm: PublicKey, + _mint: PublicKey + ): Promise { + const { result: realm } = await fetchRealmByPubkey( + this.program.provider.connection, + _realm + ) + return realm?.account.config?.communityMintMaxVoteWeightSource.value ?? null // TODO this code should not actually be called because this is not a max voter weight plugin + } + + async calculateVoterWeight( + voter: PublicKey, + realm: PublicKey, + mint: PublicKey + ): Promise { + const { registrar: registrarPk } = this.getRegistrarPDA(realm, mint) + const registrar = await this.program.account.registrar.fetch(registrarPk) + const countOfTokensUserHas = ( + await Promise.all( + registrar.mints.map(async (mint) => { + const tokenAccountPk = await getAssociatedTokenAddress(mint, voter) + const tokenAccount = await fetchTokenAccountByPubkey( + this.program.provider.connection, + tokenAccountPk + ) + return (tokenAccount.result?.amount ?? new BN(0)).gt(new BN(0)) + ? 1 + : 0 + }) + ) + ).reduce((acc, curr) => acc + curr, 0) + return new BN(countOfTokensUserHas).muln(10 ** 6) + } + + async updateVoterWeightRecord( + voter: PublicKey, + realm: PublicKey, + mint: PublicKey + //action?: VoterWeightAction | undefined, + //inputRecordCallback?: (() => Promise) | undefined + ): Promise<{ + pre: TransactionInstruction[] + post?: TransactionInstruction[] | undefined + }> { + const connection = this.program.provider.connection + const { result: realmAccount } = await fetchRealmByPubkey(connection, realm) + if (!realmAccount) throw new Error('Realm not found') + + const { voterWeightPk } = await this.getVoterWeightRecordPDA( + realm, + mint, + voter + ) + const { registrar: registrarPk } = this.getRegistrarPDA(realm, mint) + const registrar = await this.program.account.registrar.fetch(registrarPk) + + const tokenAccounts = ( + await Promise.all( + registrar.mints.map(async (mint) => { + const tokenAccountPk = await getAssociatedTokenAddress(mint, voter) + // filter out empty accounts + const account = await fetchTokenAccountByPubkey( + this.program.provider.connection, + tokenAccountPk + ) + return account.found ? tokenAccountPk : null + }) + ) + ).filter((x) => x !== null) as PublicKey[] + + const ix = await this.program.methods + .updateVoterWeightRecord() + .accountsStrict({ + voterWeightRecord: voterWeightPk, + registrar: registrarPk, + }) + .remainingAccounts( + tokenAccounts.map((pubkey) => ({ + pubkey, + isSigner: false, + isWritable: false, + })) + ) + .instruction() + + return { pre: [ix] } + } + + // NO-OP + async createMaxVoterWeightRecord(): Promise { + return null + } + + // NO-OP + async updateMaxVoterWeightRecord(): Promise { + return null + } + + static async connect( + provider: Provider, + programId = new PublicKey(TOKEN_HAVER_PLUGIN), + devnet = false + ) { + return new TokenHaverClient( + new Program(IDL, programId, provider), + devnet + ) + } + + async createVoterWeightRecord( + voter: PublicKey, + realm: PublicKey, + mint: PublicKey + ): Promise { + const { voterWeightPk } = await this.getVoterWeightRecordPDA( + realm, + mint, + voter + ) + const { registrar } = this.getRegistrarPDA(realm, mint) + + return this.program.methods + .createVoterWeightRecord(voter) + .accounts({ + voterWeightRecord: voterWeightPk, + registrar, + payer: voter, + systemProgram: SYSTEM_PROGRAM_ID, + }) + .instruction() + } +} diff --git a/TokenHaverPlugin/constants.ts b/TokenHaverPlugin/constants.ts new file mode 100644 index 0000000000..2969211106 --- /dev/null +++ b/TokenHaverPlugin/constants.ts @@ -0,0 +1,5 @@ +import { PublicKey } from '@solana/web3.js' + +export const TOKEN_HAVER_PLUGIN = new PublicKey( + '7gobfUihgoxA14RUnVaseoah89ggCgYAzgz1JoaPAXam' +) diff --git a/TokenHaverPlugin/idl/token_haver.ts b/TokenHaverPlugin/idl/token_haver.ts new file mode 100644 index 0000000000..3cefd2367e --- /dev/null +++ b/TokenHaverPlugin/idl/token_haver.ts @@ -0,0 +1,867 @@ +export type TokenHaver = { + version: '0.0.1' + name: 'token_haver' + instructions: [ + { + name: 'createRegistrar' + accounts: [ + { + name: 'registrar' + isMut: true + isSigner: false + docs: [ + 'The Realm Voter Registrar', + 'There can only be a single registrar per governance Realm and governing mint of the Realm' + ] + }, + { + name: 'governanceProgramId' + isMut: false + isSigner: false + docs: [ + 'The program id of the spl-governance program the realm belongs to' + ] + }, + { + name: 'realm' + isMut: false + isSigner: false + docs: [ + 'An spl-governance Realm', + '', + 'Realm is validated in the instruction:', + '- Realm is owned by the governance_program_id', + '- governing_token_mint must be the community or council mint', + '- realm_authority is realm.authority' + ] + }, + { + name: 'governingTokenMint' + isMut: false + isSigner: false + docs: [ + 'Either the realm community mint or the council mint.', + 'It must match Realm.community_mint or Realm.config.council_mint', + '', + 'Note: Once the Realm voter plugin is enabled the governing_token_mint is used only as identity', + 'for the voting population and the tokens of that are no longer used' + ] + }, + { + name: 'realmAuthority' + isMut: false + isSigner: true + docs: ['realm_authority must sign and match Realm.authority'] + }, + { + name: 'payer' + isMut: true + isSigner: true + }, + { + name: 'systemProgram' + isMut: false + isSigner: false + } + ] + args: [ + { + name: 'mints' + type: { + vec: 'publicKey' + } + } + ] + }, + { + name: 'createVoterWeightRecord' + accounts: [ + { + name: 'registrar' + isMut: false + isSigner: false + }, + { + name: 'voterWeightRecord' + isMut: true + isSigner: false + }, + { + name: 'payer' + isMut: true + isSigner: true + }, + { + name: 'systemProgram' + isMut: false + isSigner: false + } + ] + args: [ + { + name: 'governingTokenOwner' + type: 'publicKey' + } + ] + }, + { + name: 'updateVoterWeightRecord' + accounts: [ + { + name: 'registrar' + isMut: false + isSigner: false + docs: ['The RealmVoter voting Registrar'] + }, + { + name: 'voterWeightRecord' + isMut: true + isSigner: false + } + ] + args: [] + }, + { + name: 'configureMints' + accounts: [ + { + name: 'registrar' + isMut: true + isSigner: false + docs: ['The Registrar for the given realm and governing_token_mint'] + }, + { + name: 'realm' + isMut: false + isSigner: false + }, + { + name: 'payer' + isMut: false + isSigner: true + }, + { + name: 'realmAuthority' + isMut: false + isSigner: true + docs: ['Authority of the Realm must sign and match realm.authority'] + }, + { + name: 'systemProgram' + isMut: false + isSigner: false + } + ] + args: [ + { + name: 'mints' + type: { + vec: 'publicKey' + } + } + ] + } + ] + accounts: [ + { + name: 'maxVoterWeightRecord' + docs: [ + 'MaxVoterWeightRecord account as defined in spl-governance-addin-api', + "It's redefined here without account_discriminator for Anchor to treat it as native account", + '', + 'The account is used as an api interface to provide max voting power to the governance program from external addin contracts' + ] + type: { + kind: 'struct' + fields: [ + { + name: 'realm' + docs: ['The Realm the MaxVoterWeightRecord belongs to'] + type: 'publicKey' + }, + { + name: 'governingTokenMint' + docs: [ + 'Governing Token Mint the MaxVoterWeightRecord is associated with', + 'Note: The addin can take deposits of any tokens and is not restricted to the community or council tokens only' + ] + type: 'publicKey' + }, + { + name: 'maxVoterWeight' + docs: [ + 'Max voter weight', + 'The max voter weight provided by the addin for the given realm and governing_token_mint' + ] + type: 'u64' + }, + { + name: 'maxVoterWeightExpiry' + docs: [ + 'The slot when the max voting weight expires', + 'It should be set to None if the weight never expires', + 'If the max vote weight decays with time, for example for time locked based weights, then the expiry must be set', + 'As a pattern Revise instruction to update the max weight should be invoked before governance instruction within the same transaction', + 'and the expiry set to the current slot to provide up to date weight' + ] + type: { + option: 'u64' + } + }, + { + name: 'reserved' + docs: ['Reserved space for future versions'] + type: { + array: ['u8', 8] + } + } + ] + } + }, + { + name: 'registrar' + docs: [ + 'Registrar which stores spl-governance configurations for the given Realm' + ] + type: { + kind: 'struct' + fields: [ + { + name: 'governanceProgramId' + docs: ['spl-governance program the Realm belongs to'] + type: 'publicKey' + }, + { + name: 'realm' + docs: ['Realm of the Registrar'] + type: 'publicKey' + }, + { + name: 'governingTokenMint' + docs: [ + 'Governing token mint the Registrar is for', + 'It can either be the Community or the Council mint of the Realm', + 'When the plugin is enabled the mint is only used as the identity of the governing power (voting population)', + 'and the actual token of the mint is not used' + ] + type: 'publicKey' + }, + { + name: 'mints' + type: { + vec: 'publicKey' + } + } + ] + } + }, + { + name: 'voterWeightRecord' + docs: [ + 'VoterWeightRecord account as defined in spl-governance-addin-api', + "It's redefined here without account_discriminator for Anchor to treat it as native account", + '', + 'The account is used as an api interface to provide voting power to the governance program from external addin contracts' + ] + type: { + kind: 'struct' + fields: [ + { + name: 'realm' + docs: ['The Realm the VoterWeightRecord belongs to'] + type: 'publicKey' + }, + { + name: 'governingTokenMint' + docs: [ + 'Governing Token Mint the VoterWeightRecord is associated with', + 'Note: The addin can take deposits of any tokens and is not restricted to the community or council tokens only' + ] + type: 'publicKey' + }, + { + name: 'governingTokenOwner' + docs: [ + 'The owner of the governing token and voter', + 'This is the actual owner (voter) and corresponds to TokenOwnerRecord.governing_token_owner' + ] + type: 'publicKey' + }, + { + name: 'voterWeight' + docs: [ + "Voter's weight", + 'The weight of the voter provided by the addin for the given realm, governing_token_mint and governing_token_owner (voter)' + ] + type: 'u64' + }, + { + name: 'voterWeightExpiry' + docs: [ + 'The slot when the voting weight expires', + 'It should be set to None if the weight never expires', + 'If the voter weight decays with time, for example for time locked based weights, then the expiry must be set', + 'As a common pattern Revise instruction to update the weight should be invoked before governance instruction within the same transaction', + 'and the expiry set to the current slot to provide up to date weight' + ] + type: { + option: 'u64' + } + }, + { + name: 'weightAction' + docs: [ + "The governance action the voter's weight pertains to", + "It allows to provided voter's weight specific to the particular action the weight is evaluated for", + 'When the action is provided then the governance program asserts the executing action is the same as specified by the addin' + ] + type: { + option: { + defined: 'VoterWeightAction' + } + } + }, + { + name: 'weightActionTarget' + docs: [ + "The target the voter's weight action pertains to", + "It allows to provided voter's weight specific to the target the weight is evaluated for", + 'For example when addin supplies weight to vote on a particular proposal then it must specify the proposal as the action target', + 'When the target is provided then the governance program asserts the target is the same as specified by the addin' + ] + type: { + option: 'publicKey' + } + }, + { + name: 'reserved' + docs: ['Reserved space for future versions'] + type: { + array: ['u8', 8] + } + } + ] + } + } + ] + types: [ + { + name: 'CollectionItemChangeType' + docs: ['Enum defining collection item change type'] + type: { + kind: 'enum' + variants: [ + { + name: 'Upsert' + }, + { + name: 'Remove' + } + ] + } + }, + { + name: 'VoterWeightAction' + docs: [ + 'VoterWeightAction enum as defined in spl-governance-addin-api', + "It's redefined here for Anchor to export it to IDL" + ] + type: { + kind: 'enum' + variants: [ + { + name: 'CastVote' + }, + { + name: 'CommentProposal' + }, + { + name: 'CreateGovernance' + }, + { + name: 'CreateProposal' + }, + { + name: 'SignOffProposal' + } + ] + } + } + ] + errors: [ + { + code: 6000 + name: 'InvalidRealmAuthority' + msg: 'Invalid Realm Authority' + }, + { + code: 6001 + name: 'InvalidRealmForRegistrar' + msg: 'Invalid Realm for Registrar' + }, + { + code: 6002 + name: 'InvalidVoterWeightRecordRealm' + msg: 'Invalid VoterWeightRecord Realm' + }, + { + code: 6003 + name: 'InvalidVoterWeightRecordMint' + msg: 'Invalid VoterWeightRecord Mint' + }, + { + code: 6004 + name: 'GoverningTokenOwnerMustMatch' + msg: 'Governing TokenOwner must match' + }, + { + code: 6005 + name: 'TokenAccountWrongOwner' + msg: 'All token accounts must be owned by the governing token owner' + }, + { + code: 6006 + name: 'TokenAccountWrongMint' + msg: "All token accounts' mints must be included in the registrar" + }, + { + code: 6007 + name: 'TokenAccountNotLocked' + msg: "All token accounts' mints must be included in the registrar" + } + ] +} + +export const IDL: TokenHaver = { + version: '0.0.1', + name: 'token_haver', + instructions: [ + { + name: 'createRegistrar', + accounts: [ + { + name: 'registrar', + isMut: true, + isSigner: false, + docs: [ + 'The Realm Voter Registrar', + 'There can only be a single registrar per governance Realm and governing mint of the Realm', + ], + }, + { + name: 'governanceProgramId', + isMut: false, + isSigner: false, + docs: [ + 'The program id of the spl-governance program the realm belongs to', + ], + }, + { + name: 'realm', + isMut: false, + isSigner: false, + docs: [ + 'An spl-governance Realm', + '', + 'Realm is validated in the instruction:', + '- Realm is owned by the governance_program_id', + '- governing_token_mint must be the community or council mint', + '- realm_authority is realm.authority', + ], + }, + { + name: 'governingTokenMint', + isMut: false, + isSigner: false, + docs: [ + 'Either the realm community mint or the council mint.', + 'It must match Realm.community_mint or Realm.config.council_mint', + '', + 'Note: Once the Realm voter plugin is enabled the governing_token_mint is used only as identity', + 'for the voting population and the tokens of that are no longer used', + ], + }, + { + name: 'realmAuthority', + isMut: false, + isSigner: true, + docs: ['realm_authority must sign and match Realm.authority'], + }, + { + name: 'payer', + isMut: true, + isSigner: true, + }, + { + name: 'systemProgram', + isMut: false, + isSigner: false, + }, + ], + args: [ + { + name: 'mints', + type: { + vec: 'publicKey', + }, + }, + ], + }, + { + name: 'createVoterWeightRecord', + accounts: [ + { + name: 'registrar', + isMut: false, + isSigner: false, + }, + { + name: 'voterWeightRecord', + isMut: true, + isSigner: false, + }, + { + name: 'payer', + isMut: true, + isSigner: true, + }, + { + name: 'systemProgram', + isMut: false, + isSigner: false, + }, + ], + args: [ + { + name: 'governingTokenOwner', + type: 'publicKey', + }, + ], + }, + { + name: 'updateVoterWeightRecord', + accounts: [ + { + name: 'registrar', + isMut: false, + isSigner: false, + docs: ['The RealmVoter voting Registrar'], + }, + { + name: 'voterWeightRecord', + isMut: true, + isSigner: false, + }, + ], + args: [], + }, + { + name: 'configureMints', + accounts: [ + { + name: 'registrar', + isMut: true, + isSigner: false, + docs: ['The Registrar for the given realm and governing_token_mint'], + }, + { + name: 'realm', + isMut: false, + isSigner: false, + }, + { + name: 'payer', + isMut: false, + isSigner: true, + }, + { + name: 'realmAuthority', + isMut: false, + isSigner: true, + docs: ['Authority of the Realm must sign and match realm.authority'], + }, + { + name: 'systemProgram', + isMut: false, + isSigner: false, + }, + ], + args: [ + { + name: 'mints', + type: { + vec: 'publicKey', + }, + }, + ], + }, + ], + accounts: [ + { + name: 'maxVoterWeightRecord', + docs: [ + 'MaxVoterWeightRecord account as defined in spl-governance-addin-api', + "It's redefined here without account_discriminator for Anchor to treat it as native account", + '', + 'The account is used as an api interface to provide max voting power to the governance program from external addin contracts', + ], + type: { + kind: 'struct', + fields: [ + { + name: 'realm', + docs: ['The Realm the MaxVoterWeightRecord belongs to'], + type: 'publicKey', + }, + { + name: 'governingTokenMint', + docs: [ + 'Governing Token Mint the MaxVoterWeightRecord is associated with', + 'Note: The addin can take deposits of any tokens and is not restricted to the community or council tokens only', + ], + type: 'publicKey', + }, + { + name: 'maxVoterWeight', + docs: [ + 'Max voter weight', + 'The max voter weight provided by the addin for the given realm and governing_token_mint', + ], + type: 'u64', + }, + { + name: 'maxVoterWeightExpiry', + docs: [ + 'The slot when the max voting weight expires', + 'It should be set to None if the weight never expires', + 'If the max vote weight decays with time, for example for time locked based weights, then the expiry must be set', + 'As a pattern Revise instruction to update the max weight should be invoked before governance instruction within the same transaction', + 'and the expiry set to the current slot to provide up to date weight', + ], + type: { + option: 'u64', + }, + }, + { + name: 'reserved', + docs: ['Reserved space for future versions'], + type: { + array: ['u8', 8], + }, + }, + ], + }, + }, + { + name: 'registrar', + docs: [ + 'Registrar which stores spl-governance configurations for the given Realm', + ], + type: { + kind: 'struct', + fields: [ + { + name: 'governanceProgramId', + docs: ['spl-governance program the Realm belongs to'], + type: 'publicKey', + }, + { + name: 'realm', + docs: ['Realm of the Registrar'], + type: 'publicKey', + }, + { + name: 'governingTokenMint', + docs: [ + 'Governing token mint the Registrar is for', + 'It can either be the Community or the Council mint of the Realm', + 'When the plugin is enabled the mint is only used as the identity of the governing power (voting population)', + 'and the actual token of the mint is not used', + ], + type: 'publicKey', + }, + { + name: 'mints', + type: { + vec: 'publicKey', + }, + }, + ], + }, + }, + { + name: 'voterWeightRecord', + docs: [ + 'VoterWeightRecord account as defined in spl-governance-addin-api', + "It's redefined here without account_discriminator for Anchor to treat it as native account", + '', + 'The account is used as an api interface to provide voting power to the governance program from external addin contracts', + ], + type: { + kind: 'struct', + fields: [ + { + name: 'realm', + docs: ['The Realm the VoterWeightRecord belongs to'], + type: 'publicKey', + }, + { + name: 'governingTokenMint', + docs: [ + 'Governing Token Mint the VoterWeightRecord is associated with', + 'Note: The addin can take deposits of any tokens and is not restricted to the community or council tokens only', + ], + type: 'publicKey', + }, + { + name: 'governingTokenOwner', + docs: [ + 'The owner of the governing token and voter', + 'This is the actual owner (voter) and corresponds to TokenOwnerRecord.governing_token_owner', + ], + type: 'publicKey', + }, + { + name: 'voterWeight', + docs: [ + "Voter's weight", + 'The weight of the voter provided by the addin for the given realm, governing_token_mint and governing_token_owner (voter)', + ], + type: 'u64', + }, + { + name: 'voterWeightExpiry', + docs: [ + 'The slot when the voting weight expires', + 'It should be set to None if the weight never expires', + 'If the voter weight decays with time, for example for time locked based weights, then the expiry must be set', + 'As a common pattern Revise instruction to update the weight should be invoked before governance instruction within the same transaction', + 'and the expiry set to the current slot to provide up to date weight', + ], + type: { + option: 'u64', + }, + }, + { + name: 'weightAction', + docs: [ + "The governance action the voter's weight pertains to", + "It allows to provided voter's weight specific to the particular action the weight is evaluated for", + 'When the action is provided then the governance program asserts the executing action is the same as specified by the addin', + ], + type: { + option: { + defined: 'VoterWeightAction', + }, + }, + }, + { + name: 'weightActionTarget', + docs: [ + "The target the voter's weight action pertains to", + "It allows to provided voter's weight specific to the target the weight is evaluated for", + 'For example when addin supplies weight to vote on a particular proposal then it must specify the proposal as the action target', + 'When the target is provided then the governance program asserts the target is the same as specified by the addin', + ], + type: { + option: 'publicKey', + }, + }, + { + name: 'reserved', + docs: ['Reserved space for future versions'], + type: { + array: ['u8', 8], + }, + }, + ], + }, + }, + ], + types: [ + { + name: 'CollectionItemChangeType', + docs: ['Enum defining collection item change type'], + type: { + kind: 'enum', + variants: [ + { + name: 'Upsert', + }, + { + name: 'Remove', + }, + ], + }, + }, + { + name: 'VoterWeightAction', + docs: [ + 'VoterWeightAction enum as defined in spl-governance-addin-api', + "It's redefined here for Anchor to export it to IDL", + ], + type: { + kind: 'enum', + variants: [ + { + name: 'CastVote', + }, + { + name: 'CommentProposal', + }, + { + name: 'CreateGovernance', + }, + { + name: 'CreateProposal', + }, + { + name: 'SignOffProposal', + }, + ], + }, + }, + ], + errors: [ + { + code: 6000, + name: 'InvalidRealmAuthority', + msg: 'Invalid Realm Authority', + }, + { + code: 6001, + name: 'InvalidRealmForRegistrar', + msg: 'Invalid Realm for Registrar', + }, + { + code: 6002, + name: 'InvalidVoterWeightRecordRealm', + msg: 'Invalid VoterWeightRecord Realm', + }, + { + code: 6003, + name: 'InvalidVoterWeightRecordMint', + msg: 'Invalid VoterWeightRecord Mint', + }, + { + code: 6004, + name: 'GoverningTokenOwnerMustMatch', + msg: 'Governing TokenOwner must match', + }, + { + code: 6005, + name: 'TokenAccountWrongOwner', + msg: 'All token accounts must be owned by the governing token owner', + }, + { + code: 6006, + name: 'TokenAccountWrongMint', + msg: "All token accounts' mints must be included in the registrar", + }, + { + code: 6007, + name: 'TokenAccountNotLocked', + msg: "All token accounts' mints must be included in the registrar", + }, + ], +} diff --git a/TokenHaverPlugin/readme.md b/TokenHaverPlugin/readme.md new file mode 100644 index 0000000000..bb8a3c0fbc --- /dev/null +++ b/TokenHaverPlugin/readme.md @@ -0,0 +1 @@ +This plugin simply detects if token accounts for a list of mints are present and nonzero. It was used for Voshy's "Greed Experiment" DAO. The contract can be found on the governance-program-library. \ No newline at end of file diff --git a/VoteStakeRegistry/actions/closeDeposit.ts b/VoteStakeRegistry/actions/closeDeposit.ts index 6305ba16d4..77158520a4 100644 --- a/VoteStakeRegistry/actions/closeDeposit.ts +++ b/VoteStakeRegistry/actions/closeDeposit.ts @@ -27,12 +27,12 @@ export const closeDeposit = async ({ const instructions: TransactionInstruction[] = [] const clientProgramId = client!.program.programId - const { registrar } = await getRegistrarPDA( + const { registrar } = getRegistrarPDA( realmPk, communityMintPk, client!.program.programId ) - const { voter } = await getVoterPDA( + const { voter } = getVoterPDA( registrar, wallet!.publicKey!, clientProgramId diff --git a/VoteStakeRegistry/actions/getClawbackInstruction.ts b/VoteStakeRegistry/actions/getClawbackInstruction.ts index f73ec97f6e..c468363c83 100644 --- a/VoteStakeRegistry/actions/getClawbackInstruction.ts +++ b/VoteStakeRegistry/actions/getClawbackInstruction.ts @@ -29,12 +29,12 @@ export const getClawbackInstruction = async ({ }) => { const clientProgramId = client!.program.programId - const { registrar } = await getRegistrarPDA( + const { registrar } = getRegistrarPDA( realmPk, realmCommunityMintPk, clientProgramId ) - const { voter } = await getVoterPDA( + const { voter } = getVoterPDA( registrar, voterWalletAddress, clientProgramId diff --git a/VoteStakeRegistry/actions/getGrantInstruction.ts b/VoteStakeRegistry/actions/getGrantInstruction.ts index 0460bbfcb1..ed37997754 100644 --- a/VoteStakeRegistry/actions/getGrantInstruction.ts +++ b/VoteStakeRegistry/actions/getGrantInstruction.ts @@ -46,17 +46,17 @@ export const getGrantInstruction = async ({ const systemProgram = SystemProgram.programId const clientProgramId = client!.program.programId - const { registrar } = await getRegistrarPDA( + const { registrar } = getRegistrarPDA( realmPk, communityMintPk, clientProgramId ) - const { voter, voterBump } = await getVoterPDA( + const { voter, voterBump } = getVoterPDA( registrar, toPk, clientProgramId ) - const { voterWeightPk, voterWeightBump } = await getVoterWeightPDA( + const { voterWeightPk, voterWeightBump } = getVoterWeightPDA( registrar, toPk, clientProgramId @@ -73,7 +73,7 @@ export const getGrantInstruction = async ({ .grant( voterBump, voterWeightBump, - { [lockupKind]: {} }, + { [lockupKind]: {} } as any, // The cast to any works around an anchor issue with interpreting enums new BN(startTime), lockupPeriod, allowClawback, diff --git a/VoteStakeRegistry/actions/voteRegistryLockDeposit.ts b/VoteStakeRegistry/actions/voteRegistryLockDeposit.ts index 83dc685111..0488cbb34d 100644 --- a/VoteStakeRegistry/actions/voteRegistryLockDeposit.ts +++ b/VoteStakeRegistry/actions/voteRegistryLockDeposit.ts @@ -114,7 +114,8 @@ export const voteRegistryLockDeposit = async ({ if (!amountFromVoteRegistryDeposit.isZero()) { const period = getPeriod(lockUpPeriodInDays, lockupKind) const resetLockup = await client?.program.methods - .resetLockup(depositIdx, { [lockupKind]: {} }, period) + // The cast to any works around an anchor issue with interpreting enums + .resetLockup(depositIdx, { [lockupKind]: {} } as any, period) .accounts({ registrar: registrar, voter: voter, diff --git a/VoteStakeRegistry/components/Account/DepositCard.tsx b/VoteStakeRegistry/components/Account/DepositCard.tsx index 9c77485096..5ccf139dd5 100644 --- a/VoteStakeRegistry/components/Account/DepositCard.tsx +++ b/VoteStakeRegistry/components/Account/DepositCard.tsx @@ -7,6 +7,7 @@ import { voteRegistryWithdraw } from 'VoteStakeRegistry/actions/voteRegistryWith import { DepositWithMintAccount, LockupType, + Registrar, } from 'VoteStakeRegistry/sdk/accounts' import useDepositStore from 'VoteStakeRegistry/stores/useDepositStore' import tokenPriceService from '@utils/services/tokenPrice' @@ -15,7 +16,6 @@ import { useState } from 'react' import { closeDeposit } from 'VoteStakeRegistry/actions/closeDeposit' import { abbreviateAddress } from '@utils/formatting' import { notify } from '@utils/notifications' -import useVotePluginsClientStore from 'stores/useVotePluginsClientStore' import dayjs from 'dayjs' import { getMinDurationFmt, @@ -31,20 +31,23 @@ import { useRealmQuery } from '@hooks/queries/realm' import { useConnection } from '@solana/wallet-adapter-react' import queryClient from '@hooks/queries/queryClient' import { tokenAccountQueryKeys } from '@hooks/queries/tokenAccount' +import {useVsrClient} from "../../../VoterWeightPlugins/useVsrClient"; const DepositCard = ({ deposit, vsrClient, + registrar }: { deposit: DepositWithMintAccount - vsrClient?: VsrClient | undefined + vsrClient?: VsrClient | undefined, + registrar?: Registrar | undefined }) => { const { getOwnedDeposits } = useDepositStore() const ownTokenRecord = useUserCommunityTokenOwnerRecord().data?.result const realm = useRealmQuery().data?.result const { realmInfo } = useRealm() - const client = useVotePluginsClientStore((s) => s.state.vsrClient) + const { vsrClient: client } = useVsrClient() const actualClient = vsrClient || client const wallet = useWalletOnePointOh() const { connection } = useConnection() @@ -146,6 +149,7 @@ const DepositCard = ({ const tokenInfo = tokenPriceService.getTokenInfo( deposit.mint.publicKey.toBase58() ) + return (
@@ -209,7 +213,7 @@ const DepositCard = ({ )}`} value={`${dayjs( deposit!.nextVestingTimestamp!.toNumber() * 1000 - ).format('DD-MM-YYYY HH:mm')}`} + ).format('MM-DD-YYYY HH:mm')}`} /> )} {isRealmCommunityMint && ( @@ -217,7 +221,16 @@ const DepositCard = ({ label="Vote Multiplier" value={(deposit.votingPower.isZero() || deposit.votingPowerBaseline.isZero() - ? 0 + ? + registrar && deposit.amountDepositedNative.gt(new BN(0)) ? + deposit.votingPower.mul(new BN(100)).div( + deposit.amountDepositedNative.mul( + new BN(10).pow( + new BN(registrar!.votingMints[deposit.votingMintConfigIdx].digitShift) + ) + ) + ).toNumber() / 100 + : 0 : deposit.votingPower.mul(new BN(100)).div(deposit.votingPowerBaseline).toNumber() / 100 ).toFixed(2)} /> diff --git a/VoteStakeRegistry/components/Account/LockTokensAccount.tsx b/VoteStakeRegistry/components/Account/LockTokensAccount.tsx index 1176a3f8e0..57a96382e4 100644 --- a/VoteStakeRegistry/components/Account/LockTokensAccount.tsx +++ b/VoteStakeRegistry/components/Account/LockTokensAccount.tsx @@ -17,10 +17,9 @@ import { MintInfo } from '@solana/spl-token' import { BN } from '@coral-xyz/anchor' import tokenPriceService from '@utils/services/tokenPrice' import { getDeposits } from 'VoteStakeRegistry/tools/deposits' -import { DepositWithMintAccount } from 'VoteStakeRegistry/sdk/accounts' +import {DepositWithMintAccount, Registrar} from 'VoteStakeRegistry/sdk/accounts' import useDepositStore from 'VoteStakeRegistry/stores/useDepositStore' import { notify } from '@utils/notifications' -import useVotePluginsClientStore from 'stores/useVotePluginsClientStore' import { getTokenOwnerRecordAddress, GoverningTokenRole, @@ -44,6 +43,7 @@ import { } from '@hooks/queries/mintInfo' import { useConnection } from '@solana/wallet-adapter-react' import { useVsrGovpower } from '@hooks/queries/plugins/vsr' +import {useVsrClient} from "../../../VoterWeightPlugins/useVsrClient"; interface DepositBox { mintPk: PublicKey @@ -63,10 +63,9 @@ const LockTokensAccount: React.FC<{ const councilMint = useRealmCouncilMintInfoQuery().data?.result const { realmInfo } = useRealm() const [isLockModalOpen, setIsLockModalOpen] = useState(false) - const client = useVotePluginsClientStore((s) => s.state.vsrClient) - const registrar = useVotePluginsClientStore( - (s) => s.state.voteStakeRegistryRegistrar - ) + const { vsrClient: client, plugin } = useVsrClient(); + const registrar = plugin?.params as Registrar | undefined; + const isZeroMultiplierConfig = !registrar?.votingMints.filter( (x) => !x.maxExtraLockupVoteWeightScaledFactor.isZero() ).length @@ -74,7 +73,7 @@ const LockTokensAccount: React.FC<{ const [reducedDeposits, setReducedDeposits] = useState([]) const ownDeposits = useDepositStore((s) => s.state.deposits) const [deposits, setDeposits] = useState([]) - const votingPower = useVsrGovpower().result?.result ?? new BN(0) + const votingPower = useVsrGovpower().data?.result ?? new BN(0) const [votingPowerFromDeposits, setVotingPowerFromDeposits] = useState( new BN(0) ) @@ -366,7 +365,7 @@ const LockTokensAccount: React.FC<{ )?.index ) ?.map((x, idx) => ( - + ))} {!isZeroMultiplierConfig && (
@@ -415,7 +414,6 @@ const LockTokensAccount: React.FC<{
diff --git a/VoteStakeRegistry/components/Account/LockTokensAccountWithdraw.tsx b/VoteStakeRegistry/components/Account/LockTokensAccountWithdraw.tsx index c38c6c77f0..c0a4c58665 100644 --- a/VoteStakeRegistry/components/Account/LockTokensAccountWithdraw.tsx +++ b/VoteStakeRegistry/components/Account/LockTokensAccountWithdraw.tsx @@ -62,7 +62,7 @@ const LockTokensAccount = ({ tokenOwnerRecordPk }) => { const [reducedDeposits, setReducedDeposits] = useState([]) const [ownDeposits, setOwnDeposits] = useState([]) const [deposits, setDeposits] = useState([]) - const votingPower = useVsrGovpower().result?.result ?? new BN(0) + const votingPower = useVsrGovpower().data?.result ?? new BN(0) const [votingPowerFromDeposits, setVotingPowerFromDeposits] = useState( new BN(0) ) @@ -438,7 +438,6 @@ const LockTokensAccount = ({ tokenOwnerRecordPk }) => {
diff --git a/VoteStakeRegistry/components/Account/LockTokensModal.tsx b/VoteStakeRegistry/components/Account/LockTokensModal.tsx index 7e94e1aa01..7662e523a0 100644 --- a/VoteStakeRegistry/components/Account/LockTokensModal.tsx +++ b/VoteStakeRegistry/components/Account/LockTokensModal.tsx @@ -17,7 +17,7 @@ import { import { precision } from '@utils/formatting' import { useCallback, useEffect, useMemo, useState } from 'react' import { voteRegistryLockDeposit } from 'VoteStakeRegistry/actions/voteRegistryLockDeposit' -import { DepositWithMintAccount } from 'VoteStakeRegistry/sdk/accounts' +import { DepositWithMintAccount, Registrar } from 'VoteStakeRegistry/sdk/accounts' import { yearsToDays, daysToMonths, @@ -26,6 +26,7 @@ import { getFormattedStringFromDays, secsToDays, yearsToSecs, + daysToSecs, } from '@utils/dateTools' import useDepositStore from 'VoteStakeRegistry/stores/useDepositStore' import { voteRegistryStartUnlock } from 'VoteStakeRegistry/actions/voteRegistryStartUnlock' @@ -39,7 +40,6 @@ import { vestingPeriods, } from 'VoteStakeRegistry/tools/types' import BigNumber from 'bignumber.js' -import useVotePluginsClientStore from 'stores/useVotePluginsClientStore' import { calcMintMultiplier } from 'VoteStakeRegistry/tools/deposits' import ButtonGroup from '@components/ButtonGroup' import InlineNotification from '@components/InlineNotification' @@ -52,6 +52,7 @@ import { useRealmCommunityMintInfoQuery } from '@hooks/queries/mintInfo' import { useConnection } from '@solana/wallet-adapter-react' import { tokenAccountQueryKeys } from '@hooks/queries/tokenAccount' import queryClient from '@hooks/queries/queryClient' +import {useVsrClient} from "../../../VoterWeightPlugins/useVsrClient"; const YES = 'Yes' const NO = 'No' @@ -71,24 +72,42 @@ const LockTokensModal = ({ const { realmTokenAccount, realmInfo } = useRealm() const { data: tokenOwnerRecordPk } = useAddressQuery_CommunityTokenOwner() - const client = useVotePluginsClientStore((s) => s.state.vsrClient) - const voteStakeRegistryRegistrar = useVotePluginsClientStore( - (s) => s.state.voteStakeRegistryRegistrar - ) + const { vsrClient: client, plugin } = useVsrClient(); + const voteStakeRegistryRegistrar = plugin?.params as Registrar | undefined; + const saturationSecs = realm && voteStakeRegistryRegistrar ? + voteStakeRegistryRegistrar.votingMints.find(x => x.mint.equals( + realm.account.communityMint + ))?.lockupSaturationSecs : + undefined + const { connection } = useConnection() const endpoint = connection.rpcEndpoint const wallet = useWalletOnePointOh() const deposits = useDepositStore((s) => s.state.deposits) const fiveYearsSecs = yearsToSecs(5) - const maxLockupSecs = - (realm && - voteStakeRegistryRegistrar?.votingMints - .find((x) => x.mint.equals(realm.account.communityMint)) - ?.lockupSaturationSecs.toNumber()) || - fiveYearsSecs const lockupPeriods: Period[] = useMemo(() => { return [ + { + defaultValue: 30, + display: '30d', + }, + { + defaultValue: 60, + display: '60d', + }, + { + defaultValue: 90, + display: '90d', + }, + { + defaultValue: 120, + display: '120d', + }, + { + defaultValue: 180, + display: '180d', + }, { defaultValue: yearsToDays(1), display: '1y', @@ -110,7 +129,14 @@ const LockTokensModal = ({ display: '5y', }, { - defaultValue: 1, + defaultValue: depositToUnlock + ? Math.ceil( + secsToDays( + depositToUnlock?.lockup.endTs.toNumber() - + depositToUnlock.lockup.startTs.toNumber() + ) + ) + : 1, display: 'Custom', }, ] @@ -122,8 +148,17 @@ const LockTokensModal = ({ ) <= x.defaultValue || x.display === 'Custom' : true ) - .filter((x) => x.defaultValue <= secsToDays(maxLockupSecs)) - }, [depositToUnlock, maxLockupSecs]) + .filter((x) => { + return x.defaultValue <= secsToDays(fiveYearsSecs) + }) + }, [depositToUnlock, fiveYearsSecs]) + + const lockupLen = lockupPeriods.length + const fixedlockupPeriods = lockupPeriods.slice(0, lockupLen-1) + + const withinPeriod = saturationSecs ? + lockupPeriods.findIndex(p => daysToSecs(p.defaultValue) >= saturationSecs.toNumber()) : + 5 const maxNonCustomDaysLockup = lockupPeriods .map((x) => x.defaultValue) @@ -132,7 +167,7 @@ const LockTokensModal = ({ }) const maxMultiplier = calcMintMultiplier( maxNonCustomDaysLockup * SECS_PER_DAY, - voteStakeRegistryRegistrar, + voteStakeRegistryRegistrar ?? null, realm ) @@ -142,6 +177,7 @@ const LockTokensModal = ({ x.lockup.kind.none ) const [lockupPeriodDays, setLockupPeriodDays] = useState(0) + const allowClawback = false const [lockupPeriod, setLockupPeriod] = useState(lockupPeriods[0]) const [amount, setAmount] = useState() @@ -169,6 +205,7 @@ const LockTokensModal = ({ depositToUnlock?.amountInitiallyLockedNative ) : 0 + const maxAmountToLock = depositRecord && mint ? wantToLockMoreThenDeposited @@ -207,8 +244,9 @@ const LockTokensModal = ({ : '' const currentMultiplier = calcMintMultiplier( lockupPeriodDays * SECS_PER_DAY, - voteStakeRegistryRegistrar, - realm + voteStakeRegistryRegistrar ?? null, + realm, + lockupType.value !== 'constant' ) const currentPercentOfMaxMultiplier = (100 * currentMultiplier) / maxMultiplier @@ -427,10 +465,19 @@ const LockTokensModal = ({ lockupPeriods.find((p) => p.display === period) ) } - values={lockupPeriods.map((p) => p.display)} + values={ + withinPeriod === -1 ? + [...fixedlockupPeriods.filter((_, i) => i%2 === 0), lockupPeriods[lockupLen-1]] + .map((p) => p.display) : + [ + ...fixedlockupPeriods.slice(Math.floor(withinPeriod/2), Math.floor(withinPeriod/2)+6), + lockupPeriods[lockupLen-1] + ] + .map((p) => p.display) + } />
- {lockupPeriod.defaultValue === 1 && ( + {lockupPeriod.display === 'Custom' && ( <>
Number of days diff --git a/VoteStakeRegistry/components/TokenBalance/DepositCommunityTokensBtn.tsx b/VoteStakeRegistry/components/TokenBalance/DepositCommunityTokensBtn.tsx index 1e8d1ee51d..d5faea2244 100644 --- a/VoteStakeRegistry/components/TokenBalance/DepositCommunityTokensBtn.tsx +++ b/VoteStakeRegistry/components/TokenBalance/DepositCommunityTokensBtn.tsx @@ -9,20 +9,19 @@ import { notify } from '@utils/notifications' import { useState } from 'react' import { voteRegistryDepositWithoutLockup } from 'VoteStakeRegistry/actions/voteRegistryDepositWithoutLockup' import useDepositStore from 'VoteStakeRegistry/stores/useDepositStore' -import useVotePluginsClientStore from 'stores/useVotePluginsClientStore' import useWalletOnePointOh from '@hooks/useWalletOnePointOh' import { useRealmQuery } from '@hooks/queries/realm' import { useUserCommunityTokenOwnerRecord } from '@hooks/queries/tokenOwnerRecord' import { useConnection } from '@solana/wallet-adapter-react' import queryClient from '@hooks/queries/queryClient' import { tokenAccountQueryKeys } from '@hooks/queries/tokenAccount' +import {useVsrClient} from "../../../VoterWeightPlugins/useVsrClient"; const DepositCommunityTokensBtn = ({ className = '', inAccountDetails }) => { const { getOwnedDeposits } = useDepositStore() const realm = useRealmQuery().data?.result const { realmInfo, realmTokenAccount } = useRealm() - const client = useVotePluginsClientStore((s) => s.state.vsrClient) const [isLoading, setIsLoading] = useState(false) const wallet = useWalletOnePointOh() const connected = !!wallet?.connected @@ -30,6 +29,7 @@ const DepositCommunityTokensBtn = ({ className = '', inAccountDetails }) => { const endpoint = connection.rpcEndpoint const currentTokenOwnerRecord = useUserCommunityTokenOwnerRecord().data ?.result + const {vsrClient} = useVsrClient(); const depositAllTokens = async function () { if (!realm) { @@ -57,14 +57,14 @@ const DepositCommunityTokensBtn = ({ className = '', inAccountDetails }) => { programVersion: realmInfo?.programVersion!, amount: realmTokenAccount!.account.amount, tokenOwnerRecordPk, - client: client, + client: vsrClient, communityMintPk: realm.account.communityMint, }) await getOwnedDeposits({ realmPk: realm!.pubkey, communityMintPk: realm!.account.communityMint, walletPk: wallet!.publicKey!, - client: client!, + client: vsrClient!, connection, }) queryClient.invalidateQueries( @@ -89,16 +89,7 @@ const DepositCommunityTokensBtn = ({ className = '', inAccountDetails }) => { ? "You don't have any governance tokens in your wallet to deposit." : '' - return hasTokensInWallet && !inAccountDetails ? ( - - {isLoading ? : 'Deposit'} - - ) : inAccountDetails ? ( + return hasTokensInWallet || inAccountDetails ? (
@@ -166,7 +166,7 @@ const TokenDepositLock = ({ const wallet = useWalletOnePointOh() const connected = !!wallet?.connected const deposits = useDepositStore((s) => s.state.deposits) - const votingPower = useVsrGovpower().result?.result ?? new BN(0) + const votingPower = useVsrGovpower().data?.result ?? new BN(0) const votingPowerFromDeposits = useDepositStore( (s) => s.state.votingPowerFromDeposits ) @@ -277,9 +277,7 @@ const TokenDepositLock = ({
)}
- + {inAccountDetails && ( )} diff --git a/VoteStakeRegistry/components/TokenBalance/VSRVotingPower.tsx b/VoteStakeRegistry/components/TokenBalance/VSRVotingPower.tsx new file mode 100644 index 0000000000..05c0be0f3b --- /dev/null +++ b/VoteStakeRegistry/components/TokenBalance/VSRVotingPower.tsx @@ -0,0 +1,147 @@ +import { BigNumber } from 'bignumber.js' +import classNames from 'classnames' + +import useDepositStore from 'VoteStakeRegistry/stores/useDepositStore' +import { getMintDecimalAmount } from '@tools/sdk/units' + +import { useRealmQuery } from '@hooks/queries/realm' +import { useRealmCommunityMintInfoQuery } from '@hooks/queries/mintInfo' +import BN from 'bn.js' +import { useVsrGovpowerMulti } from '@hooks/queries/plugins/vsr' +import VotingPowerBox from 'VoteStakeRegistry/components/TokenBalance/VotingPowerBox' +import { getMintMetadata } from '@components/instructions/programs/splToken' +import { useTokenOwnerRecordsDelegatedToUser } from '@hooks/queries/tokenOwnerRecord' +import { useMemo } from 'react' +import { useSelectedDelegatorStore } from 'stores/useSelectedDelegatorStore' + +interface Props { + className?: string, + votingPower: BN | undefined, + votingPowerLoading: boolean + isLastPlugin: boolean +} + +export default function VSRCommunityVotingPower({ className, votingPower, votingPowerLoading, isLastPlugin }: Props) { + const realm = useRealmQuery().data?.result + const mint = useRealmCommunityMintInfoQuery().data?.result + + const deposits = useDepositStore((s) => s.state.deposits) + + const votingPowerFromDeposits = useDepositStore( + (s) => s.state.votingPowerFromDeposits + ) + const isLoading = useDepositStore((s) => s.state.isLoading) + + const depositRecord = deposits.find( + (deposit) => + deposit.mint.publicKey.toBase58() === + realm?.account.communityMint.toBase58() && deposit.lockup.kind.none + ) + + const depositMint = realm?.account.communityMint + + const tokenName = + getMintMetadata(depositMint)?.name ?? realm?.account.name ?? '' + + const tokenAmount = + depositRecord && mint + ? new BigNumber( + getMintDecimalAmount(mint, depositRecord.amountDepositedNative) + ) + : new BigNumber('0') + + const lockedTokensAmount = mint + ? deposits + .filter( + (x) => + typeof x.lockup.kind['none'] === 'undefined' && + x.mint.publicKey.toBase58() === + realm?.account.communityMint.toBase58() + ) + .reduce( + (curr, next) => + curr.plus(new BigNumber(next.currentlyLocked.toString())), + new BigNumber(0) + ) + .shiftedBy(-mint.decimals) + : new BigNumber('0') + + const { data: delegatedTors } = useTokenOwnerRecordsDelegatedToUser() + const selectedDelegator = useSelectedDelegatorStore( + (s) => s.communityDelegator + ) + // memoize useAsync inputs to prevent constant refetch + const relevantDelegators = useMemo( + () => + selectedDelegator !== undefined // ignore delegators if any delegator is selected + ? [] + : delegatedTors + ?.filter( + (x) => + x.account.governingTokenMint.toString() === + realm?.account.communityMint.toString() + ) + .map((x) => x.account.governingTokenOwner), + [delegatedTors, realm?.account.communityMint, selectedDelegator] + ) + const { data: delegatorPowers } = useVsrGovpowerMulti(relevantDelegators) + const totalDelegatorPower = + delegatorPowers && + mint && + Object.values(delegatorPowers).reduce( + (sum, curr) => sum.add(curr), + new BN(0) + ) + const formattedDelegatorPower = + totalDelegatorPower && + new BigNumber(totalDelegatorPower.toString()).shiftedBy(-mint.decimals) + + //const totalPower = votingPower.add(totalDelegatorPower ?? new BN(0)) + + if (isLoading || mint === undefined || votingPowerLoading) { + return ( +
+ ) + } + + return ( +
+ +
+

+ {tokenName} Deposited + + {tokenAmount.isNaN() ? '0' : tokenAmount.toFormat()} + +

+

+ {tokenName} Locked + + {lockedTokensAmount.isNaN() ? '0' : lockedTokensAmount.toFormat()} + +

+ {formattedDelegatorPower?.gt(new BigNumber(0)) && ( +

+ Votes from delegators + + {formattedDelegatorPower.isNaN() + ? '0' + : formattedDelegatorPower.toFormat()} + +

+ )} +
+
+ ) +} diff --git a/VoteStakeRegistry/components/TokenBalance/VotingPowerBox.tsx b/VoteStakeRegistry/components/TokenBalance/VotingPowerBox.tsx index 1ab65abefc..4a4c38a995 100644 --- a/VoteStakeRegistry/components/TokenBalance/VotingPowerBox.tsx +++ b/VoteStakeRegistry/components/TokenBalance/VotingPowerBox.tsx @@ -5,17 +5,20 @@ import { getMintDecimalAmount } from '@tools/sdk/units' import { LightningBoltIcon } from '@heroicons/react/solid' import Tooltip from '@components/Tooltip' import VotingPowerPct from '@components/ProposalVotingPower/VotingPowerPct' +import clsx from "clsx"; const VotingPowerBox = ({ votingPower, mint, votingPowerFromDeposits, + isLastPlugin = true, className = '', style, }: { votingPower: BN mint: MintInfo votingPowerFromDeposits: BN + isLastPlugin?: boolean className?: string style?: any }) => { @@ -33,14 +36,13 @@ const VotingPowerBox = ({ return ( <> - {' '}
-

Votes

- +

{ isLastPlugin ? 'Votes' : 'Token Power via Locking'}

+ {votingPowerBigNum.toFormat(2)}{' '} {!votingPowerFromDeposits.isZero() && !votingPower.isZero() && ( diff --git a/VoteStakeRegistry/components/TokenBalance/WithdrawCommunityTokensBtn.tsx b/VoteStakeRegistry/components/TokenBalance/WithdrawCommunityTokensBtn.tsx index 6d530505c6..01112eff22 100644 --- a/VoteStakeRegistry/components/TokenBalance/WithdrawCommunityTokensBtn.tsx +++ b/VoteStakeRegistry/components/TokenBalance/WithdrawCommunityTokensBtn.tsx @@ -15,7 +15,6 @@ import { withVoteRegistryWithdraw } from 'VoteStakeRegistry/sdk/withVoteRegistry import useDepositStore from 'VoteStakeRegistry/stores/useDepositStore' import { getProgramVersionForRealm } from '@models/registry/api' import { notify } from '@utils/notifications' -import useVotePluginsClientStore from 'stores/useVotePluginsClientStore' import { useState } from 'react' import Loading from '@components/Loading' import { useMaxVoteRecord } from '@hooks/useMaxVoteRecord' @@ -28,12 +27,13 @@ import { proposalQueryKeys } from '@hooks/queries/proposal' import queryClient from '@hooks/queries/queryClient' import asFindable from '@utils/queries/asFindable' import { tokenAccountQueryKeys } from '@hooks/queries/tokenAccount' +import { useVsrClient } from '../../../VoterWeightPlugins/useVsrClient' const WithDrawCommunityTokens = () => { const { getOwnedDeposits } = useDepositStore() - const client = useVotePluginsClientStore((s) => s.state.vsrClient) const ownTokenRecord = useUserCommunityTokenOwnerRecord().data?.result const realm = useRealmQuery().data?.result + const { vsrClient } = useVsrClient() const { realmInfo, @@ -91,7 +91,10 @@ const WithDrawCommunityTokens = () => { ) ).result if (!governance) throw new Error('failed to fetch governance') - if (proposal.account.getTimeToVoteEnd(governance.account) > 0) { + if ( + proposal.account.getTimeToVoteEnd(governance.account) > 0 && + governance.account.realm.equals(realm!.pubkey) + ) { setIsLoading(false) // Note: It's technically possible to withdraw the vote here but I think it would be confusing and people would end up unconsciously withdrawing their votes notify({ @@ -147,7 +150,7 @@ const WithDrawCommunityTokens = () => { tokenOwnerRecordPubKey: ownTokenRecord!.pubkey, depositIndex: depositRecord!.index, connection, - client: client, + client: vsrClient, splProgramId: realm!.owner, splProgramVersion: realmInfo!.programVersion, }) @@ -176,7 +179,7 @@ const WithDrawCommunityTokens = () => { realmPk: realm!.pubkey, communityMintPk: realm!.account.communityMint, walletPk: wallet!.publicKey!, - client: client!, + client: vsrClient!, connection, }) queryClient.invalidateQueries( diff --git a/VoteStakeRegistry/components/instructions/Clawback.tsx b/VoteStakeRegistry/components/instructions/Clawback.tsx index 49a13b802f..dbfd23c180 100644 --- a/VoteStakeRegistry/components/instructions/Clawback.tsx +++ b/VoteStakeRegistry/components/instructions/Clawback.tsx @@ -5,7 +5,7 @@ import React, { useMemo, useState, } from 'react' -import { TransactionInstruction } from '@solana/web3.js' +import { PublicKey, TransactionInstruction } from '@solana/web3.js' import { tryGetMint } from '@utils/tokens' import { ClawbackForm, @@ -22,7 +22,6 @@ import { NewProposalContext } from 'pages/dao/[symbol]/proposal/new' import GovernedAccountSelect from 'pages/dao/[symbol]/proposal/components/GovernedAccountSelect' import * as yup from 'yup' import { - Deposit, DepositWithMintAccount, getRegistrarPDA, emptyPk, @@ -34,11 +33,11 @@ import { fmtMintAmount } from '@tools/sdk/units' import tokenPriceService from '@utils/services/tokenPrice' import { getClawbackInstruction } from 'VoteStakeRegistry/actions/getClawbackInstruction' import { abbreviateAddress } from '@utils/formatting' -import useVotePluginsClientStore from 'stores/useVotePluginsClientStore' import { AssetAccount } from '@utils/uiTypes/assets' import { useRealmQuery } from '@hooks/queries/realm' import useLegacyConnectionContext from '@hooks/useLegacyConnectionContext' import Input from '@components/inputs/Input' +import {useVsrClient} from "VoterWeightPlugins/useVsrClient"; const Clawback = ({ index, @@ -47,7 +46,7 @@ const Clawback = ({ index: number governance: ProgramAccount | null }) => { - const client = useVotePluginsClientStore((s) => s.state.vsrClient) + const { vsrClient } = useVsrClient(); const connection = useLegacyConnectionContext() const realm = useRealmQuery().data?.result @@ -111,7 +110,7 @@ const Clawback = ({ voterDepositIndex: form.deposit.index, grantMintPk: form.deposit.mint.publicKey, realmCommunityMintPk: realm!.account.communityMint, - client, + client: vsrClient, }) serializedInstruction = serializeInstructionToBase64(clawbackIx!) } @@ -124,7 +123,7 @@ const Clawback = ({ customHoldUpTime: form.holdupTime, } return obj - }, [client, form, realmAuthorityGov, realm, schema]) + }, [vsrClient, form, realmAuthorityGov, realm, schema]) useEffect(() => { handleSetInstructions( @@ -142,12 +141,12 @@ const Clawback = ({ }, [form.governedTokenAccount, governancesArray, realm?.account.authority]) useEffect(() => { const getVoters = async () => { - const { registrar } = await getRegistrarPDA( - realm!.pubkey, - realm!.account.communityMint, - client!.program.programId + const { registrar } = getRegistrarPDA( + realm!.pubkey, + realm!.account.communityMint, + vsrClient!.program.programId ) - const resp = await client?.program.account.voter.all([ + const resp = await vsrClient?.program.account.voter.all([ { memcmp: { offset: 40, @@ -159,26 +158,27 @@ const Clawback = ({ resp ?.filter( (x) => - (x.account.deposits as Deposit[]).filter( + (x.account.deposits).filter( (depo) => depo.allowClawback ).length ) - .map((x) => x.account as Voter) || [] + // The cast works around an anchor issue with interpreting enums + .map((x) => x.account as unknown as Voter) || [] setVoters([...voters]) } - if (client) { + if (vsrClient) { getVoters() } - }, [client, realm]) + }, [vsrClient, realm]) useEffect(() => { const getOwnedDepositsInfo = async () => { - const { registrar } = await getRegistrarPDA( - realm!.pubkey, - realm!.account.communityMint, - client!.program.programId + const { registrar } = getRegistrarPDA( + realm!.pubkey, + realm!.account.communityMint, + vsrClient!.program.programId ) - const existingRegistrar = await tryGetRegistrar(registrar, client!) + const existingRegistrar = await tryGetRegistrar(registrar, vsrClient!) const mintCfgs = existingRegistrar?.votingMints const mints = {} if (mintCfgs) { @@ -211,7 +211,7 @@ const Clawback = ({ deposit: null, governedTokenAccount: undefined, })) - }, [client, connection, form.voter, realm]) + }, [vsrClient, connection, form.voter, realm]) useEffect(() => { setForm((prevForm) => ({ ...prevForm, governedTokenAccount: undefined })) @@ -231,6 +231,23 @@ const Clawback = ({ } return ( <> +

+ Use only with realm authority governance cant be executed with other + governances +

+

governance: {realmAuthorityGov?.pubkey.toBase58()}

+

+ wallet:{' '} + {realmAuthorityGov + ? PublicKey.findProgramAddressSync( + [ + Buffer.from('native-treasury'), + realmAuthorityGov!.pubkey.toBuffer(), + ], + realm!.owner + )[0].toBase58() + : null} +

+ handleSetForm({ + value: evt.target.value, + propertyName: 'bufferSpillAddress', + }) + } + noMaxWidth={true} + error={formErrors['bufferSpillAddress']} + /> @@ -232,13 +253,13 @@ const UpgradeProgram = ({ program }: { program: AssetAccount }) => { }) } /> - {canChooseWhoVote && ( - { - setVoteByCouncil(!voteByCouncil) - }} - /> + {shouldShowVoteByCouncilToggle && ( + { + setVoteByCouncil(!voteByCouncil) + }} + > )} )} diff --git a/components/ButtonGroup.tsx b/components/ButtonGroup.tsx index 9fa8aa559e..d9a2d4e746 100644 --- a/components/ButtonGroup.tsx +++ b/components/ButtonGroup.tsx @@ -44,7 +44,7 @@ const ButtonGroup: FunctionComponent = ({ key={`${v}${i}`} onClick={() => onChange(v)} style={{ - width: `${100 / values.length}%`, + width: `${98 / values.length}%`, }} > {names ? (unit ? names[i] + unit : names[i]) : unit ? v + unit : v} diff --git a/components/DepositTokensButton.tsx b/components/DepositTokensButton.tsx index cc9c977995..e84d34957e 100644 --- a/components/DepositTokensButton.tsx +++ b/components/DepositTokensButton.tsx @@ -2,7 +2,7 @@ import { BigNumber } from 'bignumber.js' import Button, { ButtonProps, SecondaryButton } from '@components/Button' import BN from 'bn.js' import useUserGovTokenAccountQuery from '@hooks/useUserGovTokenAccount' -import { useDepositCallback } from './GovernancePower/Vanilla/useDepositCallback' +import { useDepositCallback } from './GovernancePower/Power/Vanilla/useDepositCallback' import useWalletOnePointOh from '@hooks/useWalletOnePointOh' import Modal from './Modal' import { useState, useEffect } from 'react' @@ -94,7 +94,7 @@ export const DepositTokensButton = ({ await deposit(nativeAmount) setOpenModal(false) }} - disabled={humanReadableMax !== undefined && (parseInt(amount) > humanReadableMax || parseInt(amount)<= 0)} + disabled={humanReadableMax !== undefined && (parseFloat(amount) > humanReadableMax || parseFloat(amount)<= 0)} > Confirm diff --git a/components/Dialect/index.tsx b/components/Dialect/index.tsx new file mode 100644 index 0000000000..b1f0bc4e8f --- /dev/null +++ b/components/Dialect/index.tsx @@ -0,0 +1,17 @@ +'use client' + +import { web3 } from '@coral-xyz/anchor' +import { DialectSolanaSdk } from '@dialectlabs/react-sdk-blockchain-solana' +import { NotificationsButton } from '@dialectlabs/react-ui' + +const REALMS_PUBLIC_KEY = new web3.PublicKey( + 'BUxZD6aECR5B5MopyvvYqJxwSKDBhx2jSSo1U32en6mj' +) + +export default function DialectNotifications() { + return ( + + + + ) +} diff --git a/components/DialectNotificationsModal/index.tsx b/components/DialectNotificationsModal/index.tsx deleted file mode 100644 index d972c59755..0000000000 --- a/components/DialectNotificationsModal/index.tsx +++ /dev/null @@ -1,152 +0,0 @@ -import { - ConfigProps, - defaultVariables, - DialectThemeProvider, - DialectUiManagementProvider, - IncomingThemeVariables, - Notifications, -} from '@dialectlabs/react-ui' -import { - DialectSolanaSdk, - DialectSolanaWalletAdapter, - SolanaConfigProps, -} from '@dialectlabs/react-sdk-blockchain-solana' -import { SignerWalletAdapter } from '@solana/wallet-adapter-base' -import { useTheme } from 'next-themes' -import { useEffect, useMemo, useState } from 'react' -import { web3 } from '@coral-xyz/anchor' -import useWalletOnePointOh from '@hooks/useWalletOnePointOh' - -const REALMS_PUBLIC_KEY = new web3.PublicKey( - 'BUxZD6aECR5B5MopyvvYqJxwSKDBhx2jSSo1U32en6mj' -) - -const themeVariables: IncomingThemeVariables = { - dark: { - bellButton: - '!bg-bkg-2 !shadow-none text-fgd-1 h-10 rounded-full w-10 hover:bg-bkg-3', - iconButton: `${defaultVariables.dark.iconButton} hover:opacity-100`, - buttonLoading: `${defaultVariables.dark.buttonLoading} rounded-full min-h-[40px]`, - adornmentButton: `${defaultVariables.dark.adornmentButton} !text-bkg-1 bg-primary-light border-primary-light font-bold rounded-full hover:bg-fgd-1 hover:opacity-100`, - colors: { - ...defaultVariables.dark.colors, - bg: 'bg-bkg-1', - toggleBackgroundActive: 'bg-primary-light', - }, - textStyles: { - input: 'text-primary-1 bg-bkg-1 border-none hover:border-none', - }, - outlinedInput: `${defaultVariables.dark.outlinedInput} focus-within:border-primary-light`, - disabledButton: `${defaultVariables.dark.disabledButton} border-primary-light font-bold rounded-full border-fgd-3 text-fgd-3 cursor-not-allowed`, - modal: `${defaultVariables.dark.modal} bg-bkg-1 sm:border sm:border-fgd-4 shadow-md sm:rounded-md`, - modalWrapper: `${defaultVariables.dark.modalWrapper} sm:top-14 rounded-md`, - secondaryDangerButton: `${defaultVariables.dark.secondaryDangerButton} rounded-full`, - }, - light: { - bellButton: - '!bg-bkg-2 !shadow-none text-fgd-1 h-10 rounded-full w-10 hover:bg-bkg-3', - iconButton: `${defaultVariables.light.iconButton} hover:opacity-100`, - buttonLoading: `${defaultVariables.dark.buttonLoading} rounded-full min-h-[40px]`, - adornmentButton: `${defaultVariables.light.adornmentButton} bg-primary-light border-primary-light font-bold rounded-full hover:bg-fgd-1 hover:opacity-100`, - colors: { - ...defaultVariables.light.colors, - toggleBackgroundActive: 'bg-primary-light', - }, - textStyles: { - input: `${defaultVariables.light.textStyles.input} text-fgd-1 placeholder:text-fgd-3`, - body: `${defaultVariables.light.textStyles.body} text-fgd-1`, - }, - outlinedInput: `${defaultVariables.light.outlinedInput} focus-within:border-primary-light`, - modal: `${defaultVariables.light.modal} sm:border sm:rounded-md sm:border-fgd-4 sm:shadow-md`, - modalWrapper: `${defaultVariables.dark.modalWrapper} sm:top-14`, - secondaryDangerButton: `${defaultVariables.light.secondaryDangerButton} rounded-full`, - }, -} - -const solanaWalletToDialectWallet = ( - wallet?: SignerWalletAdapter -): DialectSolanaWalletAdapter | null => { - if (!wallet || !wallet.connected || wallet.connecting || !wallet.publicKey) { - return null - } - - return { - publicKey: wallet.publicKey!, - // @ts-ignore - signMessage: wallet?.signMessage - ? // @ts-ignore - (msg) => wallet.signMessage(msg) - : undefined, - - signTransaction: wallet.signTransaction as any, - signAllTransactions: wallet.signAllTransactions as any, - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - diffieHellman: wallet.wallet?.adapter?._wallet?.diffieHellman - ? async (pubKey: any) => { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - return wallet.wallet?.adapter?._wallet?.diffieHellman(pubKey) - } - : undefined, - } -} - -interface DialectNotificationsModalProps { - onBackClick?: () => void - onModalClose: () => void -} - -export default function DialectNotificationsModal( - props: DialectNotificationsModalProps -) { - const { theme } = useTheme() - const wallet = useWalletOnePointOh() - - const [ - dialectSolanaWalletAdapter, - setDialectSolanaWalletAdapter, - ] = useState(() => - solanaWalletToDialectWallet(wallet) - ) - - useEffect(() => { - setDialectSolanaWalletAdapter(solanaWalletToDialectWallet(wallet)) - }, [wallet]) - - const dialectConfig = useMemo( - (): ConfigProps => ({ - environment: 'production', - dialectCloud: { - tokenStore: 'local-storage', - }, - }), - [] - ) - - const solanaConfig: SolanaConfigProps = useMemo( - () => ({ - wallet: dialectSolanaWalletAdapter, - }), - [dialectSolanaWalletAdapter] - ) - - return ( - - - - - - - - ) -} diff --git a/components/Footer.tsx b/components/Footer.tsx index ef5688283d..26ea1dba66 100644 --- a/components/Footer.tsx +++ b/components/Footer.tsx @@ -35,7 +35,7 @@ const Footer = () => { 'lg:pb-24', 'gap-y-8', 'md:gap-y-0', - 'z-10' + 'z-[1]' )} >
{
- © 2023 Solana Technology Services LLC + © 2024 Realms Today Ltd
| diff --git a/components/ForwarderProgram/ForwarderProgram.tsx b/components/ForwarderProgram/ForwarderProgram.tsx new file mode 100644 index 0000000000..c6f168ccbd --- /dev/null +++ b/components/ForwarderProgram/ForwarderProgram.tsx @@ -0,0 +1,116 @@ +import Checkbox from '@components/inputs/Checkbox' +import Input from '@components/inputs/Input' +import { MANGO_INSTRUCTION_FORWARDER } from '@components/instructions/tools' +import { BN } from '@coral-xyz/anchor' +import { PublicKey, TransactionInstruction } from '@solana/web3.js' +import { tryParsePublicKey } from '@tools/core/pubkey' +import { useCallback, useState } from 'react' + +export function wrapWithForwarder( + ix: TransactionInstruction, + executableBy: PublicKey, + executableUntilUnixTs: number +): TransactionInstruction { + return new TransactionInstruction({ + keys: [ + { + pubkey: executableBy, + isSigner: true, + isWritable: false, + }, + { + pubkey: ix.programId, + isSigner: false, + isWritable: false, + }, + ...ix.keys, + ], + programId: new PublicKey(MANGO_INSTRUCTION_FORWARDER), + data: Buffer.concat([ + new BN(executableUntilUnixTs).toArrayLike(Buffer, 'le', 8), + ix.data, + ]), + }) +} + +export const useForwarderProgramHelpers = () => { + const [form, setForm] = useState({ + useExecutableBy: false, + wallet: '', + timestamp: '', + }) + + const withForwarderWrapper = useCallback( + (ix: TransactionInstruction) => { + if (form.useExecutableBy) { + return wrapWithForwarder( + ix, + new PublicKey(form.wallet), + Number(form.timestamp) + ) + } else { + return ix + } + }, + [form] + ) + return { form, setForm, withForwarderWrapper } +} + +type ForwarderProgramForm = { + useExecutableBy: boolean + wallet: string + timestamp: string +} + +const ForwarderProgram = ({ + form, + setForm, +}: ReturnType) => { + const isInvalidPubkey = form.wallet && !tryParsePublicKey(form.wallet) + return ( +
+
+ { + setForm({ + ...form, + useExecutableBy: e.target.checked, + }) + }} + label={'Use executable by'} + /> +
+ {form.useExecutableBy && ( + <> + + setForm({ + ...form, + wallet: evt.target.value, + }) + } + error={isInvalidPubkey ? 'Invalid publickey' : ''} + /> + + setForm({ + ...form, + timestamp: evt.target.value, + }) + } + /> + + )} +
+ ) +} + +export default ForwarderProgram diff --git a/components/Gateway/GatewayButton.tsx b/components/Gateway/GatewayButton.tsx index b0b91b86c5..6031376932 100644 --- a/components/Gateway/GatewayButton.tsx +++ b/components/Gateway/GatewayButton.tsx @@ -1,29 +1,8 @@ -import { IdentityButton, useGateway } from '@civic/solana-gateway-react' -import { FC, useEffect } from 'react' -import useGatewayPluginStore from '../../GatewayPlugin/store/gatewayPluginStore' -import useVotePluginsClientStore from '../../stores/useVotePluginsClientStore' +import { IdentityButton } from '@civic/solana-gateway-react' +import { FC } from 'react' export const GatewayButton: FC = () => { - const { setGatewayToken, state } = useGatewayPluginStore() - const currentClient = useVotePluginsClientStore( - (s) => s.state.currentRealmVotingClient - ) - - const { gatewayToken } = useGateway() - - // As soon as the Civic GatewayProvider finds a gateway token - // add it to the state, so that the voting plugin can use it - useEffect(() => { - if ( - gatewayToken && - state.gatewayToken?.toBase58() !== gatewayToken.publicKey.toBase58() - ) { - setGatewayToken(gatewayToken.publicKey, currentClient) - } - // eslint-disable-next-line react-hooks/exhaustive-deps -- TODO please fix, it can cause difficult bugs. You might wanna check out https://bobbyhadz.com/blog/react-hooks-exhaustive-deps for info. -@asktree - }, [gatewayToken]) - return ( - + ) } diff --git a/components/Gateway/GatewayCard.tsx b/components/Gateway/GatewayCard.tsx index faa256cde7..e5d37d1997 100644 --- a/components/Gateway/GatewayCard.tsx +++ b/components/Gateway/GatewayCard.tsx @@ -1,162 +1,134 @@ -/* eslint-disable @typescript-eslint/no-non-null-asserted-optional-chain */ -import Button from '@components/Button' -import Loading from '@components/Loading' -import useRealm from '@hooks/useRealm' -import { GatewayClient } from '@solana/governance-program-library' -import { - getTokenOwnerRecordAddress, - SYSTEM_PROGRAM_ID, - withCreateTokenOwnerRecord, -} from '@solana/spl-governance' -import { Transaction, TransactionInstruction } from '@solana/web3.js' -import { sendTransaction } from '@utils/send' -import { useState, useEffect, useMemo } from 'react' -import useVotePluginsClientStore from 'stores/useVotePluginsClientStore' -import useGatewayPluginStore from '../../GatewayPlugin/store/gatewayPluginStore' import { GatewayButton } from '@components/Gateway/GatewayButton' -import { getRegistrarPDA, getVoterWeightRecord } from '@utils/plugin/accounts' -import { useRecords } from '@components/Gateway/useRecords' +import Modal from '@components/Modal' +import { InformationCircleIcon } from '@heroicons/react/solid' import useWalletOnePointOh from '@hooks/useWalletOnePointOh' -import { useRealmQuery } from '@hooks/queries/realm' +import { useState } from 'react' +import { useGatewayVoterWeightPlugin } from '../../VoterWeightPlugins' import { - useRealmCommunityMintInfoQuery, - useRealmCouncilMintInfoQuery, -} from '@hooks/queries/mintInfo' -import useLegacyConnectionContext from '@hooks/useLegacyConnectionContext' + useRealmVoterWeightPlugins, + useRealmVoterWeights, +} from '@hooks/useRealmVoterWeightPlugins' +import { BN } from '@coral-xyz/anchor' +import { GatewayStatus, useGateway } from '@civic/solana-gateway-react' +import useUserGovTokenAccountQuery from '@hooks/useUserGovTokenAccount' +import BigNumber from 'bignumber.js' +import PluginVotingPower from '@components/ProposalVotingPower/PluginVotingPower' + +interface Props { + role: 'community' | 'council' +} -// TODO lots of overlap with NftBalanceCard here - we need to separate the logic for creating the Token Owner Record -// from the rest of this logic -const GatewayCard = () => { +const GatewayCard = ({ role }: Props) => { + const [showGatewayModal, setShowGatewayModal] = useState(false) const wallet = useWalletOnePointOh() const connected = !!wallet?.connected - const client = useVotePluginsClientStore( - (s) => s.state.currentRealmVotingClient - ) - const gatekeeperNetwork = useGatewayPluginStore( - (s) => s.state.gatekeeperNetwork - ) - const isLoading = useGatewayPluginStore((s) => s.state.isLoadingGatewayToken) - const connection = useLegacyConnectionContext() - const [, setTokenOwneRecordPk] = useState('') //@asktree: ????????????????????????????????????????? - const realm = useRealmQuery().data?.result - const mint = useRealmCommunityMintInfoQuery().data?.result - const councilMint = useRealmCouncilMintInfoQuery().data?.result - - const { realmInfo } = useRealm() - const records = useRecords() - - // show the join button if any of the records required by the chain of plugins are not yet created - const showJoinButton = useMemo(() => { - return ( - (!records.tokenOwnerRecord.accountExists && - records.tokenOwnerRecord.accountRequired) || - (!records.voteWeightRecord.accountExists && - records.voteWeightRecord.accountRequired) - ) - // eslint-disable-next-line react-hooks/exhaustive-deps -- TODO please fix, it can cause difficult bugs. You might wanna check out https://bobbyhadz.com/blog/react-hooks-exhaustive-deps for info. -@asktree - }, [records, client]) - - const handleRegister = async () => { - const instructions: TransactionInstruction[] = [] - const { voterWeightPk } = await getVoterWeightRecord( - realm!.pubkey, - realm!.account.communityMint, - wallet!.publicKey!, - client.client!.program.programId - ) - const { registrar } = await getRegistrarPDA( - realm!.pubkey, - realm!.account.communityMint, - client.client!.program.programId - ) - // If a vote weight record is needed (i.e. the realm has a voter weight plugin) - // but doesn't exist yet, add the instruction to create it to the list - if ( - !records.voteWeightRecord.accountExists && - records.voteWeightRecord.accountRequired - ) { - const createVoterWeightRecordIx = await (client.client as GatewayClient).program.methods - .createVoterWeightRecord(wallet!.publicKey!) - .accounts({ - voterWeightRecord: voterWeightPk, - registrar, - payer: wallet!.publicKey!, - systemProgram: SYSTEM_PROGRAM_ID, - }) - .instruction() + const { gatewayStatus } = useGateway() - instructions.push(createVoterWeightRecordIx) - } + const { + gatekeeperNetwork, + isReady, + isEnabled, + } = useGatewayVoterWeightPlugin() + const { communityWeight, councilWeight } = useRealmVoterWeights() - // If a token owner record doesn't exist yet, - // add the instruction to create it to the list - if ( - !records.tokenOwnerRecord.accountExists && - records.tokenOwnerRecord.accountRequired - ) { - await withCreateTokenOwnerRecord( - instructions, - realm!.owner!, - realmInfo?.programVersion!, - realm!.pubkey, - wallet!.publicKey!, - realm!.account.communityMint, - wallet!.publicKey! - ) - } - const transaction = new Transaction() - transaction.add(...instructions) + const { plugins } = useRealmVoterWeightPlugins(role) - await sendTransaction({ - transaction: transaction, - wallet: wallet!, - connection: connection.current, - signers: [], - sendingMessage: `Registering`, - successMessage: `Registered`, - }) - } + const userAta = useUserGovTokenAccountQuery('community').data?.result + const depositAmount = userAta?.amount + ? new BigNumber(userAta.amount.toString()) + : new BigNumber(0) - useEffect(() => { - const getTokenOwnerRecord = async () => { - const defaultMint = !mint?.supply.isZero() - ? realm!.account.communityMint - : !councilMint?.supply.isZero() - ? realm!.account.config.councilMint - : undefined - const tokenOwnerRecordAddress = await getTokenOwnerRecordAddress( - realm!.owner, - realm!.pubkey, - defaultMint!, - wallet!.publicKey! - ) - setTokenOwneRecordPk(tokenOwnerRecordAddress.toBase58()) - } - if (realm && wallet?.connected) { - getTokenOwnerRecord() - } - // eslint-disable-next-line react-hooks/exhaustive-deps -- TODO please fix, it can cause difficult bugs. You might wanna check out https://bobbyhadz.com/blog/react-hooks-exhaustive-deps for info. -@asktree - }, [realm?.pubkey.toBase58(), wallet?.connected]) + const hasTokens = + depositAmount.isGreaterThan(0) || + councilWeight?.value?.gt(new BN(0)) || + communityWeight?.value?.gt(new BN(0)) - return ( -
-
- {!connected && ( + if (!connected) { + return ( +
+
Please connect your wallet
+
+
+ ) + } + if (hasTokens) { + return ( +
+
+
+

Verify to vote

+ + setShowGatewayModal(true)} + /> + +
+ {isEnabled && + isReady && + wallet && + wallet.publicKey && + gatekeeperNetwork && + hasTokens && } +
+

+ {gatewayStatus === GatewayStatus.ACTIVE + ? 'You are approved to vote' + : 'Verify your personhood with Civic Pass to vote.'} +

+ { + // check if the last plugin is gateway to show the voting power + plugins?.voterWeight[plugins.voterWeight.length - 1].name === + 'gateway' && + } + {showGatewayModal && ( + setShowGatewayModal(false)} + isOpen={showGatewayModal} + > +
+

Verify to vote

+
+
+ 1 +
+
+

Connect Governance Wallet

+
+ Connect the wallet with your governance token(s). + Consolidate multiple tokens into one wallet before voting. +
+
+
+
+
+ 2 +
+
+

Verify Identity

+
+ Verify your identity using Civic Pass to prevent voting + abuse, such as Sybil attacks. +
+
+
+
+
+ 3 +
+
+

Vote

+
Vote with confidence in a fair system.
+
+
+
+
)} - {isLoading && } - {!isLoading && - connected && - wallet && - wallet.publicKey && - gatekeeperNetwork && }
- {connected && showJoinButton && ( - - )} -
- ) + ) + } + return null } export default GatewayCard diff --git a/components/Gateway/GatewayProvider.tsx b/components/Gateway/GatewayProvider.tsx index c7f9017ad8..39b0e3a957 100644 --- a/components/Gateway/GatewayProvider.tsx +++ b/components/Gateway/GatewayProvider.tsx @@ -1,9 +1,9 @@ import { FC } from 'react' import { GatewayProvider as InternalGatewayProvider } from '@civic/solana-gateway-react' -import useVotePluginsClientStore from '../../stores/useVotePluginsClientStore' -import useGatewayPluginStore from '../../GatewayPlugin/store/gatewayPluginStore' import useWalletOnePointOh from '@hooks/useWalletOnePointOh' -import useLegacyConnectionContext from '@hooks/useLegacyConnectionContext' +import { useGatewayVoterWeightPlugin } from 'VoterWeightPlugins' +import { useConnection } from '@solana/wallet-adapter-react' +import {getNetworkFromEndpoint} from "@utils/connection"; /** * Wrapper for the Civic Gateway Provider react component. This component is responsible for @@ -14,22 +14,19 @@ import useLegacyConnectionContext from '@hooks/useLegacyConnectionContext' */ export const GatewayProvider: FC = ({ children }) => { const wallet = useWalletOnePointOh() - const client = useVotePluginsClientStore( - (s) => s.state.currentRealmVotingClient - ) - const gatekeeperNetwork = useGatewayPluginStore( - (s) => s.state.gatekeeperNetwork - ) - const connection = useLegacyConnectionContext() - const cluster = - connection.cluster === 'mainnet' ? 'mainnet-beta' : connection.cluster + const { gatekeeperNetwork, isReady } = useGatewayVoterWeightPlugin() + const { connection } = useConnection() + const network = getNetworkFromEndpoint(connection.rpcEndpoint); + const cluster = network === 'mainnet' + ? 'mainnet-beta' + : network - if (!wallet || !wallet.publicKey || !client || !gatekeeperNetwork) + if (!wallet || !wallet.publicKey || !isReady || !gatekeeperNetwork) return <>{children} return ( { - const client = useVotePluginsClientStore( - (s) => s.state.currentRealmVotingClient - ) - const wallet = useWalletOnePointOh() - const connection = useLegacyConnectionContext() - const realm = useRealmQuery().data?.result - const ownTokenRecord = useUserCommunityTokenOwnerRecord().data?.result - - // TODO replace these with useDispatch - const [tokenOwnerRecord, setTokenOwnerRecord] = useState({ - publicKey: null, - accountExists: false, - accountRequired: true, - }) - const [voteWeightRecord, setVoteWeightRecord] = useState({ - publicKey: null, - accountExists: false, - accountRequired: true, - }) - - const getVoteWeightRecordPK = useCallback( - async (client: Client) => { - if (realm && wallet && wallet.publicKey) { - const { voterWeightPk } = await getPluginVoterWeightRecord( - realm.pubkey, - realm.account.communityMint, - wallet.publicKey, - client.program.programId - ) - return voterWeightPk - } else { - return undefined - } - }, - // eslint-disable-next-line react-hooks/exhaustive-deps -- TODO please fix, it can cause difficult bugs. You might wanna check out https://bobbyhadz.com/blog/react-hooks-exhaustive-deps for info. -@asktree - [realm, wallet, client] - ) - - const accountExists = useCallback( - async (publicKey: PublicKey) => { - const account = await connection.current.getAccountInfo(publicKey) - return !!account - }, - [connection] - ) - - useEffect(() => { - const func = async () => { - // tokenOwnerRecord - if (ownTokenRecord) { - setTokenOwnerRecord({ - publicKey: ownTokenRecord.pubkey, - accountExists: true, - accountRequired: true, - }) - } else { - console.log('useRecords: token owner record not found') - } - - // voteWeightRecord - if (client && client.client) { - const voteWeightRecordPK = await getVoteWeightRecordPK(client.client) - if (voteWeightRecordPK) { - setVoteWeightRecord({ - publicKey: voteWeightRecordPK, - accountExists: await accountExists(voteWeightRecordPK), - accountRequired: true, - }) - } else { - console.log('useRecords: voter weight record not found') - } - } else { - console.log('useRecords: voter weight record not needed') - setVoteWeightRecord({ - publicKey: null, - accountExists: false, - accountRequired: true, - }) - } - } - func() - // eslint-disable-next-line react-hooks/exhaustive-deps -- TODO please fix, it can cause difficult bugs. You might wanna check out https://bobbyhadz.com/blog/react-hooks-exhaustive-deps for info. -@asktree - }, [client, wallet]) - - return { - tokenOwnerRecord, - voteWeightRecord, - } -} diff --git a/components/GovernancePower/GovernancePowerCard.tsx b/components/GovernancePower/GovernancePowerCard.tsx index 1465eceebf..ae2686428f 100644 --- a/components/GovernancePower/GovernancePowerCard.tsx +++ b/components/GovernancePower/GovernancePowerCard.tsx @@ -1,5 +1,4 @@ import { ChevronRightIcon } from '@heroicons/react/solid' -import { useGovernancePowerAsync } from '@hooks/queries/governancePower' import { useRealmConfigQuery } from '@hooks/queries/realmConfig' import useQueryContext from '@hooks/useQueryContext' import useWalletOnePointOh from '@hooks/useWalletOnePointOh' @@ -7,6 +6,7 @@ import { GoverningTokenType } from '@solana/spl-governance' import Link from 'next/link' import { useRouter } from 'next/router' import GovernancePowerForRole from './GovernancePowerForRole' +import { useRealmVoterWeightPlugins } from '@hooks/useRealmVoterWeightPlugins' const GovernancePowerTitle = () => { const { symbol } = useRouter().query @@ -47,10 +47,9 @@ const VanillaDeposit = ({ role }: { role: 'community' | 'council' }) => { const GovernancePowerCard = () => { const connected = useWalletOnePointOh()?.connected ?? false - const communityPower = useGovernancePowerAsync('community') - const councilPower = useGovernancePowerAsync('council') - - const bothLoading = communityPower.loading && councilPower.loading + const { isReady: communityIsReady } = useRealmVoterWeightPlugins('community') + const { isReady: councilIsReady } = useRealmVoterWeightPlugins('council') + const isReady = communityIsReady && councilIsReady const realmConfig = useRealmConfigQuery().data?.result @@ -61,7 +60,7 @@ const GovernancePowerCard = () => {
Connect your wallet to see governance power
- ) : bothLoading ? ( + ) : !isReady ? ( <>
diff --git a/components/GovernancePower/GovernancePowerForRole.tsx b/components/GovernancePower/GovernancePowerForRole.tsx index 6b627cbc9f..ebfdbafa8c 100644 --- a/components/GovernancePower/GovernancePowerForRole.tsx +++ b/components/GovernancePower/GovernancePowerForRole.tsx @@ -3,15 +3,13 @@ import { useAsync } from 'react-async-hook' import { determineVotingPowerType } from '@hooks/queries/governancePower' import { useConnection } from '@solana/wallet-adapter-react' import useSelectedRealmPubkey from '@hooks/selectedRealm/useSelectedRealmPubkey' -import LockedCommunityVotingPower from '@components/ProposalVotingPower/LockedCommunityVotingPower' -import NftVotingPower from '@components/ProposalVotingPower/NftVotingPower' -import LockedCommunityNFTRecordVotingPower from '@components/ProposalVotingPower/LockedCommunityNFTRecordVotingPower' -import VanillaVotingPower from './Vanilla/VanillaVotingPower' -import { Deposit } from './Vanilla/Deposit' -import { useUserCommunityTokenOwnerRecord } from '@hooks/queries/tokenOwnerRecord' -import { ExclamationIcon } from '@heroicons/react/solid' -import { VSR_PLUGIN_PKS } from '@constants/plugins' -import { useRealmConfigQuery } from '@hooks/queries/realmConfig' +import VanillaVotingPower from './Power/Vanilla/VanillaVotingPower' +import { Deposit } from './Power/Vanilla/Deposit' +import { useRealmVoterWeightPlugins } from '@hooks/useRealmVoterWeightPlugins' +import { PluginName } from '@constants/plugins' +import { VotingPowerCards } from '@components/GovernancePower/Power/VotingPowerCards' + +type VotingPowerDisplayType = PluginName | 'composite' export default function GovernancePowerForRole({ role, @@ -23,59 +21,30 @@ export default function GovernancePowerForRole({ }) { const { connection } = useConnection() const realmPk = useSelectedRealmPubkey() - const config = useRealmConfigQuery().data?.result - - const ownTokenRecord = useUserCommunityTokenOwnerRecord().data?.result - //if dao transited to use plugin and some users have still deposited tokens they should withdraw before - //depositing to plugin - const isVsr = - config?.account?.communityTokenConfig?.voterWeightAddin && - VSR_PLUGIN_PKS.includes( - config?.account?.communityTokenConfig?.voterWeightAddin?.toBase58() - ) - const didWithdrawFromVanillaSetup = - !ownTokenRecord || - ownTokenRecord.account.governingTokenDepositAmount.isZero() - + const { plugins } = useRealmVoterWeightPlugins(role) const wallet = useWalletOnePointOh() const connected = !!wallet?.connected - const { result: kind } = useAsync(async () => { + const { result: kind } = useAsync< + VotingPowerDisplayType | undefined + >(async () => { if (realmPk === undefined) return undefined - return didWithdrawFromVanillaSetup - ? determineVotingPowerType(connection, realmPk, role) - : 'vanilla' - }, [connection, realmPk, role, didWithdrawFromVanillaSetup]) + // if there are multiple plugins, show the generic plugin voting power + if ((plugins?.voterWeight.length ?? 0) > 1) return 'composite' + return determineVotingPowerType(connection, realmPk, role) + }, [connection, plugins?.voterWeight.length, realmPk, role]) if (connected && kind === undefined && !props.hideIfZero) { return (
) } - return ( <> {role === 'community' ? ( - kind === 'vanilla' ? ( -
- - - {isVsr && !didWithdrawFromVanillaSetup && ( - - - Please withdraw your tokens and deposit again to get governance - power - - )} -
- ) : kind === 'VSR' ? ( - - ) : kind === 'NFT' ? ( - - ) : kind === 'HeliumVSR' ? ( - - ) : null - ) : kind === 'vanilla' ? ( + + ) : // council + kind === 'vanilla' ? (
diff --git a/components/GovernancePower/Power/VSRCard.tsx b/components/GovernancePower/Power/VSRCard.tsx new file mode 100644 index 0000000000..c9cbbd09b9 --- /dev/null +++ b/components/GovernancePower/Power/VSRCard.tsx @@ -0,0 +1,40 @@ +import {FC} from "react"; +import {useUserCommunityTokenOwnerRecord} from "@hooks/queries/tokenOwnerRecord"; +import LockedCommunityVotingPower from "@components/ProposalVotingPower/LockedCommunityVotingPower"; +import VanillaVotingPower from "@components/GovernancePower/Power/Vanilla/VanillaVotingPower"; +import {ExclamationIcon} from "@heroicons/react/solid"; +import VanillaWithdrawTokensButton from "@components/TokenBalance/VanillaWithdrawTokensButton"; +import {VotingCardProps} from "@components/GovernancePower/Power/VotingPowerCards"; + +export const VSRCard: FC = ({role, ...props}) => { + const ownTokenRecord = useUserCommunityTokenOwnerRecord().data?.result + + //VSR if dao transited to use plugin and some users have still deposited tokens they should withdraw before + //depositing to plugin + const didWithdrawFromVanillaSetup = + !ownTokenRecord || + ownTokenRecord.account.governingTokenDepositAmount.isZero() + + return ( + didWithdrawFromVanillaSetup ? ( + + ) : ( + //TODO make a better generic little prompt for when a plugin is used but there are still tokens in vanilla + <> + +
+
+ + + Please withdraw your tokens and deposit again to get + governance power + +
+
+ +
+
+ + ) + ) +} \ No newline at end of file diff --git a/components/GovernancePower/Vanilla/Deposit.tsx b/components/GovernancePower/Power/Vanilla/Deposit.tsx similarity index 94% rename from components/GovernancePower/Vanilla/Deposit.tsx rename to components/GovernancePower/Power/Vanilla/Deposit.tsx index f51433d19c..36c1dcec3f 100644 --- a/components/GovernancePower/Vanilla/Deposit.tsx +++ b/components/GovernancePower/Power/Vanilla/Deposit.tsx @@ -29,7 +29,7 @@ export const Deposit = ({ role }: { role: 'community' | 'council' }) => { {mintInfo ? depositAmount.shiftedBy(-mintInfo.decimals).toFormat() : depositAmount.toFormat()}{' '} - more {tokenName} votes in your wallet. Do you want to deposit them to + more {tokenName} tokens in your wallet. Do you want to deposit them to increase your voting power in this Dao?
diff --git a/components/GovernancePower/Power/Vanilla/VanillaCard.tsx b/components/GovernancePower/Power/Vanilla/VanillaCard.tsx new file mode 100644 index 0000000000..79f896ac80 --- /dev/null +++ b/components/GovernancePower/Power/Vanilla/VanillaCard.tsx @@ -0,0 +1,11 @@ +import VanillaVotingPower from "@components/GovernancePower/Power/Vanilla/VanillaVotingPower"; +import {Deposit} from "@components/GovernancePower/Power/Vanilla/Deposit"; +import { VotingCardProps } from "../VotingPowerCards"; +import {FC} from "react"; + +export const VanillaCard:FC = (props) => ( +
+ + +
+) \ No newline at end of file diff --git a/components/GovernancePower/Vanilla/VanillaVotingPower.tsx b/components/GovernancePower/Power/Vanilla/VanillaVotingPower.tsx similarity index 84% rename from components/GovernancePower/Vanilla/VanillaVotingPower.tsx rename to components/GovernancePower/Power/Vanilla/VanillaVotingPower.tsx index 938df7198b..6a0b8a8a34 100644 --- a/components/GovernancePower/Vanilla/VanillaVotingPower.tsx +++ b/components/GovernancePower/Power/Vanilla/VanillaVotingPower.tsx @@ -5,7 +5,7 @@ import { useTokenOwnerRecordsDelegatedToUser } from '@hooks/queries/tokenOwnerRe import { useRealmQuery } from '@hooks/queries/realm' import { useMintInfoByPubkeyQuery } from '@hooks/queries/mintInfo' import { useConnection } from '@solana/wallet-adapter-react' -import { getVanillaGovpower } from '@hooks/queries/governancePower' +import { getVanillaGovpower, useVanillaGovpower } from '@hooks/queries/governancePower' import { useAddressQuery_CommunityTokenOwner, useAddressQuery_CouncilTokenOwner, @@ -19,11 +19,14 @@ import { abbreviateAddress } from '@utils/formatting' import clsx from 'clsx' import { useRealmConfigQuery } from '@hooks/queries/realmConfig' import { GoverningTokenType } from '@solana/spl-governance' +import {ExclamationIcon, QuestionMarkCircleIcon} from "@heroicons/react/outline"; +import Tooltip from "@components/Tooltip"; interface Props { className?: string role: 'community' | 'council' hideIfZero?: boolean + unrecognizedPlugin?: boolean children?: React.ReactNode } @@ -31,6 +34,7 @@ export default function VanillaVotingPower({ role, hideIfZero, children, + unrecognizedPlugin = false, ...props }: Props) { const realm = useRealmQuery().data?.result @@ -49,17 +53,14 @@ export default function VanillaVotingPower({ const mintInfo = useMintInfoByPubkeyQuery(relevantMint).data?.result - const { result: personalAmount } = useAsync( - async () => relevantTOR && getVanillaGovpower(connection, relevantTOR), - [connection, relevantTOR] - ) + const personalAmount = useVanillaGovpower(relevantTOR) // If the user is using a delegator, we want to show that and not count the other delegators const selectedDelegator = useSelectedDelegatorStore((s) => role === 'community' ? s.communityDelegator : s.councilDelegator ) - const torsDelegatedToUser = useTokenOwnerRecordsDelegatedToUser() + const { data: torsDelegatedToUser } = useTokenOwnerRecordsDelegatedToUser() const { result: delegatorsAmount } = useAsync( async () => @@ -121,10 +122,17 @@ export default function VanillaVotingPower({ disabled && 'hidden' )} > + {unrecognizedPlugin &&
+ + Unrecognized plugin + + + +
}
{tokenName} - {role === 'council' ? ' Council' : ''} Votes + {role === 'council' ? ' Council' : ''} votes
diff --git a/components/GovernancePower/Vanilla/useDepositCallback.tsx b/components/GovernancePower/Power/Vanilla/useDepositCallback.tsx similarity index 70% rename from components/GovernancePower/Vanilla/useDepositCallback.tsx rename to components/GovernancePower/Power/Vanilla/useDepositCallback.tsx index 08b47f7bde..94a69be61f 100644 --- a/components/GovernancePower/Vanilla/useDepositCallback.tsx +++ b/components/GovernancePower/Power/Vanilla/useDepositCallback.tsx @@ -2,7 +2,7 @@ import { useCallback } from 'react' import useWalletOnePointOh from '@hooks/useWalletOnePointOh' import { fetchRealmByPubkey } from '@hooks/queries/realm' import { useConnection } from '@solana/wallet-adapter-react' -import { Keypair, Transaction, TransactionInstruction } from '@solana/web3.js' +import { Keypair, TransactionInstruction } from '@solana/web3.js' import { approveTokenTransfer } from '@utils/tokens' import useSelectedRealmPubkey from '@hooks/selectedRealm/useSelectedRealmPubkey' import { withDepositGoverningTokens } from '@solana/spl-governance' @@ -12,12 +12,15 @@ import { TOKEN_PROGRAM_ID, } from '@solana/spl-token' import BN from 'bn.js' -import { sendTransaction } from '@utils/send' import { fetchProgramVersion } from '@hooks/queries/useProgramVersionQuery' +import queryClient from "@hooks/queries/queryClient"; +import {useJoinRealm} from "@hooks/useJoinRealm"; +import { SequenceType, sendTransactionsV3 } from '@utils/sendTransactions' export const useDepositCallback = ( role: 'community' | 'council' | 'undefined' ) => { + const { handleRegister } = useJoinRealm(); const wallet = useWalletOnePointOh() const walletPk = wallet?.publicKey ?? undefined const realmPk = useSelectedRealmPubkey() @@ -75,17 +78,32 @@ export const useDepositCallback = ( amount ) - const transaction = new Transaction() - transaction.add(...instructions) + // instructions required to create voter weight records for any plugins connected to the realm + // no need to create the TOR, as it is already created by the deposit. + const pluginRegisterInstructions = await handleRegister(false) - await sendTransaction({ + const txes = [[...instructions, ...pluginRegisterInstructions]].map((txBatch) => { + return { + instructionsSet: txBatch.map((x) => { + return { + transactionInstruction: x, + signers: signers, + } + }), + sequenceType: SequenceType.Sequential, + } + }) + + await sendTransactionsV3({ connection, wallet: wallet!, - transaction, - signers, - sendingMessage: 'Depositing tokens', - successMessage: 'Tokens have been deposited', + transactionInstructions: txes, }) + + // Force the UI to recalculate voter weight + queryClient.invalidateQueries({ + queryKey: ['calculateVoterWeight'], + }) }, [connection, realmPk, role, wallet, walletPk] ) diff --git a/components/GovernancePower/Power/VotingPowerCards.tsx b/components/GovernancePower/Power/VotingPowerCards.tsx new file mode 100644 index 0000000000..01d34b4c58 --- /dev/null +++ b/components/GovernancePower/Power/VotingPowerCards.tsx @@ -0,0 +1,114 @@ +import { FC, ReactNode } from 'react' +import { PluginName } from '@constants/plugins' +import { useRealmVoterWeightPlugins } from '@hooks/useRealmVoterWeightPlugins' +import PythVotingPower from '../../../PythVotePlugin/components/PythVotingPower' +import GatewayCard from '@components/Gateway/GatewayCard' +import NftVotingPower from '@components/ProposalVotingPower/NftVotingPower' +import LockedCommunityNFTRecordVotingPower from '@components/ProposalVotingPower/LockedCommunityNFTRecordVotingPower' +import QuadraticVotingPower from '@components/ProposalVotingPower/QuadraticVotingPower' +import { VSRCard } from '@components/GovernancePower/Power/VSRCard' +import { VanillaCard } from '@components/GovernancePower/Power/Vanilla/VanillaCard' +import DriftVotingPower from 'DriftStakeVoterPlugin/components/DriftVotingPower' +import TokenHaverVotingPower from '@components/ProposalVotingPower/TokenHaverVotingPower' +import ParclVotingPower from 'ParclVotePlugin/components/ParclVotingPower' + +/**** + * Note to plugin implementors. + * + * To add a plugin with a dedicated Vote Power UI, add the plugin name to the `pluginsWithDedicatedVotingPowerUI` list below. + * Then register the dedicated UI in CardForPlugin + * + ***/ + +// A list of all the plugins that have a dedicated voting power UI in realms. +// Plugins will use the vanilla voting power UI if they are not in this list. +// The vanilla voting power UI will: +// - assume the user can "deposit" tokens into the DAO +// - show the votes simply using the plucin's calculatedVoteWeight without explanation +// This is a reasonable placeholder for some plugins, but to make life easier for users, +// plugin developers may want to add their own. +const pluginsWithDedicatedVotingPowerUI = [ + 'NFT', + 'pyth', + 'HeliumVSR', + 'VSR', + 'gateway', + 'QV', + 'drift', + 'token_haver', + "parcl" +] as const + +export type VotingCardProps = { + role: 'community' | 'council' + hideIfZero?: boolean + className?: string +} + +// True if the plugin has a dedicated voting power UI +// The type assertion here does the following: +// - pass any plugin name +// - narrow the type to a plugin that requires a dedicated UI +// - adding to the pluginsWithDedicatedVotingPowerUI list forces the CardForPlugin component to be updated +const hasDedicatedVotingPowerUI = ( + plugin: PluginName +): plugin is typeof pluginsWithDedicatedVotingPowerUI[number] => + pluginsWithDedicatedVotingPowerUI.includes( + plugin as typeof pluginsWithDedicatedVotingPowerUI[number] + ) + +const CardForPlugin: FC< + { plugin: typeof pluginsWithDedicatedVotingPowerUI[number] } & VotingCardProps +> = ({ plugin, role, ...props }) => { + switch (plugin) { + case 'NFT': + return + case 'pyth': + return + case 'HeliumVSR': + return + case 'VSR': + return + case 'gateway': + return + case 'QV': + return + case 'drift': + return + case 'token_haver': + return + case 'parcl': + return + } +} + +/** + * A component that renders the voting power cards for a given set of plugins. + * + * NOTE: This applies only to the community role at present, as practically plugins are only applied to community + * governances. However, there is little reason why this could not be extended to the council role, so to ensure + * future-compatibility we are passing the role here. + */ +export const VotingPowerCards: FC = (props) => { + const { plugins } = useRealmVoterWeightPlugins(props.role) + const cards = (plugins?.voterWeight ?? []) + .map((plugin, pluginIdx): ReactNode | undefined => { + return hasDedicatedVotingPowerUI(plugin.name) ? ( + + ) : undefined + }) + .filter(Boolean) // filter out undefined + + const includesUnrecognizedPlugin = plugins?.voterWeight.some( + (plugin) => plugin.name === 'unknown' + ) + + if (!cards.length) { + // No dedicated plugin cards - add the vanilla card + cards.push( + + ) + } + + return <>{cards} +} diff --git a/components/Mango/ProgramSelector.tsx b/components/Mango/ProgramSelector.tsx new file mode 100644 index 0000000000..ba8bf37f85 --- /dev/null +++ b/components/Mango/ProgramSelector.tsx @@ -0,0 +1,73 @@ +import { useEffect, useState } from 'react' +import { + BOOST_MAINNET_GROUP, + MANGO_BOOST_PROGRAM_ID, + MANGO_V4_MAINNET_GROUP, +} from '@hooks/useMangoV4' +import { MANGO_V4_ID } from '@blockworks-foundation/mango-v4' +import { PublicKey } from '@metaplex-foundation/js' +import useProgramSelector from './useProgramSelector' +import { InstructionInputType } from 'pages/dao/[symbol]/proposal/components/instructions/inputInstructionType' +import InstructionForm, { + InstructionInput, +} from 'pages/dao/[symbol]/proposal/components/instructions/FormCreator' + +type Program = { name: string; val: PublicKey; group: PublicKey } + +interface ProgramSelectorForm { + program: Program +} + +const ProgramSelector = ({ + programSelectorHook, +}: { + programSelectorHook: ReturnType +}) => { + const programs: Program[] = [ + { + name: 'Mango v4 program', + val: MANGO_V4_ID['mainnet-beta'], + group: MANGO_V4_MAINNET_GROUP, + }, + { + name: 'JLP boost program', + val: MANGO_BOOST_PROGRAM_ID, + group: BOOST_MAINNET_GROUP, + }, + ] + const [form, setForm] = useState({ + program: programs[0], + }) + + useEffect(() => { + if (programSelectorHook.setProgram) { + programSelectorHook.setProgram(form.program) + } + }, [form.program, programSelectorHook]) + + const inputs: InstructionInput[] = [ + { + label: 'Program', + name: 'program', + type: InstructionInputType.SELECT, + initialValue: form.program, + options: programs, + }, + ] + + return ( + <> + {form && ( + null} + formErrors={{}} + > + )} + + ) +} + +export default ProgramSelector diff --git a/components/Mango/useProgramSelector.tsx b/components/Mango/useProgramSelector.tsx new file mode 100644 index 0000000000..03a61bff6b --- /dev/null +++ b/components/Mango/useProgramSelector.tsx @@ -0,0 +1,14 @@ +import { PublicKey } from '@solana/web3.js' +import { useState } from 'react' + +type Program = { name: string; val: PublicKey; group: PublicKey } + +const useProgramSelector = () => { + const [program, setProgram] = useState() + return { + program, + setProgram, + } +} + +export default useProgramSelector diff --git a/components/Members/AddMemberForm.tsx b/components/Members/AddMemberForm.tsx index 178f4ff356..672de73e32 100644 --- a/components/Members/AddMemberForm.tsx +++ b/components/Members/AddMemberForm.tsx @@ -5,7 +5,7 @@ import Button, { SecondaryButton } from '@components/Button' import VoteBySwitch from 'pages/dao/[symbol]/proposal/components/VoteBySwitch' import { abbreviateAddress, precision } from 'utils/formatting' import { getMintSchema } from 'utils/validations' -import { FC, useMemo, useState } from 'react' +import React, { FC, useMemo, useState } from 'react' import { MintForm, UiInstruction } from 'utils/uiTypes/proposalCreationTypes' import useGovernanceAssets from 'hooks/useGovernanceAssets' import { @@ -32,6 +32,7 @@ import useWalletOnePointOh from '@hooks/useWalletOnePointOh' import { useRealmQuery } from '@hooks/queries/realm' import { DEFAULT_GOVERNANCE_PROGRAM_VERSION } from '@components/instructions/tools' import useLegacyConnectionContext from '@hooks/useLegacyConnectionContext' +import {useVoteByCouncilToggle} from "@hooks/useVoteByCouncilToggle"; interface AddMemberForm extends Omit { description: string @@ -43,7 +44,6 @@ const AddMemberForm: FC<{ close: () => void; mintAccount: AssetAccount }> = ({ mintAccount, }) => { const programVersion = useProgramVersion() - const [voteByCouncil, setVoteByCouncil] = useState(false) const [showOptions, setShowOptions] = useState(false) const [isLoading, setIsLoading] = useState(false) const [formErrors, setFormErrors] = useState({}) @@ -51,12 +51,13 @@ const AddMemberForm: FC<{ close: () => void; mintAccount: AssetAccount }> = ({ const router = useRouter() const connection = useLegacyConnectionContext() const wallet = useWalletOnePointOh() + const { voteByCouncil, shouldShowVoteByCouncilToggle, setVoteByCouncil } = useVoteByCouncilToggle(); const { fmtUrlWithCluster } = useQueryContext() const { symbol } = router.query const realm = useRealmQuery().data?.result - const { realmInfo, canChooseWhoVote } = useRealm() + const { realmInfo } = useRealm() const { data: mintInfo } = useMintInfoByPubkeyQuery(mintAccount.pubkey) const programId: PublicKey | undefined = realmInfo?.programId @@ -242,6 +243,7 @@ const AddMemberForm: FC<{ close: () => void; mintAccount: AssetAccount }> = ({ router.push(url) } catch (error) { + console.log('Error creating proposal', error); notify({ type: 'error', message: `${error}`, @@ -343,13 +345,13 @@ const AddMemberForm: FC<{ close: () => void; mintAccount: AssetAccount }> = ({ onBlur={validateAmountOnBlur} /> - {canChooseWhoVote && ( - { - setVoteByCouncil(!voteByCouncil) - }} - /> + {shouldShowVoteByCouncilToggle && ( + { + setVoteByCouncil(!voteByCouncil) + }} + > )} )} diff --git a/components/Members/MemberOverview.tsx b/components/Members/MemberOverview.tsx index 08e9128011..2446768d97 100644 --- a/components/Members/MemberOverview.tsx +++ b/components/Members/MemberOverview.tsx @@ -53,7 +53,7 @@ import { } from '@hooks/queries/digitalAssets' import { useNftRegistrarCollection } from '@hooks/useNftRegistrarCollection' import { NFT_PLUGINS_PKS } from '@constants/plugins' -import {ProfileName} from "@components/Profile/ProfileName"; +import { ProfileName } from '@components/Profile/ProfileName' const RevokeMembership: FC<{ member: PublicKey; mint: PublicKey }> = ({ member, @@ -221,9 +221,11 @@ const NftDisplayList = ({ const MemberOverview = ({ member, activeMembers, + vsrDisplay }: { member: Member - activeMembers: any[] | undefined + activeMembers: any[] | undefined, + vsrDisplay?: boolean }) => { const programVersion = useProgramVersion() const realm = useRealmQuery().data?.result @@ -436,9 +438,9 @@ const MemberOverview = ({
- {(communityAmount || !councilAmount) && ( + {(communityAmount || !councilAmount && !vsrDisplay) && (
-

{tokenName} Votes

+

{tokenName} votes

{communityAmount || 0}{' '} {hasCommunityTokenOutsideRealm && ( @@ -449,9 +451,9 @@ const MemberOverview = ({

Vote Power Rank: {memberVotePowerRank}

)} - {councilAmount && ( + {councilAmount && !vsrDisplay && (
-

Council Votes

+

Council votes

{councilAmount}{' '} {hasCouncilTokenOutsideRealm && ( diff --git a/components/Members/MembersTabs.tsx b/components/Members/MembersTabs.tsx index 175b97465c..f33188870c 100644 --- a/components/Members/MembersTabs.tsx +++ b/components/Members/MembersTabs.tsx @@ -12,19 +12,21 @@ import { } from '@hooks/queries/mintInfo' import { useRealmConfigQuery } from '@hooks/queries/realmConfig' import { NFT_PLUGINS_PKS } from '@constants/plugins' -import {ProfileName} from "@components/Profile/ProfileName"; -import {ProfileImage} from "@components/Profile"; +import { ProfileName } from '@components/Profile/ProfileName' +import { ProfileImage } from '@components/Profile' interface MembersTabsProps { activeTab: Member onChange: (x) => void tabs: Array + vsrMode?: boolean } const MembersTabs: FunctionComponent = ({ activeTab, onChange, tabs, + vsrMode }) => { const realm = useRealmQuery().data?.result const mint = useRealmCommunityMintInfoQuery().data?.result @@ -69,6 +71,7 @@ const MembersTabs: FunctionComponent = ({ activeTab={activeTab} tokenName={tokenName || nftName || ''} onChange={onChange} + vsrMode={vsrMode} > ) ) @@ -86,13 +89,15 @@ const MemberItems = ({ activeTab, tokenName, onChange, + vsrMode }: { member: Member mint?: MintInfo councilMint?: MintInfo activeTab: Member tokenName: string - onChange: (member: Member) => void + onChange: (member: Member) => void, + vsrMode?: boolean }) => { const { walletAddress, @@ -123,7 +128,11 @@ const MemberItems = ({ }, [walletAddress]) const renderAddressImage = useMemo( () => ( - + ), // eslint-disable-next-line react-hooks/exhaustive-deps -- TODO please fix, it can cause difficult bugs. You might wanna check out https://bobbyhadz.com/blog/react-hooks-exhaustive-deps for info. -@asktree [walletAddress] @@ -145,25 +154,27 @@ const MemberItems = ({

{renderAddressName}

- {/*

Votes Cast: {votesCasted}

*/} - - {(communityAmount || !councilAmount) && ( - - {tokenName} Votes {communityAmount || 0} - {hasCommunityTokenOutsideRealm && ( - - )} - - )} - {councilAmount && ( - - Council Votes {councilAmount}{' '} - {hasCouncilTokenOutsideRealm && ( - - )} - - )} - + {vsrMode ? + '' : + + {(communityAmount || !councilAmount) && ( + + {tokenName} votes {communityAmount || 0} + {hasCommunityTokenOutsideRealm && ( + + )} + + )} + {councilAmount && ( + + Council votes {councilAmount}{' '} + {hasCouncilTokenOutsideRealm && ( + + )} + + )} + + }
diff --git a/components/Members/RevokeMyMembership.tsx b/components/Members/RevokeMyMembership.tsx index 27d3304288..a5618f43b2 100644 --- a/components/Members/RevokeMyMembership.tsx +++ b/components/Members/RevokeMyMembership.tsx @@ -5,7 +5,6 @@ import Modal from '@components/Modal' import { ExclamationCircleIcon, XCircleIcon } from '@heroicons/react/outline' import { useMintInfoByPubkeyQuery } from '@hooks/queries/mintInfo' import { useRealmQuery } from '@hooks/queries/realm' -import useGovernanceForGovernedAddress from '@hooks/useGovernanceForGovernedAddress' import useProgramVersion from '@hooks/useProgramVersion' import useWalletOnePointOh from '@hooks/useWalletOnePointOh' import { createRevokeGoverningTokens } from '@solana/spl-governance' @@ -62,9 +61,9 @@ const Form: FC<{ closeModal: () => void }> = ({ closeModal }) => { : (membershipTypes[selectedMembershipType] as PublicKey | undefined), [membershipTypes, selectedMembershipType] ) + const { data: mintInfo } = useMintInfoByPubkeyQuery(selectedMint) - const governance = useGovernanceForGovernedAddress(selectedMint) - + // erase errors on dirtying useEffect(() => { setFormErrors({}) @@ -94,7 +93,6 @@ const Form: FC<{ closeModal: () => void }> = ({ closeModal }) => { if ( realm === undefined || mintInfo?.result === undefined || - governance === undefined || !wallet?.publicKey ) { throw new Error('proposal created before necessary data is fetched') @@ -134,7 +132,6 @@ const Form: FC<{ closeModal: () => void }> = ({ closeModal }) => { closeModal, connection, form.amount, - governance, mintInfo?.result, programVersion, realm, diff --git a/components/Members/useMembers.tsx b/components/Members/useMembers.tsx index f4579d56bf..40bec70b02 100644 --- a/components/Members/useMembers.tsx +++ b/components/Members/useMembers.tsx @@ -9,13 +9,13 @@ import { BN_ZERO } from '@solana/spl-governance' import { getMultipleAccountInfoChunked, getTokenAccountsByMint, - parseTokenAccountData, TokenProgramAccount, } from '@utils/tokens' +import { parseTokenAccountData } from '@utils/parseTokenAccountData' import { capitalize } from '@utils/helpers' import { Member } from 'utils/uiTypes/members' import { useRealmQuery } from '@hooks/queries/realm' -import { useTokenOwnerRecordsForRealmQuery } from '@hooks/queries/tokenOwnerRecord' +import { useCouncilTokenOwnerRecordsForRealmQuery } from '@hooks/queries/tokenOwnerRecord' import { useQuery } from '@tanstack/react-query' import { useConnection } from '@solana/wallet-adapter-react' import { useRealmConfigQuery } from '@hooks/queries/realmConfig' @@ -31,7 +31,7 @@ import { BN } from '@coral-xyz/anchor' */ export const useMembersQuery = () => { const realm = useRealmQuery().data?.result - const { data: tors } = useTokenOwnerRecordsForRealmQuery() + const { data: tors } = useCouncilTokenOwnerRecordsForRealmQuery() const connection = useConnection() const config = useRealmConfigQuery().data?.result diff --git a/components/NFTVotePluginSettingsDisplay.tsx b/components/NFTVotePluginSettingsDisplay.tsx index 83268b11da..2dec9cf6c5 100644 --- a/components/NFTVotePluginSettingsDisplay.tsx +++ b/components/NFTVotePluginSettingsDisplay.tsx @@ -2,7 +2,6 @@ import { PublicKey } from '@solana/web3.js' import type BN from 'bn.js' import cx from 'classnames' -import useVotePluginsClientStore from 'stores/useVotePluginsClientStore' import NFTIcon from '@components/treasuryV2/icons/NFTCollectionPreviewIcon' import { useConnection } from '@solana/wallet-adapter-react' import { useAsync } from 'react-async-hook' @@ -10,6 +9,7 @@ import { fetchDigitalAssetById } from '@hooks/queries/digitalAssets' import { getNetworkFromEndpoint } from '@utils/connection' import { useMintInfoByPubkeyQuery } from '@hooks/queries/mintInfo' import BigNumber from 'bignumber.js' +import {useNftRegistrar} from "@hooks/useNftRegistrar"; interface CollectionConfig { collection: PublicKey @@ -23,7 +23,7 @@ interface Props { export function NFTVotePluginSettingsDisplay(props: Props) { const { connection } = useConnection() - const registrar = useVotePluginsClientStore((s) => s.state.nftMintRegistrar) + const registrar = useNftRegistrar(); const { result: configsWithNames } = useAsync(async () => { const collectionConfigs = (registrar?.collectionConfigs || diff --git a/components/NavBar.tsx b/components/NavBar.tsx index f0802479f3..ab3a147c9d 100644 --- a/components/NavBar.tsx +++ b/components/NavBar.tsx @@ -1,9 +1,9 @@ import useQueryContext from '@hooks/useQueryContext' import Link from 'next/link' import dynamic from 'next/dynamic' -import NotificationsSwitch from './NotificationsSwitch' import ThemeSwitch from './ThemeSwitch' import { ExternalLinkIcon } from '@heroicons/react/outline' +import DialectNotifications from './Dialect' const ConnectWalletButtonDynamic = dynamic( async () => await import('./ConnectWalletButton'), @@ -38,7 +38,7 @@ const NavBar = () => { - +
diff --git a/components/NewRealmWizard/components/AdvancedOptionsDropdown.tsx b/components/NewRealmWizard/components/AdvancedOptionsDropdown.tsx index 79d3cf0dd3..144eed3fd2 100644 --- a/components/NewRealmWizard/components/AdvancedOptionsDropdown.tsx +++ b/components/NewRealmWizard/components/AdvancedOptionsDropdown.tsx @@ -1,4 +1,4 @@ -import { useState } from 'react' +import { ReactNode, useState } from 'react' import { Transition } from '@headlessui/react' import Text from '@components/Text' @@ -6,6 +6,12 @@ export default function AdvancedOptionsDropdown({ className = 'mt-10 md:mt-16 w-fit', children, title = 'Advanced Options', + icon, +}: { + children: ReactNode + icon?: ReactNode + className?: string + title?: string }) { const [open, setOpen] = useState(false) return ( @@ -20,6 +26,7 @@ export default function AdvancedOptionsDropdown({ {title} + {icon}
diff --git a/components/NewRealmWizard/components/CivicPassSelector.tsx b/components/NewRealmWizard/components/CivicPassSelector.tsx new file mode 100644 index 0000000000..99e2172c4f --- /dev/null +++ b/components/NewRealmWizard/components/CivicPassSelector.tsx @@ -0,0 +1,100 @@ +import * as DropdownMenu from '@radix-ui/react-dropdown-menu' +import { useEffect, useState } from 'react' +interface Props { + className?: string + selectedPass: string + onPassSelected: (string) => void +} +import cx from '@hub/lib/cx' +import { availablePasses } from 'GatewayPlugin/config' +import { ChevronDownIcon } from '@heroicons/react/solid' + +const itemStyles = cx( + 'border', + 'cursor-pointer', + 'gap-x-4', + 'grid-cols-[150px,1fr,20px]', + 'grid', + 'h-14', + 'items-center', + 'px-4', + 'w-full', + 'rounded-md', + 'text-left', + 'transition-colors', + 'dark:bg-neutral-800', + 'dark:border-neutral-700', + 'dark:hover:bg-neutral-700' +) + +const labelStyles = cx('font-700', 'dark:text-neutral-50', 'w-full') +const iconStyles = cx('fill-neutral-500', 'h-5', 'transition-transform', 'w-4') +const descriptionStyles = cx('dark:text-neutral-400 text-sm') + +export default function CivicPassSelector({ + className, + onPassSelected, +}: Props) { + const [open, setOpen] = useState(false) + const [selectedPassState, setSelectedPass] = useState(availablePasses[0]) + + useEffect(() => { + onPassSelected(selectedPassState.value) + }, [onPassSelected, selectedPassState.value]) + + return ( +
+ +
+ +
+ {selectedPassState?.name || 'Select a Civic Pass'} +
+
+ {selectedPassState?.description || ''} +
+ +
+ + + {availablePasses.slice(0, -1).map((config, i) => ( + { + setSelectedPass(config) + }} + > +
{config.name}
+
{config.description}
+
+ ))} +
+
+
+
+ {!selectedPassState.isSybilResistance && ( +
+ Warning: This pass type does + not provide sybil resistance. +
+ )} +
+ ) +} diff --git a/components/NewRealmWizard/components/FormField.tsx b/components/NewRealmWizard/components/FormField.tsx index db40264e57..ef45925017 100644 --- a/components/NewRealmWizard/components/FormField.tsx +++ b/components/NewRealmWizard/components/FormField.tsx @@ -3,7 +3,7 @@ interface Props { advancedOption?: boolean children: React.ReactNode className?: string - description: string | React.ReactNode + description?: string | React.ReactNode disabled?: boolean optional?: boolean title: string @@ -59,14 +59,16 @@ export default function FormField({ {titleExtra}
- - {description} - + {description && ( + + {description} + + )}
{children}
) diff --git a/components/NewRealmWizard/components/FormSummary.tsx b/components/NewRealmWizard/components/FormSummary.tsx index 617664fd96..c73ffe76b7 100644 --- a/components/NewRealmWizard/components/FormSummary.tsx +++ b/components/NewRealmWizard/components/FormSummary.tsx @@ -240,6 +240,16 @@ export default function WizardSummary({ >
{formData?.name}
+ {formData?.isQuadratic && ( + + + Civic Plugin + + + Quadratic Voting Plugin + + + )} {type === MULTISIG_FORM ? (
@@ -298,8 +308,10 @@ export default function WizardSummary({ type === MULTISIG_FORM ? 'Create wallet' : `Create ${ - type === COMMUNITY_TOKEN_FORM + type === COMMUNITY_TOKEN_FORM && !formData?.isQuadratic ? 'Community Token' + : type === COMMUNITY_TOKEN_FORM && formData?.isQuadratic + ? 'Quadratic Voting' : 'NFT Community' } DAO` } diff --git a/components/NewRealmWizard/components/TokenInput.tsx b/components/NewRealmWizard/components/TokenInput.tsx index 3ac137120d..13a06a8df1 100644 --- a/components/NewRealmWizard/components/TokenInput.tsx +++ b/components/NewRealmWizard/components/TokenInput.tsx @@ -221,7 +221,7 @@ export default function TokenInput({ render={({ field, fieldState: { error } }) => ( () + useEffect(() => { + const fetchCoefficients = async () => { + const coefficients = await getCoefficients( + undefined, + new PublicKey(communityTokenMintAddress), + connection + ) + setValue('coefficientA', coefficients[0].toFixed(2)) + setValue('coefficientB', coefficients[1]) + setValue('coefficientC', coefficients[2]) + } + + // If the user wants to use a pre-existing token, we need to adjust the coefficients ot match the decimals of that token + if (communityTokenMintAddress) { + fetchCoefficients() + } + }, [connection, communityTokenMintAddress, setValue]) + useEffect(() => { updateUserInput(formData, CommunityTokenSchema, setValue) }, [formData, setValue]) @@ -221,7 +261,7 @@ export default function CommunityTokenForm({ render={({ field, fieldState: { error } }) => ( + {isQuadratic &&
+
+ Note:  + Quadratic Voting DAOs typically have a lower circulating supply factor + than non-quadratic DAOs. This is because the quadratic formula + reduces the weight of votes overall. +
+
+ Consider optionally setting a value < 1 here to increase the accuracy of approval thresholds. +
+
} )} + + ( +
+ + + +
+ )} + /> + + {isQuadratic && ( + + } + > +
+

+ Changes advised for advanced users only +

+
+ ( + +
+ +
+
+ )} + /> + + +
+

See Docs

+ + + +
+
+
+ ( + + { + preventNegativeNumberInput(ev) + field.onChange(ev) + }} + /> + + )} + /> + ( + + { + preventNegativeNumberInput(ev) + field.onChange(ev) + }} + /> + + )} + /> + ( + + { + preventNegativeNumberInput(ev) + field.onChange(ev) + }} + /> + + )} + /> +
+
+ )} { updateUserInput( @@ -97,6 +98,10 @@ export default function YesVotePercentageForm({ onSubmit({ step: currentStep, data: values }) } + useEffect(() => { + setValue(fieldName, percentageValue) + }, [fieldName, formData.isQuadratic, percentageValue, setValue]) + return (
( - + )} /> @@ -143,8 +144,9 @@ export default function YesVotePercentageForm({ > {forCommunity ? ( - Typically, newer DAOs start their community approval quorums around - 60% of total token supply. + {!formData.isQuadratic + ? 'Typically, newer DAOs start their community approval quorums around 60% of total token supply.' + : "Setting a high percentage approval quorum may result in proposals never passing in a quadratic voting DAO, as the voting power is influenced by token distribution. It's recomended to start with a low percentage and adjust as needed."} ) : forCouncil && formData?.memberAddresses?.length >= 0 ? ( <> diff --git a/components/NftVotingCountingModal.tsx b/components/NftVotingCountingModal.tsx index 5a746225d5..64d3375c5d 100644 --- a/components/NftVotingCountingModal.tsx +++ b/components/NftVotingCountingModal.tsx @@ -1,12 +1,12 @@ +import { useVotingNfts } from '@hooks/queries/plugins/nftVoter' import { usePrevious } from '@hooks/usePrevious' +import useUserOrDelegator from '@hooks/useUserOrDelegator' import useWalletOnePointOh from '@hooks/useWalletOnePointOh' -import { NftVoterClient } from '@utils/uiTypes/NftVoterClient' import useNftProposalStore from 'NftVotePlugin/NftProposalStore' -import useNftPluginStore from 'NftVotePlugin/store/nftPluginStore' import { useEffect, useState } from 'react' import useTransactionsStore from 'stores/useTransactionStore' -import useVotePluginsClientStore from 'stores/useVotePluginsClientStore' import Modal from './Modal' +import {useNftClient} from "../VoterWeightPlugins/useNftClient"; const NftVotingCountingModal = () => { const votingInProgress = useNftProposalStore((s) => s.votingInProgress) @@ -21,19 +21,18 @@ const NftVotingComponent = () => { countedNftsForProposal, proposal, } = useNftProposalStore() - const client = useVotePluginsClientStore( - (s) => s.state.currentRealmVotingClient - ) const wallet = useWalletOnePointOh() - const { votingNfts } = useNftPluginStore((s) => s.state) + const userPk = useUserOrDelegator() + const votingNfts = useVotingNfts(userPk) ?? [] const votingInProgress = useNftProposalStore((s) => s.votingInProgress) const usedNfts = countedNftsForProposal.length - const totalVotingPower = votingNfts.length + const totalVotingPower = votingNfts.length // TODO this is sometimes incorrect, power per nft is determined by config const remainingNftsToCount = totalVotingPower - usedNfts //in last tx there is max of 5 nfts const lastTransactionNftsCount = 5 const maxNftsPerTransaction = 8 + const { nftClient } = useNftClient() const [usedNftsCount, setUsedNftsCount] = useState(0) const [remainingVotingPower, setRemainingVotingPower] = useState(0) const handleCalcCountedNfts = (val: number) => { @@ -81,7 +80,7 @@ const NftVotingComponent = () => { wrapperStyle={{ top: '-350px' }} onClose={() => closeNftVotingCountingModal( - (client.client as unknown) as NftVoterClient, + nftClient!, proposal!, wallet!.publicKey! ) diff --git a/components/NotifiIcon.tsx b/components/NotifiIcon.tsx deleted file mode 100644 index 6883c5f639..0000000000 --- a/components/NotifiIcon.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import { useTheme } from 'next-themes' -import NotifiIconDark from './NotificationsSwitch/NotifiIconDark' -import NotifiIconLight from './NotificationsSwitch/NotifiIconLight' - -const NotifiIcon = ({ height = '30' }) => { - const { theme } = useTheme() - return theme === 'Dark' ? ( - - ) : ( - - ) -} - -export default NotifiIcon diff --git a/components/NotificationsCard/NotifiFullLogo.tsx b/components/NotificationsCard/NotifiFullLogo.tsx deleted file mode 100644 index b97ddc2c9b..0000000000 --- a/components/NotificationsCard/NotifiFullLogo.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import { useTheme } from 'next-themes' -import NotifiLogoFullDark from './NotifiLogoFullDark' -import NotifiLogoFullLight from './NotifiLogoFullLight ' - -const NotifiFullLogo = ({ height = '23', width = '340' }) => { - const { theme } = useTheme() - return theme === 'Dark' ? ( - - ) : ( - - ) -} - -export default NotifiFullLogo diff --git a/components/NotificationsCard/NotifiLogoFullDark.tsx b/components/NotificationsCard/NotifiLogoFullDark.tsx deleted file mode 100644 index 0022f6eabd..0000000000 --- a/components/NotificationsCard/NotifiLogoFullDark.tsx +++ /dev/null @@ -1,63 +0,0 @@ -const NotifiIconDark = ({ height = '40', width = '340' }) => { - return ( - - - - - - - - - - - - - - - - - ) -} - -export default NotifiIconDark diff --git a/components/NotificationsCard/NotifiLogoFullLight .tsx b/components/NotificationsCard/NotifiLogoFullLight .tsx deleted file mode 100644 index 93066a6eeb..0000000000 --- a/components/NotificationsCard/NotifiLogoFullLight .tsx +++ /dev/null @@ -1,63 +0,0 @@ -const NotifiIconDark = ({ height = '50', width = '340' }) => { - return ( - - - - - - - - - - - - - - - - - ) -} - -export default NotifiIconDark diff --git a/components/NotificationsCard/NotifiLogoLightLong.svg b/components/NotificationsCard/NotifiLogoLightLong.svg deleted file mode 100644 index e5edd54c09..0000000000 --- a/components/NotificationsCard/NotifiLogoLightLong.svg +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - - - - - - - - - - diff --git a/components/NotificationsCard/NotifiPreviewCard.tsx b/components/NotificationsCard/NotifiPreviewCard.tsx deleted file mode 100644 index aca28419b9..0000000000 --- a/components/NotificationsCard/NotifiPreviewCard.tsx +++ /dev/null @@ -1,203 +0,0 @@ -import Switch from './NotifiSwitch' -import { XIcon } from '@heroicons/react/solid' -import { Source, useNotifiClient } from '@notifi-network/notifi-react-hooks' -import React, { - FunctionComponent, - useCallback, - useEffect, - useMemo, - useState, -} from 'react' - -import NotifiFullLogo from './NotifiFullLogo' -type NotifiClientReturnType = ReturnType - -type NotifiPreviewCardProps = { - onClick: () => void - onClose: () => void - telegramEnabled: boolean - email: string - phoneNumber: string - telegram: string - handleDelete: (source: Source) => Promise -} & Pick - -const NotifiPreviewCard: FunctionComponent = ({ - createAlert, - data, - email, - handleDelete, - onClose, - onClick, - phoneNumber, - telegram, - telegramEnabled, - isAuthenticated, -}) => { - const alerts = data?.alerts - const sources = data?.sources - const [isLoading, setLoading] = useState(false) - - const handleEdit = useCallback(() => { - onClick() - }, [onClick]) - - const handleUnsubscribe = useCallback( - async (source: Source) => { - if (isLoading) { - return - } - handleDelete(source) - }, - [handleDelete, isLoading] - ) - - useEffect(() => { - if (!isAuthenticated) { - onClick() - } - // eslint-disable-next-line react-hooks/exhaustive-deps -- TODO please fix, it can cause difficult bugs. You might wanna check out https://bobbyhadz.com/blog/react-hooks-exhaustive-deps for info. -@asktree - }, [isAuthenticated]) - - const handleSubscribe = useCallback( - async (source: Source) => { - if (isLoading) { - return - } - - if (!source) { - throw new Error('No source provided') - } - const filterId = source.applicableFilters[0].id - - if (!filterId) { - throw new Error('No filter id found') - } - try { - setLoading(true) - const alertResult = await createAlert({ - emailAddress: email === '' ? null : email, - filterId: filterId ?? '', - name: `${source.name} notification`, - phoneNumber: phoneNumber === '' ? null : phoneNumber, - sourceId: source.id ?? '', - telegramId: telegram === '' ? null : telegram, - }) - - if (alertResult) { - if (alertResult.targetGroup?.telegramTargets?.length > 0) { - const target = alertResult.targetGroup?.telegramTargets[0] - if (target && target.isConfirmed === false) { - if (target.confirmationUrl) { - window.open(target.confirmationUrl) - } - } - } - } - setLoading(false) - } catch (e) { - throw new Error(e) - } - }, - [createAlert, email, phoneNumber, telegram, isLoading] - ) - - const daoNotifications = useMemo( - () => (source: Source) => { - const handleClick = (source: Source) => { - isChecked ? handleUnsubscribe(source) : handleSubscribe(source) - } - const sourceId = source.id - - const isChecked = Boolean( - alerts?.some((alert) => - alert.sourceGroup.sources.some((source) => source.id === sourceId) - ) - ) - - return ( -
-
- {source.name} Notifications On -
-
- handleClick(source)} /> -
-
- ) - }, - // eslint-disable-next-line react-hooks/exhaustive-deps -- TODO please fix, it can cause difficult bugs. You might wanna check out https://bobbyhadz.com/blog/react-hooks-exhaustive-deps for info. -@asktree - [alerts, handleDelete, handleSubscribe] - ) - - const notificationsToggle = useMemo( - () => - sources - ?.map((source) => { - if (source.type === 'DIRECT_PUSH') { - return - } - return daoNotifications(source) - }) - ?.filter((source) => source), - [daoNotifications, sources] - ) - - return ( -
-
-
-

Notifications

- -
- {data && data?.sources?.length > 0 ? ( -
-

{email}

-

{phoneNumber}

- {telegramEnabled &&

{telegram}

} -
- Edit Information -
-
- ) : ( -
-

No governance memberships found

-
- )} -
- {notificationsToggle && notificationsToggle.length >= 1 ? ( -
- {notificationsToggle} -
- ) : null} -
-
-

Powered by

- - - -
- -
-
- ) -} - -export default NotifiPreviewCard diff --git a/components/NotificationsCard/NotifiSwitch.tsx b/components/NotificationsCard/NotifiSwitch.tsx deleted file mode 100644 index 1ad9adf671..0000000000 --- a/components/NotificationsCard/NotifiSwitch.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import { FunctionComponent } from 'react' - -interface SwitchProps { - checked: boolean - onChange: (x: boolean) => void -} - -const Switch: FunctionComponent = ({ - checked = false, - onChange, -}) => { - const handleClick = () => { - onChange(!checked) - } - - return ( -
- -
- ) -} - -export default Switch diff --git a/components/NotificationsCard/NotificationCardContainer.tsx b/components/NotificationsCard/NotificationCardContainer.tsx deleted file mode 100644 index fb54b149ee..0000000000 --- a/components/NotificationsCard/NotificationCardContainer.tsx +++ /dev/null @@ -1,181 +0,0 @@ -import NotificationsCard from '@components/NotificationsCard' -import NotifiPreviewCard from '@components/NotificationsCard/NotifiPreviewCard' -import useWalletOnePointOh from '@hooks/useWalletOnePointOh' -import { EndpointTypes } from '@models/types' -import { - BlockchainEnvironment, - Source, - useNotifiClient, -} from '@notifi-network/notifi-react-hooks' -import { firstOrNull } from '@utils/helpers' -import { useRouter } from 'next/router' -import { useCallback, useEffect, useState } from 'react' - -type Props = { - onClose: () => void - onBackClick: () => void -} - -const NotificationCardContainer: React.FC = ({ - onClose, - onBackClick, -}) => { - const [showPreview, setPreview] = useState(true) - const router = useRouter() - - const { cluster } = router.query - - const endpoint = cluster ? (cluster as EndpointTypes) : 'mainnet' - const wallet = useWalletOnePointOh() - const connected = !!wallet?.connected - let env = BlockchainEnvironment.MainNetBeta - - switch (endpoint) { - case 'mainnet': - break - case 'devnet': - env = BlockchainEnvironment.DevNet - break - case 'localnet': - env = BlockchainEnvironment.LocalNet - break - } - const notifiClient = useNotifiClient({ - dappAddress: 'solanarealmsdao', - env, - walletPublicKey: wallet?.publicKey?.toString() ?? '', - }) - - const { data, getConfiguration, deleteAlert, isInitialized } = notifiClient - - const [email, setEmail] = useState('') - const [phoneNumber, setPhone] = useState('') - const [telegram, setTelegram] = useState('') - const [telegramEnabled, setTelegramEnabled] = useState(false) - - useEffect(() => { - const targetGroup = firstOrNull(data?.targetGroups) - if (targetGroup) { - setEmail(firstOrNull(targetGroup?.emailTargets)?.emailAddress ?? '') - setPhone(firstOrNull(targetGroup?.smsTargets)?.phoneNumber ?? '') - setTelegram(firstOrNull(targetGroup?.telegramTargets)?.telegramId ?? '') - } else { - setEmail(firstOrNull(data?.emailTargets)?.emailAddress ?? '') - setPhone(firstOrNull(data?.smsTargets)?.phoneNumber ?? '') - setTelegram(firstOrNull(data?.telegramTargets)?.telegramId ?? '') - } - }, [data]) - - const updateTelegramSupported = useCallback(async () => { - const { supportedTargetTypes } = await getConfiguration() - const telegram = supportedTargetTypes.find( - (targetType) => targetType === 'TELEGRAM' - ) - setTelegramEnabled(telegram !== undefined) - }, [getConfiguration, setTelegramEnabled]) - - useEffect(() => { - updateTelegramSupported().catch((e) => { - console.error('Failed to get supported type information: ', e) - }) - }, [updateTelegramSupported]) - - useEffect(() => { - if (connected && isInitialized) { - const targetGroup = firstOrNull(data?.targetGroups) - - if (targetGroup) { - setEmail(firstOrNull(targetGroup?.emailTargets)?.emailAddress ?? '') - setPhone(firstOrNull(targetGroup?.smsTargets)?.phoneNumber ?? '') - setTelegram(firstOrNull(targetGroup?.telegramTargets)?.telegramId ?? '') - } else { - setEmail(firstOrNull(data?.emailTargets)?.emailAddress ?? '') - setPhone(firstOrNull(data?.smsTargets)?.phoneNumber ?? '') - setTelegram(firstOrNull(data?.telegramTargets)?.telegramId ?? '') - } - } - - if (data && data?.sources.length > 0) { - if (email || phoneNumber || telegram) { - setPreview(true) - } - } - }, [connected, data, email, setPreview, isInitialized, phoneNumber, telegram]) - - const handleDelete = useCallback( - async (source: Source) => { - try { - if (data?.alerts) { - const sourceId = source.id - const alertToDelete = data.alerts?.find((alert) => - alert.sourceGroup.sources.find((source) => source.id === sourceId) - ) - - alertToDelete?.id && - (await deleteAlert({ - alertId: alertToDelete.id, - keepSourceGroup: true, - keepTargetGroup: true, - })) - } - } catch (e) { - throw new Error(e) - } - }, - [data?.alerts, deleteAlert] - ) - - return ( -
-
-
- {!isInitialized && ( -
-
-
-
-
-
-
-
-
-
-
-
-
-
- )} - {showPreview && isInitialized && ( - setPreview(false)} - /> - )} - {!showPreview && isInitialized && ( - - )} -
-
-
- ) -} - -export default NotificationCardContainer diff --git a/components/NotificationsCard/PhoneInput.tsx b/components/NotificationsCard/PhoneInput.tsx deleted file mode 100644 index c9d79a1dc4..0000000000 --- a/components/NotificationsCard/PhoneInput.tsx +++ /dev/null @@ -1,151 +0,0 @@ -import Input from '@components/inputs/Input' -import { Listbox, Transition } from '@headlessui/react' -import { ChatAltIcon, ChevronDownIcon } from '@heroicons/react/outline' -import { isValidPhoneNumber } from 'libphonenumber-js' -import { Dispatch, SetStateAction } from 'react' -import { Fragment, useCallback, useEffect, useState } from 'react' - -import { InputRow } from '.' -import { countryMap } from './data' -import { splitPhoneNumber } from './phoneUtils' - -type Props = { - handlePhone: (input: string) => void - phoneNumber: string - setErrorMessage: Dispatch> -} - -const PhoneInput = ({ handlePhone, phoneNumber, setErrorMessage }: Props) => { - const [selectedCountryCode, setCountryCode] = useState('US') - const [dialCode, setDialCode] = useState('+1') - const [baseNumber, setBaseNumber] = useState('') - - const selectCountryHandler = useCallback( - (value: string) => { - setCountryCode(value) - const dialCode = countryMap[value].dialCode - setDialCode(dialCode) - - const input = baseNumber !== '' ? dialCode + baseNumber : '' - handlePhone(input) - }, - [baseNumber, handlePhone] - ) - - const splitPhoneNumbers = useCallback( - (phoneNumber: string) => { - const { baseNumber, countryCode } = splitPhoneNumber(phoneNumber) - if (!countryCode || !baseNumber) { - setErrorMessage('Improper phone, please try again') - } - setBaseNumber(baseNumber) - setCountryCode(countryCode) - }, - [setErrorMessage] - ) - - useEffect(() => { - if (phoneNumber && isValidPhoneNumber(phoneNumber)) { - splitPhoneNumbers(phoneNumber) - } - // eslint-disable-next-line react-hooks/exhaustive-deps -- TODO please fix, it can cause difficult bugs. You might wanna check out https://bobbyhadz.com/blog/react-hooks-exhaustive-deps for info. -@asktree - }, [phoneNumber, splitPhoneNumbers, isValidPhoneNumber]) - - const onChange = useCallback( - (event: React.ChangeEvent) => { - const onlyNumberInput = event.target.value.replace(/[^\d]/g, '') - - setBaseNumber(onlyNumberInput) - const input = onlyNumberInput !== '' ? dialCode + onlyNumberInput : '' - handlePhone(input) - }, - [dialCode, handlePhone] - ) - - const validatePhoneNumber = () => { - if (!isValidPhoneNumber(phoneNumber) && phoneNumber !== '') { - setErrorMessage('You have entered an invalid number') - } - } - - return ( - - } - label="phone" - > - setErrorMessage('')} - onBlur={validatePhoneNumber} - placeholder="XXX-XXX-XXXX" - type="tel" - value={baseNumber} - /> -
- -
- - {dialCode} - - - - - - {Object.entries(countryMap).map( - ([countryCode, countryMetadata], idx) => { - const { dialCode, flag, name } = countryMetadata - return ( - - `relative cursor-default select-none py-2 pl-2 pr-4 z-20 ${ - active - ? 'bg-gray-800 text-grey-300' - : 'text-gray-300' - }` - } - key={idx} - value={countryCode} - > - {({ selected }) => ( - <> - -
-
- {flag} - {name} -
-
{dialCode}
-
-
- - )} -
- ) - } - )} -
-
-
-
-
-
- ) -} - -export default PhoneInput diff --git a/components/NotificationsCard/data.tsx b/components/NotificationsCard/data.tsx deleted file mode 100644 index d7f90c2279..0000000000 --- a/components/NotificationsCard/data.tsx +++ /dev/null @@ -1,137 +0,0 @@ -type CountryMetadata = { - name: string - dialCode: string - flag: string -} - -type CountryMap = { - [countryCode: string]: CountryMetadata -} - -export const countryMap: CountryMap = { - US: { - dialCode: '+1', - flag: '🇺🇸', - name: 'United States', - }, - AU: { - dialCode: '+61', - flag: '🇦🇺', - name: 'Australia', - }, - AT: { - dialCode: '+43', - flag: '🇦🇹', - name: 'Austria', - }, - BE: { - dialCode: '+32', - flag: '🇧🇪', - name: 'Belgium', - }, - BR: { - dialCode: '+55', - flag: '🇧🇷', - name: 'Brazil', - }, - CA: { - dialCode: '+1', - flag: '🇨🇦', - name: 'Canada', - }, - DK: { - dialCode: '+45', - flag: '🇩🇰', - name: 'Denmark', - }, - FI: { - dialCode: '+358', - flag: '🇫🇮', - name: 'Finland', - }, - FR: { - dialCode: '+33', - flag: '🇫🇷', - name: 'France', - }, - DE: { - dialCode: '+49', - flag: '🇩🇪', - name: 'Germany', - }, - HK: { - dialCode: '+852', - flag: '🇭🇰', - name: 'Hong Kong', - }, - HU: { - dialCode: '+36', - flag: '🇭🇺', - name: 'Hungary', - }, - IS: { - dialCode: '+354', - flag: '🇮🇸', - name: 'Iceland', - }, - MY: { - dialCode: '+60', - flag: '🇲🇾', - name: 'Malaysia', - }, - NO: { - dialCode: '+47', - flag: '🇳🇴', - name: 'Norway', - }, - PH: { - dialCode: '+63', - flag: '🇵🇭', - name: 'Philippines', - }, - PL: { - dialCode: '+48', - flag: '🇵🇱', - name: 'Poland', - }, - PT: { - dialCode: '+351', - flag: '🇵🇹', - name: 'Portugal', - }, - SG: { - dialCode: '+65', - flag: '🇸🇬', - name: 'Singapore', - }, - KR: { - dialCode: '+82', - flag: '🇰🇷', - name: 'Korea, Republic of South Korea', - }, - ES: { - dialCode: '+34', - flag: '🇪🇸', - name: 'Spain', - }, - SE: { - dialCode: '+46', - flag: '🇸🇪', - name: 'Sweden', - }, - CH: { - dialCode: '+41', - flag: '🇨🇭', - name: 'Switzerland', - }, - TW: { - dialCode: '+886', - flag: '🇹🇼', - name: 'Taiwan', - }, - GB: { - dialCode: '+44', - flag: '🇬🇧', - name: 'United Kingdom', - }, -} diff --git a/components/NotificationsCard/index.tsx b/components/NotificationsCard/index.tsx deleted file mode 100644 index 7b1fdf8443..0000000000 --- a/components/NotificationsCard/index.tsx +++ /dev/null @@ -1,463 +0,0 @@ -import { - ArrowLeftIcon, - MailIcon, - PaperAirplaneIcon, -} from '@heroicons/react/solid' -import useWalletOnePointOh from '@hooks/useWalletOnePointOh' -import { - Alert, - GqlError, - MessageSigner, - useNotifiClient, -} from '@notifi-network/notifi-react-hooks' -import { firstOrNull } from '@utils/helpers' -import { isValidPhoneNumber } from 'libphonenumber-js' -import React, { - Dispatch, - FunctionComponent, - SetStateAction, - useEffect, - useState, -} from 'react' -import { useCallback } from 'react' - -import Button from '../Button' -import Input from '../inputs/Input' -import NotifiFullLogo from './NotifiFullLogo' -import PhoneInput from './PhoneInput' - -type NotifiClientReturnType = ReturnType - -type NotificationCardProps = { - onBackClick: () => void - email: string - phoneNumber: string - telegram: string - setPreview: Dispatch> - setEmail: Dispatch> - setTelegram: Dispatch> - setPhone: Dispatch> -} & Pick< - NotifiClientReturnType, - | 'createAlert' - | 'logIn' - | 'fetchData' - | 'data' - | 'isAuthenticated' - | 'updateAlert' - | 'getConfiguration' -> - -const NotificationsCard = ({ - createAlert, - data, - email, - getConfiguration, - isAuthenticated, - logIn, - onBackClick, - phoneNumber, - setEmail, - setPhone, - setPreview, - setTelegram, - telegram, - updateAlert, -}: NotificationCardProps) => { - const [isLoading, setLoading] = useState(false) - const [hasUnsavedChanges, setUnsavedChanges] = useState(false) - const [errorMessage, setErrorMessage] = useState('') - const [telegramEnabled, setTelegramEnabled] = useState(false) - const [firstTimeUser, setFirstTimeUser] = useState(false) - - const wallet = useWalletOnePointOh() - const connected = !!wallet?.connected - - const alerts = data?.alerts - const sources = data?.sources - - const [localEmail, setLocalEmail] = useState('') - const [localPhoneNumber, setLocalPhone] = useState('') - const [localTelegram, setLocalTelegram] = useState('') - - const updateTelegramSupported = useCallback(async () => { - const { supportedTargetTypes } = await getConfiguration() - const telegram = supportedTargetTypes.find((it) => it === 'TELEGRAM') - setTelegramEnabled(telegram !== undefined) - }, [getConfiguration, setTelegramEnabled]) - - useEffect(() => { - updateTelegramSupported().catch((e) => { - console.error('Failed to get supported type information: ', e) - }) - }, [updateTelegramSupported]) - - useEffect(() => { - const targetGroup = firstOrNull(data?.targetGroups) - - if (email || telegram || phoneNumber) { - setLocalEmail(email ?? '') - setLocalTelegram(telegram ?? '') - setLocalPhone(phoneNumber ?? '') - setUnsavedChanges(true) - } else if (targetGroup) { - setLocalEmail(firstOrNull(targetGroup?.emailTargets)?.emailAddress ?? '') - setLocalPhone(firstOrNull(targetGroup?.smsTargets)?.phoneNumber ?? '') - setLocalTelegram( - firstOrNull(targetGroup?.telegramTargets)?.telegramId ?? '' - ) - setUnsavedChanges(true) - } else { - setLocalEmail(firstOrNull(data?.emailTargets)?.emailAddress ?? '') - setLocalPhone(firstOrNull(data?.smsTargets)?.phoneNumber ?? '') - setLocalTelegram(firstOrNull(data?.telegramTargets)?.telegramId ?? '') - setUnsavedChanges(true) - } - }, [ - data, - data?.emailTargets, - data?.smsTargets, - data?.telegramTargets, - email, - phoneNumber, - telegram, - ]) - - const checkTelegramUnconfirmed = useCallback((alertsResponse: Alert[]) => { - const hasTelegramAlert = alertsResponse.find( - (alert) => alert.targetGroup.telegramTargets.length >= 0 - ) - const target = hasTelegramAlert?.targetGroup.telegramTargets[0] - - if (target && !target.isConfirmed) { - if (target.confirmationUrl) { - window.open(target.confirmationUrl) - } - } - - return alertsResponse.some((alertResponse) => - alertResponse.targetGroup.telegramTargets.some( - (target) => !target.isConfirmed - ) - ) - }, []) - - const handleError = (errors: { message: string }[]) => { - const error = errors.length > 0 ? errors[0] : null - if (error instanceof GqlError) { - setErrorMessage( - `${error.message}: ${error.getErrorMessages().join(', ')}` - ) - } else { - setErrorMessage(error?.message ?? 'Unknown error') - } - setLoading(false) - } - - const handleRefresh = useCallback( - async function () { - setLoading(true) - setErrorMessage('') - // user is not authenticated - if (!isAuthenticated && wallet && wallet.publicKey) { - try { - await logIn((wallet as unknown) as MessageSigner) - } catch (e) { - handleError([e]) - } - setLoading(false) - } else { - setPreview(true) - } - setLoading(false) - }, - // eslint-disable-next-line react-hooks/exhaustive-deps -- TODO please fix, it can cause difficult bugs. You might wanna check out https://bobbyhadz.com/blog/react-hooks-exhaustive-deps for info. -@asktree - [setLoading, isAuthenticated, wallet, setErrorMessage, logIn] - ) - - const handleUpdate = async () => { - if (alerts && alerts.length >= 1) { - const results: Alert[] = [] - - for (const alert of alerts) { - const alertRes = await updateAlert({ - alertId: alert.id ?? '', - emailAddress: localEmail === '' ? null : localEmail, - phoneNumber: isValidPhoneNumber(localPhoneNumber) - ? localPhoneNumber - : null, - telegramId: localTelegram === '' ? null : localTelegram, - }) - if (alertRes) { - results.push(alertRes) - } - } - if (results) { - setEmail(results[0].targetGroup?.emailTargets[0]?.emailAddress ?? '') - setPhone(results[0].targetGroup?.smsTargets[0]?.phoneNumber ?? '') - setTelegram( - results[0].targetGroup?.telegramTargets[0]?.telegramId ?? '' - ) - setPreview(true) - } - checkTelegramUnconfirmed(results) - if (results) { - setPreview(true) - } - } else { - const results: Alert[] = [] - if (sources && sources.length >= 1) { - for (const source of sources) { - const filterId = source.applicableFilters[0].id - const alertRes = await createAlert({ - emailAddress: localEmail === '' ? null : localEmail, - filterId: filterId ?? '', - name: `${source.name} notification`, - phoneNumber: isValidPhoneNumber(localPhoneNumber) - ? localPhoneNumber - : null, - sourceId: source?.id ?? '', - telegramId: localTelegram === '' ? null : localTelegram, - }) - if (alertRes) { - results.push(alertRes) - } - } - } - if (telegram) { - checkTelegramUnconfirmed(results) - } - if (results && results.length >= 1) { - setPreview(true) - setEmail(results[0].targetGroup?.emailTargets[0]?.emailAddress ?? '') - setPhone(results[0].targetGroup?.smsTargets[0]?.phoneNumber ?? '') - setTelegram( - results[0].targetGroup?.telegramTargets[0]?.telegramId ?? '' - ) - } - } - setUnsavedChanges(false) - } - - useEffect(() => { - const handleLogIn = async () => { - await logIn((wallet as unknown) as MessageSigner) - } - - const anotherhandleUpdate = async () => { - await handleUpdate() - } - - if (firstTimeUser && sources === undefined) { - handleLogIn() - } - if (firstTimeUser && sources) { - anotherhandleUpdate() - } - // eslint-disable-next-line react-hooks/exhaustive-deps -- TODO please fix, it can cause difficult bugs. You might wanna check out https://bobbyhadz.com/blog/react-hooks-exhaustive-deps for info. -@asktree - }, [firstTimeUser, sources]) - - const handleSave = useCallback(async () => { - setLoading(true) - if (!isAuthenticated && wallet && wallet.publicKey) { - try { - setFirstTimeUser(true) - } catch (e) { - setPreview(false) - handleError([e]) - } - } - if (connected && isAuthenticated) { - try { - setFirstTimeUser(false) - await handleUpdate() - setUnsavedChanges(false) - } catch (e) { - setPreview(false) - handleError([e]) - } - } - setLoading(false) - // eslint-disable-next-line react-hooks/exhaustive-deps -- TODO please fix, it can cause difficult bugs. You might wanna check out https://bobbyhadz.com/blog/react-hooks-exhaustive-deps for info. -@asktree - }, [ - alerts, - checkTelegramUnconfirmed, - connected, - createAlert, - isAuthenticated, - localEmail, - localPhoneNumber, - localTelegram, - logIn, - setEmail, - setPhone, - setPreview, - setTelegram, - sources, - telegram, - updateAlert, - wallet, - ]) - - const handleEmail = (e: React.ChangeEvent) => { - setLocalEmail(e.target.value) - setUnsavedChanges(true) - } - - const handlePhone = (input: string) => { - setLocalPhone(input) - setUnsavedChanges(true) - } - - const handleTelegram = (e: React.ChangeEvent) => { - setLocalTelegram(e.target.value) - setUnsavedChanges(true) - } - - const disabled = - (isAuthenticated && !hasUnsavedChanges) || - (localEmail === '' && localTelegram === '' && localPhoneNumber === '') || - errorMessage !== '' - - return ( -
-
- - -
- {!connected ? ( - <> -
- Connect wallet to see options -
- - ) : ( - <> -
- Get notifications for proposals, voting, and results. Add your email - address, phone number, and/or Telegram. -
-
- {errorMessage.length > 0 ? ( -
{errorMessage}
- ) : ( - !isAuthenticated && ( -
- When prompted, sign the transaction. -
- ) - )} -
-
- - } - label="email" - > - - - - {telegramEnabled && ( - - } - label="Telegram" - > - - - )} -
-
-
- Already Subscribed?{' '} - - Click here to load your alert details. - -
-
-
- - - -
- - )} -
- ) -} - -interface InputRowProps { - label: string - icon: React.ReactNode -} - -export const InputRow: FunctionComponent = ({ - children, - icon, - label, -}) => { - return ( - - ) -} - -export default NotificationsCard diff --git a/components/NotificationsCard/phoneUtils.tsx b/components/NotificationsCard/phoneUtils.tsx deleted file mode 100644 index 95746b2fa5..0000000000 --- a/components/NotificationsCard/phoneUtils.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { parsePhoneNumber } from 'libphonenumber-js' - -type PhoneData = { - countryCode: string - baseNumber: string -} - -export const splitPhoneNumber = (phoneNumber: string): PhoneData => { - const { country: countryCode, nationalNumber: baseNumber } = parsePhoneNumber( - phoneNumber - ) - if (!countryCode || !baseNumber) { - throw new Error('No country or phone found') - } - - return { baseNumber, countryCode } -} diff --git a/components/NotificationsSwitch/NotifiIconDark.tsx b/components/NotificationsSwitch/NotifiIconDark.tsx deleted file mode 100644 index 7e18a4ff2e..0000000000 --- a/components/NotificationsSwitch/NotifiIconDark.tsx +++ /dev/null @@ -1,37 +0,0 @@ -const NotifiIconDark = ({ height, width }) => { - return ( - - - - - - - - - - - ) -} - -export default NotifiIconDark diff --git a/components/NotificationsSwitch/NotifiIconLight.tsx b/components/NotificationsSwitch/NotifiIconLight.tsx deleted file mode 100644 index cfdf44e1cf..0000000000 --- a/components/NotificationsSwitch/NotifiIconLight.tsx +++ /dev/null @@ -1,37 +0,0 @@ -const NotifiIconLight = ({ height, width }) => { - return ( - - - - - - - - - - - ) -} - -export default NotifiIconLight diff --git a/components/NotificationsSwitch/TelegramIcon.tsx b/components/NotificationsSwitch/TelegramIcon.tsx deleted file mode 100644 index 7661c4f62f..0000000000 --- a/components/NotificationsSwitch/TelegramIcon.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import * as React from 'react' -import { SVGProps } from 'react' - -const SvgComponent = (props: SVGProps) => ( - - - -) - -export default SvgComponent diff --git a/components/NotificationsSwitch/index.tsx b/components/NotificationsSwitch/index.tsx deleted file mode 100644 index 5835b00afd..0000000000 --- a/components/NotificationsSwitch/index.tsx +++ /dev/null @@ -1,210 +0,0 @@ -import Button from '@components/Button' -import NotifiIcon from '@components/NotifiIcon' -import { defaultVariables } from '@dialectlabs/react-ui' -import styled from '@emotion/styled' -import { Transition } from '@headlessui/react' -import { DeviceMobileIcon } from '@heroicons/react/outline' -import { BellIcon, KeyIcon, MailIcon } from '@heroicons/react/solid' - -import { useEffect, useRef, useState } from 'react' -import useNotificationStore, { ModalStates } from 'stores/useNotificationStore' - -import DialectNotificationsModal from '@components/DialectNotificationsModal' -import NotificationCardContainer from '@components/NotificationsCard/NotificationCardContainer' -import TelegramIcon from './TelegramIcon' - -function useOutsideAlerter( - ref: React.MutableRefObject, - bellRef: React.MutableRefObject, - setOpen: CallableFunction -) { - useEffect(() => { - /** - * Alert if clicked on outside of element - */ - function handleClickOutside(event: MouseEvent) { - if ( - ref.current && - !ref?.current.contains(event.target as Element) && - bellRef.current && - !bellRef?.current.contains(event.target as Element) - ) { - setOpen(false) - } - } - - // Bind the event listener - document.addEventListener('mousedown', handleClickOutside) - return () => { - // Unbind the event listener on clean up - document.removeEventListener('mousedown', handleClickOutside) - } - }, [ref, bellRef, setOpen]) -} - -const TagToIcon = { - Email: , - NotifiCenter: , - Telegram: , - Text: , - Wallet: , -} - -type ChannelType = 'Wallet' | 'Email' | 'Text' | 'Telegram' | 'Notifi Center' - -interface NotificationSolutionType { - name: string - channels: ChannelType[] - description: string - modalState: ModalStates -} - -const NotificationSolutions: NotificationSolutionType[] = [ - { - channels: ['Wallet', 'Email', 'Text', 'Telegram'], - description: `Get notifications when new proposals are created & when proposals are completed or canceled. By wallet, email, Telegram or text message.`, - modalState: ModalStates.Dialect, - name: 'Dialect', - }, - { - channels: ['Email', 'Text', 'Telegram', 'Notifi Center'], - description: ` - Get notifications for proposals, voting, and results. Add your email address, phone number, and/or Telegram.`, - modalState: ModalStates.Notifi, - name: 'notifi', - }, -] - -export default function NotificationsSwitch() { - const { modalState, set: setNotificationStore } = useNotificationStore( - (s) => s - ) - - const wrapperRef = useRef(null) - const bellRef = useRef(null) - const [openModal, setOpenModal] = useState(false) - useOutsideAlerter(wrapperRef, bellRef, setOpenModal) - - const StyledChannelName = styled.span` - font-size: 0.8rem; - white-space: nowrap; - ` - - const removeSpaces = (str: string): string => { - return str.split(' ').join('') - } - - const formatName = (str: string): string => { - return str[0].toUpperCase() + str.substring(1) - } - - const Tag = ({ channelName }: { channelName: ChannelType }) => { - return ( - -
- {TagToIcon[removeSpaces(channelName)]} - {channelName} -
-
- ) - } - - const NotificationBox = ({ - channels, - description, - modalState, - name, - }: NotificationSolutionType) => ( -
-
-
- {name === 'notifi' && } -

{name}

-
-
- {channels.map((channel) => ( - - ))} -
- -
-
-

{description}

-
-
- -
- -
-
-
- ) - - const DialectBellIcon = defaultVariables.dark.icons.bell - - return ( -
- - {modalState === ModalStates.Selection && ( -
-
-

Realms Notifications

- {NotificationSolutions.map((solution) => ( - - ))} -
-
- )} - - {modalState === ModalStates.Dialect && ( - { - setOpenModal(false) - }} - onBackClick={() => - setNotificationStore((state) => { - state.modalState = ModalStates.Selection - }) - } - /> - )} - {modalState === ModalStates.Notifi && ( - setOpenModal(!openModal)} - onBackClick={() => - setNotificationStore((state) => { - state.modalState = ModalStates.Selection - }) - } - /> - )} -
- -
- ) -} diff --git a/components/PageBodyContainer.tsx b/components/PageBodyContainer.tsx index ae6b27969f..5f3ee13697 100644 --- a/components/PageBodyContainer.tsx +++ b/components/PageBodyContainer.tsx @@ -1,10 +1,19 @@ import { useRouter } from 'next/router' import Footer from '@components/Footer' +import {PluginDebug} from "../VoterWeightPlugins/lib/PluginDebug"; +import React from "react"; const PageBodyContainer = ({ children }) => { - const { pathname } = useRouter() + const { pathname, query } = useRouter() const isNewRealmsWizard = /\/realms\/new\/\w+/.test(pathname) + // TODO TEMP DEBUG - REMOVE BEFORE MERGE + if ( + query['debug'] !== undefined + ) { + return + } + return ( <>
>() - -const getProfile = async ( - publicKey: PublicKey, - connection?: Connection -): Promise => { - const cached = profiles.get(publicKey.toBase58()); - if (cached) return cached; - const promise = CivicProfile.get(publicKey.toBase58(), { - solana: { - connection, - }, - }); - - profiles.set(publicKey.toBase58(), promise) - - return promise; -} - const profileIsSet = (profile: BaseProfile): boolean => !!profile.name || !!profile.image || !!profile.headline @@ -35,25 +16,24 @@ export const useProfile = ( ): { profile: Profile | undefined; loading: boolean } => { const connection = useLegacyConnectionContext() const connectedWallet = useWalletOnePointOh() - const [profile, setProfile] = useState() - const [loading, setLoading] = useState(true) const profileWalletPublicKey = publicKey || connectedWallet?.publicKey - - useEffect(() => { - if (profileWalletPublicKey) { - getProfile(profileWalletPublicKey, connection?.current).then( - (profile) => { - setProfile({ - ...profile, - exists: profileIsSet(profile), - }) - setLoading(false) - } - ) + const options = connection + ? { solana: { connection: connection?.current } } + : undefined + + const { data: profile, isLoading } = useQuery( + ['Civic Profile', profileWalletPublicKey?.toBase58() + 'Civic'], + // @ts-ignore we won't run this if there is no profileWalletPublicKey + () => CivicProfile.get(profileWalletPublicKey?.toBase58(), options), + { + enabled: !!profileWalletPublicKey, // Only run query if profileWalletPublicKey is available + select: (data) => ({ + ...data, + exists: profileIsSet(data), + }), } - // eslint-disable-next-line react-hooks/exhaustive-deps -- TODO please fix, it can cause difficult bugs. You might wanna check out https://bobbyhadz.com/blog/react-hooks-exhaustive-deps for info. -@asktree - }, [publicKey, connectedWallet?.publicKey, connection.current]) + ) - return { profile, loading } + return { profile, loading: isLoading } } diff --git a/components/ProposalActions.tsx b/components/ProposalActions.tsx index f989309343..71085a4d26 100644 --- a/components/ProposalActions.tsx +++ b/components/ProposalActions.tsx @@ -17,10 +17,8 @@ import { Proposal } from '@solana/spl-governance' import { ProgramAccount } from '@solana/spl-governance' import { cancelProposal } from 'actions/cancelProposal' import { getProgramVersionForRealm } from '@models/registry/api' -import useVotePluginsClientStore from 'stores/useVotePluginsClientStore' import dayjs from 'dayjs' import { diffTime } from './ProposalRemainingVotingTime' -import { useMaxVoteRecord } from '@hooks/useMaxVoteRecord' import useWalletOnePointOh from '@hooks/useWalletOnePointOh' import { proposalQueryKeys, @@ -38,6 +36,7 @@ import { InstructionDataWithHoldUpTime } from 'actions/createProposal' import { TransactionInstruction } from '@solana/web3.js' import useQueryContext from '@hooks/useQueryContext' import { useRouter } from 'next/router' +import { useRealmVoterWeightPlugins } from '@hooks/useRealmVoterWeightPlugins' const ProposalActionsPanel = () => { const { propose } = useCreateProposal() @@ -56,11 +55,9 @@ const ProposalActionsPanel = () => { const hasVoteTimeExpired = useHasVoteTimeExpired(governance, proposal!) const connection = useLegacyConnectionContext() - const maxVoteRecordPk = useMaxVoteRecord()?.pubkey - const votePluginsClientMaxVoterWeight = useVotePluginsClientStore( - (s) => s.state.maxVoterWeight - ) - const maxVoterWeight = maxVoteRecordPk || votePluginsClientMaxVoterWeight + // TODO check the kind to be passed + const { maxVoterWeightPk } = useRealmVoterWeightPlugins() + const canFinalizeVote = hasVoteTimeExpired && proposal?.account.state === ProposalState.Voting const now = new Date().getTime() / 1000 // unix timestamp in seconds @@ -174,7 +171,7 @@ const ProposalActionsPanel = () => { rpcContext, governance?.account.realm, proposal, - maxVoterWeight, + maxVoterWeightPk, proposalOwner ) } @@ -259,12 +256,15 @@ const ProposalActionsPanel = () => { const handleRepropose = async () => { try { - if (proposal && realmInfo && signatoryRecord) { + if (proposal && realmInfo) { const proposalAddress = await propose({ title: proposal.account.name, description: proposal.account.descriptionLink, voteByCouncil: - proposal.account.governingTokenMint !== realmInfo.communityMint, + !realmInfo.communityMint || + !proposal.account.governingTokenMint.equals( + realmInfo.communityMint + ), instructionsData: transactions ? [ ...transactions.flatMap((tx) => @@ -282,6 +282,7 @@ const ProposalActionsPanel = () => { isValid: true, governance: undefined, customHoldUpTime: tx.account.holdUpTime, + chunkBy: 1, }, }) ) diff --git a/components/ProposalVotingPower/LockedCommunityNFTRecordVotingPower.tsx b/components/ProposalVotingPower/LockedCommunityNFTRecordVotingPower.tsx index 3ed4de72a6..992d0db0c8 100644 --- a/components/ProposalVotingPower/LockedCommunityNFTRecordVotingPower.tsx +++ b/components/ProposalVotingPower/LockedCommunityNFTRecordVotingPower.tsx @@ -10,12 +10,12 @@ import { BN } from '@coral-xyz/anchor' import Link from 'next/link' import useQueryContext from '@hooks/useQueryContext' import InlineNotification from '@components/InlineNotification' -import useVotePluginsClientStore from 'stores/useVotePluginsClientStore' import { useAddressQuery_CommunityTokenOwner } from '@hooks/queries/addresses/tokenOwnerRecord' import useWalletOnePointOh from '@hooks/useWalletOnePointOh' import { useUserCommunityTokenOwnerRecord } from '@hooks/queries/tokenOwnerRecord' import { useRealmQuery } from '@hooks/queries/realm' import { useRealmCommunityMintInfoQuery } from '@hooks/queries/mintInfo' +import {useVotingClients} from "@hooks/useVotingClients"; interface Props { className?: string @@ -32,9 +32,8 @@ export default function LockedCommunityNFTRecordVotingPower(props: Props) { const wallet = useWalletOnePointOh() const connected = !!wallet?.connected const { data: tokenOwnerRecordPk } = useAddressQuery_CommunityTokenOwner() - const [currentClient] = useVotePluginsClientStore((s) => [ - s.state.currentRealmVotingClient, - ]) + // this is only available for the community role as long as the rest of the hooks are hard-coding it + const votingClient = useVotingClients()('community') const [ loadingPositions, votingPower, @@ -50,10 +49,10 @@ export default function LockedCommunityNFTRecordVotingPower(props: Props) { ]) useEffect(() => { - if (currentClient.heliumVsrVotingPositions.length !== positions.length) { - propagatePositions({ votingClient: currentClient }) + if (votingClient.heliumVsrVotingPositions.length !== positions.length) { + propagatePositions({ votingClient }) } - }, [positions, currentClient, propagatePositions]) + }, [positions, votingClient, propagatePositions]) useEffect(() => { if (mint && votingPower) { diff --git a/components/ProposalVotingPower/LockedCommunityVotingPower.tsx b/components/ProposalVotingPower/LockedCommunityVotingPower.tsx index 784a4ed966..d5178139a3 100644 --- a/components/ProposalVotingPower/LockedCommunityVotingPower.tsx +++ b/components/ProposalVotingPower/LockedCommunityVotingPower.tsx @@ -1,66 +1,47 @@ import useRealm from '@hooks/useRealm' import { BigNumber } from 'bignumber.js' -import { LightningBoltIcon } from '@heroicons/react/solid' -import { useCallback } from 'react' import classNames from 'classnames' -import { calculateMaxVoteScore } from '@models/proposal/calulateMaxVoteScore' import useDepositStore from 'VoteStakeRegistry/stores/useDepositStore' -import { getMintDecimalAmount } from '@tools/sdk/units' -import Tooltip from '@components/Tooltip' -import { SecondaryButton } from '@components/Button' -import useVotePluginsClientStore from 'stores/useVotePluginsClientStore' -import { notify } from '@utils/notifications' import { getMintMetadata } from '../instructions/programs/splToken' -import depositTokensVSR from './depositTokensVSR' -import VotingPowerPct from './VotingPowerPct' -import useWalletOnePointOh from '@hooks/useWalletOnePointOh' import { useRealmQuery } from '@hooks/queries/realm' -import { useUserCommunityTokenOwnerRecord } from '@hooks/queries/tokenOwnerRecord' import { useRealmCommunityMintInfoQuery } from '@hooks/queries/mintInfo' -import { useRouteProposalQuery } from '@hooks/queries/proposal' -import { useConnection } from '@solana/wallet-adapter-react' -import BN from 'bn.js' -import { useVsrGovpower } from '@hooks/queries/plugins/vsr' +import VSRCommunityVotingPower from 'VoteStakeRegistry/components/TokenBalance/VSRVotingPower' +import DepositCommunityTokensBtn from 'VoteStakeRegistry/components/TokenBalance/DepositCommunityTokensBtn' +import useDelegators from '@components/VotePanel/useDelegators' +import {useRealmVoterWeightPlugins} from "@hooks/useRealmVoterWeightPlugins"; +import {CalculatedWeight, VoterWeightPlugins} from "../../VoterWeightPlugins/lib/types"; +import { BN } from '@coral-xyz/anchor' interface Props { className?: string } +const findVSRVoterWeight = (calculatedVoterWeight: CalculatedWeight | undefined): BN|undefined => + calculatedVoterWeight?.details.find((detail) => detail.pluginName === 'VSR')?.pluginWeight ?? undefined; + +const isVSRLastVoterWeightPlugin = (plugins: VoterWeightPlugins | undefined) => plugins?.voterWeight[plugins.voterWeight.length - 1].name === 'VSR'; + export default function LockedCommunityVotingPower(props: Props) { const realm = useRealmQuery().data?.result - const mint = useRealmCommunityMintInfoQuery().data?.result + const { + data: mintData, + isLoading: mintLoading, + } = useRealmCommunityMintInfoQuery() + const mint = mintData?.result - const { realmInfo, realmTokenAccount } = useRealm() - const proposal = useRouteProposalQuery().data?.result - const client = useVotePluginsClientStore((s) => s.state.vsrClient) - const { connection } = useConnection() - const deposits = useDepositStore((s) => s.state.deposits) + const { realmTokenAccount } = useRealm() + const { totalCalculatedVoterWeight, isReady: votingPowerReady, plugins } = useRealmVoterWeightPlugins('community'); - const endpoint = connection.rpcEndpoint + // in case the VSR plugin is the last plugin, this is the final calculated voter weight. + // however, if it is one in a chain, we are just showing an intermediate calculation here. + // This affects how it appears in the UI + const votingPower = findVSRVoterWeight(totalCalculatedVoterWeight) + const isLastVoterWeightPlugin = isVSRLastVoterWeightPlugin(plugins); - const getOwnedDeposits = useDepositStore((s) => s.getOwnedDeposits) - const votingPower = useVsrGovpower().result?.result ?? new BN(0) - const votingPowerFromDeposits = useDepositStore( - (s) => s.state.votingPowerFromDeposits - ) - const wallet = useWalletOnePointOh() const isLoading = useDepositStore((s) => s.state.isLoading) - const currentTokenOwnerRecord = useUserCommunityTokenOwnerRecord().data - ?.result - - const tokenOwnerRecordPk = currentTokenOwnerRecord - ? currentTokenOwnerRecord.pubkey - : null - - const depositRecord = deposits.find( - (deposit) => - deposit.mint.publicKey.toBase58() === - realm?.account.communityMint.toBase58() && deposit.lockup.kind.none - ) - const depositMint = realm?.account.communityMint const depositAmount = realmTokenAccount ? new BigNumber(realmTokenAccount.account.amount.toString()) @@ -69,144 +50,31 @@ export default function LockedCommunityVotingPower(props: Props) { const tokenName = getMintMetadata(depositMint)?.name ?? realm?.account.name ?? '' - const amount = - votingPower && mint - ? getMintDecimalAmount(mint, votingPower) - : new BigNumber('0') - - const multiplier = - !votingPower.isZero() && !votingPowerFromDeposits.isZero() - ? votingPower.div(votingPowerFromDeposits).toNumber().toFixed(2) + 'x' - : null - - const tokenAmount = - depositRecord && mint - ? new BigNumber( - getMintDecimalAmount(mint, depositRecord.amountDepositedNative) - ) - : new BigNumber('0') + // memoize useAsync inputs to prevent constant refetch + const relevantDelegators = useDelegators('community') - const lockedTokensAmount = mint - ? deposits - .filter( - (x) => - typeof x.lockup.kind['none'] === 'undefined' && - x.mint.publicKey.toBase58() === - realm?.account.communityMint.toBase58() - ) - .reduce( - (curr, next) => - curr.plus(new BigNumber(next.currentlyLocked.toString())), - new BigNumber(0) - ) - .shiftedBy(-mint.decimals) - : new BigNumber('0') - - const max = - realm && proposal && mint - ? new BigNumber( - calculateMaxVoteScore(realm, proposal, mint).toString() - ).shiftedBy(-mint.decimals) - : null - - const deposit = useCallback(async () => { - if ( - client && - realm && - realmInfo && - realmTokenAccount && - wallet && - wallet.publicKey - ) { - try { - await depositTokensVSR({ - client, - connection, - endpoint, - realm, - realmInfo, - realmTokenAccount, - tokenOwnerRecordPk, - wallet, - }) - - await getOwnedDeposits({ - client, - connection, - communityMintPk: realm.account.communityMint, - realmPk: realm.pubkey, - walletPk: wallet.publicKey, - }) - } catch (e) { - console.error(e) - notify({ message: `Something went wrong ${e}`, type: 'error' }) - } - } - }, [ - client, - connection, - endpoint, - getOwnedDeposits, - realm, - realmInfo, - realmTokenAccount, - tokenOwnerRecordPk, - wallet, - ]) - - if (isLoading || !(votingPower && mint)) { + if (isLoading || !votingPowerReady || mintLoading) { return (
) } return (
- {amount.isZero() ? ( + {(votingPower === undefined || votingPower.isZero()) && + (relevantDelegators?.length ?? 0) < 1 ? (
You do not have any voting power in this dao.
) : ( - <> -
-
{tokenName} Votes
-
-
- {amount.toFormat(2)}{' '} - {multiplier && ( - -
- - {multiplier} -
-
- )} -
- {max && !max.isZero() && ( - - )} -
-
-
-

- {tokenName} Deposited - - {tokenAmount.isNaN() ? '0' : tokenAmount.toFormat()} - -

-

- {tokenName} Locked - - {lockedTokensAmount.isNaN() - ? '0' - : lockedTokensAmount.toFormat()} - -

-
- + )} + {depositAmount.isGreaterThan(0) && ( <>
@@ -217,9 +85,7 @@ export default function LockedCommunityVotingPower(props: Props) { more {tokenName} votes in your wallet. Do you want to deposit them to increase your voting power in this Dao?
- - Deposit - + )}
diff --git a/components/ProposalVotingPower/NftVotingPower.tsx b/components/ProposalVotingPower/NftVotingPower.tsx index 80d8dfadae..9ad5c03a43 100644 --- a/components/ProposalVotingPower/NftVotingPower.tsx +++ b/components/ProposalVotingPower/NftVotingPower.tsx @@ -1,26 +1,18 @@ /* eslint-disable @typescript-eslint/no-non-null-asserted-optional-chain */ import classNames from 'classnames' import { BigNumber } from 'bignumber.js' -import { Transaction, TransactionInstruction } from '@solana/web3.js' -import { - SYSTEM_PROGRAM_ID, - withCreateTokenOwnerRecord, -} from '@solana/spl-governance' -import { NftVoterClient } from '@utils/uiTypes/NftVoterClient' - -import useNftPluginStore from 'NftVotePlugin/store/nftPluginStore' -import useRealm from '@hooks/useRealm' +import { Transaction } from '@solana/web3.js' import Button from '@components/Button' -import { getVoterWeightRecord } from '@utils/plugin/accounts' -import useVotePluginsClientStore from 'stores/useVotePluginsClientStore' import { sendTransaction } from '@utils/send' import VotingPowerPct from './VotingPowerPct' import useWalletOnePointOh from '@hooks/useWalletOnePointOh' -import { useUserCommunityTokenOwnerRecord } from '@hooks/queries/tokenOwnerRecord' import { useRealmQuery } from '@hooks/queries/realm' -import useLegacyConnectionContext from '@hooks/useLegacyConnectionContext' -import { useGovernancePowerAsync } from '@hooks/queries/governancePower' +import useUserOrDelegator from '@hooks/useUserOrDelegator' +import { useConnection } from '@solana/wallet-adapter-react' +import { useVotingNfts } from '@hooks/queries/plugins/nftVoter' +import { useRealmVoterWeightPlugins } from '@hooks/useRealmVoterWeightPlugins' +import {useJoinRealm} from "@hooks/useJoinRealm"; interface Props { className?: string @@ -28,76 +20,65 @@ interface Props { children?: React.ReactNode } -export default function NftVotingPower(props: Props) { - const nfts = useNftPluginStore((s) => s.state.votingNfts) - const { result: votingPower } = useGovernancePowerAsync('community') - const maxWeight = useNftPluginStore((s) => s.state.maxVoteRecord) - const isLoading = useNftPluginStore((s) => s.state.isLoadingNfts) +const Join = () => { + const { connection } = useConnection() + const actingAsWalletPk = useUserOrDelegator() const wallet = useWalletOnePointOh() const connected = !!wallet?.connected - const connection = useLegacyConnectionContext() - const ownTokenRecord = useUserCommunityTokenOwnerRecord().data?.result const realm = useRealmQuery().data?.result - const { realmInfo } = useRealm() - const client = useVotePluginsClientStore( - (s) => s.state.currentRealmVotingClient - ) + const { userNeedsTokenOwnerRecord, userNeedsVoterWeightRecords, handleRegister } = useJoinRealm(); - const displayNfts = nfts.slice(0, 3) - const remainingCount = Math.max(nfts.length - 3, 0) - const max = maxWeight - ? new BigNumber(maxWeight.account.maxVoterWeight.toString()) - : null - const amount = new BigNumber((votingPower ?? 0).toString()) + const join = async () => { + if (!realm || !wallet?.publicKey) throw new Error() - const handleRegister = async () => { - if (!realm || !wallet?.publicKey || !client.client || !realmInfo) - throw new Error() - const instructions: TransactionInstruction[] = [] - const { voterWeightPk } = await getVoterWeightRecord( - realm.pubkey, - realm.account.communityMint, - wallet.publicKey, - client.client.program.programId - ) - const createVoterWeightRecordIx = await (client.client as NftVoterClient).program.methods - .createVoterWeightRecord(wallet.publicKey) - .accounts({ - voterWeightRecord: voterWeightPk, - governanceProgramId: realm.owner, - realm: realm.pubkey, - realmGoverningTokenMint: realm.account.communityMint, - payer: wallet.publicKey, - systemProgram: SYSTEM_PROGRAM_ID, - }) - .instruction() - instructions.push(createVoterWeightRecordIx) - await withCreateTokenOwnerRecord( - instructions, - realm.owner, - realmInfo.programVersion!, - realm.pubkey, - wallet.publicKey, - realm.account.communityMint, - wallet.publicKey - ) + const instructions = await handleRegister(); const transaction = new Transaction() transaction.add(...instructions) await sendTransaction({ - transaction: transaction, + transaction, wallet: wallet, - connection: connection.current, + connection: connection, signers: [], sendingMessage: `Registering`, successMessage: `Registered`, }) } - if (isLoading) { + return ( + (actingAsWalletPk?.toString() === wallet?.publicKey?.toString() && + connected && + (userNeedsTokenOwnerRecord || userNeedsVoterWeightRecords) && ( + + )) || + null + ) +} + +export default function NftVotingPower(props: Props) { + const userPk = useUserOrDelegator() + const nfts = useVotingNfts(userPk) + + const { isReady, totalCalculatedVoterWeight, calculatedMaxVoterWeight } = useRealmVoterWeightPlugins( + 'community', + ) + + const displayNfts = (nfts ?? []).slice(0, 3) + const remainingCount = Math.max((nfts ?? []).length - 3, 0) + const max = calculatedMaxVoterWeight?.value + ? new BigNumber(calculatedMaxVoterWeight.value.toString()) + : null + const amount = new BigNumber((totalCalculatedVoterWeight?.value ?? 0).toString()) + + if (!isReady || nfts === undefined) { return (
) } @@ -136,11 +117,7 @@ export default function NftVotingPower(props: Props) { )}
- {connected && !ownTokenRecord && ( - - )} +
) } diff --git a/components/ProposalVotingPower/PluginVotingPower.tsx b/components/ProposalVotingPower/PluginVotingPower.tsx new file mode 100644 index 0000000000..42e9659939 --- /dev/null +++ b/components/ProposalVotingPower/PluginVotingPower.tsx @@ -0,0 +1,110 @@ +import classNames from 'classnames' +import useDepositStore from 'VoteStakeRegistry/stores/useDepositStore' + +import { getMintMetadata } from '@components/instructions/programs/splToken' +import { TokenDeposit } from '@components/TokenBalance/TokenDeposit' +import { useLegacyVoterWeight } from '@hooks/queries/governancePower' +import { useMintInfoByPubkeyQuery } from '@hooks/queries/mintInfo' +import { useRealmQuery } from '@hooks/queries/realm' +import { useDelegatorAwareVoterWeight } from '@hooks/useDelegatorAwareVoterWeight' +import { useRealmVoterWeightPlugins } from '@hooks/useRealmVoterWeightPlugins' +import { GoverningTokenRole } from '@solana/spl-governance' +import { BigNumber } from 'bignumber.js' +import clsx from 'clsx' +import { useMemo } from 'react' + +interface Props { + className?: string + role: 'community' | 'council' + showDepositButton?: boolean +} + +export default function PluginVotingPower({ + role, + className, + showDepositButton = true, +}: Props) { + const realm = useRealmQuery().data?.result + const voterWeight = useDelegatorAwareVoterWeight(role) + + const mintInfo = useMintInfoByPubkeyQuery(realm?.account.communityMint).data + ?.result + + const isLoading = useDepositStore((s) => s.state.isLoading) + const { isReady } = useRealmVoterWeightPlugins(role) + const { result: ownVoterWeight } = useLegacyVoterWeight() + + const formattedTokenAmount = useMemo( + () => + mintInfo && ownVoterWeight?.communityTokenRecord + ? new BigNumber( + ownVoterWeight?.communityTokenRecord?.account?.governingTokenDepositAmount?.toString() + ) + .shiftedBy(-mintInfo.decimals) + .toFixed(2) + .toString() + : undefined, + [mintInfo, ownVoterWeight?.communityTokenRecord] + ) + + const relevantMint = + role === 'community' + ? realm?.account.communityMint + : realm?.account.config.councilMint + + const tokenName = + getMintMetadata(relevantMint)?.name ?? realm?.account.name ?? '' + + const formattedTotal = useMemo( + () => + mintInfo && voterWeight?.value + ? new BigNumber(voterWeight?.value.toString()) + .shiftedBy(-mintInfo.decimals) + .toFixed(2) + .toString() + : undefined, + [mintInfo, voterWeight?.value] + ) + + if (isLoading || !isReady) { + return ( +
+ ) + } + + return ( +
+
+
+
+
+ {tokenName} + {role === 'council' ? ' Council' : ''} votes +
+
+

{formattedTotal ?? '0'}

+

+ ({formattedTokenAmount ?? '0'} tokens) +

+
+
+ {showDepositButton && ( +
+ +
+ )} +
+
+
+ ) +} diff --git a/components/ProposalVotingPower/QuadraticVotingInfoModal.tsx b/components/ProposalVotingPower/QuadraticVotingInfoModal.tsx new file mode 100644 index 0000000000..3f9ee6e488 --- /dev/null +++ b/components/ProposalVotingPower/QuadraticVotingInfoModal.tsx @@ -0,0 +1,90 @@ +import { useState } from 'react' + +import { InformationCircleIcon, UserGroupIcon } from '@heroicons/react/solid' +import Modal from '@components/Modal' + +interface QuadraticVotingInfoModalProps { + voteWeight: string + totalVoteWeight: string + totalMembers: number + tokenAmount: string +} + +export default function QuadraticVotingInfoModal({ + voteWeight, + totalVoteWeight, + totalMembers, + tokenAmount, +}: QuadraticVotingInfoModalProps) { + const [showQuadraticModal, setShowQuadraticModal] = useState(false) + + return ( +
+ + setShowQuadraticModal(true)} + /> + + {showQuadraticModal && ( + setShowQuadraticModal(false)} + isOpen={showQuadraticModal} + > +
+
+
+
+

+ {voteWeight ?? '0'} votes +

+

+ ({tokenAmount ?? '0'} tokens) +

+
+
+ +
+

{totalMembers} DAO members

+

holding {totalVoteWeight} tokens

+
+
+
+
+

What is Quadratic Voting?

+
+ Quadratic voting empowers individual voter groups, lessening + the influence of token-rich whales and giving more of a + proportional vote to smaller token holders. +
+
+
+
+
+

How is it calculated?

+
+ Quadratic voting is based on the square root of the amount of + tokens held by a member. The result is the number of{' '} + actual votes. +
+
+
+
+
+
Example Calculation:
+ +
+ 1 votes = 1 token +
+ 2 votes = 4 tokens +
3 votes = 9 tokens +
+
+
+
+
+ )} +
+ ) +} diff --git a/components/ProposalVotingPower/QuadraticVotingPower.tsx b/components/ProposalVotingPower/QuadraticVotingPower.tsx new file mode 100644 index 0000000000..b212c41949 --- /dev/null +++ b/components/ProposalVotingPower/QuadraticVotingPower.tsx @@ -0,0 +1,126 @@ +import classNames from 'classnames' +import useDepositStore from 'VoteStakeRegistry/stores/useDepositStore' + +import { GatewayStatus, useGateway } from '@civic/solana-gateway-react' +import { BN } from '@coral-xyz/anchor' +import { useLegacyVoterWeight } from '@hooks/queries/governancePower' +import { useMintInfoByPubkeyQuery } from '@hooks/queries/mintInfo' +import { useRealmQuery } from '@hooks/queries/realm' +import { useDelegatorAwareVoterWeight } from '@hooks/useDelegatorAwareVoterWeight' +import { + useRealmVoterWeightPlugins, + useRealmVoterWeights, +} from '@hooks/useRealmVoterWeightPlugins' +import { BigNumber } from 'bignumber.js' +import { useMemo } from 'react' +import PluginVotingPower from './PluginVotingPower' +import QuadraticVotingInfoModal from './QuadraticVotingInfoModal' +import { useTokenOwnerRecordsForRealmQuery } from '@hooks/queries/tokenOwnerRecord' +import { Member } from '@utils/uiTypes/members' + +interface Props { + className?: string + role: 'community' | 'council' +} + +export default function QuadraticVotingPower({ role, className }: Props) { + const realm = useRealmQuery().data?.result + const { data: activeMembersData } = useTokenOwnerRecordsForRealmQuery() + const voterWeight = useDelegatorAwareVoterWeight(role) + + const mintInfo = useMintInfoByPubkeyQuery(realm?.account.communityMint).data + ?.result + + const activeMembers: Member[] | undefined = useMemo(() => activeMembersData?.map(member => ({ + walletAddress: member.account.governingTokenOwner.toBase58(), + communityVotes: new BN(0), + councilVotes: new BN(0) + })), [activeMembersData]) + + const isLoading = useDepositStore((s) => s.state.isLoading) + const { + isReady, + calculatedMaxVoterWeight, + plugins, + } = useRealmVoterWeightPlugins(role) + const { result: ownVoterWeight } = useLegacyVoterWeight() + + const formattedTokenAmount = useMemo( + () => + mintInfo && ownVoterWeight?.communityTokenRecord + ? new BigNumber( + ownVoterWeight?.communityTokenRecord?.account?.governingTokenDepositAmount?.toString() + ) + .shiftedBy(-mintInfo.decimals) + .toFixed(2) + .toString() + : undefined, + [mintInfo, ownVoterWeight?.communityTokenRecord] + ) + + const formattedMax = + mintInfo && calculatedMaxVoterWeight?.value + ? new BigNumber(calculatedMaxVoterWeight?.value.toString()) + .shiftedBy(-mintInfo.decimals) + .toString() + : undefined + + const formattedTotal = useMemo( + () => + mintInfo && voterWeight?.value + ? new BigNumber(voterWeight?.value.toString()) + .shiftedBy(-mintInfo.decimals) + .toFixed(2) + .toString() + : undefined, + [mintInfo, voterWeight?.value] + ) + + const { communityWeight, councilWeight } = useRealmVoterWeights() + const { gatewayStatus } = useGateway() + const isQVEnabled = plugins?.voterWeight.some((p) => p.name === 'QV') + const isGatewayEnabled = plugins?.voterWeight.some( + (p) => p.name === 'gateway' + ) + + const hasAnyVotingPower = + councilWeight?.value?.gt(new BN(0)) && communityWeight?.value?.gt(new BN(0)) + + if (isLoading || !isReady) { + return ( +
+ ) + } + + return ( +
+ {hasAnyVotingPower && isQVEnabled && ( +
+

Quadratic Voting

+ +
+ )} + { + // check if the last plugin is gateway to show the voting power + plugins?.voterWeight[plugins.voterWeight.length - 1].name === 'QV' && ( + + ) + } +
+ ) +} diff --git a/components/ProposalVotingPower/TokenHaverVotingPower.tsx b/components/ProposalVotingPower/TokenHaverVotingPower.tsx new file mode 100644 index 0000000000..6870c7e1a2 --- /dev/null +++ b/components/ProposalVotingPower/TokenHaverVotingPower.tsx @@ -0,0 +1,119 @@ +import classNames from 'classnames' +import useDepositStore from 'VoteStakeRegistry/stores/useDepositStore' + +import { getMintMetadata } from '@components/instructions/programs/splToken' +import { useMintInfoByPubkeyQuery } from '@hooks/queries/mintInfo' +import { useRealmQuery } from '@hooks/queries/realm' +import { useDelegatorAwareVoterWeight } from '@hooks/useDelegatorAwareVoterWeight' +import { useRealmVoterWeightPlugins } from '@hooks/useRealmVoterWeightPlugins' +import { BigNumber } from 'bignumber.js' +import clsx from 'clsx' +import { useMemo } from 'react' +import { useJoinRealm } from '@hooks/useJoinRealm' +import { sendTransaction } from '@utils/send' +import { Transaction } from '@solana/web3.js' +import useWalletOnePointOh from '@hooks/useWalletOnePointOh' +import { useConnection } from '@solana/wallet-adapter-react' +import Button from '@components/Button' + +interface Props { + className?: string + role: 'community' | 'council' + showDepositButton?: boolean +} + +export default function TokenHaverVotingPower({ role, className }: Props) { + /** ideally this would all be removed and registration would be automatic upon acting */ + const wallet = useWalletOnePointOh() + const { connection } = useConnection() + const { + userNeedsTokenOwnerRecord, + userNeedsVoterWeightRecords, + handleRegister, + } = useJoinRealm() + const join = async () => { + const instructions = await handleRegister() + const transaction = new Transaction() + transaction.add(...instructions) + + await sendTransaction({ + transaction: transaction, + wallet: wallet!, + connection, + signers: [], + sendingMessage: `Registering`, + successMessage: `Registered`, + }) + } + const showJoinButton = + !!wallet?.connected && + (userNeedsTokenOwnerRecord || userNeedsVoterWeightRecords) + + /** */ + + const realm = useRealmQuery().data?.result + const voterWeight = useDelegatorAwareVoterWeight(role) + + const mintInfo = useMintInfoByPubkeyQuery(realm?.account.communityMint).data + ?.result + + const isLoading = useDepositStore((s) => s.state.isLoading) + const { isReady } = useRealmVoterWeightPlugins(role) + + const relevantMint = + role === 'community' + ? realm?.account.communityMint + : realm?.account.config.councilMint + + const tokenName = + getMintMetadata(relevantMint)?.name ?? realm?.account.name ?? '' + + const formattedTotal = useMemo( + () => + mintInfo && voterWeight?.value + ? new BigNumber(voterWeight?.value.toString()) + .shiftedBy(-mintInfo.decimals) + .toFixed(0) + .toString() + : undefined, + [mintInfo, voterWeight?.value] + ) + + if (isLoading || !isReady) { + return ( +
+ ) + } + + return ( +
+
+
+
+
+
+ {tokenName} + {role === 'council' ? ' Council' : ''} votes +
+
+

+ {formattedTotal ?? '0'} +

+
+
+
+
+
+ {showJoinButton && ( + + )} +
+ ) +} diff --git a/components/QuorumProgress.tsx b/components/QuorumProgress.tsx index 489d3e49c8..7509d42f88 100644 --- a/components/QuorumProgress.tsx +++ b/components/QuorumProgress.tsx @@ -39,6 +39,7 @@ const QuorumProgress = ({ ).toLocaleString(undefined, { maximumFractionDigits: 0, })} ${(progress ?? 0) > 0 ? 'more' : ''} ${voteKindTitle} vote${ + //@ts-ignore (votesRequired ?? 0) > 1 ? 's' : '' } required`}

) : ( diff --git a/components/RealmHeader.tsx b/components/RealmHeader.tsx index f319c8d8cb..41f219c657 100644 --- a/components/RealmHeader.tsx +++ b/components/RealmHeader.tsx @@ -6,22 +6,20 @@ import Link from 'next/link' import useQueryContext from 'hooks/useQueryContext' import { ExternalLinkIcon } from '@heroicons/react/outline' import { getRealmExplorerHost } from 'tools/routing' - import { tryParsePublicKey } from '@tools/core/pubkey' import { useRealmQuery } from '@hooks/queries/realm' -import { useRealmConfigQuery } from '@hooks/queries/realmConfig' -import { NFT_PLUGINS_PKS } from '@constants/plugins' +import { useConnection } from '@solana/wallet-adapter-react' const RealmHeader = () => { const { fmtUrlWithCluster } = useQueryContext() const realm = useRealmQuery().data?.result - const config = useRealmConfigQuery().data?.result const { REALM } = process.env + const { connection } = useConnection() const { realmInfo, symbol, vsrMode } = useRealm() const explorerHost = getRealmExplorerHost(realmInfo) - const realmUrl = `https://${explorerHost}/#/realm/${realmInfo?.realmId.toBase58()}?programId=${realmInfo?.programId.toBase58()}` + const realmUrl = `https://${explorerHost}/account/${realmInfo?.realmId.toBase58()}${connection.rpcEndpoint.includes("devnet") ? "?cluster=devnet" : ""}` const [isBackNavVisible, setIsBackNavVisible] = useState(true) @@ -68,17 +66,6 @@ const RealmHeader = () => {
)}
- {(!config?.account.communityTokenConfig.voterWeightAddin || - NFT_PLUGINS_PKS.includes( - config?.account.communityTokenConfig.voterWeightAddin.toBase58() - )) && ( - - - - Members - - - )} {vsrMode === 'default' && ( @@ -90,6 +77,12 @@ const RealmHeader = () => { )} + + + + Members + + diff --git a/components/SelectPrimaryDelegators.tsx b/components/SelectPrimaryDelegators.tsx index b30f259482..6f58515587 100644 --- a/components/SelectPrimaryDelegators.tsx +++ b/components/SelectPrimaryDelegators.tsx @@ -8,6 +8,11 @@ import { useMemo } from 'react' import { ProgramAccount, TokenOwnerRecord } from '@solana/spl-governance' import { capitalize } from '@utils/helpers' import { ProfileName } from './Profile/ProfileName' +import { useAsync } from 'react-async-hook' +import { determineVotingPowerType } from '@hooks/queries/governancePower' +import { useConnection } from '@solana/wallet-adapter-react' +import useSelectedRealmPubkey from '@hooks/selectedRealm/useSelectedRealmPubkey' +import { DELEGATOR_BATCH_VOTE_SUPPORT_BY_PLUGIN } from '@constants/flags' const YOUR_WALLET_VALUE = 'Yourself + all delegators' const JUST_YOUR_WALLET = 'Yourself only' @@ -17,7 +22,7 @@ const SelectPrimaryDelegators = () => { const walletId = wallet?.publicKey?.toBase58() const realm = useRealmQuery().data?.result - const delegatesArray = useTokenOwnerRecordsDelegatedToUser() + const { data: delegatesArray } = useTokenOwnerRecordsDelegatedToUser() // returns array of community tokenOwnerRecords that connected wallet has been delegated const communityTorsDelegatedToUser = useMemo( @@ -92,10 +97,19 @@ const SelectPrimaryDelegators = () => { export default SelectPrimaryDelegators -function PrimaryDelegatorSelect({ +const usePluginNameAsync = (kind: 'community' | 'council') => { + const { connection } = useConnection() + const realmPk = useSelectedRealmPubkey() + return useAsync( + async () => + kind && realmPk && determineVotingPowerType(connection, realmPk, kind), + [connection, realmPk, kind] + ) +} + +function PrimaryDelegatorSelectBatchSupported({ selectedDelegator, handleSelect, - kind, tors, }: { @@ -106,6 +120,11 @@ function PrimaryDelegatorSelect({ }) { const wallet = useWalletOnePointOh() const walletPk = wallet?.publicKey ?? undefined + + const { result: plugin } = usePluginNameAsync(kind) + const batchDelegatorUxSupported = + plugin && DELEGATOR_BATCH_VOTE_SUPPORT_BY_PLUGIN[plugin] + return (
@@ -131,13 +150,15 @@ function PrimaryDelegatorSelect({
) - ) : ( + ) : batchDelegatorUxSupported ? ( YOUR_WALLET_VALUE + ) : ( + JUST_YOUR_WALLET ) } > - {YOUR_WALLET_VALUE} + {batchDelegatorUxSupported ? YOUR_WALLET_VALUE : JUST_YOUR_WALLET} {walletPk ? ( ) } + +// its a conditional, make it use the old or new component depending on support. thanks. +const PrimaryDelegatorSelect = ( + props: Parameters[0] +) => { + const { result: plugin } = usePluginNameAsync(props.kind) + const batchDelegatorUxSupported = + plugin && DELEGATOR_BATCH_VOTE_SUPPORT_BY_PLUGIN[plugin] + return batchDelegatorUxSupported ? ( + + ) : ( + + ) +} + +/** Used when batched delegator voting is not supported */ +function PrimaryDelegatorSelectOld({ + selectedDelegator, + handleSelect, + + kind, + tors, +}: { + selectedDelegator: PublicKey | undefined + handleSelect: (tokenRecordPk: string) => void + kind: 'community' | 'council' + tors: ProgramAccount[] +}) { + const wallet = useWalletOnePointOh() + const walletPk = wallet?.publicKey ?? undefined + + return ( +
+
+

+ Perform {capitalize(kind)} actions as: +

+ +
+
+ ) +} diff --git a/components/SendNft.tsx b/components/SendNft.tsx index 1ac4949ec8..255ee4df2b 100644 --- a/components/SendNft.tsx +++ b/components/SendNft.tsx @@ -1,10 +1,9 @@ import Button from '@components/Button' import Input from '@components/inputs/Input' -import useRealm from '@hooks/useRealm' import { PublicKey } from '@solana/web3.js' import { tryParseKey } from '@tools/validators/pubkey' import { abbreviateAddress } from '@utils/formatting' -import { useMemo, useState } from 'react' +import React, { useMemo, useState } from 'react' import { ArrowCircleDownIcon, ArrowCircleUpIcon, @@ -50,6 +49,7 @@ import { SUPPORT_CNFTS } from '@constants/flags' import clsx from 'clsx' import { getNetworkFromEndpoint } from '@utils/connection' import { buildTransferCnftInstruction } from '@hooks/instructions/useTransferCnftInstruction' +import {useVoteByCouncilToggle} from "@hooks/useVoteByCouncilToggle"; const SendNft = ({ initialNftAndGovernanceSelected, @@ -60,7 +60,6 @@ const SendNft = ({ }) => { const { connection } = useConnection() const realm = useRealmQuery().data?.result - const { canChooseWhoVote } = useRealm() const { propose } = useCreateProposal() const { canUseTransferInstruction } = useGovernanceAssets() const { fmtUrlWithCluster } = useQueryContext() @@ -75,7 +74,7 @@ const SendNft = ({ const [selectedGovernance, setSelectedGovernance] = useGovernanceSelect( initialNftAndGovernanceSelected?.[1] ) - const [voteByCouncil, setVoteByCouncil] = useState(false) + const { voteByCouncil, shouldShowVoteByCouncilToggle, setVoteByCouncil } = useVoteByCouncilToggle(); const [showOptions, setShowOptions] = useState(false) const [destination, setDestination] = useState('') const [title, setTitle] = useState('') @@ -283,13 +282,13 @@ const SendNft = ({ value={description} onChange={(evt) => setDescription(evt.target.value)} > - {canChooseWhoVote && ( - { - setVoteByCouncil(!voteByCouncil) - }} - > + {shouldShowVoteByCouncilToggle && ( + { + setVoteByCouncil(!voteByCouncil) + }} + > )} )} diff --git a/components/TermsPopup.tsx b/components/TermsPopup.tsx new file mode 100644 index 0000000000..a2360e0c6e --- /dev/null +++ b/components/TermsPopup.tsx @@ -0,0 +1,57 @@ +import Modal from "./Modal" +import Button, { SecondaryButton } from './Button' +import { useEffect, useState } from "react" +import { useRouter } from "next/router" + +const TermsPopupModal = () => { + const [openModal, setOpenModal] = useState(true) + const [isClient, setIsClient] = useState(false) + const router = useRouter() + + useEffect(() => { + setIsClient(true) + }, []) + + useEffect(() => { + if (localStorage) { + const isTermAccepted = typeof window !== "undefined" ? + localStorage.getItem("accept-terms") === "true" : + false + + if (isTermAccepted) { + setOpenModal(false) + } + } + }) + + const acceptTerms = () => { + localStorage.setItem("accept-terms", "true") + setOpenModal(false) + } + + const rejectTerms = () => { + localStorage.setItem("accept-terms", "false") + router.push("https://realms.today?terms=rejected") + } + + return ( + <> + {isClient && openModal ? + ( setOpenModal(false)} bgClickClose={false} hideClose={true}> +

+ The operating entity of this site and owner of the related intellectual property has + changed. The new operator is Realms Today Ltd. (the New Operator). We have accordingly + amended the Terms and the Private Policy governing the relationship between our users + and the New Operator. By clicking "accept", you represent and warrant that you agree to + the revised Terms and Private Policy. +

+
+ + Reject +
+
) : null + } + ) +} + +export default TermsPopupModal; \ No newline at end of file diff --git a/components/TokenBalance/ClaimUnreleasedNFTs.tsx b/components/TokenBalance/ClaimUnreleasedNFTs.tsx index 8c05dbdfa4..bc40cfdd23 100644 --- a/components/TokenBalance/ClaimUnreleasedNFTs.tsx +++ b/components/TokenBalance/ClaimUnreleasedNFTs.tsx @@ -1,17 +1,14 @@ import useRealm from '@hooks/useRealm' import { useEffect, useState } from 'react' -import { TransactionInstruction } from '@solana/web3.js' +import { PublicKey, TransactionInstruction } from '@solana/web3.js' import { SecondaryButton } from '@components/Button' -import useVotePluginsClientStore from 'stores/useVotePluginsClientStore' -import { NftVoterClient } from '@utils/uiTypes/NftVoterClient' import { chunks } from '@utils/helpers' -import { getRegistrarPDA, getVoterWeightRecord } from '@utils/plugin/accounts' import { sendTransactionsV3, SequenceType, txBatchesToInstructionSetWithSigners, } from '@utils/sendTransactions' -import { ProposalState, getProposal } from '@solana/spl-governance' +import { ProgramAccount, Proposal, ProposalState, getProposal } from '@solana/spl-governance' import useWalletOnePointOh from '@hooks/useWalletOnePointOh' import { useAddressQuery_CommunityTokenOwner } from '@hooks/queries/addresses/tokenOwnerRecord' import { useRealmQuery } from '@hooks/queries/realm' @@ -22,9 +19,15 @@ import { proposalQueryKeys, useRealmProposalsQuery, } from '@hooks/queries/proposal' +import {useNftClient} from "../../VoterWeightPlugins/useNftClient"; const NFT_SOL_BALANCE = 0.0014616 +type NftRecordsSet = { + proposal: PublicKey, + records: PublicKey[] +} + const ClaimUnreleasedNFTs = ({ inAccountDetails, }: { @@ -37,9 +40,7 @@ const ClaimUnreleasedNFTs = ({ const [solToBeClaimed, setSolToBeClaimed] = useState(0) const ownNftVoteRecordsFilterd = ownNftVoteRecords const realm = useRealmQuery().data?.result - const client = useVotePluginsClientStore( - (s) => s.state.currentRealmVotingClient - ) + const { nftClient } = useNftClient(); const { isNftMode } = useRealm() const { data: tokenOwnerRecord } = useAddressQuery_CommunityTokenOwner() @@ -49,63 +50,88 @@ const ClaimUnreleasedNFTs = ({ if (!wallet?.publicKey) throw new Error('no wallet') if (!realm) throw new Error() if (!tokenOwnerRecord) throw new Error() + if (!nftClient) throw new Error("not an NFT realm") setIsLoading(true) const instructions: TransactionInstruction[] = [] - const { registrar } = await getRegistrarPDA( - realm.pubkey, - realm.account.communityMint, - client.client!.program.programId - ) - const { voterWeightPk } = await getVoterWeightRecord( - realm.pubkey, - realm.account.communityMint, - wallet.publicKey, - client.client!.program.programId - ) + const { registrar } = nftClient.getRegistrarPDA(realm.pubkey, realm.account.communityMint); + + const { voterWeightPk } = await nftClient.getVoterWeightRecordPDA(realm.pubkey, realm.account.communityMint, wallet.publicKey) const nfts = ownNftVoteRecordsFilterd.slice( 0, count ? count : ownNftVoteRecordsFilterd.length ) + + const fetchedProposals: ProgramAccount[] = []; + const nftRecordsSet: NftRecordsSet[] = []; + for (const i of nfts) { - const proposalQuery = await queryClient.fetchQuery({ - queryKey: proposalQueryKeys.byPubkey( - connection.rpcEndpoint, - i.account.proposal - ), - staleTime: 0, - queryFn: () => - asFindable(() => getProposal(connection, i.account.proposal))(), - }) - const proposal = proposalQuery.result + const isProposalFetched = fetchedProposals.find(proposal => proposal.pubkey.equals(i.account.proposal)) + let currentProposal: ProgramAccount | undefined; + + if (isProposalFetched) { + currentProposal = isProposalFetched + } else { + const proposalQuery = await queryClient.fetchQuery({ + queryKey: proposalQueryKeys.byPubkey( + connection.rpcEndpoint, + i.account.proposal + ), + staleTime: 0, + queryFn: () => + asFindable(() => getProposal(connection, i.account.proposal))(), + }) + currentProposal = proposalQuery.result + if (proposalQuery.result) { + fetchedProposals.push(proposalQuery.result) + nftRecordsSet.push({ + proposal: proposalQuery.result.pubkey, + records: [] + }) + } + } + if ( - proposal === undefined || - proposal.account.state === ProposalState.Voting + currentProposal === undefined || + currentProposal.account.state === ProposalState.Voting ) { // ignore this one as it's still in voting continue } - const relinquishNftVoteIx = await (client.client as NftVoterClient).program.methods - .relinquishNftVote() - .accounts({ - registrar, - voterWeightRecord: voterWeightPk, - governance: proposal.account.governance, - proposal: i.account.proposal, - voterTokenOwnerRecord: tokenOwnerRecord, - voterAuthority: wallet.publicKey, - voteRecord: i.publicKey, - beneficiary: wallet!.publicKey!, - }) - .remainingAccounts([ - { pubkey: i.publicKey, isSigner: false, isWritable: true }, - ]) - .instruction() - instructions.push(relinquishNftVoteIx) + const currentRecordsIndex = nftRecordsSet.findIndex(r => r.proposal.equals(currentProposal!.pubkey)) + nftRecordsSet[currentRecordsIndex].records.push(i.publicKey) + } + + for (const r of nftRecordsSet) { + const ixChunks = chunks(r.records, 25) + + for (const ix of ixChunks) { + const proposal = fetchedProposals.find(p => p.pubkey.equals(r.proposal)) + + const relinquishNftVoteIx = await nftClient.program.methods + .relinquishNftVote() + .accounts({ + registrar, + voterWeightRecord: voterWeightPk, + governance: proposal!.account.governance, + proposal: r.proposal, + voterTokenOwnerRecord: tokenOwnerRecord, + voterAuthority: wallet.publicKey!, + voteRecord: ix[0], + beneficiary: wallet!.publicKey!, + }) + .remainingAccounts(ix.map(c => ( + { pubkey: c, isSigner: false, isWritable: true } + ))) + .instruction() + + instructions.push(relinquishNftVoteIx) + } } + try { - const insertChunks = chunks(instructions, 10).map((txBatch, batchIdx) => { + const insertChunks = chunks(instructions, 1).map((txBatch, batchIdx) => { return { instructionsSet: txBatchesToInstructionSetWithSigners( txBatch, @@ -115,11 +141,13 @@ const ClaimUnreleasedNFTs = ({ sequenceType: SequenceType.Parallel, } }) + await sendTransactionsV3({ connection, wallet: wallet!, transactionInstructions: insertChunks, }) + setIsLoading(false) getNftsVoteRecord() } catch (e) { @@ -128,7 +156,7 @@ const ClaimUnreleasedNFTs = ({ } } const getNftsVoteRecord = async () => { - const nftClient = client.client as NftVoterClient + if (!nftClient) throw new Error("not an NFT realm"); const nftVoteRecords = await nftClient.program.account.nftVoteRecord?.all([ { memcmp: { @@ -149,15 +177,16 @@ const ClaimUnreleasedNFTs = ({ proposal.account.state !== ProposalState.Voting ) }) + setOwnNftVoteRecords(nftVoteRecordsFiltered) setSolToBeClaimed(nftVoteRecordsFiltered.length * NFT_SOL_BALANCE) } useEffect(() => { - if (wallet?.publicKey && isNftMode && client.client) { + if (wallet?.publicKey && isNftMode && nftClient) { getNftsVoteRecord() } // eslint-disable-next-line react-hooks/exhaustive-deps -- TODO please fix, it can cause difficult bugs. You might wanna check out https://bobbyhadz.com/blog/react-hooks-exhaustive-deps for info. -@asktree - }, [client.clientType, isNftMode, wallet?.publicKey?.toBase58()]) + }, [nftClient, isNftMode, wallet?.publicKey?.toBase58()]) if (isNftMode) { return ( diff --git a/components/TokenBalance/TokenBalanceCardWrapper.tsx b/components/TokenBalance/TokenBalanceCardWrapper.tsx index c34a674772..75de2f2fa9 100644 --- a/components/TokenBalance/TokenBalanceCardWrapper.tsx +++ b/components/TokenBalance/TokenBalanceCardWrapper.tsx @@ -2,18 +2,23 @@ import useRealm from '@hooks/useRealm' import dynamic from 'next/dynamic' import { ChevronRightIcon } from '@heroicons/react/solid' import useQueryContext from '@hooks/useQueryContext' -import { GATEWAY_PLUGINS_PKS, NFT_PLUGINS_PKS } from '@constants/plugins' import GatewayCard from '@components/Gateway/GatewayCard' import ClaimUnreleasedNFTs from './ClaimUnreleasedNFTs' import Link from 'next/link' import { useAddressQuery_CommunityTokenOwner } from '@hooks/queries/addresses/tokenOwnerRecord' import useWalletOnePointOh from '@hooks/useWalletOnePointOh' import { useUserCommunityTokenOwnerRecord } from '@hooks/queries/tokenOwnerRecord' -import { useRealmConfigQuery } from '@hooks/queries/realmConfig' import ClaimUnreleasedPositions from 'HeliumVotePlugin/components/ClaimUnreleasedPositions' import VanillaAccountDetails from './VanillaAccountDetails' import GovernancePowerCard from '@components/GovernancePower/GovernancePowerCard' import SelectPrimaryDelegators from '@components/SelectPrimaryDelegators' +import PythAccountDetails from 'PythVotePlugin/components/PythAccountDetails' +import { useRealmVoterWeightPlugins } from '@hooks/useRealmVoterWeightPlugins' +import { ReactNode } from 'react' +import QuadraticVotingPower from '@components/ProposalVotingPower/QuadraticVotingPower' +import VanillaVotingPower from '@components/GovernancePower/Power/Vanilla/VanillaVotingPower' +import React from 'react' +import ParclAccountDetails from 'ParclVotePlugin/components/ParclAccountDetails' const LockPluginTokenBalanceCard = dynamic( () => @@ -65,40 +70,40 @@ const TokenBalanceCardInner = ({ inAccountDetails?: boolean }) => { const ownTokenRecord = useUserCommunityTokenOwnerRecord().data?.result - const config = useRealmConfigQuery().data?.result + const { plugins } = useRealmVoterWeightPlugins('community') + const requiredCards = plugins?.voterWeight.map((plugin) => plugin.name) - const { vsrMode } = useRealm() - const currentPluginPk = config?.account?.communityTokenConfig.voterWeightAddin - const isNftMode = - currentPluginPk && NFT_PLUGINS_PKS.includes(currentPluginPk?.toBase58()) - const isGatewayMode = - currentPluginPk && GATEWAY_PLUGINS_PKS.includes(currentPluginPk?.toBase58()) + const showHeliumCard = requiredCards?.includes('HeliumVSR') + const showDefaultVSRCard = requiredCards?.includes('VSR') + const showPythCard = requiredCards?.includes('pyth') + const showNftCard = requiredCards?.includes('NFT') + const showGatewayCard = requiredCards?.includes('gateway') + const showQvCard = requiredCards?.includes('QV') + const showParclCard = requiredCards?.includes('parcl'); - if ( - vsrMode === 'default' && - (!ownTokenRecord || - ownTokenRecord.account.governingTokenDepositAmount.isZero()) - ) { - return + if (showDefaultVSRCard && inAccountDetails) { + return // does this ever actually occur in the component hierarchy? } + const cards: ReactNode[] = [] + if ( - vsrMode === 'helium' && + showHeliumCard && (!ownTokenRecord || ownTokenRecord.account.governingTokenDepositAmount.isZero()) ) { - return ( - <> + cards.push( + {!inAccountDetails && } - + ) } - if (isNftMode && inAccountDetails) { - return ( -
+ if (showNftCard && inAccountDetails) { + cards.push( +
@@ -108,14 +113,58 @@ const TokenBalanceCardInner = ({ ) } + if (showPythCard) { + cards.push( + + {inAccountDetails ? : } + + ) + } + + if (showGatewayCard) { + cards.push( + + {inAccountDetails ? ( + + ) : ( + + )} + + ) + } + + if (showQvCard) { + cards.push( + + {inAccountDetails && ( + <> + + + + )} + + ) + } + + if (showParclCard) { + cards.push( + + {!inAccountDetails && } + + + ) + } + //Default - return ( - <> - {inAccountDetails ? : } + if (cards.length === 0) { + cards.push( + + {inAccountDetails ? : } + + ) + } - {isGatewayMode && } - - ) + return <>{cards} } const TokenBalanceCardWrapper = ({ diff --git a/components/TokenBalance/TokenDeposit.tsx b/components/TokenBalance/TokenDeposit.tsx index 9f8e5baefd..ce6d35a150 100644 --- a/components/TokenBalance/TokenDeposit.tsx +++ b/components/TokenBalance/TokenDeposit.tsx @@ -1,34 +1,11 @@ -import { - ASSOCIATED_TOKEN_PROGRAM_ID, - MintInfo, - Token, - TOKEN_PROGRAM_ID, -} from '@solana/spl-token' -import { PublicKey, Transaction, TransactionInstruction } from '@solana/web3.js' +import { MintInfo } from '@solana/spl-token' import BN from 'bn.js' import useRealm from '@hooks/useRealm' -import { - getProposal, - GoverningTokenType, - ProposalState, -} from '@solana/spl-governance' -import { getUnrelinquishedVoteRecords } from '@models/api' -import { withRelinquishVote } from '@solana/spl-governance' -import { withWithdrawGoverningTokens } from '@solana/spl-governance' -import { sendTransaction } from '@utils/send' -import { SecondaryButton } from '../Button' +import { GoverningTokenType } from '@solana/spl-governance' import { GoverningTokenRole } from '@solana/spl-governance' import { fmtMintAmount } from '@tools/sdk/units' import { getMintMetadata } from '../instructions/programs/splToken' -import { withFinalizeVote } from '@solana/spl-governance' -import { chunks } from '@utils/helpers' -import { getProgramVersionForRealm } from '@models/registry/api' -import { notify } from '@utils/notifications' -import { ExclamationIcon } from '@heroicons/react/outline' import { useEffect } from 'react' -import useVotePluginsClientStore from 'stores/useVotePluginsClientStore' -import { VSR_PLUGIN_PKS } from '@constants/plugins' -import { useMaxVoteRecord } from '@hooks/useMaxVoteRecord' import useWalletOnePointOh from '@hooks/useWalletOnePointOh' import { useUserCommunityTokenOwnerRecord, @@ -36,40 +13,41 @@ import { } from '@hooks/queries/tokenOwnerRecord' import { useRealmQuery } from '@hooks/queries/realm' import { useRealmConfigQuery } from '@hooks/queries/realmConfig' -import { fetchGovernanceByPubkey } from '@hooks/queries/governance' -import { useConnection } from '@solana/wallet-adapter-react' -import queryClient from '@hooks/queries/queryClient' -import { proposalQueryKeys } from '@hooks/queries/proposal' -import asFindable from '@utils/queries/asFindable' -import VanillaVotingPower from '@components/GovernancePower/Vanilla/VanillaVotingPower' +import VanillaVotingPower from '@components/GovernancePower/Power/Vanilla/VanillaVotingPower' import { DepositTokensButton } from '@components/DepositTokensButton' +import VanillaWithdrawTokensButton from './VanillaWithdrawTokensButton' +import Button from '@components/Button' +import { useJoinRealm } from '@hooks/useJoinRealm' +import { Transaction } from '@solana/web3.js' +import { sendTransaction } from '@utils/send' +import { useConnection } from '@solana/wallet-adapter-react' +/** deposit + withdraw for vanilla govtokens, used only in account view. plugin views still use this for council. */ export const TokenDeposit = ({ mint, tokenRole, - councilVote, inAccountDetails, setHasGovPower, + hideVotes, }: { mint: MintInfo | undefined tokenRole: GoverningTokenRole - councilVote?: boolean inAccountDetails?: boolean setHasGovPower?: (hasGovPower: boolean) => void + hideVotes?: boolean }) => { const wallet = useWalletOnePointOh() const connected = !!wallet?.connected - const { connection } = useConnection() - - const client = useVotePluginsClientStore( - (s) => s.state.currentRealmVotingClient - ) - - const maxVoterWeight = useMaxVoteRecord()?.pubkey || undefined + const { + userNeedsTokenOwnerRecord, + userNeedsVoterWeightRecords, + handleRegister, + } = useJoinRealm() const ownTokenRecord = useUserCommunityTokenOwnerRecord().data?.result const ownCouncilTokenRecord = useUserCouncilTokenOwnerRecord().data?.result const realm = useRealmQuery().data?.result const config = useRealmConfigQuery().data?.result + const { connection } = useConnection() const relevantTokenConfig = tokenRole === GoverningTokenRole.Community @@ -78,13 +56,7 @@ export const TokenDeposit = ({ const isMembership = relevantTokenConfig?.tokenType === GoverningTokenType.Membership - const { - realmInfo, - realmTokenAccount, - councilTokenAccount, - toManyCommunityOutstandingProposalsForUser, - toManyCouncilOutstandingProposalsForUse, - } = useRealm() + const { realmTokenAccount, councilTokenAccount } = useRealm() const depositTokenRecord = tokenRole === GoverningTokenRole.Community @@ -107,150 +79,6 @@ export const TokenDeposit = ({ tokenRole === GoverningTokenRole.Community ? '' : 'Council' }` - const withdrawAllTokens = async function () { - const instructions: TransactionInstruction[] = [] - // If there are unrelinquished votes for the voter then let's release them in the same instruction as convenience - if (depositTokenRecord!.account!.unrelinquishedVotesCount > 0) { - const voteRecords = await getUnrelinquishedVoteRecords( - connection, - realmInfo!.programId, - depositTokenRecord!.account!.governingTokenOwner - ) - - for (const voteRecord of Object.values(voteRecords)) { - const proposalQuery = await queryClient.fetchQuery({ - queryKey: proposalQueryKeys.byPubkey( - connection.rpcEndpoint, - voteRecord.account.proposal - ), - staleTime: 0, - queryFn: () => - asFindable(() => - getProposal(connection, voteRecord.account.proposal) - )(), - }) - const proposal = proposalQuery.result - if (!proposal) { - continue - } - - if (proposal.account.state === ProposalState.Voting) { - if (proposal.account.state === ProposalState.Voting) { - const governance = ( - await fetchGovernanceByPubkey( - connection, - proposal.account.governance - ) - ).result - if (!governance) throw new Error('failed to fetch governance') - if (proposal.account.getTimeToVoteEnd(governance.account) > 0) { - // Note: It's technically possible to withdraw the vote here but I think it would be confusing and people would end up unconsciously withdrawing their votes - notify({ - type: 'error', - message: `Can't withdraw tokens while Proposal ${proposal.account.name} is being voted on. Please withdraw your vote first`, - }) - throw new Error( - `Can't withdraw tokens while Proposal ${proposal.account.name} is being voted on. Please withdraw your vote first` - ) - } else { - // finalize proposal before withdrawing tokens so we don't stop the vote from succeeding - await withFinalizeVote( - instructions, - realmInfo!.programId, - getProgramVersionForRealm(realmInfo!), - realm!.pubkey, - proposal.account.governance, - proposal.pubkey, - proposal.account.tokenOwnerRecord, - proposal.account.governingTokenMint, - maxVoterWeight - ) - } - } - } - // Note: We might hit single transaction limits here (accounts and size) if user has too many unrelinquished votes - // It's not going to be an issue for now due to the limited number of proposals so I'm leaving it for now - // As a temp. work around I'm leaving the 'Release Tokens' button on finalized Proposal to make it possible to release the tokens from one Proposal at a time - await withRelinquishVote( - instructions, - realmInfo!.programId, - realmInfo!.programVersion!, - realmInfo!.realmId, - proposal.account.governance, - proposal.pubkey, - depositTokenRecord!.pubkey, - proposal.account.governingTokenMint, - voteRecord.pubkey, - depositTokenRecord!.account.governingTokenOwner, - wallet!.publicKey! - ) - await client.withRelinquishVote( - instructions, - proposal, - voteRecord.pubkey, - depositTokenRecord!.pubkey - ) - } - } - let ata: PublicKey | null = null - if (!depositTokenAccount) { - ata = await Token.getAssociatedTokenAddress( - ASSOCIATED_TOKEN_PROGRAM_ID, - TOKEN_PROGRAM_ID, - depositMint!, - wallet!.publicKey!, - true - ) - const ataIx = Token.createAssociatedTokenAccountInstruction( - ASSOCIATED_TOKEN_PROGRAM_ID, - TOKEN_PROGRAM_ID, - depositMint!, - ata, - wallet!.publicKey!, - wallet!.publicKey! // fee payer - ) - instructions.push(ataIx) - } - - await withWithdrawGoverningTokens( - instructions, - realmInfo!.programId, - realmInfo!.programVersion!, - realm!.pubkey, - depositTokenAccount?.publicKey - ? depositTokenAccount!.publicKey - : new PublicKey(ata!), - depositTokenRecord!.account.governingTokenMint, - wallet!.publicKey! - ) - - try { - // use chunks of 8 here since we added finalize, - // because previously 9 withdraws used to fit into one tx - const ixChunks = chunks(instructions, 8) - for (const [index, chunk] of ixChunks.entries()) { - const transaction = new Transaction().add(...chunk) - await sendTransaction({ - connection, - wallet: wallet!, - transaction, - sendingMessage: - index == ixChunks.length - 1 - ? 'Withdrawing tokens' - : `Releasing tokens (${index}/${ixChunks.length - 2})`, - successMessage: - index == ixChunks.length - 1 - ? 'Tokens have been withdrawn' - : `Released tokens (${index}/${ixChunks.length - 2})`, - }) - } - } catch (ex) { - //TODO change to more friendly notification - notify({ type: 'error', message: `${ex}` }) - console.error("Can't withdraw tokens", ex) - } - } - const hasTokensInWallet = depositTokenAccount && depositTokenAccount.account.amount.gt(new BN(0)) @@ -258,16 +86,6 @@ export const TokenDeposit = ({ depositTokenRecord && depositTokenRecord.account.governingTokenDepositAmount.gt(new BN(0)) - const withdrawTooltipContent = !connected - ? 'Connect your wallet to withdraw' - : !hasTokensDeposited - ? "You don't have any tokens deposited to withdraw." - : !councilVote && - (toManyCouncilOutstandingProposalsForUse || - toManyCommunityOutstandingProposalsForUser) - ? 'You have to many outstanding proposals to withdraw.' - : '' - const availableTokens = depositTokenRecord && mint ? fmtMintAmount( @@ -276,6 +94,21 @@ export const TokenDeposit = ({ ) : '0' + const join = async () => { + const instructions = await handleRegister() + const transaction = new Transaction() + transaction.add(...instructions) + + await sendTransaction({ + transaction: transaction, + wallet: wallet!, + connection, + signers: [], + sendingMessage: `Registering`, + successMessage: `Registered`, + }) + } + useEffect(() => { if (availableTokens != '0' || hasTokensDeposited || hasTokensInWallet) { if (setHasGovPower) setHasGovPower(true) @@ -289,12 +122,15 @@ export const TokenDeposit = ({ : hasTokensInWallet ? availableTokens : 0 - const isVsr = - config?.account?.communityTokenConfig?.voterWeightAddin && - VSR_PLUGIN_PKS.includes( - config?.account?.communityTokenConfig?.voterWeightAddin?.toBase58() - ) && - tokenRole === GoverningTokenRole.Community + + // There are two buttons available on this UI: + // The Deposit button - available if you have tokens to deposit + // The Join button - available if you have already deposited tokens (you have a Token Owner Record) + // but you may not have all your Voter Weight Records yet. + // This latter case may occur if the DAO changes its configuration and new Voter Weight Records are required. + // For example if a new plugin is added. + const showJoinButton = + !userNeedsTokenOwnerRecord && userNeedsVoterWeightRecords // Do not show deposits for mints with zero supply because nobody can deposit anyway if (!mint || mint.supply.isZero()) { @@ -303,7 +139,7 @@ export const TokenDeposit = ({ return (
- {(availableTokens != '0' || inAccountDetails) && ( + {(availableTokens != '0' || inAccountDetails) && !hideVotes && (
{tokenRole === GoverningTokenRole.Community ? ( @@ -313,57 +149,42 @@ export const TokenDeposit = ({
)} - { - <> -
- You have {tokensToShow} {hasTokensDeposited ? `more ` : ``} - {depositTokenName} tokens available to deposit. -
- -
- {hasTokensInWallet || inAccountDetails ? ( - - ) : null} - {!isMembership && // Membership tokens can't be withdrawn (that is their whole point, actually) - (inAccountDetails || isVsr) && ( - - Withdraw - - )} -
- - } - {isVsr && ( - - - Please withdraw your tokens and deposit again to get governance power - - )} +
+ You have {tokensToShow} {hasTokensDeposited ? `more ` : ``} + {depositTokenName} tokens available to deposit. +
+ +
+ {connected && showJoinButton ? ( + + ) : hasTokensInWallet || inAccountDetails ? ( + + ) : null} + {!isMembership && // Membership tokens can't be withdrawn (that is their whole point, actually) + inAccountDetails && ( + + )} +
) } diff --git a/components/TokenBalance/VanillaAccountDetails.tsx b/components/TokenBalance/VanillaAccountDetails.tsx index 7d35da1b41..0004c0bea2 100644 --- a/components/TokenBalance/VanillaAccountDetails.tsx +++ b/components/TokenBalance/VanillaAccountDetails.tsx @@ -34,7 +34,6 @@ const VanillaAccountDetails = () => { )} @@ -42,7 +41,6 @@ const VanillaAccountDetails = () => { )} diff --git a/components/TokenBalance/VanillaWithdrawTokensButton.tsx b/components/TokenBalance/VanillaWithdrawTokensButton.tsx new file mode 100644 index 0000000000..eb4fc438ee --- /dev/null +++ b/components/TokenBalance/VanillaWithdrawTokensButton.tsx @@ -0,0 +1,331 @@ +import { + ASSOCIATED_TOKEN_PROGRAM_ID, + Token, + TOKEN_PROGRAM_ID, +} from '@solana/spl-token' +import { PublicKey, TransactionInstruction } from '@solana/web3.js' +import BN from 'bn.js' +import useRealm from '@hooks/useRealm' +import { + getProposal, + Governance, + GoverningTokenType, + ProgramAccount, +} from '@solana/spl-governance' +import { getProposalsAtVotingStateByTOR, getUnrelinquishedVoteRecords } from '@models/api' +import { withRelinquishVote } from '@solana/spl-governance' +import { withWithdrawGoverningTokens } from '@solana/spl-governance' +import { SecondaryButton } from '../Button' +import { withFinalizeVote } from '@solana/spl-governance' +import { chunks } from '@utils/helpers' +import { getProgramVersionForRealm } from '@models/registry/api' +import { notify } from '@utils/notifications' +import { useMaxVoteRecord } from '@hooks/useMaxVoteRecord' +import useWalletOnePointOh from '@hooks/useWalletOnePointOh' +import { + useUserCommunityTokenOwnerRecord, + useUserCouncilTokenOwnerRecord, +} from '@hooks/queries/tokenOwnerRecord' +import { useRealmQuery } from '@hooks/queries/realm' +import { useRealmConfigQuery } from '@hooks/queries/realmConfig' +import { fetchGovernanceByPubkey } from '@hooks/queries/governance' +import { useConnection } from '@solana/wallet-adapter-react' +import queryClient from '@hooks/queries/queryClient' +import { proposalQueryKeys } from '@hooks/queries/proposal' +import asFindable from '@utils/queries/asFindable' +import { fetchTokenAccountByPubkey } from '@hooks/queries/tokenAccount' +import {useVotingClients} from "@hooks/useVotingClients"; +import { sendTransactionsV3, SequenceType } from '@utils/sendTransactions' +import { useState } from 'react' + +// TODO make this have reasonable props +// TODO, also just refactor it +const VanillaWithdrawTokensButton = ({ + role, +}: { + role: 'community' | 'council' +}) => { + const [disableButton, setDisableButton] = useState(false) + const wallet = useWalletOnePointOh() + const connected = !!wallet?.connected + const { connection } = useConnection() + const maxVoterWeight = useMaxVoteRecord()?.pubkey || undefined + const ownTokenRecord = useUserCommunityTokenOwnerRecord().data?.result + const ownCouncilTokenRecord = useUserCouncilTokenOwnerRecord().data?.result + const realm = useRealmQuery().data?.result + const config = useRealmConfigQuery().data?.result + const votingClient = useVotingClients()(role); + + const relevantTokenConfig = + role === 'community' + ? config?.account.communityTokenConfig + : config?.account.councilTokenConfig + const isMembership = + relevantTokenConfig?.tokenType === GoverningTokenType.Membership + + const { + realmInfo, + realmTokenAccount, + councilTokenAccount, + toManyCommunityOutstandingProposalsForUser, + toManyCouncilOutstandingProposalsForUse, + } = useRealm() + + const depositTokenRecord = + role === 'community' ? ownTokenRecord : ownCouncilTokenRecord + + const depositTokenAccount = + role === 'community' ? realmTokenAccount : councilTokenAccount + + const depositMint = + role === 'community' + ? realm?.account.communityMint + : realm?.account.config.councilMint + + const vetoMint = + role === 'community' + ? realm?.account.config.councilMint + : realm?.account.communityMint + + const withdrawAllTokens = async function () { + const instructions: TransactionInstruction[] = [] + setDisableButton(true) + + try { + if (depositTokenRecord!.account!.unrelinquishedVotesCount > 0) { + const voteRecords = await getUnrelinquishedVoteRecords( + connection, + realmInfo!.programId, + depositTokenRecord!.account!.governingTokenOwner + ) + + for (const voteRecord of Object.values(voteRecords)) { + const proposalQuery = await queryClient.fetchQuery({ + queryKey: proposalQueryKeys.byPubkey( + connection.rpcEndpoint, + voteRecord.account.proposal + ), + staleTime: 0, + queryFn: () => + asFindable(() => + getProposal(connection, voteRecord.account.proposal) + )(), + }) + + const proposal = proposalQuery.result + if (!proposal) { + continue + } + + if (voteRecord.account.vote?.veto) { + if (vetoMint && !proposal.account.governingTokenMint.equals(vetoMint)) { + continue; + } + } else { + if (!proposal.account.governingTokenMint.equals(depositMint!)) { + continue; + } + } + + const governance = ( + await fetchGovernanceByPubkey( + connection, + proposal.account.governance + ) + ).result + + if (!governance) throw new Error('failed to fetch governance') + + if (!governance.account.realm.equals(realm!.pubkey)) { + continue + } + + if (proposal.account.getTimeToVoteEnd(governance.account) > 0) { + notify({ + type: 'error', + message: `Can't withdraw tokens while Proposal ${proposal.account.name} is being voted on. Please withdraw your vote first`, + }) + throw new Error( + `Can't withdraw tokens while Proposal ${proposal.account.name} is being voted on. Please withdraw your vote first` + ) + } else { + await withRelinquishVote( + instructions, + realmInfo!.programId, + realmInfo!.programVersion!, + realmInfo!.realmId, + proposal.account.governance, + proposal.pubkey, + depositTokenRecord!.pubkey, + depositMint!, + voteRecord.pubkey, + depositTokenRecord!.account.governingTokenOwner, + wallet!.publicKey! + ) + await votingClient.withRelinquishVote( + instructions, + proposal, + voteRecord.pubkey, + depositTokenRecord!.pubkey + ) + } + } + } + + if (depositTokenRecord!.account.outstandingProposalCount > 0) { + const activeProposals = await getProposalsAtVotingStateByTOR( + connection, + realmInfo!.programId, + depositTokenRecord!.pubkey + ) + + for (const proposal of Object.values(activeProposals)) { + const fetchedGovernances: ProgramAccount[] = [] + const isGovernanceFetched = fetchedGovernances.find(governance => governance.pubkey.equals(proposal.pubkey)) + + const currentGovernance = isGovernanceFetched ? isGovernanceFetched : ( + await fetchGovernanceByPubkey( + connection, + proposal.account.governance + ) + ).result + + if (!currentGovernance) throw new Error('failed to fetch governance') + + if(fetchedGovernances.indexOf(currentGovernance) === -1) { + fetchedGovernances.push(currentGovernance) + } + + if (proposal.account.getTimeToVoteEnd(currentGovernance.account) > 0) { + notify({ + type: 'error', + message: `Can't withdraw tokens while Proposal ${proposal.account.name} is being voted on.`, + }) + throw new Error( + `Can't withdraw tokens while Proposal ${proposal.account.name} is being voted on.` + ) + } else { + await withFinalizeVote( + instructions, + realmInfo!.programId, + getProgramVersionForRealm(realmInfo!), + realm!.pubkey, + proposal.account.governance, + proposal.pubkey, + proposal.account.tokenOwnerRecord, + proposal.account.governingTokenMint, + maxVoterWeight + ) + } + } + } + + const ataPk = await Token.getAssociatedTokenAddress( + ASSOCIATED_TOKEN_PROGRAM_ID, + TOKEN_PROGRAM_ID, + depositMint!, + wallet!.publicKey!, + true + ) + const ata = await fetchTokenAccountByPubkey(connection, ataPk) + + if (!ata.found) { + const ataIx = Token.createAssociatedTokenAccountInstruction( + ASSOCIATED_TOKEN_PROGRAM_ID, + TOKEN_PROGRAM_ID, + depositMint!, + ataPk, + wallet!.publicKey!, + wallet!.publicKey! // fee payer + ) + instructions.push(ataIx) + } + + await withWithdrawGoverningTokens( + instructions, + realmInfo!.programId, + realmInfo!.programVersion!, + realm!.pubkey, + depositTokenAccount?.publicKey + ? depositTokenAccount!.publicKey + : new PublicKey(ataPk), + depositTokenRecord!.account.governingTokenMint, + wallet!.publicKey! + ) + + // Force the UI to recalculate voter weight + queryClient.invalidateQueries({ + queryKey: ['calculateVoterWeight'], + }) + + + try { + const ixChunks = chunks(instructions, 8) + for (const chunk of ixChunks.values()) { + const txes = [chunk].map((txBatch) => { + return { + instructionsSet: txBatch.map((x) => { + return { + transactionInstruction: x, + } + }), + sequenceType: SequenceType.Sequential, + } + }) + + await sendTransactionsV3({ + connection, + wallet: wallet!, + transactionInstructions: txes + }) + } + setDisableButton(false) + } catch (ex) { + setDisableButton(false) + //TODO change to more friendly notification + notify({ type: 'error', message: `${ex}` }) + console.error("Can't withdraw tokens", ex) + } + } catch(e) { + setDisableButton(false) + notify({ type: 'error', message: `${e}` }) + console.error("Can't withdraw tokens", e) + } + } + + + const hasTokensDeposited = + depositTokenRecord && + depositTokenRecord.account.governingTokenDepositAmount.gt(new BN(0)) + + const withdrawTooltipContent = !connected + ? 'Connect your wallet to withdraw' + : !hasTokensDeposited + ? "You don't have any tokens deposited to withdraw." + : role === 'community' && + (toManyCouncilOutstandingProposalsForUse || + toManyCommunityOutstandingProposalsForUser) + ? 'You have to many outstanding proposals to withdraw.' + : '' + + return ( + + Withdraw + + ) +} +export default VanillaWithdrawTokensButton diff --git a/components/TransactionLoader.tsx b/components/TransactionLoader.tsx index bd4dfb0aa2..ca01c57f53 100644 --- a/components/TransactionLoader.tsx +++ b/components/TransactionLoader.tsx @@ -48,7 +48,9 @@ const TransactionLoader = () => {
Error
-
{error}
+
+ {error} +
diff --git a/components/TreasuryAccount/ConvertToMsol.tsx b/components/TreasuryAccount/ConvertToMsol.tsx index 81bf6fc693..15ec4be60f 100644 --- a/components/TreasuryAccount/ConvertToMsol.tsx +++ b/components/TreasuryAccount/ConvertToMsol.tsx @@ -3,7 +3,7 @@ import useTreasuryAccountStore from 'stores/useTreasuryAccountStore' import AccountLabel from './AccountHeader' import GovernedAccountSelect from 'pages/dao/[symbol]/proposal/components/GovernedAccountSelect' import useGovernanceAssets from '@hooks/useGovernanceAssets' -import { useEffect, useState } from 'react' +import React, { useEffect, useState } from 'react' import { StakingViewForm, UiInstruction, @@ -27,10 +27,11 @@ import { AssetAccount } from '@utils/uiTypes/assets' import useWalletOnePointOh from '@hooks/useWalletOnePointOh' import { useRealmQuery } from '@hooks/queries/realm' import useLegacyConnectionContext from '@hooks/useLegacyConnectionContext' +import {useVoteByCouncilToggle} from "@hooks/useVoteByCouncilToggle"; const ConvertToMsol = () => { const realm = useRealmQuery().data?.result - const { canChooseWhoVote, symbol } = useRealm() + const {symbol } = useRealm() const { canUseTransferInstruction } = useGovernanceAssets() const { governedTokenAccounts } = useGovernanceAssets() const { fmtUrlWithCluster } = useQueryContext() @@ -51,7 +52,7 @@ const ConvertToMsol = () => { description: '', }) const [showOptions, setShowOptions] = useState(false) - const [voteByCouncil, setVoteByCouncil] = useState(false) + const { voteByCouncil, shouldShowVoteByCouncilToggle, setVoteByCouncil } = useVoteByCouncilToggle(); const [isLoading, setIsLoading] = useState(false) const mSolTokenAccounts = governedTokenAccounts.filter( @@ -223,13 +224,13 @@ const ConvertToMsol = () => { }) } > - {canChooseWhoVote && ( - { - setVoteByCouncil(!voteByCouncil) - }} - > + {shouldShowVoteByCouncilToggle && ( + { + setVoteByCouncil(!voteByCouncil) + }} + > )} )} diff --git a/components/TreasuryAccount/ConvertToStSol.tsx b/components/TreasuryAccount/ConvertToStSol.tsx index 2a154c8f94..0fdef041b7 100644 --- a/components/TreasuryAccount/ConvertToStSol.tsx +++ b/components/TreasuryAccount/ConvertToStSol.tsx @@ -3,7 +3,7 @@ import useTreasuryAccountStore from 'stores/useTreasuryAccountStore' import AccountLabel from './AccountHeader' import GovernedAccountSelect from 'pages/dao/[symbol]/proposal/components/GovernedAccountSelect' import useGovernanceAssets from '@hooks/useGovernanceAssets' -import { useEffect, useState } from 'react' +import React, { useEffect, useState } from 'react' import { StakingViewForm, UiInstruction, @@ -32,6 +32,7 @@ import { LIDO_PROGRAM_ID, LIDO_PROGRAM_ID_DEVNET, } from '@constants/pubkeys/lido' +import {useVoteByCouncilToggle} from "@hooks/useVoteByCouncilToggle"; const defaultFormState = { destinationAccount: undefined, @@ -55,7 +56,7 @@ const STSOL_MINT_DEVNET = '5nnLCgZn1EQaLj1ub8vYbQgBhkWi97x4JC5ARVPhci4V' const ConvertToStSol = () => { const realm = useRealmQuery().data?.result - const { canChooseWhoVote, symbol } = useRealm() + const {symbol } = useRealm() const { canUseTransferInstruction } = useGovernanceAssets() const { governedTokenAccounts } = useGovernanceAssets() const { fmtUrlWithCluster } = useQueryContext() @@ -72,7 +73,7 @@ const ConvertToStSol = () => { ) const [form, setForm] = useState(defaultFormState) const [showOptions, setShowOptions] = useState(false) - const [voteByCouncil, setVoteByCouncil] = useState(false) + const { voteByCouncil, shouldShowVoteByCouncilToggle, setVoteByCouncil } = useVoteByCouncilToggle(); const [isLoading, setIsLoading] = useState(false) const mintMinAmount = form.governedTokenAccount?.extensions?.mint @@ -260,13 +261,13 @@ const ConvertToStSol = () => { }) } /> - {canChooseWhoVote && ( - { - setVoteByCouncil(!voteByCouncil) - }} - /> + {shouldShowVoteByCouncilToggle && ( + { + setVoteByCouncil(!voteByCouncil) + }} + > )} )} diff --git a/components/TreasuryAccount/HoldTokensTotalPrice.tsx b/components/TreasuryAccount/HoldTokensTotalPrice.tsx index 040ed1ca7e..963a41c543 100644 --- a/components/TreasuryAccount/HoldTokensTotalPrice.tsx +++ b/components/TreasuryAccount/HoldTokensTotalPrice.tsx @@ -1,11 +1,14 @@ import { useTotalTreasuryPrice } from '@hooks/useTotalTreasuryPrice' +import { formatNumber } from '@utils/formatNumber' + const HoldTokensTotalPrice = () => { - const { totalPriceFormatted } = useTotalTreasuryPrice() + const { totalPriceFormatted, isFetching } = useTotalTreasuryPrice() + return (

Treasury Balance

- ${totalPriceFormatted ? totalPriceFormatted : 0} + {isFetching ? 'Fetching ...' : `$${formatNumber(totalPriceFormatted)}`}
) diff --git a/components/TreasuryAccount/MangoModal.tsx b/components/TreasuryAccount/MangoModal.tsx index 18316a40a5..fec0b5f447 100644 --- a/components/TreasuryAccount/MangoModal.tsx +++ b/components/TreasuryAccount/MangoModal.tsx @@ -1,6 +1,12 @@ -import { Group, MangoAccount } from '@blockworks-foundation/mango-v4' +import { + Group, + MangoAccount, + USDC_MINT, + toUiDecimals, + toNative, +} from '@blockworks-foundation/mango-v4' import AdditionalProposalOptions from '@components/AdditionalProposalOptions' -import Button from '@components/Button' +import Button, { LinkButton } from '@components/Button' import Input from '@components/inputs/Input' import Select from '@components/inputs/Select' import { BN } from '@coral-xyz/anchor' @@ -16,141 +22,337 @@ import { AccountMeta, PublicKey } from '@solana/web3.js' import { AssetAccount } from '@utils/uiTypes/assets' import { useRouter } from 'next/router' import { useEffect, useState } from 'react' +import useGovernanceAssets from '@hooks/useGovernanceAssets' +import Tooltip from '@components/Tooltip' +import { + getMintDecimalAmount, + getMintMinAmountAsDecimal, +} from '@tools/sdk/units' +import BigNumber from 'bignumber.js' +import { precision } from '@utils/formatting' +import * as yup from 'yup' +import tokenPriceService from '@utils/services/tokenPrice' +import { validateInstruction } from '@utils/instructionTools' +import Switch from '@components/Switch' +import { InstructionDataWithHoldUpTime } from 'actions/createProposal' +import ProgramSelector from '@components/Mango/ProgramSelector' +import useProgramSelector from '@components/Mango/useProgramSelector' +import ButtonGroup from '@components/ButtonGroup' + +enum ProposalType { + DEPOSIT = 'Deposit', + WITHDRAW = 'Withdraw', +} +import { useVoteByCouncilToggle } from '@hooks/useVoteByCouncilToggle' const MangoModal = ({ account }: { account: AssetAccount }) => { - const { mangoClient, mangoGroup } = UseMangoV4() + const { canUseTransferInstruction } = useGovernanceAssets() + const programSelectorHook = useProgramSelector() + const { mangoClient, mangoGroup, getMaxWithdrawForBank } = UseMangoV4( + programSelectorHook.program?.val, + programSelectorHook.program?.group + ) const { fmtUrlWithCluster } = useQueryContext() const { handleCreateProposal } = useCreateProposal() const router = useRouter() const { symbol } = useRealm() - const [mangoAccount, setSelectedMangoAccount] = useState( - null + const [isProposing, setIsProposing] = useState(false) + const { voteByCouncil, setVoteByCouncil } = useVoteByCouncilToggle() + const [isLoadingMangoAccount, setIsLoadingMangoAccount] = useState( + true ) const [mangoAccounts, setMangoAccounts] = useState([]) - const [mangoAccName, setMangoAccName] = useState('') - const [isProposing, setIsProposing] = useState(false) - const [voteByCouncil, setVoteByCouncil] = useState(false) - const [proposalTitle, setProposalTitle] = useState('Create mango account') - const [proposalDescription, setProposalDescription] = useState('') + const tabs = [ProposalType.DEPOSIT, ProposalType.WITHDRAW] + const [proposalType, setProposalType] = useState(ProposalType.DEPOSIT) + const [form, setForm] = useState<{ + mangoAccount: MangoAccount | null | undefined + accountName: string + amount: number + title: string + description: string + delegate: boolean + delegateWallet: string + }>({ + accountName: '', + title: '', + description: '', + amount: 0, + mangoAccount: undefined, + delegate: false, + delegateWallet: '', + }) + + const [formErrors, setFormErrors] = useState({}) + const [maxWithdrawBalance, setMaxWithdrawBalance] = useState(0) + + const handleSetForm = ({ propertyName, value }) => { + setForm({ ...form, [propertyName]: value }) + setFormErrors({}) + } + + useEffect(() => { + setForm({ ...form, mangoAccount: undefined }) + }, [programSelectorHook.program?.val.toBase58()]) + + useEffect(() => { + setFormErrors({}) + setForm({ + ...form, + accountName: '', + amount: 0, + mangoAccount: undefined, + title: `${proposalType} ${ + tokenPriceService.getTokenInfo( + account.extensions.mint!.publicKey.toBase58() + )?.symbol || 'tokens' + } ${proposalType === 'Withdraw' ? 'from' : 'to'} Mango`, + }) + }, [proposalType]) + + const SOL_BUFFER = 0.02 + + const treasuryAmount = new BN( + account.isSol + ? account.extensions.amount!.toNumber() + : account.extensions.token!.account.amount + ) + const mintInfo = account.extensions.mint!.account! + const mintMinAmount = mintInfo ? getMintMinAmountAsDecimal(mintInfo) : 1 + let maxAmount = mintInfo + ? getMintDecimalAmount(mintInfo, treasuryAmount) + : new BigNumber(0) + if (account.isSol) { + maxAmount = maxAmount.minus(SOL_BUFFER) + } + const maxAmountFtm = maxAmount.toNumber().toFixed(4) + const currentPrecision = precision(mintMinAmount) + + const schema = yup.object().shape({ + mangoAccount: yup + .object() + .nullable(true) + .test( + 'is-not-undefined', + 'Please select an Account', + (value) => value !== undefined + ), + accountName: yup.string().when('mangoAccount', { + is: null, + then: yup.string().required('Account name is required'), + otherwise: yup.string().notRequired(), + }), + title: yup.string().required('Title is required'), + amount: yup + .number() + .required('Amount is required') + .min(mintMinAmount) + .max( + proposalType === ProposalType.DEPOSIT + ? maxAmount.toNumber() + : maxWithdrawBalance + ), + delegate: yup.boolean().required('Delegate is required'), + delegateWallet: yup + .string() + .nullable() + .when('delegate', { + is: true, + then: yup.string().required('Delegate Wallet is required'), + otherwise: yup.string().notRequired(), + }), + }) + + useEffect(() => { + setMangoAccounts([]) + }, [programSelectorHook.program?.val]) + useEffect(() => { const getMangoAccounts = async () => { const accounts = await mangoClient?.getMangoAccountsForOwner( mangoGroup!, account.extensions.token!.account.owner! ) + if (accounts) { setMangoAccounts(accounts) } } - getMangoAccounts() - }, [mangoClient !== null && mangoGroup !== null]) + if (mangoClient && mangoGroup) { + setMangoAccounts([]) + setIsLoadingMangoAccount(true) + getMangoAccounts().then(() => setIsLoadingMangoAccount(false)) + } + }, [account.extensions.token, mangoClient, mangoGroup]) + + useEffect(() => { + if (proposalType === ProposalType.DEPOSIT) return + if (!mangoGroup || !form.mangoAccount) return + const bank = mangoGroup!.getFirstBankByMint( + account.extensions.mint!.publicKey! + ) + const maxWithdrawForBank = getMaxWithdrawForBank( + mangoGroup, + bank, + form.mangoAccount + ) + setMaxWithdrawBalance(maxWithdrawForBank.toNumber()) + }, [proposalType, mangoGroup, form]) + const handleCreateAccount = async () => { + const isValid = await validateInstruction({ schema, form, setFormErrors }) + if (!isValid) return + try { setIsProposing(true) - const newAccountNum = getNextAccountNumber(mangoAccounts) + const instructions: InstructionDataWithHoldUpTime[] = [] + let mangoAccountPk = form.mangoAccount?.publicKey const bank = mangoGroup!.getFirstBankByMint( account.extensions.mint!.publicKey! ) - const createAccIx = await mangoClient!.program.methods - .accountCreate( - newAccountNum, - 8, - 4, - 4, - 32, - mangoAccName || `Account ${newAccountNum + 1}` - ) - .accounts({ - group: mangoGroup!.publicKey, - owner: account.extensions.token!.account.owner!, - payer: account.extensions.token!.account.owner!, - }) - .instruction() - - const acctNumBuffer = Buffer.alloc(4) - acctNumBuffer.writeUInt32LE(newAccountNum) - - const [mangoAccount] = PublicKey.findProgramAddressSync( - [ - Buffer.from('MangoAccount'), - mangoGroup!.publicKey.toBuffer(), - account.extensions.token!.account.owner!.toBuffer(), - acctNumBuffer, - ], - mangoClient!.programId - ) - const depositIx = await mangoClient!.program.methods - .tokenDeposit(new BN(100000000000), false) - .accounts({ - group: mangoGroup!.publicKey, - account: mangoAccount, - owner: account.extensions.token!.account.owner!, - bank: bank.publicKey, - vault: bank.vault, - oracle: bank.oracle, - tokenAccount: account.pubkey, - tokenAuthority: account.extensions.token!.account.owner!, - }) - .remainingAccounts( - [bank.publicKey, bank.oracle].map( - (pk) => - ({ - pubkey: pk, - isWritable: false, - isSigner: false, - } as AccountMeta) + if (form.mangoAccount === null) { + const newAccountNum = getNextAccountNumber(mangoAccounts) + const createAccIx = await mangoClient!.program.methods + .accountCreate( + newAccountNum, + 8, + 4, + 4, + 32, + form.accountName || `Account ${newAccountNum + 1}` ) + .accounts({ + group: mangoGroup!.publicKey, + owner: account.extensions.token!.account.owner!, + payer: account.extensions.token!.account.owner!, + }) + .instruction() + + const createAccInstData = { + data: getInstructionDataFromBase64( + serializeInstructionToBase64(createAccIx) + ), + holdUpTime: + account?.governance.account?.config.minInstructionHoldUpTime, + prerequisiteInstructions: [], + chunkBy: 1, + } + + instructions.push(createAccInstData) + + const acctNumBuffer = Buffer.alloc(4) + acctNumBuffer.writeUInt32LE(newAccountNum) + + const [mangoAccount] = PublicKey.findProgramAddressSync( + [ + Buffer.from('MangoAccount'), + mangoGroup!.publicKey.toBuffer(), + account.extensions.token!.account.owner!.toBuffer(), + acctNumBuffer, + ], + mangoClient!.programId ) - .instruction() - - const delegateIx = await mangoClient!.program.methods - .accountEdit( - null, - new PublicKey('EsWMqyaEDoAqMgiWG9McSmpetBiYjL4VkHPkfevxKu4D'), - null, - null + mangoAccountPk = mangoAccount + } + + const tokens = toNative( + form.amount, + account.extensions.mint!.account!.decimals + ) + + if (proposalType === ProposalType.DEPOSIT) { + const methodByProposal = mangoClient!.program.methods.tokenDeposit + const methodByProposalInstruction = await methodByProposal( + tokens, + false ) - .accounts({ - group: mangoGroup!.publicKey, - account: mangoAccount, - owner: account.extensions.token!.account.owner!, - }) - .instruction() - - const createAccInstData = { - data: getInstructionDataFromBase64( - serializeInstructionToBase64(createAccIx!) - ), - holdUpTime: - account?.governance.account?.config.minInstructionHoldUpTime, - prerequisiteInstructions: [], + .accounts({ + group: mangoGroup!.publicKey, + account: mangoAccountPk, + owner: account.extensions.token!.account.owner!, + bank: bank.publicKey, + vault: bank.vault, + oracle: bank.oracle, + tokenAccount: account.pubkey, + tokenAuthority: account.extensions.token!.account.owner!, + }) + .remainingAccounts( + [bank.publicKey, bank.oracle].map( + (pk) => + ({ + pubkey: pk, + isWritable: false, + isSigner: false, + } as AccountMeta) + ) + ) + .instruction() + + const depositAccInstData = { + data: getInstructionDataFromBase64( + serializeInstructionToBase64(methodByProposalInstruction!) + ), + holdUpTime: + account?.governance.account?.config.minInstructionHoldUpTime, + prerequisiteInstructions: [], + chunkBy: 1, + } + + instructions.push(depositAccInstData) } - const depositAccInstData = { - data: getInstructionDataFromBase64( - serializeInstructionToBase64(depositIx!) - ), - holdUpTime: - account?.governance.account?.config.minInstructionHoldUpTime, - prerequisiteInstructions: [], + + if (proposalType === ProposalType.WITHDRAW) { + const withdrawTxs = await mangoClient!.tokenWithdrawNativeIx( + mangoGroup!, + form.mangoAccount!, + account.extensions.mint!.publicKey!, + tokens, + false + ) + + for (const withdrawTx of withdrawTxs) { + const withdrawAccInsData = { + data: getInstructionDataFromBase64( + serializeInstructionToBase64(withdrawTx) + ), + holdUpTime: + account?.governance.account?.config.minInstructionHoldUpTime, + prerequisiteInstructions: [], + chunkBy: 1, + } + + instructions.push(withdrawAccInsData) + } } - const delegateAccInstData = { - data: getInstructionDataFromBase64( - serializeInstructionToBase64(delegateIx!) - ), - holdUpTime: - account?.governance.account?.config.minInstructionHoldUpTime, - prerequisiteInstructions: [], + + if (form.delegate) { + const delegateIx = await mangoClient!.program.methods + .accountEdit(null, new PublicKey(form.delegateWallet), null, null) + .accounts({ + group: mangoGroup!.publicKey, + account: mangoAccountPk, + owner: account.extensions.token!.account.owner!, + }) + .instruction() + + const delegateAccInstData = { + data: getInstructionDataFromBase64( + serializeInstructionToBase64(delegateIx!) + ), + holdUpTime: + account?.governance.account?.config.minInstructionHoldUpTime, + prerequisiteInstructions: [], + chunkBy: 1, + } + + instructions.push(delegateAccInstData) } + const proposalAddress = await handleCreateProposal({ - title: proposalTitle, - description: proposalDescription, + title: form.title, + description: form.description, voteByCouncil, - instructionsData: [ - createAccInstData, - depositAccInstData, - delegateAccInstData, - ], + instructionsData: instructions, governance: account.governance!, }) const url = fmtUrlWithCluster( @@ -162,66 +364,371 @@ const MangoModal = ({ account }: { account: AssetAccount }) => { } setIsProposing(false) } + return ( -
-

Mango

+
+
+ +
+

Mango

+
+
- {console.log(mangoAccount)} - {mangoGroup && ( - + } + placeholder={ + form.mangoAccount === undefined + ? 'Please select...' + : form.mangoAccount?.name || 'Create new account' + } + onChange={(value) => + handleSetForm({ + propertyName: 'mangoAccount', + value, + }) + } + > + {isLoadingMangoAccount && !mangoAccounts.length ? ( +
Loading accounts...
+ ) : ( + mangoAccounts.map((x) => ( + + + + )) + )} + + +
Create new account
- ))} - -
Create new account
-
- + + {form.mangoAccount === null && ( + + handleSetForm({ + propertyName: 'accountName', + value: e.target.value, + }) + } + /> + )} +
+ Amount +
+ {maxWithdrawBalance} + { + handleSetForm({ + propertyName: 'amount', + value: maxWithdrawBalance, + }) + }} + > + Max + +
+
+ + handleSetForm({ + propertyName: 'amount', + value: e.target.value, + }) + } + onBlur={() => { + handleSetForm({ + propertyName: 'amount', + value: parseFloat( + Math.max( + Number(mintMinAmount), + Math.min( + Number(Number.MAX_SAFE_INTEGER), + Number(form.amount) + ) + ).toFixed(currentPrecision) + ), + }) + }} + /> +
+

Delegate

+ + handleSetForm({ + propertyName: 'delegate', + value: !form.delegate, + }) + } + /> +
+ {form.delegate && ( + + handleSetForm({ + propertyName: 'delegateWallet', + value: e.target.value, + }) + } + /> + )} + { + handleSetForm({ + propertyName: 'title', + value: evt.target.value, + }) + }} + setDescription={(evt) => { + handleSetForm({ + propertyName: 'description', + value: evt.target.value, + }) + }} + voteByCouncil={voteByCouncil} + setVoteByCouncil={setVoteByCouncil} + /> +
+ +
+
)} - {!mangoAccount && ( - setMangoAccName(e.target.value)} - /> + {proposalType === ProposalType.DEPOSIT && ( +
+ {account.extensions.mint?.publicKey.toBase58() === + USDC_MINT.toBase58() && ( + + )} + + + {form.mangoAccount === null && ( + + handleSetForm({ + propertyName: 'accountName', + value: e.target.value, + }) + } + /> + )} +
+ Amount +
+ {maxAmountFtm} + { + handleSetForm({ + propertyName: 'amount', + value: maxAmount.toNumber(), + }) + }} + > + Max + +
+
+ + handleSetForm({ + propertyName: 'amount', + value: e.target.value, + }) + } + onBlur={() => { + handleSetForm({ + propertyName: 'amount', + value: parseFloat( + Math.max( + Number(mintMinAmount), + Math.min( + Number(Number.MAX_SAFE_INTEGER), + Number(form.amount) + ) + ).toFixed(currentPrecision) + ), + }) + }} + /> +
+

Delegate

+ + handleSetForm({ + propertyName: 'delegate', + value: !form.delegate, + }) + } + /> +
+ {form.delegate && ( + + handleSetForm({ + propertyName: 'delegateWallet', + value: e.target.value, + }) + } + /> + )} + { + handleSetForm({ + propertyName: 'title', + value: evt.target.value, + }) + }} + setDescription={(evt) => { + handleSetForm({ + propertyName: 'description', + value: evt.target.value, + }) + }} + voteByCouncil={voteByCouncil} + setVoteByCouncil={setVoteByCouncil} + /> +
+ +
+
)} - { - setProposalTitle(evt.target.value) - }} - setDescription={(evt) => setProposalDescription(evt.target.value)} - voteByCouncil={voteByCouncil} - setVoteByCouncil={setVoteByCouncil} - /> -
- -
) @@ -232,13 +739,15 @@ const MangoAccountItem = ({ group, }: { account: MangoAccount | null - group: Group + group: Group | null }) => { - return account ? ( + return account && group ? (
Name: {account.name}
{account.publicKey.toBase58()}
-
Pnl: ${account.getPnl(group).toString()}
+
+ Account Value: ${toUiDecimals(account.getAssetsValue(group), 6)} +
) : (
Create new account
diff --git a/components/TreasuryAccount/ProposalOptions.tsx b/components/TreasuryAccount/ProposalOptions.tsx index acd48ed0cf..f5597a7559 100644 --- a/components/TreasuryAccount/ProposalOptions.tsx +++ b/components/TreasuryAccount/ProposalOptions.tsx @@ -6,13 +6,13 @@ import React from 'react' const ProposalOptions: React.FC<{ handleSetForm: (obj: { propertyName: string; value: any }) => void form: any - canChooseWhoVote?: boolean + shouldShowVoteByCouncilToggle?: boolean voteByCouncil: boolean setVoteByCouncil: React.Dispatch> }> = ({ handleSetForm, form, - canChooseWhoVote, + shouldShowVoteByCouncilToggle, voteByCouncil, setVoteByCouncil, }) => { @@ -46,14 +46,14 @@ const ProposalOptions: React.FC<{ }) } > - {canChooseWhoVote && ( - { - setVoteByCouncil(!voteByCouncil) - }} - > - )} + {shouldShowVoteByCouncilToggle && ( + { + setVoteByCouncil(!voteByCouncil) + }} + > + )} ) } diff --git a/components/TreasuryAccount/SendTokens.tsx b/components/TreasuryAccount/SendTokens.tsx index f5db7c538b..5955daf8bd 100644 --- a/components/TreasuryAccount/SendTokens.tsx +++ b/components/TreasuryAccount/SendTokens.tsx @@ -11,16 +11,16 @@ import { } from '@tools/sdk/units' import { tryParseKey } from '@tools/validators/pubkey' import { debounce } from '@utils/debounce' -import { abbreviateAddress, precision } from '@utils/formatting' +import { precision } from '@utils/formatting' import { TokenProgramAccount, tryGetTokenAccount } from '@utils/tokens' import { SendTokenCompactViewForm, UiInstruction, } from '@utils/uiTypes/proposalCreationTypes' -import { useEffect, useState } from 'react' +import React, { ChangeEvent, useEffect, useState } from 'react' import useTreasuryAccountStore from 'stores/useTreasuryAccountStore' -import { getTokenTransferSchema } from '@utils/validations' +import { getBatchTokenTransferSchema } from '@utils/validations' import { ArrowCircleDownIcon, ArrowCircleUpIcon, @@ -37,8 +37,8 @@ import AccountLabel from './AccountHeader' import Tooltip from '@components/Tooltip' import useGovernanceAssets from '@hooks/useGovernanceAssets' import { - getSolTransferInstruction, - getTransferInstruction, + getBatchSolTransferInstruction, + getBatchTransferInstruction, } from '@utils/instructionTools' import VoteBySwitch from 'pages/dao/[symbol]/proposal/components/VoteBySwitch' import useCreateProposal from '@hooks/useCreateProposal' @@ -46,13 +46,15 @@ import useWalletOnePointOh from '@hooks/useWalletOnePointOh' import { useRealmQuery } from '@hooks/queries/realm' import useLegacyConnectionContext from '@hooks/useLegacyConnectionContext' import { fetchJupiterPrice } from '@hooks/queries/jupiterPrice' -import { useAsync } from 'react-async-hook' +import {useVoteByCouncilToggle} from "@hooks/useVoteByCouncilToggle"; +import { AddAlt } from '@carbon/icons-react' +import { StyledLabel } from '@components/inputs/styles' const SendTokens = () => { const currentAccount = useTreasuryAccountStore((s) => s.currentAccount) const connection = useLegacyConnectionContext() const realm = useRealmQuery().data?.result - const { realmInfo, symbol, canChooseWhoVote } = useRealm() + const { realmInfo, symbol} = useRealm() const { handleCreateProposal } = useCreateProposal() const { canUseTransferInstruction } = useGovernanceAssets() const tokenInfo = useTreasuryAccountStore((s) => s.tokenInfo) @@ -62,25 +64,29 @@ const SendTokens = () => { const router = useRouter() const programId: PublicKey | undefined = realmInfo?.programId const [form, setForm] = useState({ - destinationAccount: '', - amount: undefined, + destinationAccount: [''], + txDollarAmount: [undefined], + amount: [undefined], governedTokenAccount: undefined, programId: programId?.toString(), mintInfo: undefined, title: '', description: '', }) - const [voteByCouncil, setVoteByCouncil] = useState(false) + const { voteByCouncil, shouldShowVoteByCouncilToggle, setVoteByCouncil } = useVoteByCouncilToggle(); const [showOptions, setShowOptions] = useState(false) const [ destinationAccount, setDestinationAccount, - ] = useState | null>(null) + ] = useState<(TokenProgramAccount | null)[]>([null]) + const [isLoading, setIsLoading] = useState(false) const [formErrors, setFormErrors] = useState({}) - const destinationAccountName = - destinationAccount?.publicKey && - getAccountName(destinationAccount?.account.address) + const destinationAccountName = destinationAccount.map(acc => ( + acc?.publicKey && + getAccountName(acc?.account.address) + )) + const mintMinAmount = form.governedTokenAccount?.extensions?.mint ? getMintMinAmountAsDecimal( form.governedTokenAccount.extensions.mint.account @@ -92,28 +98,64 @@ const SendTokens = () => { setFormErrors({}) setForm({ ...form, [propertyName]: value }) } - const setAmount = (event) => { + + const handleSetMultipleProps = ( + {destinationAccount, amount, txDollarAmount} : + {amount: any, txDollarAmount: any, destinationAccount?: any} + ) => { + setFormErrors({}) + + setForm({ ...form, + destinationAccount : destinationAccount ? destinationAccount : form.destinationAccount, + amount, + txDollarAmount + }) + } + + const setAmount = (idx: number, event: ChangeEvent) => { const value = event.target.value + const newAmounts: any[] = [...form.amount] + newAmounts[idx] = value + handleSetForm({ - value: value, + value: newAmounts, 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', + const validateAmountOnBlur = async(idx: number) => { + const value = form.amount[idx] + const newAmounts = [...form.amount] + const newTxDollars = [...form.txDollarAmount] + + const newVal = parseFloat( + Math.max( + Number(mintMinAmount), + Math.min(Number(Number.MAX_SAFE_INTEGER), Number(value)) + ).toFixed(currentPrecision) + ) + + newAmounts[idx] = newVal + + const mint = currentAccount?.extensions.mint?.publicKey + if (mint === undefined) { + newTxDollars[idx] = undefined + } else { + const priceData = await fetchJupiterPrice(mint) + const price = priceData.result?.price ?? 0 + + const totalPrice = newVal * price + const totalPriceFormatted = newVal && price ? new BigNumber(totalPrice).toFormat(2) : '' + newTxDollars[idx] = totalPriceFormatted + } + + handleSetMultipleProps({ + amount: newAmounts, + txDollarAmount: newTxDollars }) } - async function getInstruction(): Promise { + async function getInstruction(): Promise { const defaultProps = { schema, form, @@ -124,34 +166,37 @@ const SendTokens = () => { setFormErrors, } if (isSol) { - return getSolTransferInstruction(defaultProps) + return getBatchSolTransferInstruction(defaultProps) } - return getTransferInstruction(defaultProps) + return getBatchTransferInstruction(defaultProps) } const handleProposeTransfer = async () => { setIsLoading(true) - const instruction: UiInstruction = await getInstruction() - if (instruction.isValid) { + const instruction: UiInstruction[] = await getInstruction() + + if (instruction.every(ix => ix.isValid)) { const governance = currentAccount?.governance let proposalAddress: PublicKey | null = null if (!realm) { setIsLoading(false) throw 'No realm selected' } - const instructionData = { - data: instruction.serializedInstruction - ? getInstructionDataFromBase64(instruction.serializedInstruction) + const instructionsData = instruction.map(ix => ({ + data: ix.serializedInstruction + ? getInstructionDataFromBase64(ix.serializedInstruction) : null, holdUpTime: governance?.account?.config.minInstructionHoldUpTime, - prerequisiteInstructions: instruction.prerequisiteInstructions || [], - } + prerequisiteInstructions: ix.prerequisiteInstructions || [], + chunkBy: 4 + })) + try { proposalAddress = await handleCreateProposal({ title: form.title ? form.title : proposalTitle, description: form.description ? form.description : '', voteByCouncil, - instructionsData: [instructionData], + instructionsData, governance: governance!, }) const url = fmtUrlWithCluster( @@ -165,10 +210,10 @@ const SendTokens = () => { setIsLoading(false) } - const IsAmountNotHigherThenBalance = () => { + const IsAmountNotHigherThenBalance = (idx: number) => { try { const mintValue = getMintNaturalAmountFromDecimalAsBN( - form.amount!, + form.amount[idx]!, form.governedTokenAccount!.extensions.mint!.account.decimals ) let gte: boolean | undefined = false @@ -188,50 +233,66 @@ const SendTokens = () => { } // eslint-disable-next-line react-hooks/exhaustive-deps -- TODO please fix, it can cause difficult bugs. You might wanna check out https://bobbyhadz.com/blog/react-hooks-exhaustive-deps for info. -@asktree }, [currentAccount]) - useEffect(() => { - if (form.destinationAccount) { - debounce.debounceFcn(async () => { - const pubKey = tryParseKey(form.destinationAccount) - if (pubKey) { - const account = await tryGetTokenAccount(connection.current, pubKey) - setDestinationAccount(account ? account : null) - } else { - setDestinationAccount(null) - } - }) - } else { - setDestinationAccount(null) - } - // eslint-disable-next-line react-hooks/exhaustive-deps -- TODO please fix, it can cause difficult bugs. You might wanna check out https://bobbyhadz.com/blog/react-hooks-exhaustive-deps for info. -@asktree - }, [form.destinationAccount]) - const schema = getTokenTransferSchema({ form, connection, nftMode: false }) + const schema = getBatchTokenTransferSchema({ form, connection, nftMode: false }) - const { result: transactionDolarAmount } = useAsync(async () => { - const mint = currentAccount?.extensions.mint?.publicKey - if (mint === undefined) return undefined - const amount = form.amount ?? 0 - const priceData = await fetchJupiterPrice(mint) - const price = priceData.result?.price ?? 0 - - const totalPrice = amount * price - const totalPriceFormatted = - amount && price ? new BigNumber(totalPrice).toFormat(2) : '' - return totalPriceFormatted - }, [form.amount, currentAccount?.extensions.mint?.publicKey]) - - const proposalTitle = `Pay ${form.amount}${ - tokenInfo ? ` ${tokenInfo?.symbol} ` : ' ' - }to ${ - tryParseKey(form.destinationAccount) - ? abbreviateAddress(new PublicKey(form.destinationAccount)) - : '' - }` + const proposalTitle = `Transfer tokens` + // ${ + // tokenInfo ? ` ${tokenInfo?.symbol} ` : ' ' + // }to ${ + // tryParseKey(form.destinationAccount) + // ? abbreviateAddress(new PublicKey(form.destinationAccount)) + // : '' + // }` if (!currentAccount) { return null } + const addRecipient = () => { + const newAddresses = [...form.destinationAccount] + const newAmounts = [...form.amount] + const newTxDollars = [...form.txDollarAmount] + + newAddresses.push("") + newAmounts.push(undefined) + newTxDollars.push(undefined) + + handleSetMultipleProps({ + destinationAccount: newAddresses, + amount: newAmounts, + txDollarAmount: newTxDollars + }) + + const currentAccounts = [...destinationAccount] + currentAccounts.push(null) + setDestinationAccount(currentAccounts) + } + + const setAddress = (idx: number, address: string) => { + const newAddresses = [...form.destinationAccount] + newAddresses[idx] = address + + handleSetForm({ + value: newAddresses, + propertyName: 'destinationAccount', + }) + + const currentAccounts = [...destinationAccount] + + debounce.debounceFcn(async () => { + const pubKey = tryParseKey(address) + if (pubKey) { + const account = await tryGetTokenAccount(connection.current, pubKey) + currentAccounts[idx] = account ? account : null + setDestinationAccount(currentAccounts) + } else { + currentAccounts[idx] = null + setDestinationAccount(currentAccounts) + } + }) + } + return ( <>

@@ -239,53 +300,63 @@ const SendTokens = () => {

- - handleSetForm({ - value: evt.target.value, - propertyName: 'destinationAccount', - }) - } - noMaxWidth={true} - error={formErrors['destinationAccount']} - /> - {destinationAccount && ( -
-
Account owner
-
- {destinationAccount.account.owner.toString()} -
-
- )} - {destinationAccountName && ( -
-
Account name
-
{destinationAccountName}
-
- )} + {form.destinationAccount.map((acc, idx) => ( +
+ Recipient {idx+1} + setAddress(idx, e.target.value)} + noMaxWidth={true} + error={ + formErrors['destinationAccount'] && formErrors['destinationAccount'][idx] ? + formErrors['destinationAccount'][idx] : "" + } + /> + {destinationAccount[idx] && ( +
+
Account owner
+
+ {destinationAccount[idx]!.account.owner.toString()} +
+
+ )} + {destinationAccountName[idx] && ( +
+
Account name
+
{destinationAccountName[idx]}
+
+ )} - + setAmount(idx, e)} + step={mintMinAmount} + error={formErrors['amount'] && formErrors['amount'][idx] ? formErrors['amount'][idx] : ""} + onBlur={() => validateAmountOnBlur(idx)} + noMaxWidth={true} + /> - - {transactionDolarAmount - ? IsAmountNotHigherThenBalance() - ? `~$${transactionDolarAmount}` - : 'Insufficient balance' - : null} - + + {form.txDollarAmount[idx] + ? IsAmountNotHigherThenBalance(idx) + ? `~$${form.txDollarAmount[idx]}` + : 'Insufficient balance' + : null} + +
+ ))} +
+ +
Add another recipient
+
setShowOptions(!showOptions)} @@ -345,13 +416,13 @@ const SendTokens = () => { }) } > - {canChooseWhoVote && ( - { - setVoteByCouncil(!voteByCouncil) - }} - > + {shouldShowVoteByCouncilToggle && ( + { + setVoteByCouncil(!voteByCouncil) + }} + > )} )} diff --git a/components/TreasuryAccount/Trade.tsx b/components/TreasuryAccount/Trade.tsx index c920505a8d..0d7151dc00 100644 --- a/components/TreasuryAccount/Trade.tsx +++ b/components/TreasuryAccount/Trade.tsx @@ -51,6 +51,7 @@ import { import { deriveAllBoundedStrategyKeysV2 } from '@utils/instructions/PsyFinance/poseidon' import { TokenInfo } from '@utils/services/types' import useLegacyConnectionContext from '@hooks/useLegacyConnectionContext' +import {useVoteByCouncilToggle} from "@hooks/useVoteByCouncilToggle"; type TradeProps = { tokenAccount: AssetAccount } @@ -173,7 +174,7 @@ const Trade: React.FC = ({ tokenAccount }) => { const { wallet, anchorProvider } = useWalletDeprecated() const { handleCreateProposal } = useCreateProposal() const { canUseTransferInstruction } = useGovernanceAssets() - const { canChooseWhoVote, symbol } = useRealm() + const {symbol } = useRealm() const { fmtUrlWithCluster } = useQueryContext() const [form, setForm] = useState({ amount: 0, @@ -190,7 +191,7 @@ const Trade: React.FC = ({ tokenAccount }) => { }) const [formErrors, setFormErrors] = useState({}) const [showOptions, setShowOptions] = useState(false) - const [voteByCouncil, setVoteByCouncil] = useState(false) + const { voteByCouncil, shouldShowVoteByCouncilToggle, setVoteByCouncil } = useVoteByCouncilToggle(); const [isLoading, setIsLoading] = useState(false) const [destinationToken, setDestinationToken] = useState() @@ -438,7 +439,7 @@ const Trade: React.FC = ({ tokenAccount }) => { diff --git a/components/VoteCommentModal.tsx b/components/VoteCommentModal.tsx index e5ea1e1c9d..6f0e337782 100644 --- a/components/VoteCommentModal.tsx +++ b/components/VoteCommentModal.tsx @@ -30,7 +30,7 @@ const VoteCommentModal: FunctionComponent = ({ isMulti, }) => { const [comment, setComment] = useState('') - const { submitting, submitVote } = useSubmitVote() + const { submitting, submitVote , error } = useSubmitVote() const voteString = VOTE_STRINGS[vote] @@ -39,9 +39,8 @@ const VoteCommentModal: FunctionComponent = ({ vote, comment, voteWeights: isMulti, - }) - - onClose() + }).then(() => onClose()) + .catch(console.log) } return ( @@ -62,6 +61,7 @@ const VoteCommentModal: FunctionComponent = ({ onChange={(e) => setComment(e.target.value)} // placeholder={`Let the DAO know why you vote '${voteString}'`} /> + {error &&

{error.message}

}
diff --git a/components/VotePanel/CastVoteButtons.tsx b/components/VotePanel/CastVoteButtons.tsx index 97b45d944e..e4c8e03471 100644 --- a/components/VotePanel/CastVoteButtons.tsx +++ b/components/VotePanel/CastVoteButtons.tsx @@ -1,13 +1,22 @@ -import { VoteKind } from '@solana/spl-governance' +import { VoteKind, getVoteRecordAddress } from '@solana/spl-governance' import { useState } from 'react' import { ThumbUpIcon, ThumbDownIcon } from '@heroicons/react/solid' import Button from '../Button' import VoteCommentModal from '../VoteCommentModal' import { useIsInCoolOffTime, useIsVoting, useVotingPop } from './hooks' -import { useProposalVoteRecordQuery } from '@hooks/queries/voteRecord' +import { + fetchVoteRecordByPubkey, + useProposalVoteRecordQuery, +} from '@hooks/queries/voteRecord' import { useSubmitVote } from '@hooks/useSubmitVote' import { useSelectedRealmInfo } from '@hooks/selectedRealm/useSelectedRealmRegistryEntry' import { useCanVote } from './useCanVote' +import { useConnection } from '@solana/wallet-adapter-react' +import { useAsync } from 'react-async-hook' +import { useRouteProposalQuery } from '@hooks/queries/proposal' +import { useBatchedVoteDelegators } from './useDelegators' +import { useRealmVoterWeightPlugins } from '@hooks/useRealmVoterWeightPlugins' +import useWalletOnePointOh from '@hooks/useWalletOnePointOh' export const CastVoteButtons = () => { const [showVoteModal, setShowVoteModal] = useState(false) @@ -23,6 +32,61 @@ export const CastVoteButtons = () => { const isVoting = useIsVoting() const isInCoolOffTime = useIsInCoolOffTime() + const proposal = useRouteProposalQuery().data?.result + const connection = useConnection() + const communityDelegators = useBatchedVoteDelegators('community') + const councilDelegators = useBatchedVoteDelegators('council') + + const wallet = useWalletOnePointOh() + + const { voterWeightForWallet } = useRealmVoterWeightPlugins(votingPop) + + const ownVoterWeight = wallet?.publicKey + ? voterWeightForWallet(wallet?.publicKey) + : undefined + const hasVotingPower = !!( + ownVoterWeight?.value && ownVoterWeight.value?.gtn(0) + ) + + const isDelegatorsVoteCast = useAsync(async () => { + const relevantDelegators = + votingPop === 'community' ? communityDelegators : councilDelegators + + if ( + !hasVotingPower && + proposal && + relevantDelegators && + relevantDelegators.length > 0 + ) { + const delegatorisVoteCastList = await Promise.all( + relevantDelegators.map(async (delegator) => { + const pda = await getVoteRecordAddress( + proposal.owner, + proposal.pubkey, + delegator.pubkey + ) + const voteRecord = await fetchVoteRecordByPubkey( + connection.connection, + pda + ) + return !!voteRecord.found + }) + ) + + // check if there is any delegator without a vote. If so, return false + const voted = !delegatorisVoteCastList.includes(false) + setVote(voted ? 'yes' : 'no') + return voted + } + }, [ + communityDelegators?.length, + connection.connection, + councilDelegators?.length, + hasVotingPower, + proposal?.pubkey, + votingPop, + ]) + const handleVote = async (vote: 'yes' | 'no') => { setVote(vote) @@ -34,8 +98,11 @@ export const CastVoteButtons = () => { }) } } + const isFinalVoteCast = + isVoteCast || hasVotingPower ? isVoteCast : isDelegatorsVoteCast.result - return (isVoting && !isVoteCast) || (isInCoolOffTime && !isVoteCast) ? ( + return (isVoting && !isFinalVoteCast) || + (isInCoolOffTime && !isFinalVoteCast) ? (

Cast your {votingPop} vote

diff --git a/components/VotePanel/VetoButtons.tsx b/components/VotePanel/VetoButtons.tsx index 334feabf40..97bf924cd2 100644 --- a/components/VotePanel/VetoButtons.tsx +++ b/components/VotePanel/VetoButtons.tsx @@ -13,7 +13,7 @@ import { import { useProposalVoteRecordQuery } from '@hooks/queries/voteRecord' import { useSubmitVote } from '@hooks/useSubmitVote' import { useSelectedRealmInfo } from '@hooks/selectedRealm/useSelectedRealmRegistryEntry' -import { useGovernancePowerAsync } from '@hooks/queries/governancePower' +import { useRealmVoterWeightPlugins } from '@hooks/useRealmVoterWeightPlugins' const useIsVetoable = (): undefined | boolean => { const vetoingPop = useVetoingPop() @@ -30,7 +30,9 @@ const useCanVeto = (): | { canVeto: true } | { canVeto: false; message: string } => { const vetoPop = useVetoingPop() - const { result: govPower } = useGovernancePowerAsync(vetoPop) + const { calculatedMaxVoterWeight, isReady } = useRealmVoterWeightPlugins( + vetoPop + ) const wallet = useWalletOnePointOh() const connected = !!wallet?.connected const isVetoable = useIsVetoable() @@ -53,7 +55,8 @@ const useCanVeto = (): return { canVeto: false, message: 'You already voted' } // Do you have any voting power? - const hasMinAmountToVote = voterTokenRecord && govPower?.gtn(0) + const hasMinAmountToVote = + voterTokenRecord && isReady && calculatedMaxVoterWeight?.value?.gtn(0) if (hasMinAmountToVote === undefined) return undefined if (hasMinAmountToVote === false) return { diff --git a/components/VotePanel/YouVoted.tsx b/components/VotePanel/YouVoted.tsx index 0b1e5b3886..99dc5893e8 100644 --- a/components/VotePanel/YouVoted.tsx +++ b/components/VotePanel/YouVoted.tsx @@ -1,4 +1,10 @@ -import { GovernanceAccountType, VoteKind, VoteType, withFinalizeVote } from '@solana/spl-governance' +import { + GovernanceAccountType, + VoteKind, + VoteType, + getVoteRecordAddress, + withFinalizeVote, +} from '@solana/spl-governance' import { TransactionInstruction } from '@solana/web3.js' import { useState } from 'react' import { relinquishVote } from '../../actions/relinquishVote' @@ -13,13 +19,13 @@ import { } from '@heroicons/react/solid' import Button from '../Button' import { getProgramVersionForRealm } from '@models/registry/api' -import useVotePluginsClientStore from 'stores/useVotePluginsClientStore' import Tooltip from '@components/Tooltip' import { useVoterTokenRecord, useIsVoting, useIsInCoolOffTime, useUserVetoTokenRecord, + useVotingPop, } from './hooks' import assertUnreachable from '@utils/typescript/assertUnreachable' import { useHasVoteTimeExpired } from '@hooks/useHasVoteTimeExpired' @@ -31,15 +37,20 @@ import { useRouteProposalQuery, } from '@hooks/queries/proposal' import { useProposalGovernanceQuery } from '@hooks/useProposal' -import { useProposalVoteRecordQuery } from '@hooks/queries/voteRecord' +import { + fetchVoteRecordByPubkey, + useProposalVoteRecordQuery, +} from '@hooks/queries/voteRecord' import useLegacyConnectionContext from '@hooks/useLegacyConnectionContext' import queryClient from '@hooks/queries/queryClient' import { CheckmarkFilled } from '@carbon/icons-react' +import { useVotingClientForGoverningTokenMint } from '@hooks/useVotingClients' +import { useRealmVoterWeightPlugins } from '@hooks/useRealmVoterWeightPlugins' +import { useAsync } from 'react-async-hook' +import { useBatchedVoteDelegators } from './useDelegators' +import { useSelectedDelegatorStore } from 'stores/useSelectedDelegatorStore' export const YouVoted = ({ quorum }: { quorum: 'electoral' | 'veto' }) => { - const client = useVotePluginsClientStore( - (s) => s.state.currentRealmVotingClient - ) const proposal = useRouteProposalQuery().data?.result const realm = useRealmQuery().data?.result const { realmInfo } = useRealm() @@ -60,6 +71,9 @@ export const YouVoted = ({ quorum }: { quorum: 'electoral' | 'veto' }) => { const vetoVotertokenRecord = useUserVetoTokenRecord() const voterTokenRecord = quorum === 'electoral' ? electoralVoterTokenRecord : vetoVotertokenRecord + const votingClient = useVotingClientForGoverningTokenMint( + proposal?.account.governingTokenMint + ) const isWithdrawEnabled = connected && @@ -131,7 +145,7 @@ export const YouVoted = ({ quorum }: { quorum: 'electoral' | 'veto' }) => { voterTokenRecord.pubkey, ownVoteRecord.pubkey, instructions, - client + votingClient ) queryClient.invalidateQueries({ queryKey: proposalQueryKeys.all(connection.endpoint), @@ -142,40 +156,129 @@ export const YouVoted = ({ quorum }: { quorum: 'electoral' | 'veto' }) => { setIsLoading(false) } - const vote = ownVoteRecord?.account.vote + const selectedCommunityDelegator = useSelectedDelegatorStore( + (s) => s.communityDelegator + ) + const selectedCouncilDelegator = useSelectedDelegatorStore( + (s) => s.councilDelegator + ) + + const communityDelegators = useBatchedVoteDelegators('community') + const councilDelegators = useBatchedVoteDelegators('council') + const votingPop = useVotingPop() + const { voterWeightForWallet } = useRealmVoterWeightPlugins(votingPop) + + const relevantSelectedDelegator = + votingPop === 'community' + ? selectedCommunityDelegator + : selectedCouncilDelegator + + const ownVoterWeight = relevantSelectedDelegator + ? voterWeightForWallet(relevantSelectedDelegator) + : wallet?.publicKey + ? voterWeightForWallet(wallet?.publicKey) + : undefined + const hasVotingPower = !!( + ownVoterWeight?.value && ownVoterWeight.value?.gtn(0) + ) + + const delegatorVote = useAsync(async () => { + const relevantDelegators = + votingPop === 'community' ? communityDelegators : councilDelegators + + if ( + !hasVotingPower && + proposal && + relevantDelegators && + relevantDelegators.length > 0 + ) { + const delegatorisVoteList = await Promise.all( + relevantDelegators.map(async (delegator) => { + const pda = await getVoteRecordAddress( + proposal.owner, + proposal.pubkey, + delegator.pubkey + ) + const voteRecord = await fetchVoteRecordByPubkey( + connection.current, + pda + ) + return voteRecord + }) + ) + + const allVoted = !delegatorisVoteList + .map((vote) => !!vote.found) + .includes(false) + return allVoted ? delegatorisVoteList[0].result : null + } + }, [ + communityDelegators?.length, + connection.current, + councilDelegators?.length, + hasVotingPower, + proposal?.pubkey, + votingPop, + ]) + + const getDelegatorVoteForQuorum = () => { + if ( + // yes/no vote + (quorum === 'electoral' && !delegatorVote?.result?.account.vote?.veto) || + // veto vote + (quorum === 'veto' && delegatorVote?.result?.account.vote?.veto) + ) { + return delegatorVote?.result?.account.vote + } + return undefined + } + + const vote = hasVotingPower + ? ownVoteRecord?.account.vote + : getDelegatorVoteForQuorum() + + const isMulti = + proposal?.account.voteType !== VoteType.SINGLE_CHOICE && + proposal?.account.accountType === GovernanceAccountType.ProposalV2 + + const nota = '$$_NOTA_$$' - const isMulti = proposal?.account.voteType !== VoteType.SINGLE_CHOICE - && proposal?.account.accountType === GovernanceAccountType.ProposalV2 - return vote !== undefined ? (

{quorum === 'electoral' ? 'Your vote' : 'You voted to veto'}

- {vote.voteType === VoteKind.Approve ? - isMulti ? - vote.approveChoices?.map((choice, index) => ( - choice.weightPercentage ? -
- + {vote.voteType === VoteKind.Approve ? ( + isMulti ? ( + vote.approveChoices?.map((choice, index) => + choice.weightPercentage ? ( +
+ +
+ ) : null + ) + ) : ( + +
+
- : null - )) - : ( - -
- -
-
+
+ ) ) : vote.voteType === VoteKind.Deny ? (
diff --git a/components/VotePanel/useCanVote.ts b/components/VotePanel/useCanVote.ts index 077491b323..80bb661c33 100644 --- a/components/VotePanel/useCanVote.ts +++ b/components/VotePanel/useCanVote.ts @@ -1,103 +1,41 @@ -import { useVoterTokenRecord, useVotingPop } from './hooks' -import { VotingClientType } from '@utils/uiTypes/VotePlugin' -import useVotePluginsClientStore from 'stores/useVotePluginsClientStore' +import {useVoterTokenRecord, useVotingPop} from './hooks' import useWalletOnePointOh from '@hooks/useWalletOnePointOh' -import { useProposalVoteRecordQuery } from '@hooks/queries/voteRecord' -import { - determineVotingPowerType, - useGovernancePowerAsync, -} from '@hooks/queries/governancePower' - -import { useConnection } from '@solana/wallet-adapter-react' -import { useAsync } from 'react-async-hook' -import { useSelectedDelegatorStore } from 'stores/useSelectedDelegatorStore' - -import { DELEGATOR_BATCH_VOTE_SUPPORT_BY_PLUGIN } from '@constants/flags' -import useSelectedRealmPubkey from '@hooks/selectedRealm/useSelectedRealmPubkey' -import { useRealmQuery } from '@hooks/queries/realm' -import { useTokenOwnerRecordsDelegatedToUser } from '@hooks/queries/tokenOwnerRecord' +import {useProposalVoteRecordQuery} from '@hooks/queries/voteRecord' +import {useRealmVoterWeightPlugins} from '@hooks/useRealmVoterWeightPlugins' +import {useDelegatorAwareVoterWeight} from "@hooks/useDelegatorAwareVoterWeight"; const useHasAnyVotingPower = (role: 'community' | 'council' | undefined) => { - const realmPk = useSelectedRealmPubkey() - const realm = useRealmQuery().data?.result - - const { connection } = useConnection() - - const relevantMint = - role && role === 'community' - ? realm?.account.communityMint - : realm?.account.config.councilMint - - const { result: personalAmount } = useGovernancePowerAsync(role) - - const { result: plugin } = useAsync( - async () => - role && realmPk && determineVotingPowerType(connection, realmPk, role), - [connection, realmPk, role] - ) - - // DELEGATOR VOTING --------------------------------------------------------------- - - const batchVoteSupported = - plugin && DELEGATOR_BATCH_VOTE_SUPPORT_BY_PLUGIN[plugin] - // If the user is selecting a specific delegator, we want to just use that and not count the other delegators - const selectedDelegator = useSelectedDelegatorStore((s) => - role === 'community' ? s.communityDelegator : s.councilDelegator - ) - const torsDelegatedToUser = useTokenOwnerRecordsDelegatedToUser() - const relevantDelegators = selectedDelegator - ? undefined - : relevantMint && - torsDelegatedToUser?.filter((x) => - x.account.governingTokenMint.equals(relevantMint) - ) - - //--------------------------------------------------------------------------------- - // notably, this is ignoring whether the delegators actually have voting power, but it's not a big deal - const canBatchVote = - relevantDelegators === undefined || batchVoteSupported === undefined - ? undefined - : batchVoteSupported && relevantDelegators?.length !== 0 - - // technically, if you have a TOR you can vote even if there's no power. But that doesnt seem user friendly. - const canPersonallyVote = - personalAmount === undefined ? undefined : personalAmount.isZero() === false - - const canVote = canBatchVote || canPersonallyVote - - return canVote + const voterWeight = useDelegatorAwareVoterWeight(role ?? 'community'); + const {isReady } = useRealmVoterWeightPlugins(role) + return isReady && !!voterWeight?.value && voterWeight.value?.isZero() === false } export const useCanVote = () => { - const client = useVotePluginsClientStore( - (s) => s.state.currentRealmVotingClient - ) + const { isReady, includesPlugin } = useRealmVoterWeightPlugins() const votingPop = useVotingPop() const wallet = useWalletOnePointOh() const connected = !!wallet?.connected const { data: ownVoteRecord } = useProposalVoteRecordQuery('electoral') const voterTokenRecord = useVoterTokenRecord() + const { plugins } = useRealmVoterWeightPlugins(votingPop); + const hasAllVoterWeightRecords = (plugins?.voterWeight ?? []).every((plugin) => plugin.weights !== undefined) const isVoteCast = !!ownVoteRecord?.found const hasMinAmountToVote = useHasAnyVotingPower(votingPop) const canVote = connected && - !( - client.clientType === VotingClientType.NftVoterClient && !voterTokenRecord - ) && - !( - client.clientType === VotingClientType.HeliumVsrClient && - !voterTokenRecord - ) && + !(isReady && includesPlugin('NFT') && !voterTokenRecord) && + !(isReady && includesPlugin('HeliumVSR') && !voterTokenRecord) && + hasAllVoterWeightRecords && !isVoteCast && hasMinAmountToVote const voteTooltipContent = !connected ? 'You need to connect your wallet to be able to vote' - : client.clientType === VotingClientType.NftVoterClient && !voterTokenRecord + : isReady && includesPlugin('NFT') && !voterTokenRecord ? 'You must join the Realm to be able to vote' : !hasMinAmountToVote ? 'You don’t have governance power to vote in this dao' diff --git a/components/VotePanel/useDelegators.ts b/components/VotePanel/useDelegators.ts new file mode 100644 index 0000000000..d8fb8e3761 --- /dev/null +++ b/components/VotePanel/useDelegators.ts @@ -0,0 +1,71 @@ +import { DELEGATOR_BATCH_VOTE_SUPPORT_BY_PLUGIN } from '@constants/flags' +import { determineVotingPowerType } from '@hooks/queries/governancePower' +import { useRealmQuery } from '@hooks/queries/realm' +import { useTokenOwnerRecordsDelegatedToUser } from '@hooks/queries/tokenOwnerRecord' +import useSelectedRealmPubkey from '@hooks/selectedRealm/useSelectedRealmPubkey' +import { useConnection } from '@solana/wallet-adapter-react' +import { useAsync } from 'react-async-hook' +import { useSelectedDelegatorStore } from 'stores/useSelectedDelegatorStore' + +const useDelegators = (role: 'community' | 'council' | undefined) => { + const realm = useRealmQuery().data?.result + const relevantMint = + role && role === 'community' + ? realm?.account.communityMint + : realm?.account.config.councilMint + + const { data: torsDelegatedToUser } = useTokenOwnerRecordsDelegatedToUser() + const relevantDelegators = + relevantMint && + torsDelegatedToUser?.filter((x) => + x.account.governingTokenMint.equals(relevantMint) + ) + return relevantDelegators +} +/* +const fetchDelegators = async (connection: Connection, walletPk: PublicKey, realmPk: PublicKey, role: 'community' | 'council' | undefined) => { + const realm = (await fetchRealmByPubkey(connection, realmPk)).result + if (realm === undefined) {throw new Error('Realm not found')} + + const relevantMint = + role && role === 'community' + ? realm.account.communityMint + : realm.account.config.councilMint + +} */ + +/** + * if batched voting is enabled for the plugin, and by the user, then this returns array of delegators. + * otherwise, returns [] + **/ +export const useBatchedVoteDelegators = ( + role: 'community' | 'council' | undefined +) => { + const { connection } = useConnection() + const realmPk = useSelectedRealmPubkey() + const delegators = useDelegators(role) + const { result: plugin } = useAsync( + async () => + role && realmPk && determineVotingPowerType(connection, realmPk, role), + [connection, realmPk, role] + ) + const batchVoteSupported = + plugin && DELEGATOR_BATCH_VOTE_SUPPORT_BY_PLUGIN[plugin] + + // empty array if not supported + const delegatorsIfSupported = + batchVoteSupported === undefined + ? undefined + : batchVoteSupported === false + ? [] + : delegators + + // If the user is selecting a specific delegator, we want to just use that and not count the other delegators + const selectedDelegator = useSelectedDelegatorStore((s) => + role === 'community' ? s.communityDelegator : s.councilDelegator + ) + + return selectedDelegator ? [] : delegatorsIfSupported +} + +export default useDelegators diff --git a/components/chat/DiscussionForm.tsx b/components/chat/DiscussionForm.tsx index 72ec6830d3..966f385287 100644 --- a/components/chat/DiscussionForm.tsx +++ b/components/chat/DiscussionForm.tsx @@ -8,7 +8,6 @@ import { postChatMessage } from '../../actions/chat/postMessage' import Loading from '../Loading' import Tooltip from '@components/Tooltip' import { getProgramVersionForRealm } from '@models/registry/api' -import useVotePluginsClientStore from 'stores/useVotePluginsClientStore' import useWalletOnePointOh from '@hooks/useWalletOnePointOh' import { useUserCommunityTokenOwnerRecord, @@ -19,6 +18,7 @@ import { useRouteProposalQuery } from '@hooks/queries/proposal' import { useVotingPop } from '@components/VotePanel/hooks' import useLegacyConnectionContext from '@hooks/useLegacyConnectionContext' import { useLegacyVoterWeight } from '@hooks/queries/governancePower' +import {useVotingClients} from "@hooks/useVotingClients"; const DiscussionForm = () => { const [comment, setComment] = useState('') @@ -27,10 +27,9 @@ const DiscussionForm = () => { const realm = useRealmQuery().data?.result const { result: ownVoterWeight } = useLegacyVoterWeight() const { realmInfo } = useRealm() - const client = useVotePluginsClientStore( - (s) => s.state.currentRealmVotingClient - ) + const votingClients = useVotingClients(); const [submitting, setSubmitting] = useState(false) + const [error, setError] = useState('') const wallet = useWalletOnePointOh() const connected = !!wallet?.connected @@ -38,10 +37,14 @@ const DiscussionForm = () => { const proposal = useRouteProposalQuery().data?.result const tokenRole = useVotingPop() const commenterVoterTokenRecord = - tokenRole === 'community' ? ownTokenRecord : ownCouncilTokenRecord + tokenRole === 'community' ? + ownTokenRecord ?? ownCouncilTokenRecord : + ownCouncilTokenRecord + const votingClient = votingClients(tokenRole ?? 'community');// default to community if no role is provided const submitComment = async () => { setSubmitting(true) + setError('') if ( !realm || !proposal || @@ -72,12 +75,13 @@ const DiscussionForm = () => { commenterVoterTokenRecord, msg, undefined, - client + ownTokenRecord ? votingClient : undefined // use votingClient only if the community TOR is used for commenting ) setComment('') } catch (ex) { console.error("Can't post chat message", ex) + setError(ex.message); //TODO: How do we present transaction errors to users? Just the notification? } finally { setSubmitting(false) @@ -117,6 +121,7 @@ const DiscussionForm = () => {
+ {error &&

{error}

} ) } diff --git a/components/chat/DiscussionPanel.tsx b/components/chat/DiscussionPanel.tsx index f5723cc846..aaf49322a0 100644 --- a/components/chat/DiscussionPanel.tsx +++ b/components/chat/DiscussionPanel.tsx @@ -1,6 +1,5 @@ import { useMemo } from 'react' import DiscussionForm from './DiscussionForm' -import Comment from './Comment' import { useQuery } from '@tanstack/react-query' import { GOVERNANCE_CHAT_PROGRAM_ID, @@ -8,6 +7,7 @@ import { } from '@solana/spl-governance' import useLegacyConnectionContext from '@hooks/useLegacyConnectionContext' import { useSelectedProposalPk } from '@hooks/queries/proposal' +import LazyLoadComment from './LazyLoadComment' export const useChatMessagesQuery = () => { const connection = useLegacyConnectionContext() @@ -57,7 +57,7 @@ const DiscussionPanel = () => {
{sortedMessages?.map((cm) => ( - + ))}
) diff --git a/components/chat/LazyLoadComment.tsx b/components/chat/LazyLoadComment.tsx new file mode 100644 index 0000000000..773c441727 --- /dev/null +++ b/components/chat/LazyLoadComment.tsx @@ -0,0 +1,19 @@ +import React from 'react' +import { useInView } from 'react-intersection-observer' +import Comment from './Comment' +import { ChatMessage } from '@solana/spl-governance' + +const LazyLoadComment = ({ chatMessage }: { chatMessage: ChatMessage }) => { + const { ref, inView } = useInView({ + /* Optional options */ + triggerOnce: true, + }) + + return ( +
+
{inView && }
+
+ ) +} + +export default LazyLoadComment diff --git a/components/inputs/TokenAmountInput.tsx b/components/inputs/TokenAmountInput.tsx index bace53255a..6f1173cf2f 100644 --- a/components/inputs/TokenAmountInput.tsx +++ b/components/inputs/TokenAmountInput.tsx @@ -1,8 +1,5 @@ -import { useMintInfoByPubkeyQuery } from '@hooks/queries/mintInfo' import { PublicKey } from '@solana/web3.js' -import { precision } from '@utils/formatting' -import BigNumber from 'bignumber.js' -import { FC, useMemo } from 'react' +import { FC } from 'react' import Input, { InputProps } from './Input' type Props = Omit< @@ -26,20 +23,6 @@ const TokenAmountInput: FC = ({ value, ...props }) => { - const { data: mintInfo } = useMintInfoByPubkeyQuery(mint) - - const mintMinAmount = useMemo( - () => - mintInfo?.result - ? new BigNumber(1).shiftedBy(mintInfo.result.decimals).toNumber() - : 1, - [mintInfo?.result] - ) - - const currentPrecision = useMemo(() => precision(mintMinAmount), [ - mintMinAmount, - ]) - const validateAmount = () => { if (value === undefined || value === '') { if (props.required) { @@ -50,16 +33,16 @@ const TokenAmountInput: FC = ({ setValue( Math.max( - Number(mintMinAmount), + 1, Math.min(Number(Number.MAX_SAFE_INTEGER), Number(value)) - ).toFixed(currentPrecision) + ).toFixed(0) ) } return ( = ({ setValue(e.target.value) setError(undefined) }} - step={mintMinAmount} + step={1} error={props.error} onBlur={(e) => { props.onBlur?.(e) diff --git a/components/instructions/TransactionCard.tsx b/components/instructions/TransactionCard.tsx index 1c6f719602..90ba969729 100644 --- a/components/instructions/TransactionCard.tsx +++ b/components/instructions/TransactionCard.tsx @@ -23,6 +23,7 @@ export default function TransactionCard({ ? PlayState.Played : PlayState.Unplayed ) + return (

{`Transaction ${index} `}

diff --git a/components/instructions/TransactionInstructionCard.tsx b/components/instructions/TransactionInstructionCard.tsx index 847887d313..d0f6760504 100644 --- a/components/instructions/TransactionInstructionCard.tsx +++ b/components/instructions/TransactionInstructionCard.tsx @@ -5,6 +5,7 @@ import InstructionProgram from './InstructionProgram' import { useCallback, useEffect, useState } from 'react' import { InstructionDescriptor, + MANGO_INSTRUCTION_FORWARDER, WSOL_MINT, getInstructionDescriptor, } from './tools' @@ -30,6 +31,15 @@ const TransactionInstructionCard = ({ const connection = useLegacyConnectionContext() const realm = useRealmQuery().data?.result const { governedTokenAccountsWithoutNfts } = useGovernanceAssets() + const instructionUseInstructionForwarder = + instructionData.programId.toBase58() === MANGO_INSTRUCTION_FORWARDER + const instructionAccounts = instructionUseInstructionForwarder + ? [...instructionData.accounts.slice(2, instructionData.accounts.length)] + : instructionData.accounts + + const programId = instructionUseInstructionForwarder + ? instructionData.accounts[1].pubkey + : instructionData.programId const [descriptor, setDescriptor] = useState() const [nftImgUrl, setNftImgUrl] = useState('') @@ -106,17 +116,17 @@ const TransactionInstructionCard = ({
- {instructionData.accounts.map((am, idx) => ( + {instructionAccounts.map((am, idx) => ( ))}
diff --git a/components/instructions/programs/foresight.tsx b/components/instructions/programs/foresight.tsx deleted file mode 100644 index 6537d8f123..0000000000 --- a/components/instructions/programs/foresight.tsx +++ /dev/null @@ -1,239 +0,0 @@ -import { Connection, PublicKey } from '@solana/web3.js' - -import { - consts, - generatedTypes, - generatedAccounts, -} from '@foresight-tmp/foresight-sdk' -import { AccountMetaData } from '@solana/spl-governance' -import { IDL } from '@foresight-tmp/foresight-sdk/' -import { layout as initCategoryLayout } from '@foresight-tmp/foresight-sdk/dist/generated/instructions/initCategory' -import { layout as initMarketListLayout } from '@foresight-tmp/foresight-sdk/dist/generated/instructions/initMarketList' -import { layout as initMarketLayout } from '@foresight-tmp/foresight-sdk/dist/generated/instructions/initMarket' -import { layout as writeToFieldMarketMetadataLayout } from '@foresight-tmp/foresight-sdk/dist/generated/instructions/writeToFieldMarketMetadata' -import { layout as resolveMarketLayout } from '@foresight-tmp/foresight-sdk/dist/generated/instructions/resolveMarket' - -function displayParsedArg(argName: string, parsed: string): JSX.Element { - return ( -

- {argName}: {parsed} -

- ) -} - -function numArrayToString(data: number[]): string { - return Buffer.from(data).toString() -} - -function parseMetadata( - metadata: generatedAccounts.MarketMetadata, - includeImage = true -): string { - const base = { - title: numArrayToString(metadata.title), - description: numArrayToString(metadata.description), - rules: numArrayToString(metadata.rules), - } - const maybeImageObj = includeImage - ? { image: numArrayToString(metadata.image) } - : {} - return JSON.stringify({ ...base, ...maybeImageObj }, null, 2) -} - -function toDecodable(data: Uint8Array): Buffer { - return Buffer.from(data.slice(8)) -} - -function decodeIxData(data: Uint8Array, layout: any): any { - return layout.decode(toDecodable(data)) -} - -function findAccounts(ixName: string): { name: string }[] { - return IDL.instructions - .find((ix) => ix.name === ixName)! - .accounts.map((acc) => { - return { name: acc.name } - }) -} - -async function fetchId( - connection: Connection, - pubkey: PublicKey, - accountType: - | typeof generatedAccounts.Category - | typeof generatedAccounts.MarketList -): Promise { - const account = await accountType.fetch(connection, pubkey) - return account === null ? 'Error: not found' : numArrayToString(account.id) -} - -async function fetchCategoryId( - connection: Connection, - pubkey: PublicKey -): Promise { - return await fetchId(connection, pubkey, generatedAccounts.Category) -} - -async function fetchMarketListId( - connection: Connection, - pubkey: PublicKey -): Promise { - return await fetchId(connection, pubkey, generatedAccounts.MarketList) -} - -export const FORESIGHT_INSTRUCTIONS = { - [consts.PROGRAM_ID]: { - 65: { - name: 'Foresight: Init Category', - accounts: findAccounts('initCategory'), - getDataUI: async ( - _connection: Connection, - data: Uint8Array, - _accounts: AccountMetaData[] - ) => { - const args = decodeIxData(data, initCategoryLayout) - return ( - <> - {displayParsedArg('categoryId', numArrayToString(args.categoryId))} - - ) - }, - }, - 192: { - name: 'Foresight: Init Market List', - accounts: findAccounts('initMarketList'), - getDataUI: async ( - _connection: Connection, - data: Uint8Array, - _accounts: AccountMetaData[] - ) => { - const args = decodeIxData(data, initMarketListLayout) - return ( - <> - {displayParsedArg( - 'marketListId', - numArrayToString(args.marketListId) - )} - - ) - }, - }, - 33: { - name: 'Foresight: Init Market', - accounts: findAccounts('initMarket'), - getDataUI: async ( - connection: Connection, - data: Uint8Array, - accounts: AccountMetaData[] - ) => { - const args = decodeIxData(data, initMarketLayout) - const marketListId = await fetchMarketListId( - connection, - accounts[1].pubkey - ) - return ( - <> - {displayParsedArg('marketId', args.marketId[0].toString())} - {displayParsedArg('marketListId', marketListId)} - - ) - }, - }, - 218: { - name: 'Foresight: Add Market List to Category', - accounts: findAccounts('addMarketListToCategory'), - getDataUI: async ( - connection: Connection, - _data: Uint8Array, - accounts: AccountMetaData[] - ) => { - const categoryId = await fetchCategoryId(connection, accounts[1].pubkey) - const marketListId = await fetchMarketListId( - connection, - accounts[2].pubkey - ) - return ( - <> - {' '} - {displayParsedArg('categoryId', categoryId)} - {displayParsedArg('marketListId', marketListId)} - - ) - }, - }, - 90: { - name: 'Foresight: Set Market Metadata', - accounts: findAccounts('writeToFieldMarketMetadata'), - getDataUI: async ( - connection: Connection, - data: Uint8Array, - accounts: AccountMetaData[] - ) => { - const args = decodeIxData(data, writeToFieldMarketMetadataLayout) - const field = generatedTypes.MarketMetadataFields.fromDecoded( - args.field - ).kind - const metadataAccount = await generatedAccounts.MarketMetadata.fetch( - connection, - accounts[1].pubkey - ) - const currentMetadata = - metadataAccount === null - ? 'Error: not found.' - : parseMetadata(metadataAccount) - return ( - <> - {displayParsedArg('field', field)} - {displayParsedArg('content', numArrayToString(args.string))} -

Note: this is the current metadata:

-

{currentMetadata}

- - ) - }, - }, - 155: { - name: 'Foresight: Resolve Market', - accounts: findAccounts('resolveMarket'), - getDataUI: async ( - connection: Connection, - data: Uint8Array, - accounts: AccountMetaData[] - ) => { - const args = decodeIxData(data, resolveMarketLayout) - const winner = args.winningBracket === 0 ? 'YES' : 'NO' - const marketPubkey = accounts[2].pubkey - const marketAccount = await generatedAccounts.Market.fetch( - connection, - marketPubkey - ) - const notFoundMsg = 'Error: not found.' - let currentMetadata: string - if (marketAccount === null) { - currentMetadata = notFoundMsg - } else { - const marketId = Buffer.from(marketAccount.id) - const marketListId = Buffer.from(marketAccount.marketListId) - const [metadataPubkey] = PublicKey.findProgramAddressSync( - [Buffer.from('market_metadata'), marketId, marketListId], - consts.PROGRAM_ID_PUBKEY - ) - const metadataAccount = await generatedAccounts.MarketMetadata.fetch( - connection, - metadataPubkey - ) - currentMetadata = - metadataAccount === null - ? 'Error: not found.' - : parseMetadata(metadataAccount, false) - } - return ( - <> - {displayParsedArg('Winner', winner)} -

Note: here is the relevant market metadata:

-

{currentMetadata}

- - ) - }, - }, - }, -} diff --git a/components/instructions/programs/governance.tsx b/components/instructions/programs/governance.tsx index 0326295be2..6681a5459a 100644 --- a/components/instructions/programs/governance.tsx +++ b/components/instructions/programs/governance.tsx @@ -35,6 +35,7 @@ import { import { dryRunInstruction } from 'actions/dryRunInstruction' import { tryGetMint } from '../../../utils/tokens' import { fetchProgramVersion } from '@hooks/queries/useProgramVersionQuery' +import { fetchTokenAccountByPubkey } from '@hooks/queries/tokenAccount' const TOKEN_TYPES = { 0: 'Liquid', 1: 'Membership', 2: 'Disabled' } const governanceProgramId = 'GovER5Lthms3bLBqWub97yVrMmEogzX7xNjdXpPPCVZw' @@ -83,10 +84,17 @@ export const GOVERNANCE_INSTRUCTIONS = { ) //accounts[2] is token account not mint account - const mintInfoQuery = await fetchMintInfoByPubkey( + const { result: tokenAccount } = await fetchTokenAccountByPubkey( connection, accounts[2].pubkey ) + if (!tokenAccount) { + throw new Error() + } + const mintInfoQuery = await fetchMintInfoByPubkey( + connection, + tokenAccount.mint + ) const args = deserializeBorsh( getGovernanceInstructionSchema(programVersion), diff --git a/components/instructions/programs/mangoV4.tsx b/components/instructions/programs/mangoV4.tsx index a993c63b20..a5a451ee8c 100644 --- a/components/instructions/programs/mangoV4.tsx +++ b/components/instructions/programs/mangoV4.tsx @@ -1,27 +1,20 @@ -import { - Bank, - MANGO_V4_ID, - MangoClient, - OracleProvider, - USDC_MINT, - toUiDecimals, -} from '@blockworks-foundation/mango-v4' +import { Bank, USDC_MINT, toUiDecimals } from '@blockworks-foundation/mango-v4' import AdvancedOptionsDropdown from '@components/NewRealmWizard/components/AdvancedOptionsDropdown' -import { AnchorProvider, BN, BorshInstructionCoder } from '@coral-xyz/anchor' +import { BN, BorshInstructionCoder } from '@coral-xyz/anchor' import { AccountMetaData } from '@solana/spl-governance' -import { Connection, Keypair, PublicKey } from '@solana/web3.js' -import EmptyWallet, { - getSuggestedCoinTier, +import { Connection, PublicKey } from '@solana/web3.js' +import { compareObjectsAndGetDifferentKeys, FlatListingArgs, ListingArgsFormatted, getOracle, - getBestMarket, EditTokenArgsFormatted, FlatEditArgs, getFormattedListingPresets, decodePriceFromOracleAi, getFormattedBankValues, + REDUCE_ONLY_OPTIONS, + getSuggestedCoinPresetInfo, } from '@utils/Mango/listingTools' import { secondsToHours } from 'date-fns' import WarningFilledIcon from '@carbon/icons-react/lib/WarningFilled' @@ -31,17 +24,17 @@ import tokenPriceService, { TokenInfoWithoutDecimals, } from '@utils/services/tokenPrice' import { - LISTING_PRESETS, - LISTING_PRESETS_KEYS, - LISTING_PRESETS_PYTH, + LISTING_PRESETS_KEY, MidPriceImpact, coinTiersToNames, getMidPriceImpacts, - getProposedTier, + getProposedKey, } from '@blockworks-foundation/mango-v4-settings/lib/helpers/listingTools' import { tryParseKey } from '@tools/validators/pubkey' import Loading from '@components/Loading' -import queryClient from '@hooks/queries/queryClient' +import { getClient, getGroupForClient } from '@utils/mangoV4Tools' +import { tryGetMint } from '@utils/tokens' +import { formatNumber } from '@utils/formatNumber' // import { snakeCase } from 'snake-case' // import { sha256 } from 'js-sha256' @@ -233,11 +226,18 @@ const instructions = () => ({ const oracle = accounts[6].pubkey const isMintOnCurve = PublicKey.isOnCurve(proposedMint) - const [info, proposedOracle, args, oracleAi] = await Promise.all([ + const [ + info, + proposedOracle, + args, + oracleAi, + mintInfo, + ] = await Promise.all([ displayArgs(connection, data), getOracle(connection, oracle), getDataObjectFlattened(connection, data), connection.getAccountInfo(oracle), + tryGetMint(connection, proposedMint), ]) const oracleData = await decodePriceFromOracleAi( @@ -246,26 +246,53 @@ const instructions = () => ({ proposedOracle.type ) - const liqudityTier = await getSuggestedCoinTier( - proposedMint.toBase58(), - proposedOracle.type === 'Pyth' + const presetInfo = await getSuggestedCoinPresetInfo( + proposedMint.toBase58() ) const formattedProposedArgs = getFormattedListingValues(args) - const suggestedPreset = getFormattedListingPresets( - proposedOracle.type === 'Pyth' - )[liqudityTier.tier] - const suggestedUntrusted = liqudityTier.tier === 'UNTRUSTED' - const suggestedFormattedPreset: ListingArgsFormatted = - Object.keys(suggestedPreset).length && !suggestedUntrusted - ? getFormattedListingValues({ - tokenIndex: args.tokenIndex, - name: args.name, - oracle: args.oracle, - ...suggestedPreset, - }) - : ({} as ListingArgsFormatted) + const formattedSuggestedPresets = getFormattedListingPresets( + 0, + mintInfo?.account.decimals || 0, + oracleData.uiPrice + ) + + const currentListingArgsMatchedTier = Object.values( + formattedSuggestedPresets + ).find((preset) => { + const formattedPreset = getFormattedListingValues({ + tokenIndex: args.tokenIndex, + name: args.name, + oracle: args.oracle, + ...preset, + }) + + return ( + JSON.stringify({ + //deposit limit depends on current price so can be different a bit in proposal + ...formattedProposedArgs, + depositLimit: 0, + }) === + JSON.stringify({ + ...formattedPreset, + depositLimit: 0, + }) + ) + }) + + const suggestedPreset = formattedSuggestedPresets[presetInfo.presetKey] + + const suggestedFormattedPreset: ListingArgsFormatted = Object.keys( + suggestedPreset + ).length + ? getFormattedListingValues({ + tokenIndex: args.tokenIndex, + name: args.name, + oracle: args.oracle, + ...suggestedPreset, + }) + : ({} as ListingArgsFormatted) const invalidKeys: (keyof ListingArgsFormatted)[] = Object.keys( suggestedPreset @@ -275,24 +302,32 @@ const instructions = () => ({ suggestedFormattedPreset ) : [] - const invalidFields: Partial = invalidKeys.reduce( - (obj, key) => { + + const invalidFields: Partial = invalidKeys + .filter((x) => { + //soft invalid keys - some of the keys can be off by some small maring + if (x === 'depositLimit') { + return !isDifferenceWithin5Percent( + Number(formattedProposedArgs['depositLimit'] || 0), + Number(suggestedFormattedPreset['depositLimit'] || 0) + ) + } + return true + }) + .reduce((obj, key) => { return { ...obj, [key]: suggestedFormattedPreset[key], } - }, - {} - ) + }, {}) + const DisplayListingPropertyWrapped = ({ label, - suggestedUntrusted, valKey, suffix, prefix: perfix, }: { label: string - suggestedUntrusted: boolean valKey: string suffix?: string prefix?: string @@ -300,10 +335,9 @@ const instructions = () => ({ return ( ) @@ -313,32 +347,37 @@ const instructions = () => ({ return (
- {suggestedUntrusted && ( + {presetInfo.presetKey === 'UNTRUSTED' && ( <>

- Suggested token tier: UNTRUSTED. + Suggested token tier: C

- Very low liquidity Price impact of{' '} - {liqudityTier.priceImpact}% on $1000 swap. This token should - probably be listed using the Register Trustless Token - instruction check params carefully + Very low liquidity check params carefully

)} - {!suggestedUntrusted && !invalidKeys.length && ( + {!invalidKeys.length && (

Proposal params match suggested token tier -{' '} - {coinTiersToNames[liqudityTier.tier]}. + {coinTiersToNames[presetInfo.presetKey]}.

)} - {!suggestedUntrusted && invalidKeys.length > 0 && ( + {invalidKeys.length > 0 && (

Proposal params do not match suggested token tier -{' '} - {coinTiersToNames[liqudityTier.tier]} check params carefully + {coinTiersToNames[presetInfo.presetKey]} check params + carefully +

+ )} + {currentListingArgsMatchedTier && ( +

+ + Full match found with tier {/* @ts-ignore */} + {currentListingArgsMatchedTier.preset_name}

)} {isMintOnCurve && ( @@ -389,166 +428,199 @@ const instructions = () => ({
+ + + + + + +
@@ -588,32 +660,25 @@ const instructions = () => ({ const info = await displayArgs(connection, data) const client = await getClient(connection) - const mangoGroup = await client.getGroup(group) + const mangoGroup = await getGroupForClient(client, group) const banks = [...mangoGroup.banksMapByMint.values()].map((x) => x[0]) let baseMint = banks.find((x) => x.publicKey.equals(baseBank))?.mint let quoteMint = banks.find((x) => x.publicKey.equals(quoteBank))?.mint + const currentMarket = await Market.load( + connection, + openbookMarketPk, + undefined, + openBookProgram + ) if (!baseMint || !quoteMint) { - const currentMarket = await Market.load( - connection, - openbookMarketPk, - undefined, - openBookProgram - ) baseMint = currentMarket.baseMintAddress quoteMint = currentMarket.quoteMintAddress } - const bestMarket = await getBestMarket({ - baseMint: baseMint!.toBase58(), - quoteMint: quoteMint!.toBase58(), - cluster: 'mainnet-beta', - connection, - }) - try { return (
- {bestMarket && openbookMarketPk.equals(bestMarket) && ( + {/* {bestMarket && openbookMarketPk.equals(bestMarket.pubKey) && (
Proposed market match the best market according to listing @@ -626,7 +691,13 @@ const instructions = () => ({ Best market not found check market carefully
)} - {bestMarket && !openbookMarketPk.equals(bestMarket) && ( + {bestMarket?.error && ( +
+ + {bestMarket?.error} +
+ )} + {bestMarket && !openbookMarketPk.equals(bestMarket.pubKey) && (
- )} + )} */} + +
+
Tick Size: {currentMarket.tickSize}
+
+ Base Lot Size: {currentMarket.decoded?.baseLotSize?.toNumber()} +
+
+ Quote Lot Size:{' '} + {currentMarket.decoded?.quoteLotSize?.toNumber()} +
+
+ Quote decimals: {currentMarket['_quoteSplTokenDecimals']} +
+
Base decimals: {currentMarket['_baseSplTokenDecimals']}
+
{info}
) @@ -740,6 +826,7 @@ const instructions = () => ({ { name: 'Admin' }, { name: 'Mint Info' }, { name: 'Oracle' }, + { name: 'Fallback oracle' }, ], getDataUI: async ( connection: Connection, @@ -748,6 +835,7 @@ const instructions = () => ({ ) => { try { let mintData: null | TokenInfoWithoutDecimals | undefined = null + const mintInfo = accounts[2].pubkey const group = accounts[0].pubkey const client = await getClient(connection) @@ -756,15 +844,16 @@ const instructions = () => ({ displayArgs(connection, data), getDataObjectFlattened(connection, data), ]) + let priceImpact: MidPriceImpact | undefined const mint = [...mangoGroup.mintInfosMapByMint.values()].find((x) => x.publicKey.equals(mintInfo) )?.mint let liqudityTier: Partial<{ - tier: LISTING_PRESETS_KEYS + presetKey: LISTING_PRESETS_KEY priceImpact: string }> = {} - let suggestedUntrusted = false + let invalidKeys: (keyof EditTokenArgsFormatted)[] = [] let invalidFields: Partial = {} let bank: null | Bank = null @@ -775,11 +864,15 @@ const instructions = () => ({ const parsedArgs: Partial = { tokenIndex: args.tokenIndex, tokenName: args.nameOpt, - oracleConfidenceFilter: - args['oracleConfigOpt.confFilter'] !== undefined - ? (args['oracleConfigOpt.confFilter'] * 100)?.toFixed(2) - : undefined, - oracleMaxStalenessSlots: args['oracleConfigOpt.maxStalenessSlots'], + oracleConfidenceFilter: args['oracleConfigOpt.confFilter'] + ? args['oracleConfigOpt.confFilter'] >= 100 + ? args['oracleConfigOpt.confFilter'].toString() + : (args['oracleConfigOpt.confFilter'] * 100).toFixed(2) + : undefined, + oracleMaxStalenessSlots: + args['oracleConfigOpt.maxStalenessSlots'] === null + ? -1 + : args['oracleConfigOpt.maxStalenessSlots'], interestRateUtilizationPoint0: args['interestRateParamsOpt.util0'] !== undefined ? (args['interestRateParamsOpt.util0'] * 100)?.toFixed(2) @@ -808,11 +901,11 @@ const instructions = () => ({ : undefined, loanFeeRate: args.loanFeeRateOpt !== undefined - ? (args.loanFeeRateOpt * 10000)?.toFixed(2) + ? (args.loanFeeRateOpt * 100)?.toFixed(2) : undefined, loanOriginationFeeRate: args.loanOriginationFeeRateOpt !== undefined - ? (args.loanOriginationFeeRateOpt * 10000)?.toFixed(2) + ? (args.loanOriginationFeeRateOpt * 100)?.toFixed(2) : undefined, maintAssetWeight: args.maintAssetWeightOpt?.toFixed(2), initAssetWeight: args.initAssetWeightOpt?.toFixed(2), @@ -822,6 +915,10 @@ const instructions = () => ({ args['liquidationFeeOpt'] !== undefined ? (args['liquidationFeeOpt'] * 100)?.toFixed(2) : undefined, + platformLiquidationFee: + args['platformLiquidationFeeOpt'] !== undefined + ? (args['platformLiquidationFeeOpt'] * 100)?.toFixed(2) + : undefined, minVaultToDepositsRatio: args['minVaultToDepositsRatioOpt'] !== undefined ? (args['minVaultToDepositsRatioOpt'] * 100)?.toFixed(2) @@ -850,24 +947,49 @@ const instructions = () => ({ args.tokenConditionalSwapMakerFeeRateOpt, tokenConditionalSwapTakerFeeRate: args.tokenConditionalSwapTakerFeeRateOpt, - flashLoanSwapFeeRate: args.flashLoanSwapFeeRateOpt, + flashLoanSwapFeeRate: + args.flashLoanSwapFeeRateOpt !== undefined + ? (args.loanOriginationFeeRateOpt * 10000)?.toFixed(2) + : undefined, reduceOnly: args.reduceOnlyOpt !== undefined ? REDUCE_ONLY_OPTIONS[args.reduceOnlyOpt].name : undefined, + interestCurveScaling: args.interestCurveScalingOpt, + interestTargetUtilization: args.interestTargetUtilizationOpt, + maintWeightShiftStart: args.maintWeightShiftStartOpt?.toNumber(), + maintWeightShiftEnd: args.maintWeightShiftEndOpt?.toNumber(), + maintWeightShiftAssetTarget: args.maintWeightShiftAssetTargetOpt, + maintWeightShiftLiabTarget: args.maintWeightShiftLiabTargetOpt, + depositLimit: args.depositLimitOpt?.toString(), + setFallbackOracle: args.setFallbackOracle, + maintWeightShiftAbort: args.maintWeightShiftAbort, + zeroUtilRate: + args.zeroUtilRateOpt !== undefined + ? (args.zeroUtilRateOpt * 100).toFixed(2) + : undefined, + disableAssetLiquidation: args.disableAssetLiquidationOpt, + collateralFeePerDay: + args.collateralFeePerDayOpt !== undefined + ? (args.collateralFeePerDayOpt * 100)?.toFixed(4) + : undefined, + forceWithdraw: args.forceWithdrawOpt, + forceClose: args.forceCloseOpt, } if (mint) { bank = mangoGroup.getFirstBankByMint(mint) bankFormattedValues = getFormattedBankValues(mangoGroup, bank) mintData = tokenPriceService.getTokenInfo(mint.toBase58()) - const isPyth = bank?.oracleProvider === OracleProvider.Pyth - const midPriceImpacts = getMidPriceImpacts(mangoGroup.pis) - const PRESETS = isPyth ? LISTING_PRESETS_PYTH : LISTING_PRESETS + const midPriceImpacts = getMidPriceImpacts( + mangoGroup.pis.length ? mangoGroup.pis : [] + ) const tokenToPriceImpact = midPriceImpacts - .filter((x) => x.avg_price_impact_percent < 1) + .filter( + (x) => x.avg_price_impact_percent < 1 || x.target_amount <= 1000 + ) .reduce( (acc: { [key: string]: MidPriceImpact }, val: MidPriceImpact) => { if ( @@ -881,31 +1003,33 @@ const instructions = () => ({ {} ) - const priceImpact = tokenToPriceImpact[getApiTokenName(bank.name)] + priceImpact = tokenToPriceImpact[getApiTokenName(bank.name)] - const suggestedTier = getProposedTier( - PRESETS, - priceImpact?.target_amount, - bank.oracleProvider === OracleProvider.Pyth - ) + const suggestedPresetKey = priceImpact + ? getProposedKey( + priceImpact.avg_price_impact_percent < 1 + ? priceImpact?.target_amount + : undefined + ) + : 'UNTRUSTED' liqudityTier = !mint.equals(USDC_MINT) ? { - tier: suggestedTier, + presetKey: suggestedPresetKey, priceImpact: priceImpact ? priceImpact.avg_price_impact_percent.toString() : '', } : { - tier: 'ULTRA_PREMIUM', + presetKey: 'asset_250', priceImpact: '0', } const suggestedPreset = getFormattedListingPresets( - !!isPyth, - bank.nativeDeposits().mul(bank.price).toNumber() - )[liqudityTier.tier!] - suggestedUntrusted = liqudityTier.tier === 'UNTRUSTED' + bank.uiDeposits(), + bank.mintDecimals, + bank.uiPrice + )[liqudityTier.presetKey!] const suggestedFormattedPreset: | EditTokenArgsFormatted @@ -917,7 +1041,16 @@ const instructions = () => ({ oracle: args.oracleOpt, ...suggestedPreset, }), - groupInsuranceFund: suggestedPreset.insuranceFound, + groupInsuranceFund: suggestedPreset.groupInsuranceFund, + maintWeightShiftStart: args.maintWeightShiftStartOpt?.toNumber(), + maintWeightShiftEnd: args.maintWeightShiftEndOpt?.toNumber(), + maintWeightShiftAssetTarget: + args.maintWeightShiftAssetTargetOpt, + maintWeightShiftLiabTarget: args.maintWeightShiftLiabTargetOpt, + maintWeightShiftAbort: args.maintWeightShiftAbort, + setFallbackOracle: args.setFallbackOracle, + forceWithdraw: args.forceWithdrawOpt, + forceClose: args.forceCloseOpt, } : {} @@ -926,7 +1059,30 @@ const instructions = () => ({ Partial >(parsedArgs, suggestedFormattedPreset) : [] - ).filter((x) => parsedArgs[x] !== undefined) + ) + .filter((x) => parsedArgs[x] !== undefined) + .filter((x) => { + //soft invalid keys - some of the keys can be off by some small maring + if (x === 'depositLimit') { + return !isDifferenceWithin5Percent( + Number(parsedArgs['depositLimit'] || 0), + Number(suggestedFormattedPreset['depositLimit'] || 0) + ) + } + if (x === 'netBorrowLimitPerWindowQuote') { + return !isDifferenceWithin5Percent( + Number(parsedArgs['netBorrowLimitPerWindowQuote'] || 0), + Number( + suggestedFormattedPreset['netBorrowLimitPerWindowQuote'] || + 0 + ) + ) + } + if (x === 'collateralFeePerDay') { + return false + } + return true + }) invalidFields = invalidKeys.reduce((obj, key) => { return { @@ -939,36 +1095,38 @@ const instructions = () => ({ return (

{mintData &&
Token: {mintData.symbol}
}

- {suggestedUntrusted && ( + {!priceImpact && ( +

+ + No price impact data in group +

+ )} + {liqudityTier.presetKey === 'UNTRUSTED' && ( <>

- Suggested token tier: UNTRUSTED. + Suggested token tier: C

- Very low liquidity Price impact of {liqudityTier?.priceImpact} - % on $1000 swap. Check params carefully token should be listed - with untrusted instruction + Very low liquidity check params carefully

)} - {!suggestedUntrusted && !invalidKeys.length && liqudityTier.tier && ( + {!invalidKeys.length && liqudityTier.presetKey && (

Proposal params match suggested token tier -{' '} - {coinTiersToNames[liqudityTier.tier]}. + {coinTiersToNames[liqudityTier.presetKey]}. +

+ )} + {invalidKeys && invalidKeys!.length > 0 && liqudityTier.presetKey && ( +

+ + Proposal params do not match suggested token tier -{' '} + {coinTiersToNames[liqudityTier.presetKey]} check params + carefully

)} - {!suggestedUntrusted && - invalidKeys && - invalidKeys!.length > 0 && - liqudityTier.tier && ( -

- - Proposal params do not match suggested token tier -{' '} - {coinTiersToNames[liqudityTier.tier]} check params carefully -

- )}
- Current values @@ -1008,7 +1166,11 @@ const instructions = () => ({ ({ } /> + ({ `${invalidFields.liquidationFee}%` } /> + ({ `${invalidFields.netBorrowLimitWindowSizeTs}H` } /> + ({ value={args.oracleOpt?.toBase58()} currentValue={bankFormattedValues?.oracle} /> + {accounts.length && + accounts[4] && + accounts[4].pubkey.toBase58() !== + bankFormattedValues?.fallbackOracle && ( + + )} ({ /> + + + + + + + + + {parsedArgs?.disableAssetLiquidation && ( + + )} + {parsedArgs?.maintWeightShiftAbort && ( + + )} + {parsedArgs?.forceClose && ( + + )} + {parsedArgs?.forceWithdraw && ( + + )} + {parsedArgs?.setFallbackOracle && ( + + )}

Raw values

{info}
@@ -1319,7 +1669,7 @@ const instructions = () => ({ } }, }, - 73195: { + 11366: { name: 'Withdraw all token fees', accounts: [ { name: 'Group' }, @@ -1353,6 +1703,56 @@ const instructions = () => ({ } }, }, + 63223: { + name: 'Token Withdraw', + accounts: [ + { name: 'Group' }, + { name: 'Account' }, + { name: 'Owner' }, + { name: 'Bank' }, + { name: 'Vault' }, + { name: 'Oracle' }, + { name: 'TokenAccount' }, + { name: 'TokenProgram' }, + ], + getDataUI: async ( + connection: Connection, + data: Uint8Array, + accounts: AccountMetaData[] + ) => { + const args = await getDataObjectFlattened(connection, data) + const accountInfo = await connection.getParsedAccountInfo( + accounts[6].pubkey + ) + const mint = await tryGetMint( + connection, + new PublicKey(accountInfo.value?.data['parsed'].info.mint) + ) + const tokenInfo = tokenPriceService.getTokenInfo( + accountInfo.value?.data['parsed'].info.mint + ) + + try { + return ( +
+
+ amount:{' '} + {mint?.account.decimals + ? formatNumber( + toUiDecimals(args.amount, mint?.account.decimals) + ) + : args.amount}{' '} + {tokenInfo?.symbol} +
+
allowBorrow: {args.allowBorrow.toString()}
+
+ ) + } catch (e) { + console.log(e) + return
{JSON.stringify(data)}
+ } + }, + }, 15219: { name: 'Withdraw all perp fees', accounts: [ @@ -1418,12 +1818,35 @@ const instructions = () => ({ ], getDataUI: async ( connection: Connection, - data: Uint8Array - //accounts: AccountMetaData[] + data: Uint8Array, + accounts: AccountMetaData[] ) => { - const info = await displayArgs(connection, data) + const args = await getDataObjectFlattened(connection, data) + const accountInfo = await connection.getParsedAccountInfo( + accounts[6].pubkey + ) + const mint = await tryGetMint( + connection, + new PublicKey(accountInfo.value?.data['parsed'].info.mint) + ) + const tokenInfo = tokenPriceService.getTokenInfo( + accountInfo.value?.data['parsed'].info.mint + ) try { - return
{info}
+ return ( +
+
+ amount:{' '} + {mint?.account.decimals + ? formatNumber( + toUiDecimals(args.amount, mint?.account.decimals) + ) + : args.amount}{' '} + {tokenInfo?.symbol} +
+
reduce only: {args.reduceOnly.toString()}
+
+ ) } catch (e) { console.log(e) return
{JSON.stringify(data)}
@@ -1456,79 +1879,52 @@ const instructions = () => ({ export const MANGO_V4_INSTRUCTIONS = { '4MangoMjqJ2firMokCjjGgoK8d4MXcrgL7XJaL3w6fVg': instructions(), -} - -const getClient = async (connection: Connection) => { - const client = await queryClient.fetchQuery({ - queryKey: ['mangoClient', connection.rpcEndpoint], - queryFn: async () => { - const options = AnchorProvider.defaultOptions() - const adminProvider = new AnchorProvider( - connection, - new EmptyWallet(Keypair.generate()), - options - ) - const client = await MangoClient.connect( - adminProvider, - 'mainnet-beta', - MANGO_V4_ID['mainnet-beta'] - ) - - return client - }, - }) - return client -} -const getGroupForClient = async (client: MangoClient, groupPk: PublicKey) => { - const group = await queryClient.fetchQuery({ - queryKey: ['mangoGroup', groupPk.toBase58(), client.connection.rpcEndpoint], - queryFn: async () => { - const response = await client.getGroup(groupPk) - return response - }, - }) - return group + zF2vSz6V9g1YHGmfrzsY497NJzbRr84QUrPry4bLQ25: instructions(), } async function getDataObjectFlattened( connection: Connection, data: Uint8Array ) { - const client = await getClient(connection) - const decodedInstructionData = new BorshInstructionCoder( - client.program.idl - ).decode(Buffer.from(data))?.data as any + try { + const client = await getClient(connection) + const decodedInstructionData = new BorshInstructionCoder( + client.program.idl + ).decode(Buffer.from(data))?.data as any - // console.log( - // client.program.idl.instructions.map((ix) => { - // const sh = sighash('global', ix.name) - // return { - // name: ix.name, - // sh: `${sh[0]}${sh[1]}`, - // } - // }) - // ) + // console.log( + // client.program.idl.instructions.map((ix) => { + // const sh = sighash('global', ix.name) + // return { + // name: ix.name, + // sh: `${sh[0]}${sh[1]}`, + // } + // }) + // ) - const args = {} - for (const key of Object.keys(decodedInstructionData)) { - const val = decodedInstructionData[key] - if (val !== null) { - if ( - typeof val === 'object' && - !Array.isArray(val) && - !(val instanceof BN) && - !(val instanceof PublicKey) - ) { - for (const innerKey of Object.keys(val)) { - const innerVal = val[innerKey] - args[`${key}.${innerKey}`] = innerVal + const args = {} + for (const key of Object.keys(decodedInstructionData)) { + const val = decodedInstructionData[key] + if (val !== null) { + if ( + typeof val === 'object' && + !Array.isArray(val) && + !(val instanceof BN) && + !(val instanceof PublicKey) + ) { + for (const innerKey of Object.keys(val)) { + const innerVal = val[innerKey] + args[`${key}.${innerKey}`] = innerVal + } + } else { + args[key] = val } - } else { - args[key] = val } } + return args as T + } catch (e) { + return {} as T } - return args as T } const displayArgs = async (connection: Connection, data: Uint8Array) => { @@ -1543,11 +1939,21 @@ const displayArgs = async (connection: Connection, data: Uint8Array) => { if (key === 'resetNetBorrowLimit' && args[key] === false) { return false } + if (key === 'maintWeightShiftAbort' && args[key] === false) { + return false + } + if (key === 'setFallbackOracle' && args[key] === false) { + return false + } return true }) .map((key) => { const isPublicKey = tryParseKey(args[key]) + const isBN = args[key] instanceof BN + if (isBN && key === 'price.val') { + return args[key] / Math.pow(2, 48) + } return (
{key}:
@@ -1603,14 +2009,12 @@ const DisplayNullishProperty = ({ const DisplayListingProperty = ({ label, - suggestedUntrusted, val, suggestedVal, suffix, prefix, }: { label: string - suggestedUntrusted: boolean val: any suggestedVal?: any suffix?: string @@ -1619,11 +2023,9 @@ const DisplayListingProperty = ({
{label}:
-
+
{prefix} - {val} + {`${val}`} {suffix}
{suggestedVal &&
/
} @@ -1643,8 +2045,11 @@ const getFormattedListingValues = (args: FlatListingArgs) => { const formattedArgs: ListingArgsFormatted = { tokenIndex: args.tokenIndex, tokenName: args.name, - oracle: args.oracle?.toBase58(), - oracleConfidenceFilter: (args['oracleConfig.confFilter'] * 100).toFixed(2), + oracle: args.oracle?.toBase58 && args.oracle?.toBase58(), + oracleConfidenceFilter: + args['oracleConfig.confFilter'] >= 100 + ? args['oracleConfig.confFilter'].toString() + : (args['oracleConfig.confFilter'] * 100).toFixed(2), oracleMaxStalenessSlots: args['oracleConfig.maxStalenessSlots'], interestRateUtilizationPoint0: ( args['interestRateParams.util0'] * 100 @@ -1658,13 +2063,14 @@ const getFormattedListingValues = (args: FlatListingArgs) => { adjustmentFactor: ( args['interestRateParams.adjustmentFactor'] * 100 ).toFixed(2), - loanFeeRate: (args.loanFeeRate * 10000).toFixed(2), - loanOriginationFeeRate: (args.loanOriginationFeeRate * 10000).toFixed(2), + loanFeeRate: (args.loanFeeRate * 100).toFixed(2), + loanOriginationFeeRate: (args.loanOriginationFeeRate * 100).toFixed(2), maintAssetWeight: args.maintAssetWeight.toFixed(2), initAssetWeight: args.initAssetWeight.toFixed(2), maintLiabWeight: args.maintLiabWeight.toFixed(2), initLiabWeight: args.initLiabWeight.toFixed(2), liquidationFee: (args['liquidationFee'] * 100).toFixed(2), + platformLiquidationFee: (args['platformLiquidationFee'] * 100).toFixed(2), minVaultToDepositsRatio: (args['minVaultToDepositsRatio'] * 100).toFixed(2), netBorrowLimitPerWindowQuote: toUiDecimals( args['netBorrowLimitPerWindowQuote'], @@ -1688,18 +2094,19 @@ const getFormattedListingValues = (args: FlatListingArgs) => { stablePriceGrowthLimit: (args.stablePriceGrowthLimit * 100).toFixed(2), tokenConditionalSwapMakerFeeRate: args.tokenConditionalSwapMakerFeeRate, tokenConditionalSwapTakerFeeRate: args.tokenConditionalSwapTakerFeeRate, - flashLoanSwapFeeRate: args.flashLoanSwapFeeRate, + flashLoanSwapFeeRate: (args.flashLoanSwapFeeRate * 10000).toFixed(2), reduceOnly: REDUCE_ONLY_OPTIONS[args.reduceOnly].name, + depositLimit: args.depositLimit.toString(), + interestTargetUtilization: args.interestTargetUtilization, + interestCurveScaling: args.interestCurveScaling, + groupInsuranceFund: args.groupInsuranceFund, + collateralFeePerDay: (args.collateralFeePerDay * 100).toFixed(4), + zeroUtilRate: (args.zeroUtilRate * 100).toFixed(2), + disableAssetLiquidation: args.disableAssetLiquidation, } return formattedArgs } -const REDUCE_ONLY_OPTIONS = [ - { value: 0, name: 'Disabled' }, - { value: 1, name: 'No borrows and no deposits' }, - { value: 2, name: 'No borrows' }, -] - //need yarn add js-sha256 snakeCase // function sighash(nameSpace: string, ixName: string): Buffer { // const name = snakeCase(ixName) @@ -1713,3 +2120,17 @@ const getApiTokenName = (bankName: string) => { } return bankName } + +function isDifferenceWithin5Percent(a: number, b: number): boolean { + // Calculate the absolute difference + const difference = Math.abs(a - b) + + // Calculate the average of the two numbers + const average = (a + b) / 2 + + // Calculate the percentage difference + const percentageDifference = (difference / average) * 100 + + // Check if the difference is within 5% + return percentageDifference <= 5 +} diff --git a/components/instructions/programs/names.ts b/components/instructions/programs/names.ts index 774bec9f8e..b70f0abc32 100644 --- a/components/instructions/programs/names.ts +++ b/components/instructions/programs/names.ts @@ -1,5 +1,4 @@ import { PublicKey } from '@solana/web3.js' -import { consts as foresightConsts } from '@foresight-tmp/foresight-sdk/' import { LIDO_PROGRAM_ID, LIDO_PROGRAM_ID_DEVNET, @@ -10,6 +9,8 @@ import { NAME_PROGRAM_ID } from '@bonfida/spl-name-service' const GOVERNANCE_PROGRAM_NAMES = { GqTPL6qRf5aUuqscLh8Rg2HTxPUXfhhAXDptTLhp1t2J: 'Mango Governance Program', FP4PxqHTVzeG2c6eZd7974F9WvKUSdBeduUK3rjYyvBw: 'Mango v4 Program Governance ', + '89VDjvzxfNLN7EgkvkNZ4VWpskBuiuZtHkLpYJd3ynEY': + 'Mango v4 Program Governance Wallet', AVoAYTs36yB5izAaBkxRG67wL1AMwG3vo41hKtUSb8is: 'Serum Governance Program (Old)', G41fmJzd29v7Qmdi8ZyTBBYa98ghh3cwHBTexqCG1PQJ: @@ -62,7 +63,6 @@ const PROGRAM_NAMES = { 'Raydium Voter Stake Registry Program', VoteMBhDCqGLRgYpp9o7DGyq81KNmwjXQRAHStjtJsS: 'Marinade Voter Stake Registry Program', - [foresightConsts.PROGRAM_ID]: 'Foresight Dex', [NAME_PROGRAM_ID.toBase58()]: 'Solana Name Service Program', AwyKDr1Z5BfdvK3jX1UWopyjsJSV5cq4cuJpoYLofyEn: 'Validator Dao', Stake11111111111111111111111111111111111111: 'Stake Program', diff --git a/components/instructions/programs/poseidon.tsx b/components/instructions/programs/poseidon.tsx index 0f2744e576..dad579b342 100644 --- a/components/instructions/programs/poseidon.tsx +++ b/components/instructions/programs/poseidon.tsx @@ -2,7 +2,8 @@ import { BN, web3 } from '@coral-xyz/anchor' import { AccountMetaData } from '@solana/spl-governance' import { getMintDecimalAmountFromNatural } from '@tools/sdk/units' import tokenPriceService from '@utils/services/tokenPrice' -import { parseMintAccountData, parseTokenAccountData } from '@utils/tokens' +import { parseMintAccountData } from '@utils/tokens' +import { parseTokenAccountData } from '@utils/parseTokenAccountData' import { InstructionDescriptorFactory } from '../tools' const common_instructions = (): Record< diff --git a/components/instructions/programs/stake.tsx b/components/instructions/programs/stake.tsx index 87618f40b9..4b97a7b492 100644 --- a/components/instructions/programs/stake.tsx +++ b/components/instructions/programs/stake.tsx @@ -1,6 +1,7 @@ -import { Connection, LAMPORTS_PER_SOL } from '@solana/web3.js' +import { Connection, LAMPORTS_PER_SOL, PublicKey } from '@solana/web3.js' import { AccountMetaData } from '@solana/spl-governance' import * as BufferLayout from '@solana/buffer-layout' +import dayjs from 'dayjs' export const STAKE_INSTRUCTIONS = { Stake11111111111111111111111111111111111111: { @@ -60,5 +61,105 @@ export const STAKE_INSTRUCTIONS = { return <> }, }, + 6: { + name: 'Stake Program - Unlock', + accounts: [{ name: 'Stake Account' }, { name: 'Vote Account' }], + getDataUI: async ( + _connection: Connection, + _data: Uint8Array, + _accounts: AccountMetaData[] + ) => { + try { + const layout = BufferLayout.struct([ + BufferLayout.u32('instruction'), + BufferLayout.u8('hasUnixTimestamp'), + BufferLayout.ns64('unixTimestamp'), + BufferLayout.u8('hasEpoch'), + //add epoch field if needed + BufferLayout.u8('hasCustodian'), + //add custodian field if needed + ]) + // decode + const data = layout.decode(Buffer.from(_data)) + const accData = await _connection.getParsedAccountInfo( + _accounts[0].pubkey + ) + const stakeMeta = accData.value?.data['parsed'].info.meta + return ( + <> +
+
Staker: {stakeMeta.authorized.staker}
+
Withdraw authority: {stakeMeta.authorized.withdrawer}
+
Lockup authority: {stakeMeta.lockup.custodian}
+
+
+ Unlock date:{' '} + {dayjs.unix(data.unixTimestamp).format('DD-MM-YYYY HH:mm')} +
+ {(data.hasEpoch !== 0 || data.hasCustodian !== 0) && ( +
+ Warning! detected epoch or custodian change +
+ )} + + ) + } catch (e) { + return <> + } + }, + }, + 1: { + name: 'Stake Program - Change Authority', + accounts: [ + { name: 'Stake Account' }, + { name: '' }, + { name: 'Authorized' }, + ], + getDataUI: async ( + _connection: Connection, + _data: Uint8Array, + _accounts: AccountMetaData[] + ) => { + const layout = BufferLayout.struct([ + BufferLayout.u32('instruction'), + BufferLayout.blob(32, 'newAuthorized'), + BufferLayout.u32('voteAuthorizationType'), + ]) + const data = layout.decode(Buffer.from(_data)) + + return ( + <> +

New Authority: {new PublicKey(data.newAuthorized).toBase58()}

+

+ Change of:{' '} + {data.voteAuthorizationType === 0 ? 'Staker' : 'Withdrawer'} +

+ + ) + }, + }, + 9: { + name: 'Stake Program - Deposit Stake', + accounts: [ + { name: 'Stake Pool' }, + { name: 'Validator List' }, + { name: 'Deposit Authority' }, + { name: 'Withdraw Authority' }, + { name: 'Deposit Stake' }, + { name: 'Validator Stake' }, + { name: 'Reserve Stake' }, + { name: 'Destination PoolAccount' }, + { name: 'Manager Fee Account' }, + { name: 'Referral Pool Account' }, + { name: 'Pool Mint' }, + ], + getDataUI: async ( + _connection: Connection, + _data: Uint8Array, + _accounts: AccountMetaData[] + ) => { + return <> + }, + }, }, } diff --git a/components/instructions/programs/stakeSanctum.tsx b/components/instructions/programs/stakeSanctum.tsx new file mode 100644 index 0000000000..73d69889b5 --- /dev/null +++ b/components/instructions/programs/stakeSanctum.tsx @@ -0,0 +1,37 @@ +import { Connection } from '@solana/web3.js' +import { AccountMetaData } from '@solana/spl-governance' + +export const STAKE_SANCTUM_INSTRUCTIONS = { + SP12tWFxD9oJsVWNavTTBZvMbA6gkAmxtVgxdqvyvhY: { + 9: { + name: 'Stake Program - Deposit Stake', + accounts: [ + { name: 'Stake Pool' }, + { name: 'Validator List' }, + { name: 'Deposit Authority' }, + { name: 'Withdraw Authority' }, + { name: 'Deposit Stake' }, + { name: 'Validator Stake' }, + { name: 'Reserve Stake' }, + { name: 'Destination PoolAccount' }, + { name: 'Manager Fee Account' }, + { name: 'Referral Pool Account' }, + { name: 'Pool Mint' }, + ], + getDataUI: async ( + _connection: Connection, + _data: Uint8Array, + _accounts: AccountMetaData[] + ) => { + return ( + <> +
+ Check change authority instruction. New authority must match + deposit authority of pool {_accounts[2].pubkey.toBase58()} +
+ + ) + }, + }, + }, +} diff --git a/components/instructions/programs/symmetryV2.tsx b/components/instructions/programs/symmetryV2.tsx new file mode 100644 index 0000000000..b6820b2f02 --- /dev/null +++ b/components/instructions/programs/symmetryV2.tsx @@ -0,0 +1,408 @@ +import { Connection } from "@solana/web3.js" +import { BasketsSDK } from "@symmetry-hq/baskets-sdk" +import BufferLayout from 'buffer-layout' + +const targetCompositionLayout = BufferLayout.seq( + BufferLayout.u8(), + 15, + 'targetComposition' +); +const targetWeightsLayout = BufferLayout.seq( + BufferLayout.u32(), + 15, + 'targetWeights' +); +const rebalanceAndLpLayout = BufferLayout.seq( + BufferLayout.u8(), + 2, + 'rebalanceAndLp' +); + + +const SymmetryLogoLink = () => { + return + + + + + + +} + +export const SYMMETRY_V2_INSTRUCTIONS = { + "2KehYt3KsEQR53jYcxjbQp2d2kCp4AkuQW68atufRwSr": { + 78: { + name: 'Symmetry: Withdraw from Basket', + accounts: [ + { name: 'Withdrawer' }, + { name: 'Basket Address' }, + { name: 'Symmetry PDA' }, + { name: 'Withdraw Temporary State Account' }, + { name: 'Withdrawer Basket Token Account' }, + { name: 'Basket Token Mint' }, + { name: 'System Program' }, + { name: 'Token Program' }, + { name: 'Rent Program' }, + { name: 'Account' }, + ], + getDataUI: async (connection: Connection, data: Uint8Array) => { + + //@ts-ignore + const { amount, rebalance } = BufferLayout.struct([ + BufferLayout.nu64('amount'), + BufferLayout.nu64('rebalance') + ]).decode(Buffer.from(data), 8) + + return ( +
+
+ +
+

Withdraw from Basket

+

Basket Tokens will be burnt to redeem underlying assets:

+
+
+
+
+
+
+
Withdraw:
+
{(amount / 10**6).toFixed(2)} Basket Tokens
+
+
+
Rebalance to USDC:
+
+ {rebalance === 2 ? 'Yes' : 'No - Withdraw Assets Directly'} +
+
+
+
+
+
+ + ) + }, + }, + 251: { + name: 'Symmetry: Deposit into Basket', + accounts: [ + { name: 'Depositor' }, + { name: 'Basket Address' }, + { name: 'Basket Token Mint' }, + { name: 'Symmetry Token List' }, + { name: 'Symmetry PDA' }, + { name: 'USDC PDA Account' }, + { name: 'Depositor USDC Account' }, + { name: 'Manager USDC Account' }, + { name: 'Symmetry Fee Account' }, + { name: 'Host Platform USDC Account' }, + { name: 'Depositor Basket Token Account' }, + { name: 'Temporary State Account for Deposit' }, + { name: 'System Program' }, + { name: 'Token Program' }, + { name: 'Rent' }, + { name: 'Associated Token Program' }, + { name: 'Account' }, + ], + getDataUI: async (connection: Connection, data: Uint8Array) => { + + //@ts-ignore + const { amount, rebalance } = BufferLayout.struct([ + BufferLayout.nu64('amount') + ]).decode(Buffer.from(data), 8) + + return ( +
+
+ +
+

USDC Deposit Information

+

After the deposit, basket tokens will be minted to the DAO.

+
+
+
+
+
+
+
USDC Deposit Amount:
+
{(amount / 10**6).toFixed(2)} USDC
+
+
+
+
+
+ ) + }, + }, + 38: { + name: 'Symmetry: Edit Basket', + accounts: [ + { name: 'Basket Manager' }, + { name: 'Basket Address' }, + { name: 'Symmetry Token List' }, + { name: 'Manager Fee Receiver Address' }, + ], + getDataUI: async (connection: Connection, data: Uint8Array) => { + + //@ts-ignore + const { managerFee, rebalanceInterval, rebalanceThreshold, rebalanceSlippage, lpOffsetThreshold, rebalanceAndLp, numOfTokens, targetComposition, targetWeights } = BufferLayout.struct([ + BufferLayout.u16('managerFee'), + BufferLayout.nu64('rebalanceInterval'), + BufferLayout.u16('rebalanceThreshold'), + BufferLayout.u16('rebalanceSlippage'), + BufferLayout.u16('lpOffsetThreshold'), + rebalanceAndLpLayout, + BufferLayout.u8('numOfTokens'), + targetCompositionLayout, + targetWeightsLayout, + ]).decode(Buffer.from(data), 8) + + const basketsSdk = await BasketsSDK.init(connection); + const tokenData = basketsSdk.getTokenListData(); + let usdcIncluded = false; + let totalWeight = 0; targetWeights.map(w => totalWeight += w); + + let composition = targetComposition.map((tokenId, i) => { + let token = tokenData.filter(x => x.id == tokenId)[0] + if(token.id === 0) { + if(!usdcIncluded) { + usdcIncluded = true; + return { + ...token, + weight: targetWeights[i] / totalWeight * 100 + } + } + } else { + return { + ...token, + weight: targetWeights[i] / totalWeight * 100 + } + } + }).filter(x => x != null) + + return ( +
+
+ + + + + + + +
+

Editing Basket Settings

+

If the proposal passes, the basket will be edited to the following settings:

+
+
+
+
+
+
+
Manager Fee:
+
{managerFee / 100}%
+
+
+
Rebalance Check Interval:
+
{rebalanceInterval / 60} minutes
+
+
+
Rebalance Trigger Threshold:
+
{rebalanceThreshold / 100}%
+
+
+
Maximum Slippage Allowed During Rebalancing:
+
{rebalanceSlippage / 100}%
+
+
+
Liquidity Provision Threshold:
+
{lpOffsetThreshold / 100}%
+
+
+
Rebalancing Enabled:
+
{rebalanceAndLp[0] === 0 ? "Yes" : "No"}
+
+
+
Liquidity Provision Enabled:
+
{rebalanceAndLp[1] === 0 ? "Yes" : "No"}
+
+
+
Basket Composition Size:
+
{numOfTokens} Tokens
+
+
+
+
+

Basket Composition:

+ +
+
+
+ ) + }, + }, + 47: { + name: 'Symmetry: Create Basket', + accounts: [ + { name: 'Manager' }, + { name: 'Token List' }, + { name: 'Basket Address' }, + { name: 'Symmetry PDA' }, + { name: 'Basket Token Mint' }, + { name: 'Symmetry Fee Collector' }, + { name: 'Metadata Account' }, + { name: 'Metadata Program' }, + { name: 'System Program' }, + { name: 'Token Program' }, + { name: 'Rent' }, + { name: 'Host Platform' }, + { name: 'Fee Collector Address' }, + { name: 'Account' }, + ], + getDataUI: async (connection: Connection, data: Uint8Array) => { + //@ts-ignore + const { managerFee, hostFee, basketType,rebalanceInterval, rebalanceThreshold, rebalanceSlippage, lpOffsetThreshold, rebalanceAndLp, numOfTokens, targetComposition, targetWeights, } = BufferLayout.struct([ + BufferLayout.u16('managerFee'), + BufferLayout.u16('hostFee'), + BufferLayout.u8('basketType'), + BufferLayout.nu64('rebalanceInterval'), + BufferLayout.u16('rebalanceThreshold'), + BufferLayout.u16('rebalanceSlippage'), + BufferLayout.u16('lpOffsetThreshold'), + rebalanceAndLpLayout, + BufferLayout.u8('numOfTokens'), + targetCompositionLayout, + targetWeightsLayout, + ]).decode(Buffer.from(data), 8) + const getBasketType = (type) => { + switch (type) { + case 0: return "Bundle"; + case 1: return "Portfolio"; + case 2: return "Private"; + default: return "Unknown"; + } + }; + + let basketsSdk = await BasketsSDK.init(connection); + let tokenData = basketsSdk.getTokenListData(); + let usdcIncluded = false; + let totalWeight = 0; targetWeights.map(w => totalWeight += w); + + let composition = targetComposition.map((tokenId, i) => { + let token = tokenData.filter(x => x.id == tokenId)[0] + if(token.id === 0) { + if(!usdcIncluded) { + usdcIncluded = true; + return { + ...token, + weight: targetWeights[i] / totalWeight * 100 + } + } + } else + return { + ...token, + weight: targetWeights[i] / totalWeight * 100 + } + }).filter(x => x != null) + + return ( +
+
+ + + + + + + +
+

Creating a Basket

+

If the proposal passes, a basket will be created with the following settings:

+
+
+
+
+
+
+
Manager Fee:
+
{managerFee / 100}%
+
+
+
Host Platform Fee:
+
{hostFee / 100}%
+
+
+
Basket Type:
+
{getBasketType(basketType)}
+
+
+
Rebalance Check Interval:
+
{rebalanceInterval / 60} minutes
+
+
+
Rebalance Trigger Threshold:
+
{rebalanceThreshold / 100}%
+
+
+
Maximum Slippage Allowed During Rebalancing:
+
{rebalanceSlippage / 100}%
+
+
+
Liquidity Provision Threshold:
+
{lpOffsetThreshold / 100}%
+
+
+
Rebalancing Enabled:
+
{rebalanceAndLp[0] === 0 ? "Yes" : "No"}
+
+
+
Liquidity Provision Enabled:
+
{rebalanceAndLp[1] === 0 ? "Yes" : "No"}
+
+
+
Basket Composition Size:
+
{numOfTokens} Tokens
+
+
+
+
+

Basket Composition:

+ +
+
+
+ ) + }, + }, + }, +} \ No newline at end of file diff --git a/components/instructions/tools.tsx b/components/instructions/tools.tsx index 6fa7da772d..ac37a15726 100644 --- a/components/instructions/tools.tsx +++ b/components/instructions/tools.tsx @@ -22,10 +22,8 @@ import { VOTE_STAKE_REGISTRY_INSTRUCTIONS } from './programs/voteStakeRegistry' import { MARINADE_INSTRUCTIONS } from './programs/marinade' import { SOLEND_PROGRAM_INSTRUCTIONS } from './programs/solend' import { ATA_PROGRAM_INSTRUCTIONS } from './programs/associatedTokenAccount' -import { governance as foresightGov } from '@foresight-tmp/foresight-sdk' import { ConnectionContext } from '@utils/connection' import { NFT_VOTER_INSTRUCTIONS } from './programs/nftVotingClient' -import { FORESIGHT_INSTRUCTIONS } from './programs/foresight' import { LIDO_INSTRUCTIONS } from './programs/lido' import { NAME_SERVICE_INSTRUCTIONS } from './programs/nameService' import { TOKEN_AUCTION_INSTRUCTIONS } from './programs/tokenAuction' @@ -35,7 +33,10 @@ import { MANGO_V4_INSTRUCTIONS } from './programs/mangoV4' import { DUAL_INSTRUCTIONS } from './programs/dual' import { SWITCHBOARD_INSTRUCTIONS } from './programs/switchboard' import { STAKE_INSTRUCTIONS } from './programs/stake' +import dayjs from 'dayjs' import { JUPITER_REF } from './programs/jupiterRef' +import { STAKE_SANCTUM_INSTRUCTIONS } from './programs/stakeSanctum' +import { SYMMETRY_V2_INSTRUCTIONS } from './programs/symmetryV2' /** * Default governance program id instance @@ -45,6 +46,8 @@ export const DEFAULT_GOVERNANCE_PROGRAM_ID = export const DEFAULT_GOVERNANCE_PROGRAM_VERSION = 3 export const MANGO_DAO_TREASURY = '9RGoboEjmaAjSCXsKi6p6zJucnwF3Eg5NUN9jPS6ziL3' +export const MANGO_INSTRUCTION_FORWARDER = + 'ixFPGCPYEp5GzhoahhHFVL8VVzkq1kc2eeFZh3qpYca' // Well known account names displayed on the instruction card export const ACCOUNT_NAMES = { @@ -58,8 +61,9 @@ export const ACCOUNT_NAMES = { 'Mango DAO BLAZE Realm Deposit', '8gjzxiqcU87cvRc7hFiUJgxqLSV7AQnSttfWC5fD9aim': 'Mango DAO Treasury Council Mint', + oW7juZxrhaGvWw5giRp3P3qTHEZpg2t8n8aXTCpBjNK: 'Mango DAO boost council', G1Yc5696GcfL28uAWG6iCaKJwZd8sQzwPJTc2UacsjHN: - 'Mango DAO Game master Council Mint', + 'Mango DAO Game Master Council Mint', A9xaHx54B9bRYBga4V6LKFrRaARpMJFYVooEXRAanru5: 'Mango DAO Treasury Council USDC Treasury', '7zGXUAeUkY9pEGfApsY26amibvqsf2dmty1cbtxHdfaQ': 'Mango DAO Wallet Governance', @@ -103,8 +107,6 @@ export const ACCOUNT_NAMES = { MangoCzJ36AjZyKwVj3VnYU4GTonjfVEnJmvvWaxLac: 'MNGO Token Mint', H7uqouPsJkeEiLpCEoC1qYVVquDrZan6ZfdPK2gS44zm: 'FORE Devnet Token Mint', '4ahVJVavHM8DZCtjX6YuKSTFx6KJwRPmVCJtjdQYdUU7': 'FORE Mainnet Token Mint', - [foresightGov.DEVNET_TREASURY.toBase58()]: 'Foresight Devnet Governance', - [foresightGov.MAINNET_TREASURY.toBase58()]: 'Foresight Mainnet Governance', EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v: 'USDC Token Mint', MyHd6a7HWKTMeJMHBkrbMq4hZwZxwn9x7dxXcopQ4Wd: 'OMH Token', @@ -134,6 +136,12 @@ export const ACCOUNT_NAMES = { FAFDfoUkaxoMqiNur9F1iigdBNrXFf4uNmS5XrhMewvf: 'Friends and Family Community Mint', + // Dean's List DAO + '6Vjsy1KabnHtSuHZcXuuCQFWoBML9JscSy3L4NGjqmhM': 'Deans List DAO Treasury', + CLgzSdeNcf9CYHiAdmXaPaCw2vYBeiqEeZcgguqirVM9: 'DAO: (DEAN) Strategic Reserve', + bDgqY2Qt4y2jSsRNvD7FETkRJJNiYZT1Q3UnAYYzUCo: 'DAO: (DEAN) Community Reserve', + BtJaNZrZZmagHGzCU2VazSJWzBS9KY7tG41enBrT2NtU: 'DAO: (DEAN) Liquidity Reserve', + // Physis DAO '29epeLvAMyRXtpA1HaoKB1hGcAnrc1NvMCbaZ8AVRwEi': 'Physis DAO Treasury', '4i2Yjk5bUiLeVNwqBpkRdFSECSCvMgKoeCSdRSx1TPcz': 'DAO: Rewards (PHY)', @@ -320,6 +328,9 @@ export const ACCOUNT_NAMES = { '6gwjRFcW1Y9iuJwXPdz1zZUa3Hcu855dH6APA5LjD8qK': 'AllDomains Treasury Governance', AWVUWfRnHCTgo123mRXB9BRWaxt6JdZXXKhFMQ5mryKJ: 'AllDomains DAO Governance', + + // Parcl + "9Waj7NNTzEhyHf1j1F36xgtnXaLoAxVBFhf6VxE9fgaf": 'Parcl DAO' } // TODO: Add this to on-chain metadata to Governance account @@ -355,6 +366,8 @@ export const HIDDEN_PROPOSALS = new Map([ ['H5TnbSBNFKJJwKea8tUj7ETcmhRHXQ1N9XCXBSD6Q9P1', ''], ['GeMQWvFTasBoui11RqRzMtDPQ9b2BkMK8NzepWzvuXw3', ''], ['CRmUPr8CbfPQ4MAoo2yxSf5qL2nPsddL69kowMfp1JYP', ''], + ['8msNFq5VBectsGAv66zYx5QRve1p3m6ZEz49xaWX3tbd', ''], + ['3jU2YuKXKBw4cWx9taPDfhQZ8RFLmFUx3HLxMrh7w749', ''], ]) export const DEFAULT_NATIVE_SOL_MINT = @@ -386,6 +399,15 @@ const HIDDEN_MNGO_TREASURES = [ 'PuXf9LNrmtVDhBTxteNTWS8D2SpzbhYvidkSatjRArt', ] +//badly created realms +export const HIDDEN_REALMS = [ + 'BWnVbUDohApiiaWBNNGcLH2KXRKEoTBJ7schsKQWYAtj', + 'FsoDEiZ9BoGTAaCLzXkyQWEqNKa5PW2iokzmuD7YsRdL', + '9nUyxzVL2FUMuWUiVZG66gwK15CJiM3PoLkfrnGfkvt6', // old Drift dao + '7mjEBafqqKA2K6SHezMrDV1zBoyNw6SKFcTsBbH2Kxgb', // openBook v2 council wrong config + '6NzVDMfEBJvkFDnjPx53K7mLGW3yQdSjLhsamS8go4cn', // old bonfida dao +] + //owner and desired accounts we want to show const MNGO_AUXILIARY_TOKEN_ACCOUNTS = [ { @@ -396,10 +418,54 @@ const MNGO_AUXILIARY_TOKEN_ACCOUNTS = [ owner: '7hqfhmXK6uXQKmNjUVEJo5acDMLcnyN9p9bZ5Dmnifde', accounts: ['2gDu12CM56g18Ukc9R1EdmEbToXqGTrnBEqR3zKfVKgt'], }, + //treasury management council { owner: '9so7UTo6b6LXBSqdDfh18hjVj8Ng5BmLbYXLB7UrhaaJ', - accounts: ['A9xaHx54B9bRYBga4V6LKFrRaARpMJFYVooEXRAanru5'], + accounts: [ + 'A9xaHx54B9bRYBga4V6LKFrRaARpMJFYVooEXRAanru5', + '8Wkbx6Daq3RQY492HaXK2nbVLXKCL5SGcab3RHzBCzpV', + '7D2j3MpXMveMEkdR94QfMh5nS3HdFD7uQHKhaLenR8u6', + '5d5CU8viHKiwrwjgNUFtb6AxUjdiZ1xmLo2m3AMYa9K5', + ], + }, + //boost council + { + owner: 'BExGoGVK6k6mUL6oHmabbc2EtwNqhJUeNoJWijF6t3ZB', + accounts: [ + 'HuDbGjhoPMWxVUxJmaY4uinDF5RmSufg2SCwjxpCRvXX', + 'AnmvgZbSre3NyGn4CeSNZDTN7NMmribt4eNTFDAQSGuv', + ], + }, + //vsr + { + owner: 'DZZWE1PR8qTkH3dLTrD7kcNEs6xx3GmSuFbzW29dyHv7', + accounts: ['CJoHzb9FVJUKanFdmjjXD84Hg94qgE4egu8s2tGYTVdE'], + }, + { + owner: 'VrT8f16NLADvYR73YiDMwxZREPbJgiZqLvN6HLQj4hR', + accounts: ['BkNq5TQvPkDnQWNgn1j2Q2SAFe3r5m2PazRwC7YUSHAT'], + }, + { + owner: '3H5PPK1bhHKmCAG5zwUyxpKDijES3H9uRAUCBrW8rGPX', + accounts: ['3sC3vzVz9YoiR12QKgvxHD6Q2LBfhL1ev63tsUaUS2EJ'], + }, + { + owner: 'DdZWj3nWSzJMMv1LMTHm9gTJ37wHLNXTMzqjWCokvKEn', + accounts: ['6XfCUQuq4juhqWLCW6LeivZd1eGuCRp4Mw3D6nkwXwFG'], + }, + { + owner: '7v1dD4kTJcBC7zV9MSrz7Ddyz8Dvs24QUMnZeQQCxeyV', + accounts: ['CEGxhB84XffJBfXm8WphwSczrpaJX6cRJjZz3QqNWJSZ'], + }, + { + owner: 'A99Whcw3pNdYXQ1DikQsLLNNjbsw8rD1zdvX4LTvZ8pD', + accounts: ['CkxhXSSgqBM7HrZE6zrQPBNCb7eHN4nm1FHd3Ad1XARX'], + }, + { + owner: 'FRYXAjyVnvXja8chgdq47qL3CKoyBjUg4ro7M7QQn1aD', + accounts: ['24frxVoDzo7bAimBU6rDhB1McxWNvzX9qddPMSv9VACZ'], }, + // ] export const AUXILIARY_TOKEN_ACCOUNTS = { @@ -438,7 +504,6 @@ export const INSTRUCTION_DESCRIPTORS = { ...LIDO_INSTRUCTIONS, ...SWITCHBOARD_INSTRUCTIONS, ...SOLEND_PROGRAM_INSTRUCTIONS, - ...FORESIGHT_INSTRUCTIONS, ...ATA_PROGRAM_INSTRUCTIONS, ...SYSTEM_INSTRUCTIONS, ...VOTE_STAKE_REGISTRY_INSTRUCTIONS, @@ -450,7 +515,9 @@ export const INSTRUCTION_DESCRIPTORS = { ...MANGO_V4_INSTRUCTIONS, ...DUAL_INSTRUCTIONS, ...STAKE_INSTRUCTIONS, + ...STAKE_SANCTUM_INSTRUCTIONS, ...JUPITER_REF, + ...SYMMETRY_V2_INSTRUCTIONS, } export async function getInstructionDescriptor( @@ -459,38 +526,98 @@ export async function getInstructionDescriptor( realm?: ProgramAccount | undefined ) { let descriptors: any + let instructionToDecode = { ...instruction } + const isUsingForwardProgram = + instructionToDecode.programId.toBase58() === MANGO_INSTRUCTION_FORWARDER + if ( - (realm && instruction.programId.equals(realm.owner)) || - instruction.programId.equals(new PublicKey(DEFAULT_GOVERNANCE_PROGRAM_ID)) + (realm && instructionToDecode.programId.equals(realm.owner)) || + instructionToDecode.programId.equals( + new PublicKey(DEFAULT_GOVERNANCE_PROGRAM_ID) + ) ) { descriptors = GOVERNANCE_INSTRUCTIONS['GovER5Lthms3bLBqWub97yVrMmEogzX7xNjdXpPPCVZw'] + } else if (isUsingForwardProgram) { + instructionToDecode = { + accounts: instructionToDecode.accounts.slice( + 2, + instructionToDecode.accounts.length + ), + data: instructionToDecode.data.slice(8, instructionToDecode.data.length), + programId: instructionToDecode.accounts[1].pubkey, + } + descriptors = + INSTRUCTION_DESCRIPTORS[instructionToDecode.programId.toBase58()] } else { - descriptors = INSTRUCTION_DESCRIPTORS[instruction.programId.toBase58()] + descriptors = + INSTRUCTION_DESCRIPTORS[instructionToDecode.programId.toBase58()] } // Make it work for program with one instruction like ATA program // and for the one with multiple instructions - const descriptor = !instruction.data.length + const descriptor = !instructionToDecode.data.length ? descriptors - : descriptors && descriptors[instruction.data[0]] - ? descriptors[instruction.data[0]] + : descriptors && descriptors[instructionToDecode.data[0]] + ? descriptors[instructionToDecode.data[0]] : //backup if first number is same for couple of instructions inside same idl - descriptors && descriptors[`${instruction.data[0]}${instruction.data[1]}`] - ? descriptors[`${instruction.data[0]}${instruction.data[1]}`] + descriptors && + descriptors[ + `${instructionToDecode.data[0]}${instructionToDecode.data[1]}` + ] + ? descriptors[ + `${instructionToDecode.data[0]}${instructionToDecode.data[1]}` + ] : descriptors const dataUI = (descriptor?.getDataUI && (await descriptor?.getDataUI( connection.current, - instruction.data, - instruction.accounts, - instruction.programId, + instructionToDecode.data, + instructionToDecode.accounts, + instructionToDecode.programId, connection.cluster - ))) ?? <>{JSON.stringify(instruction.data)} + ))) ?? <>{JSON.stringify(instructionToDecode.data)} + + const dataUiWithAdditionalInfo = ( + <> + {isUsingForwardProgram && ( + + )} + {dataUI} + + ) return { name: descriptor?.name, accounts: descriptor?.accounts, - dataUI, + dataUI: dataUiWithAdditionalInfo, } } + +const ForwarderProgramDecode = ({ + instruction, +}: { + instruction: InstructionData +}) => { + const timestampBytes = instruction.data.slice(0, 8) + const view = new DataView(Buffer.from(timestampBytes).buffer) + const timestamp = view.getUint32(0, true) // true for little-endian + + const date = dayjs(timestamp * 1000) // Convert to milliseconds + + return ( +
+
+ Instruction use forwarder program: {MANGO_INSTRUCTION_FORWARDER} +
+
+ Only wallet: {instruction.accounts[0].pubkey.toBase58()} can execute +
+
+ Proposal is executable only until: {date.format('DD-MM-YYYY HH:mm')} +
+
+ ) +} diff --git a/components/treasuryV2/Details/TokenDetails/Investments.tsx b/components/treasuryV2/Details/TokenDetails/Investments.tsx index 7e0bec0682..845b929a12 100644 --- a/components/treasuryV2/Details/TokenDetails/Investments.tsx +++ b/components/treasuryV2/Details/TokenDetails/Investments.tsx @@ -198,7 +198,6 @@ export default function Investments(props: Props) { {alternativeInvestment === 'Mango' && ( setAlternativeInvestment(null)} > diff --git a/components/treasuryV2/Details/TokenOwnerRecordDetails/Header.tsx b/components/treasuryV2/Details/TokenOwnerRecordDetails/Header.tsx index 40fb1d2a19..a00446bc63 100644 --- a/components/treasuryV2/Details/TokenOwnerRecordDetails/Header.tsx +++ b/components/treasuryV2/Details/TokenOwnerRecordDetails/Header.tsx @@ -219,7 +219,7 @@ export default function Header({ tokenOwnerRecord, governance }: Props) {

- Community Votes: {/** todo check for council */} + Community votes: {/** todo check for council */} {fmtMintAmount( mint, new BN( diff --git a/components/treasuryV2/WalletList/NewWalletButton.tsx b/components/treasuryV2/WalletList/NewWalletButton.tsx index 5981f2caf6..a2b9a4ca98 100644 --- a/components/treasuryV2/WalletList/NewWalletButton.tsx +++ b/components/treasuryV2/WalletList/NewWalletButton.tsx @@ -6,7 +6,7 @@ import { LinkButton } from '@components/Button' import useQueryContext from '@hooks/useQueryContext' import useWalletOnePointOh from '@hooks/useWalletOnePointOh' import { useRealmQuery } from '@hooks/queries/realm' -import { useGovernancePowerAsync } from '@hooks/queries/governancePower' +import { useRealmVoterWeightPlugins } from '@hooks/useRealmVoterWeightPlugins' const NEW_TREASURY_ROUTE = `/treasury/new` @@ -15,8 +15,18 @@ export default function NewWalletButton() { const connected = !!wallet?.connected const realm = useRealmQuery().data?.result - const { result: councilGovPower } = useGovernancePowerAsync('council') - const { result: communityGovPower } = useGovernancePowerAsync('community') + const { + isReady: communityIsReady, + totalCalculatedVoterWeight: communityCalculatedVoterWeight, + } = useRealmVoterWeightPlugins('community') + const { + isReady: councilIsReady, + totalCalculatedVoterWeight: councilCalculatedVoterWeight, + } = useRealmVoterWeightPlugins('council') + const isReady = communityIsReady && councilIsReady + + const communityGovPower = communityCalculatedVoterWeight?.value + const councilGovPower = councilCalculatedVoterWeight?.value const { symbol, @@ -27,11 +37,12 @@ export default function NewWalletButton() { const { fmtUrlWithCluster } = useQueryContext() const canCreateGovernance = - councilGovPower?.gtn(0) || - (realm && - communityGovPower?.gt( - realm.account.config.minCommunityTokensToCreateGovernance - )) + isReady && + (councilGovPower?.gtn(0) || + (realm && + communityGovPower?.gt( + realm.account.config.minCommunityTokensToCreateGovernance + ))) const addNewAssetTooltip = !connected ? 'Connect your wallet to create new asset' diff --git a/components/treasuryV2/WalletList/WalletListItem/AssetList/MangoListItem.tsx b/components/treasuryV2/WalletList/WalletListItem/AssetList/MangoListItem.tsx new file mode 100644 index 0000000000..ebe9c5caf7 --- /dev/null +++ b/components/treasuryV2/WalletList/WalletListItem/AssetList/MangoListItem.tsx @@ -0,0 +1,35 @@ +import React from 'react' + +import ListItem from './ListItem' +import { CurrencyDollarIcon } from '@heroicons/react/solid' +import { formatNumber } from '@utils/formatNumber' +import BigNumber from 'bignumber.js' + +interface Props { + className?: string + selected?: boolean + amount: BigNumber + onSelect?(): void +} + +export default function MangoListItem(props: Props) { + return ( + +

+
$
+
+ {formatNumber(props.amount)} +
+
+
+ } + selected={props.selected} + onSelect={props.onSelect} + thumbnail={} + /> + ) +} diff --git a/components/treasuryV2/WalletList/WalletListItem/AssetList/NFTList.tsx b/components/treasuryV2/WalletList/WalletListItem/AssetList/NFTList.tsx index 5c011d80f2..8b9e7c9d79 100644 --- a/components/treasuryV2/WalletList/WalletListItem/AssetList/NFTList.tsx +++ b/components/treasuryV2/WalletList/WalletListItem/AssetList/NFTList.tsx @@ -33,7 +33,6 @@ export default function NFTList({ governance, ...props }: Props) { .map((x) => new PublicKey(x)), [nfts] ) - console.log('collectionIds', JSON.stringify(collectionIds)) const hasNftWithoutCollection = nfts?.find((x) => x.grouping.length < 1) diff --git a/components/treasuryV2/WalletList/WalletListItem/AssetList/OtherAssetsList.tsx b/components/treasuryV2/WalletList/WalletListItem/AssetList/OtherAssetsList.tsx index cad31ef6c4..9584e723c9 100644 --- a/components/treasuryV2/WalletList/WalletListItem/AssetList/OtherAssetsList.tsx +++ b/components/treasuryV2/WalletList/WalletListItem/AssetList/OtherAssetsList.tsx @@ -9,6 +9,7 @@ import { RealmAuthority, Unknown, Stake, + Mango, } from '@models/treasury/Asset' import Collapsible from './Collapsible' @@ -19,16 +20,25 @@ import UnknownAssetListItem from './UnknownAssetListItem' import RealmAuthorityListItem from './RealmAuthorityListItem' import StakeListItem from './StakeListItem' import { abbreviateAddress } from '@utils/formatting' +import MangoListItem from './MangoListItem' interface Props { className?: string disableCollapse?: boolean expanded?: boolean - assets: (Mint | Domains | Programs | RealmAuthority | Unknown | Stake)[] + assets: ( + | Mint + | Domains + | Programs + | RealmAuthority + | Unknown + | Stake + | Mango + )[] selectedAssetId?: string | null itemsToHide: string[] onSelect?( - asset: Mint | Domains | Programs | RealmAuthority | Unknown | Stake + asset: Mint | Domains | Programs | RealmAuthority | Unknown | Stake | Mango ): void onToggleExpand?(): void } @@ -106,6 +116,14 @@ export default function OtherAssetsList(props: Props) { onSelect={() => props.onSelect?.(asset)} > ) + case AssetType.Mango: + return ( + []} + /> + ) case AssetType.Unknown: return ( (othersFromProps) const [itemsToHide, setItemsToHide] = useState([]) useEffect(() => { @@ -173,6 +183,7 @@ export default function AssetList(props: Props) { | Domains | RealmAuthority | Stake + | Mango )[] = [] for await (const token of othersFromProps) { if (isMint(token)) { diff --git a/components/treasuryV2/WalletList/WalletListItem/typeGuards.ts b/components/treasuryV2/WalletList/WalletListItem/typeGuards.ts index cc21b3b8a0..2cbcbe65b2 100644 --- a/components/treasuryV2/WalletList/WalletListItem/typeGuards.ts +++ b/components/treasuryV2/WalletList/WalletListItem/typeGuards.ts @@ -9,6 +9,7 @@ import { Domains, Unknown, Stake, + Mango, } from '@models/treasury/Asset' export function isToken(asset: Asset): asset is Token { @@ -35,6 +36,10 @@ export function isStake(asset: Asset): asset is Stake { return asset.type === AssetType.Stake } +export function isMango(asset: Asset): asset is Mango { + return asset.type === AssetType.Mango +} + export function isSol(asset: Asset): asset is Sol { return asset.type === AssetType.Sol } diff --git a/constants/endpoints.ts b/constants/endpoints.ts index 1fb8c69421..4a65f8a4e0 100644 --- a/constants/endpoints.ts +++ b/constants/endpoints.ts @@ -1,7 +1,7 @@ export const MAINNET_RPC = process.env.NEXT_PUBLIC_MAINNET_RPC || process.env.MAINNET_RPC || - 'http://realms-realms-c335.mainnet.rpcpool.com/258d3727-bb96-409d-abea-0b1b4c48af29/' + 'http://realms-realms-c335.mainnet.rpcpool.com' export const DEVNET_RPC = process.env.NEXT_PUBLIC_DEVNET_RPC || diff --git a/constants/flags.ts b/constants/flags.ts index a928eb4260..ab4dbcfff6 100644 --- a/constants/flags.ts +++ b/constants/flags.ts @@ -9,9 +9,14 @@ export const DELEGATOR_BATCH_VOTE_SUPPORT_BY_PLUGIN: Record< boolean > = { vanilla: true, - VSR: false, + VSR: true, HeliumVSR: false, gateway: false, + QV: false, NFT: false, + pyth: false, unknown: false, + drift: false, + token_haver: false, + parcl: false } diff --git a/constants/plugins.ts b/constants/plugins.ts index daee165085..4bdf3966ee 100644 --- a/constants/plugins.ts +++ b/constants/plugins.ts @@ -1,6 +1,8 @@ import { PROGRAM_ID as HELIUM_VSR_PROGRAM_ID } from '@helium/voter-stake-registry-sdk' import { PublicKey } from '@solana/web3.js' import { DEFAULT_NFT_VOTER_PLUGIN } from '@tools/constants' +import { DRIFT_STAKE_VOTER_PLUGIN } from 'DriftStakeVoterPlugin/constants' +import { TOKEN_HAVER_PLUGIN } from 'TokenHaverPlugin/constants' export const VSR_PLUGIN_PKS: string[] = [ '4Q6WW2ouZ6V3iaNm56MTd5n2tnTm4C5fiH8miFHnAFHo', @@ -9,6 +11,7 @@ export const VSR_PLUGIN_PKS: string[] = [ 'VoteWPk9yyGmkX4U77nEWRJWpcc8kUfrPoghxENpstL', 'VoteMBhDCqGLRgYpp9o7DGyq81KNmwjXQRAHStjtJsS', '5sWzuuYkeWLBdAv3ULrBfqA51zF7Y4rnVzereboNDCPn', + 'HBZ5oXbFBFbr8Krt2oMU7ApHFeukdRS8Rye1f3T66vg5', ] export const HELIUM_VSR_PLUGINS_PKS: string[] = [ @@ -22,11 +25,37 @@ export const NFT_PLUGINS_PKS: string[] = [ ] export const GATEWAY_PLUGINS_PKS: string[] = [ - 'Ggatr3wgDLySEwA2qEjt1oiw4BUzp5yMLJyz21919dq6', - 'GgathUhdrCWRHowoRKACjgWhYHfxCEdBi5ViqYN6HVxk', // v2, supporting composition + 'GgathUhdrCWRHowoRKACjgWhYHfxCEdBi5ViqYN6HVxk', ] -export const findPluginName = (programId: PublicKey | undefined) => +export const QV_PLUGINS_PKS: string[] = [ + 'quadCSapU8nTdLg73KHDnmdxKnJQsh7GUbu5tZfnRRr', +] + +export const PYTH_PLUGIN_PK: string[] = [ + 'pytS9TjG1qyAZypk7n8rw8gfW9sUaqqYyMhJQ4E7JCQ', +] + +export const PARCL_PLUGIN_PK: string[] = [ + '2gWf5xLAzZaKX9tQj9vuXsaxTWtzTZDFRn21J3zjNVgu', +] + +export const DRIFT_PLUGIN_PK = [DRIFT_STAKE_VOTER_PLUGIN] + +export type PluginName = + | 'gateway' + | 'QV' + | 'vanilla' + | 'VSR' + | 'HeliumVSR' + | 'NFT' + | 'pyth' + | 'drift' + | 'token_haver' + | 'parcl' + | 'unknown' + +export const findPluginName = (programId: PublicKey | undefined): PluginName => programId === undefined ? ('vanilla' as const) : VSR_PLUGIN_PKS.includes(programId.toString()) @@ -37,4 +66,38 @@ export const findPluginName = (programId: PublicKey | undefined) => ? 'NFT' : GATEWAY_PLUGINS_PKS.includes(programId.toString()) ? 'gateway' + : QV_PLUGINS_PKS.includes(programId.toString()) + ? 'QV' + : PYTH_PLUGIN_PK.includes(programId.toString()) + ? 'pyth' + : DRIFT_PLUGIN_PK.includes(programId.toString()) + ? 'drift' + : TOKEN_HAVER_PLUGIN.toString() === programId.toString() + ? 'token_haver' + : PARCL_PLUGIN_PK.includes(programId.toString()) + ? 'parcl' : 'unknown' + +// Used when creating a new realm to choose which voterWeightAddin to use +export const pluginNameToCanonicalProgramId = ( + pluginName: PluginName +): PublicKey | undefined => { + const lastPk = (arr: string[]) => new PublicKey(arr[arr.length - 1]) + + switch (pluginName) { + case 'VSR': + return lastPk(VSR_PLUGIN_PKS) + case 'HeliumVSR': + return lastPk(HELIUM_VSR_PLUGINS_PKS) + case 'NFT': + return lastPk(NFT_PLUGINS_PKS) + case 'gateway': + return lastPk(GATEWAY_PLUGINS_PKS) + case 'QV': + return lastPk(QV_PLUGINS_PKS) + case 'pyth': + return lastPk(PYTH_PLUGIN_PK) + default: + return undefined + } +} diff --git a/hooks/PythNetwork/useScalingFactor.ts b/hooks/PythNetwork/useScalingFactor.ts new file mode 100644 index 0000000000..b14f0ecb82 --- /dev/null +++ b/hooks/PythNetwork/useScalingFactor.ts @@ -0,0 +1,35 @@ +import NodeWallet from "@coral-xyz/anchor/dist/cjs/nodewallet"; +import { determineVotingPowerType } from "@hooks/queries/governancePower"; +import useSelectedRealmPubkey from "@hooks/selectedRealm/useSelectedRealmPubkey"; +import { useConnection } from "@solana/wallet-adapter-react"; +import { useAsync } from "react-async-hook"; +import { useQuery } from "@tanstack/react-query"; +import { PythStakingClient } from "@pythnetwork/staking-sdk"; + +/** + * Returns undefined for everything except the Pyth DAO + */ +export default function usePythScalingFactor(): number | undefined { + const realm = useSelectedRealmPubkey() + const { connection } = useConnection() + const { result: plugin } = useAsync( + async () => + realm && determineVotingPowerType(connection, realm, 'community'), + [connection, realm] + ) + + const { data: scalingFactor } = useQuery(["pyth-scaling-factor"], + async (): Promise => { + const pythClient = new PythStakingClient({ + connection, + }) + return pythClient.getScalingFactor() + }, { enabled: plugin == "pyth" }) + + if (plugin == "pyth") { + return scalingFactor + } else { + return undefined + } +} + diff --git a/hooks/parcl/useScalingFactor.ts b/hooks/parcl/useScalingFactor.ts new file mode 100644 index 0000000000..58cd3d039f --- /dev/null +++ b/hooks/parcl/useScalingFactor.ts @@ -0,0 +1,38 @@ +import NodeWallet from '@coral-xyz/anchor/dist/cjs/nodewallet' +import { determineVotingPowerType } from '@hooks/queries/governancePower' +import useSelectedRealmPubkey from '@hooks/selectedRealm/useSelectedRealmPubkey' +import { useConnection } from '@solana/wallet-adapter-react' +import { useAsync } from 'react-async-hook' +import { useQuery } from '@tanstack/react-query' +import { StakeConnection } from '@parcl-oss/staking' + +/** + * Returns undefined for everything except the Parcl DAO + */ +export default function useParclScalingFactor(): number | undefined { + const realm = useSelectedRealmPubkey() + const { connection } = useConnection() + const { result: plugin } = useAsync( + async () => + realm && determineVotingPowerType(connection, realm, 'community'), + [connection, realm] + ) + + const { data: scalingFactor } = useQuery( + ['parcl-scaling-factor'], + async (): Promise => { + const parclClient = await StakeConnection.connect( + connection, + {} as NodeWallet + ) + return parclClient.getScalingFactor() + }, + { enabled: plugin == 'parcl' } + ) + + if (plugin == 'parcl') { + return scalingFactor + } else { + return undefined + } +} diff --git a/hooks/queries/bufferAuthority.ts b/hooks/queries/bufferAuthority.ts new file mode 100644 index 0000000000..180447caa7 --- /dev/null +++ b/hooks/queries/bufferAuthority.ts @@ -0,0 +1,42 @@ +import { BPF_UPGRADE_LOADER_ID } from "@solana/spl-governance"; +import { useConnection } from "@solana/wallet-adapter-react"; +import { PublicKey } from "@solana/web3.js"; +import { useQuery } from "@tanstack/react-query" +import { useRouteProposalQuery } from "./proposal"; +import { useSelectedProposalTransactions } from "./proposalTransaction"; + +export const useBufferAccountsAuthority = () => { + const {connection} = useConnection() + const proposal = useRouteProposalQuery().data?.result + const { data: transactions } = useSelectedProposalTransactions() + const enabled = transactions !== undefined && proposal !== undefined; + + const query = useQuery({ + queryKey: enabled ? + [connection.rpcEndpoint, 'BufferAccountsAuthority',proposal.pubkey] + : undefined, + + queryFn: async() => { + if (!enabled) throw new Error() + const ixs = transactions.flatMap((pix) => pix.account.getAllInstructions()) + const bufAuthorities: PublicKey[] = []; + + for (let i = 0; i 36) { + bufAuthorities.push(new PublicKey(bufferAccountData.data.subarray(5,37))) + } + } + } + + return bufAuthorities + }, + enabled + }) + + return query +} \ No newline at end of file diff --git a/hooks/queries/digitalAssets.ts b/hooks/queries/digitalAssets.ts index 147283d936..76409803eb 100644 --- a/hooks/queries/digitalAssets.ts +++ b/hooks/queries/digitalAssets.ts @@ -217,24 +217,35 @@ const dasByOwnerQueryFn = async (network: Network, owner: PublicKey) => { // https://docs.helius.xyz/solana-compression/digital-asset-standard-das-api/get-assets-by-owner - const response = await fetch(url, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - jsonrpc: '2.0', - id: 'Realms user', - method: 'getAssetsByOwner', - params: { - ownerAddress: owner.toString(), - page: 1, // Starts at 1 - limit: 1000, // TODO support having >1k nfts + const PAGE_LIMIT = 1000 + const items: DasNftObject[] = [] + let moreNftsRemaining = true + let page = 1 + + while (moreNftsRemaining) { + const response = await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', }, - }), - }) - const { result } = await response.json() - return result.items as DasNftObject[] + body: JSON.stringify({ + jsonrpc: '2.0', + id: 'Realms user', + method: 'getAssetsByOwner', + params: { + ownerAddress: owner.toString(), + page, // Starts at 1 + limit: PAGE_LIMIT, + }, + }), + }) + const { result } = await response.json() + const pageItems = result.items as DasNftObject[] + items.push(...pageItems) + page++ + if (pageItems.length < PAGE_LIMIT) moreNftsRemaining = false + } + return items } export const fetchDigitalAssetsByOwner = (network: Network, owner: PublicKey) => diff --git a/hooks/queries/governancePower.ts b/hooks/queries/governancePower.ts deleted file mode 100644 index f6c6a340a3..0000000000 --- a/hooks/queries/governancePower.ts +++ /dev/null @@ -1,270 +0,0 @@ -import { Connection, PublicKey } from '@solana/web3.js' -import { - fetchTokenOwnerRecordByPubkey, - useTokenOwnerRecordByPubkeyQuery, - useUserCommunityTokenOwnerRecord, - useUserCouncilTokenOwnerRecord, -} from './tokenOwnerRecord' -import BN from 'bn.js' -import { fetchNftRegistrar } from './plugins/nftVoter' -import { fetchDigitalAssetsByOwner } from './digitalAssets' -import { getNetworkFromEndpoint } from '@utils/connection' -import { ON_NFT_VOTER_V2 } from '@constants/flags' -import { fetchRealmByPubkey, useRealmQuery } from './realm' -import { fetchRealmConfigQuery } from './realmConfig' -import { - GATEWAY_PLUGINS_PKS, - HELIUM_VSR_PLUGINS_PKS, - NFT_PLUGINS_PKS, - VSR_PLUGIN_PKS, -} from '@constants/plugins' -import useHeliumVsrStore from 'HeliumVotePlugin/hooks/useHeliumVsrStore' -import useGatewayPluginStore from 'GatewayPlugin/store/gatewayPluginStore' -import { useAsync } from 'react-async-hook' -import { useConnection } from '@solana/wallet-adapter-react' -import useSelectedRealmPubkey from '@hooks/selectedRealm/useSelectedRealmPubkey' -import { - useAddressQuery_CommunityTokenOwner, - useAddressQuery_CouncilTokenOwner, -} from './addresses/tokenOwnerRecord' -import { - SimpleGatedVoterWeight, - VoteNftWeight, - VoteRegistryVoterWeight, - VoterWeight, -} from '@models/voteWeights' -import useUserOrDelegator from '@hooks/useUserOrDelegator' -import { getVsrGovpower } from './plugins/vsr' - -export const getVanillaGovpower = async ( - connection: Connection, - tokenOwnerRecord: PublicKey -) => { - const torAccount = await fetchTokenOwnerRecordByPubkey( - connection, - tokenOwnerRecord - ) - return torAccount.result - ? torAccount.result.account.governingTokenDepositAmount - : new BN(0) -} - -export const useVanillaGovpower = (tokenOwnerRecordPk: PublicKey) => { - const { data: torAccount } = useTokenOwnerRecordByPubkeyQuery( - tokenOwnerRecordPk - ) - return torAccount?.result - ? torAccount.result.account.governingTokenDepositAmount - : new BN(0) -} - -export const getNftGovpower = async ( - connection: Connection, - realmPk: PublicKey, - tokenOwnerRecordPk: PublicKey -) => { - // figure out what collections are used - const { result: registrar } = await fetchNftRegistrar(connection, realmPk) - if (registrar === undefined) throw new Error() - const { collectionConfigs } = registrar - - // grab the owner of the TOR - const { result: TOR } = await fetchTokenOwnerRecordByPubkey( - connection, - tokenOwnerRecordPk - ) - if (TOR === undefined) throw new Error() - const owner = TOR.account.governingTokenOwner - - // grab the user nfts - const network = getNetworkFromEndpoint(connection.rpcEndpoint) - if (network === 'localnet') throw new Error() - const nfts = (await fetchDigitalAssetsByOwner(network, owner)) - // filter cnfts if not supported yet - .filter((nft) => ON_NFT_VOTER_V2 || !nft.compression.compressed) - - // map nfts to power and sum them - const power = nfts - .map( - (nft) => - // find collectionConfig such that the nft's `collection` grouping matches the collection id - collectionConfigs.find( - (x) => - x.collection.toString() === - (nft.grouping.find((y) => y.group_key === 'collection') - ?.group_value ?? false) - // take the weight for that collection, or 0 if the nft matches none of the dao's collections - )?.weight ?? new BN(0) - ) - // sum - .reduce((partialSum, a) => partialSum.add(a), new BN(0)) - - return power -} - -export const findPluginName = (programId: PublicKey | undefined) => - programId === undefined - ? ('vanilla' as const) - : VSR_PLUGIN_PKS.includes(programId.toString()) - ? ('VSR' as const) - : HELIUM_VSR_PLUGINS_PKS.includes(programId.toString()) - ? 'HeliumVSR' - : NFT_PLUGINS_PKS.includes(programId.toString()) - ? 'NFT' - : GATEWAY_PLUGINS_PKS.includes(programId.toString()) - ? 'gateway' - : 'unknown' - -export const determineVotingPowerType = async ( - connection: Connection, - realmPk: PublicKey, - kind: 'council' | 'community' -) => { - const realm = (await fetchRealmByPubkey(connection, realmPk)).result - if (!realm) throw new Error() - - const config = await fetchRealmConfigQuery(connection, realmPk) - const programId = - kind === 'community' - ? config.result?.account.communityTokenConfig.voterWeightAddin - : config.result?.account.councilTokenConfig.voterWeightAddin - - return findPluginName(programId) -} - -export const useGovernancePowerAsync = ( - kind: 'community' | 'council' | undefined -) => { - const { connection } = useConnection() - const realmPk = useSelectedRealmPubkey() - - const actingAsWalletPk = useUserOrDelegator() - - const heliumVotingPower = useHeliumVsrStore((s) => s.state.votingPower) - const gatewayVotingPower = useGatewayPluginStore((s) => s.state.votingPower) - - const communityTOR = useAddressQuery_CommunityTokenOwner() - const councilTOR = useAddressQuery_CouncilTokenOwner() - const { data: TOR } = kind && kind === 'community' ? communityTOR : councilTOR - - const { result: plugin } = useAsync( - async () => - kind && realmPk && determineVotingPowerType(connection, realmPk, kind), - [connection, realmPk, kind] - ) - - return useAsync( - async () => - plugin === undefined - ? undefined - : realmPk && - TOR && - (plugin === 'vanilla' - ? getVanillaGovpower(connection, TOR) - : plugin === 'NFT' - ? getNftGovpower(connection, realmPk, TOR) - : plugin === 'VSR' - ? actingAsWalletPk - ? (await getVsrGovpower(connection, realmPk, actingAsWalletPk)) - .result ?? new BN(0) - : undefined - : plugin === 'HeliumVSR' - ? heliumVotingPower - : plugin === 'gateway' - ? gatewayVotingPower - : new BN(0)), - [ - plugin, - realmPk, - TOR, - connection, - actingAsWalletPk, - heliumVotingPower, - gatewayVotingPower, - ] - ) -} - -/** - * @deprecated - * use useGovernancePowerAsync - */ -export const useLegacyVoterWeight = () => { - const { connection } = useConnection() - const realmPk = useSelectedRealmPubkey() - const realm = useRealmQuery().data?.result - - const heliumVotingPower = useHeliumVsrStore((s) => s.state.votingPower) - const gatewayVotingPower = useGatewayPluginStore((s) => s.state.votingPower) - - const { data: communityTOR } = useUserCommunityTokenOwnerRecord() - const { data: councilTOR } = useUserCouncilTokenOwnerRecord() - - const { result: plugin } = useAsync( - async () => - realmPk && determineVotingPowerType(connection, realmPk, 'community'), - [connection, realmPk] - ) - const actingAsWalletPk = useUserOrDelegator() - - const shouldCareAboutCouncil = - realm && realm.account.config.councilMint !== undefined - - return useAsync( - async () => - realmPk && - communityTOR && - (shouldCareAboutCouncil === undefined - ? undefined - : shouldCareAboutCouncil === true && councilTOR === undefined - ? undefined - : plugin === 'vanilla' - ? new VoterWeight(communityTOR.result, councilTOR?.result) - : plugin === 'NFT' - ? communityTOR.result?.pubkey - ? new VoteNftWeight( - communityTOR.result, - councilTOR?.result, - await getNftGovpower( - connection, - realmPk, - communityTOR.result.pubkey - ) - ) - : undefined - : plugin === 'VSR' - ? actingAsWalletPk - ? new VoteRegistryVoterWeight( - communityTOR.result, - councilTOR?.result, - (await getVsrGovpower(connection, realmPk, actingAsWalletPk)) - .result ?? new BN(0) - ) - : undefined - : plugin === 'HeliumVSR' - ? new VoteRegistryVoterWeight( - communityTOR.result, - councilTOR?.result, - heliumVotingPower - ) - : plugin === 'gateway' - ? new SimpleGatedVoterWeight( - communityTOR.result, - councilTOR?.result, - gatewayVotingPower - ) - : undefined), - - [ - actingAsWalletPk, - communityTOR, - connection, - councilTOR, - gatewayVotingPower, - heliumVotingPower, - plugin, - realmPk, - shouldCareAboutCouncil, - ] - ) -} diff --git a/hooks/queries/governancePower.tsx b/hooks/queries/governancePower.tsx new file mode 100644 index 0000000000..8e3fdebf18 --- /dev/null +++ b/hooks/queries/governancePower.tsx @@ -0,0 +1,225 @@ +import { Connection, Keypair, PublicKey } from '@solana/web3.js' +import { + fetchTokenOwnerRecordByPubkey, + useTokenOwnerRecordByPubkeyQuery, + useUserCommunityTokenOwnerRecord, + useUserCouncilTokenOwnerRecord, +} from './tokenOwnerRecord' +import BN from 'bn.js' +import { fetchDigitalAssetsByOwner } from './digitalAssets' +import { getNetworkFromEndpoint } from '@utils/connection' +import { ON_NFT_VOTER_V2 } from '@constants/flags' +import { fetchRealmByPubkey } from './realm' +import { fetchRealmConfigQuery } from './realmConfig' + +import { StakeConnection } from "@parcl-oss/staking" +import { + LegacyVoterWeightAdapter, +} from '@models/voteWeights' +import { PythStakingClient } from "@pythnetwork/staking-sdk"; +import NodeWallet from '@coral-xyz/anchor/dist/cjs/nodewallet' +import { findPluginName } from '@constants/plugins' +import { useRealmVoterWeightPlugins } from '@hooks/useRealmVoterWeightPlugins' +import {IdlAccounts} from "@coral-xyz/anchor"; +import {NftVoter} from "../../idls/nft_voter"; +import {getPluginRegistrarClientCached} from "@hooks/queries/pluginRegistrar"; + +export const getVanillaGovpower = async ( + connection: Connection, + tokenOwnerRecord: PublicKey +) => { + const torAccount = await fetchTokenOwnerRecordByPubkey( + connection, + tokenOwnerRecord + ) + return torAccount.result + ? torAccount.result.account.governingTokenDepositAmount + : new BN(0) +} + +export const useVanillaGovpower = (tokenOwnerRecordPk: PublicKey | undefined) => { + const { data: torAccount } = useTokenOwnerRecordByPubkeyQuery( + tokenOwnerRecordPk + ) + return torAccount?.result + ? torAccount.result.account.governingTokenDepositAmount + : new BN(0) +} + +export const getNftGovpowerForOwnerAndRegistrar = async(connection: Connection, owner: PublicKey, registrar: IdlAccounts['registrar'] | undefined) => { + if (registrar === undefined) throw new Error() + const {collectionConfigs} = registrar + + // grab the user nfts + const network = getNetworkFromEndpoint(connection.rpcEndpoint) + if (network === 'localnet') throw new Error() + const nfts = (await fetchDigitalAssetsByOwner(network, owner)) + // filter cnfts if not supported yet + .filter((nft) => ON_NFT_VOTER_V2 || !nft.compression.compressed) + + // map nfts to power and sum them + const power = nfts + .map( + (nft) => + // find collectionConfig such that the nft's `collection` grouping matches the collection id + collectionConfigs.find( + (x) => + x.collection.toString() === + (nft.grouping.find((y) => y.group_key === 'collection') + ?.group_value ?? false) + // take the weight for that collection, or 0 if the nft matches none of the dao's collections + )?.weight ?? new BN(0) + ) + // sum + .reduce((partialSum, a) => partialSum.add(a), new BN(0)) + + return power +} + +export const getNftGovpowerForOwner = async (connection: Connection, realmPk: PublicKey, owner: PublicKey) => { + const registrar = await getPluginRegistrarClientCached(realmPk, connection, owner, 'NFT'); + return getNftGovpowerForOwnerAndRegistrar(connection, owner, registrar); +} + +export const getNftGovpower = async ( + connection: Connection, + realmPk: PublicKey, + tokenOwnerRecordPk: PublicKey +) => { + // grab the owner of the TOR + const { result: TOR } = await fetchTokenOwnerRecordByPubkey( + connection, + tokenOwnerRecordPk + ) + if (TOR === undefined) throw new Error() + const owner = TOR.account.governingTokenOwner + return getNftGovpowerForOwner(connection, realmPk, owner); +} + +export const getPythGovPower = async ( + connection: Connection, + user: PublicKey | undefined +): Promise => { + if (!user) return new BN(0) + + const pythClient = new PythStakingClient({ + connection, + }) + const voterWeight = await pythClient.getVoterWeight(user) + + return new BN(voterWeight.toString()) +} + +export const getParclGovPower = async ( + connection: Connection, + user: PublicKey | undefined +): Promise => { + if (!user) return new BN(0) + const client = await StakeConnection.connect( + connection, + new NodeWallet(new Keypair()) + ) + const stakeAccount = await client.getMainAccount(user) + if (stakeAccount) { + return stakeAccount.getVoterWeight(await client.getTime()).toBN() + } else { + return new BN(0) + } +} + + +export const determineVotingPowerType = async ( + connection: Connection, + realmPk: PublicKey, + role: 'council' | 'community' +) => { + const realm = (await fetchRealmByPubkey(connection, realmPk)).result + if (!realm) throw new Error() + + const config = await fetchRealmConfigQuery(connection, realmPk) + const programId = + role === 'community' + ? config.result?.account.communityTokenConfig.voterWeightAddin + : config.result?.account.councilTokenConfig.voterWeightAddin + + return findPluginName(programId) +} + +// TODO use HOC to provide voting power to components that need voting power without knowing plugin +// this is an efficiency thing to save on queries, since we can't have conditional useQuery hooks. +// i guess you can just use the enabled flag.. +/* +export const WithCommunityGovernancePower = < + P extends { communityGovernancePower: BN | undefined } +>( + Component: React.ComponentType

+): React.FC> => + function Enhanced(props) { + const { connection } = useConnection() + const kind = 'community' + const realmPk = useSelectedRealmPubkey() + + const { result: plugin } = useAsync( + async () => + kind && realmPk && determineVotingPowerType(connection, realmPk, kind), + [connection, realmPk, kind] + ) + // this `props as P` thing is annoying!! ts should know better -@asktree + return + } + +export const WithVsrGovernancePower = < + P extends { communityGovernancePower: BN | undefined } +>( + Component: React.ComponentType

+): React.FC> => + function Enhanced(props) { + const communityGovernancePower = useVsrGovpower().data?.result + + // this `props as P` thing is annoying!! ts should know better -@asktree + return ( + + ) + } */ + +// TODO QV-2 Check if there is no plugin and get the vanilla value (or should the usePlugins hook have the vanilla value?) +export const useGovernancePower = ( + kind: 'community' | 'council' | undefined +) => { + const { calculatedMaxVoterWeight } = useRealmVoterWeightPlugins(kind) + return calculatedMaxVoterWeight?.value +} +/** deprecated: this should not be used any more. Use useRealmVoterWeightPlugins hook */ +export const useGovernancePowerAsync = ( + kind: 'community' | 'council' | undefined +) => { + const { totalCalculatedVoterWeight } = useRealmVoterWeightPlugins(kind) + return totalCalculatedVoterWeight?.value; +} + +/** + * @deprecated + * use useRealmVoterWeightPlugins instead + */ +export const useLegacyVoterWeight = () => { + const communityPluginDetails = useRealmVoterWeightPlugins('community'); + const councilPluginDetails = useRealmVoterWeightPlugins('council'); + const ownVoterWeights = { + community: communityPluginDetails.totalCalculatedVoterWeight?.value ?? undefined, + council: councilPluginDetails.totalCalculatedVoterWeight?.value ?? undefined, + } + const { data: communityTOR } = useUserCommunityTokenOwnerRecord() + const { data: councilTOR } = useUserCouncilTokenOwnerRecord() + + const voterWeight = new LegacyVoterWeightAdapter(ownVoterWeights, { + community: communityTOR?.result, + council: councilTOR?.result, + }); + const ready = communityPluginDetails.isReady && councilPluginDetails.isReady; + + // only expose the vote weight once everything is loaded + return { result: ready ? voterWeight : undefined, ready }; +} diff --git a/hooks/queries/jupiterPrice.ts b/hooks/queries/jupiterPrice.ts index daf578e942..0c153837cc 100644 --- a/hooks/queries/jupiterPrice.ts +++ b/hooks/queries/jupiterPrice.ts @@ -92,6 +92,17 @@ export const useJupiterPricesByMintsQuery = (mints: PublicKey[]) => { (acc, next) => ({ ...acc, ...next.data }), {} as Response['data'] ) + + //override chai price if its broken + const chaiMint = '3jsFX1tx2Z8ewmamiwSU851GzyzM2DJMq7KWW5DM8Py3' + const chaiData = data[chaiMint] + + if (chaiData?.price && (chaiData.price > 1.3 || chaiData.price < 0.9)) { + data[chaiMint] = { + ...chaiData, + price: 1, + } + } return data }, onSuccess: (data) => { diff --git a/hooks/queries/mango.ts b/hooks/queries/mango.ts index 4c0771686f..e52267b86b 100644 --- a/hooks/queries/mango.ts +++ b/hooks/queries/mango.ts @@ -20,7 +20,10 @@ export const useMangoClient = (connection: Connection) => { const client = await MangoClient.connect( adminProvider, cluster, - MANGO_V4_ID[cluster] + MANGO_V4_ID[cluster], + { + idsSource: 'api', + } ) return client diff --git a/hooks/queries/pluginRegistrar.ts b/hooks/queries/pluginRegistrar.ts new file mode 100644 index 0000000000..0f1cf1e9dc --- /dev/null +++ b/hooks/queries/pluginRegistrar.ts @@ -0,0 +1,34 @@ +import queryClient from "@hooks/queries/queryClient"; +import {getPlugins} from "../../VoterWeightPlugins/lib/getPlugins"; +import {AnchorProvider, Idl, IdlAccounts} from "@coral-xyz/anchor"; +import EmptyWallet from "@utils/Mango/listingTools"; +import {Connection, Keypair, PublicKey} from "@solana/web3.js"; +import {fetchRealmByPubkey} from "@hooks/queries/realm"; +import {PluginName} from "@constants/plugins"; +import {PluginType, VoterWeightPluginInfo} from "../../VoterWeightPlugins/lib/types"; + +export const getPluginClientCachedWithEmptySigner = async (realmPk: PublicKey, connection: Connection, owner: PublicKey, pluginName: PluginName, type: PluginType): Promise => { + const realm = fetchRealmByPubkey(connection, realmPk) + const plugins = await queryClient.fetchQuery({ + queryKey: ['getCommunityPluginsWithoutWallet', realmPk.toString()], + queryFn: async () => { + const {result} = await realm; + if (!result) return []; + return getPlugins({ + realmPublicKey: realmPk, + governanceMintPublicKey: result.account.communityMint, + provider: new AnchorProvider(connection, new EmptyWallet(Keypair.generate()), AnchorProvider.defaultOptions()), + type, + wallets: [owner], + signer: new EmptyWallet(Keypair.generate()), + }) + } + }); + + return plugins.find((x) => x.name === pluginName); +} + +export const getPluginRegistrarClientCached = async (realmPk: PublicKey, connection: Connection, owner: PublicKey, pluginName: PluginName, type: PluginType = 'voterWeight'): Promise['registrar'] | undefined> => { + const plugin = await getPluginClientCachedWithEmptySigner(realmPk, connection, owner, pluginName, type); + return plugin?.params as IdlAccounts['registrar'] | undefined; +} \ No newline at end of file diff --git a/hooks/queries/plugins/nftVoter.ts b/hooks/queries/plugins/nftVoter.ts index 72a837f2b8..7f200a1f42 100644 --- a/hooks/queries/plugins/nftVoter.ts +++ b/hooks/queries/plugins/nftVoter.ts @@ -1,42 +1,141 @@ -import { Connection, PublicKey } from '@solana/web3.js' -import { fetchRealmByPubkey } from '../realm' +import {Connection, PublicKey} from '@solana/web3.js' +import {fetchRealmByPubkey} from '../realm' import { getRegistrarPDA } from '@utils/plugin/accounts' -import { Program } from '@coral-xyz/anchor' +import { Program} from '@coral-xyz/anchor' import { IDL, NftVoter } from 'idls/nft_voter' import asFindable from '@utils/queries/asFindable' -import queryClient from '../queryClient' import { fetchRealmConfigQuery } from '../realmConfig' +import { useBatchedVoteDelegators } from '@components/VotePanel/useDelegators' +import { useConnection } from '@solana/wallet-adapter-react' +import useSelectedRealmPubkey from '@hooks/selectedRealm/useSelectedRealmPubkey' +import { getNftGovpower } from '../governancePower' +import { useQueries } from '@tanstack/react-query' +import { + fetchDigitalAssetsByOwner, + useDigitalAssetsByOwner, +} from '../digitalAssets' +import { useMemo } from 'react' +import { ON_NFT_VOTER_V2 } from '@constants/flags' +import { NFT_PLUGINS_PKS } from '@constants/plugins' +import { getNetworkFromEndpoint } from '@utils/connection' +import {useNftRegistrar} from "@hooks/useNftRegistrar"; +import {getPluginRegistrarClientCached} from "@hooks/queries/pluginRegistrar"; -const nftMintRegistrarQueryFn = async ( +export const useVotingNfts = (ownerPk: PublicKey | undefined) => { + const registrar = useNftRegistrar(); + const { data: nfts } = useDigitalAssetsByOwner(ownerPk) + console.log('nfts', nfts) + + const usedCollectionsPks = useMemo( + () => registrar?.collectionConfigs.map((x) => x.collection.toBase58()), + [registrar?.collectionConfigs] + ) + + const votingNfts = nfts?.filter((nft) => { + const collection = nft.grouping.find((x) => x.group_key === 'collection') + return ( + (ON_NFT_VOTER_V2 || !nft.compression.compressed) && + collection && + usedCollectionsPks?.includes(collection.group_value) && + nft.creators?.filter((x) => x.verified).length > 0 + ) + }) + + return votingNfts +} + +export const getVotingNfts = async ( connection: Connection, - realmPk: PublicKey + realmPk: PublicKey, + ownerPk: PublicKey ) => { - const realm = (await fetchRealmByPubkey(connection, realmPk)).result - if (!realm) throw new Error() - + const realm = fetchRealmByPubkey(connection, realmPk) + if (realm === undefined) throw new Error() const config = await fetchRealmConfigQuery(connection, realmPk) - const programId = config.result?.account.communityTokenConfig.voterWeightAddin - if (programId === undefined) - return { found: false, result: undefined } as const - - const { registrar: registrarPk } = await getRegistrarPDA( - realm.pubkey, - realm.account.communityMint, - programId + if (config.result === undefined) throw new Error() + const registrar = await getPluginRegistrarClientCached(realmPk, connection, ownerPk, 'NFT'); + if (registrar === undefined) throw new Error() + const usedCollectionsPks = registrar.collectionConfigs.map((x) => + x.collection.toBase58() ) + const network = getNetworkFromEndpoint(connection.rpcEndpoint) as any + const nfts = await fetchDigitalAssetsByOwner(network, ownerPk) - // use anchor to fetch registrar :-) - const program = new Program(IDL, programId, { connection }) - - return asFindable(() => program.account.registrar.fetch(registrarPk))() + const votingNfts = nfts?.filter((nft) => { + const collection = nft.grouping.find((x) => x.group_key === 'collection') + return ( + (ON_NFT_VOTER_V2 || !nft.compression.compressed) && + collection && + usedCollectionsPks?.includes(collection.group_value) && + nft.creators?.filter((x) => x.verified).length > 0 + ) + }) + return votingNfts } -export const fetchNftRegistrar = (connection: Connection, realmPk: PublicKey) => - queryClient.fetchQuery({ - queryKey: [ - connection.rpcEndpoint, - 'Nft Plugin Registrar', - realmPk.toString(), - ], - queryFn: () => nftMintRegistrarQueryFn(connection, realmPk), +export const useNftBatchedVotePower = async ( + role: 'community' | 'council' | undefined +) => { + const { connection } = useConnection() + const realmPk = useSelectedRealmPubkey() + const batchedDelegators = useBatchedVoteDelegators(role) + + const results = useQueries({ + queries: + realmPk === undefined || batchedDelegators === undefined + ? [] + : batchedDelegators.map((delegator) => ({ + queryKey: [ + connection.rpcEndpoint, + realmPk.toString(), + 'NFT vote power (dont cache)', + delegator.pubkey.toString(), + ], + staleTime: 0, + cacheTime: 0, + queryFn: () => + getNftGovpower(connection, realmPk, delegator.pubkey), + })), }) + const total = results.map((r) => r.data?.toNumber() ?? 0) + return total +} + +// @deprecated - Use useNftClient().plugin.registrar instead to support chaining. +// Outside of react, use getPluginRegistrarClientCached instead. +export const nftRegistrarQuery = ( + connection: Connection, + realmPk: PublicKey | undefined +) => ({ + queryKey: realmPk && [ + connection.rpcEndpoint, + 'Nft Plugin Registrar', + realmPk.toString(), + ], + enabled: realmPk !== undefined, + queryFn: async () => { + if (realmPk === undefined) throw new Error() + const realm = (await fetchRealmByPubkey(connection, realmPk)).result + if (!realm) throw new Error() + + const config = await fetchRealmConfigQuery(connection, realmPk) + const programId = + config.result?.account.communityTokenConfig.voterWeightAddin + if ( + programId === undefined || + !NFT_PLUGINS_PKS.includes(programId.toString()) + ) + return { found: false, result: undefined } as const + + const { registrar: registrarPk } = getRegistrarPDA( + realm.pubkey, + realm.account.communityMint, + programId + ) + + // use anchor to fetch registrar :-) + const program = new Program(IDL, programId, { connection }) + + return asFindable(() => program.account.registrar.fetch(registrarPk))() + }, +}) diff --git a/hooks/queries/plugins/vsr.ts b/hooks/queries/plugins/vsr.ts index 975efb9f83..14bacac087 100644 --- a/hooks/queries/plugins/vsr.ts +++ b/hooks/queries/plugins/vsr.ts @@ -7,33 +7,39 @@ import { VoterStakeRegistry, IDL, } from 'VoteStakeRegistry/sdk/voter_stake_registry' -import { fetchRealmConfigQuery } from '../realmConfig' +import { fetchRealmConfigQuery, useRealmConfigQuery } from '../realmConfig' import queryClient from '../queryClient' import { useConnection } from '@solana/wallet-adapter-react' -import useSelectedRealmPubkey from '@hooks/selectedRealm/useSelectedRealmPubkey' import useUserOrDelegator from '@hooks/useUserOrDelegator' -import { useAsync } from 'react-async-hook' import { getLockTokensVotingPowerPerWallet } from 'VoteStakeRegistry/tools/deposits' +import { useQuery } from '@tanstack/react-query' +import { findPluginName } from '@constants/plugins' +import {useVsrClient} from "../../../VoterWeightPlugins/useVsrClient"; +import {getPluginClientCachedWithEmptySigner} from "@hooks/queries/pluginRegistrar"; const VOTER_INFO_EVENT_NAME = 'VoterInfo' export const vsrQueryKeys = { all: (connection: Connection) => [connection.rpcEndpoint, 'VSR'], - allVotingPower: (connection: Connection) => [ + allVotingPower: (connection: Connection, pluginId: PublicKey) => [ ...vsrQueryKeys.all(connection), + pluginId.toString(), 'voting power', ], votingPower: ( connection: Connection, + pluginId: PublicKey, registrarPk: PublicKey, voterPk: PublicKey ) => [ - ...vsrQueryKeys.allVotingPower(connection), + ...vsrQueryKeys.allVotingPower(connection, pluginId), registrarPk.toString(), voterPk.toString(), ], } +// @deprecated - use the vsr voting client instead (useVsrClient), or preferably just obtain the weight generically for all plugins using +// useRealmVoterWeightPlugins().totalCalculatedVoterWeight export const getVsrGovpower = async ( connection: Connection, realmPk: PublicKey, @@ -41,30 +47,24 @@ export const getVsrGovpower = async ( ) => { const { result: realm } = await fetchRealmByPubkey(connection, realmPk) if (realm === undefined) throw new Error() - const communityMintPk = realm.account.communityMint - const config = await fetchRealmConfigQuery(connection, realmPk) - const programId = config.result?.account.communityTokenConfig.voterWeightAddin - if (programId === undefined) - return { found: false, result: undefined } as const - - const program = new Program(IDL, programId, { - connection, - }) - - const { registrar: registrarPk } = await getRegistrarPDA( + const plugin = await getPluginClientCachedWithEmptySigner(realmPk, connection, walletPk, 'VSR', 'voterWeight'); + const votingPower =await plugin?.client?.calculateVoterWeight( + walletPk, realmPk, - communityMintPk, - programId - ) - const { voter: voterPk } = await getVoterPDA(registrarPk, walletPk, programId) + realm.account.communityMint, + new BN(0) // technically incorrect. Should obtain the voter weight from the input TOR + ); - const logs = await fetchVotingPowerSimulation( - connection, - program, - registrarPk, - voterPk - ) + if (!votingPower) { + return {found: false, result: undefined} as const + } + + return { found: true, result: votingPower } +} +const extractVotingPowerFromSimulation = ( + logs: Awaited> +) => { const votingPowerEntry = logs.find((x) => x.name === VOTER_INFO_EVENT_NAME) const votingPower = votingPowerEntry ? ({ @@ -72,15 +72,45 @@ export const getVsrGovpower = async ( result: votingPowerEntry.data.votingPower as BN, } as const) : ({ found: false, result: undefined } as const) + return votingPower +} - // you may be asking yourself, "what?". that is healthy - queryClient.setQueryData( - vsrQueryKeys.votingPower(connection, registrarPk, voterPk), - votingPower +const votingPowerQueryFn = async ( + connection: Connection, + pluginId: PublicKey, + registrarPk: PublicKey, + voterPk: PublicKey +) => { + const program = new Program(IDL, pluginId, { + connection, + }) + const logs = await fetchVotingPowerSimulation( + connection, + program, + registrarPk, + voterPk ) + const votingPower = extractVotingPowerFromSimulation(logs) return votingPower } +export const fetchVotingPower = ( + connection: Connection, + pluginId: PublicKey, + registrarPk: PublicKey, + voterPk: PublicKey +) => + queryClient.fetchQuery({ + queryKey: vsrQueryKeys.votingPower( + connection, + pluginId, + registrarPk, + voterPk + ), + queryFn: () => + votingPowerQueryFn(connection, pluginId, registrarPk, voterPk), + }) + export const fetchVotingPowerSimulation = ( connection: Connection, program: Program, @@ -92,9 +122,13 @@ export const fetchVotingPowerSimulation = ( queryClient.fetchQuery({ queryKey: [ connection.rpcEndpoint, - 'VSR: get vote power log', + 'VSR', + 'voting power', + 'simulation', registrarPk.toString(), voterPk.toString(), + depositEntryBegin, + depositEntryCount, ], queryFn: () => voterPowerLogQueryFn( @@ -107,18 +141,54 @@ export const fetchVotingPowerSimulation = ( ), }) -// TODO break apart getVsrGovpower into one function that gets the accounts you need, -// and then a query function that actually gets the account and is used by useQuery. -// so that you can invalidate. -// this will trigger an extra rerender i think, which is kind of annoying, but i do not really see an alternative. +export const useRegistrarPk = () => { + const realm = useRealmQuery().data?.result + const communityMintPk = realm?.account.communityMint + const config = useRealmConfigQuery().data?.result + const programId = config?.account.communityTokenConfig.voterWeightAddin + return realm && + communityMintPk && + programId && + getRegistrarPDA(realm.pubkey, communityMintPk, programId) +} + +export const useVoterPk = (walletPk: PublicKey | undefined) => { + const registrar = useRegistrarPk() + const config = useRealmConfigQuery().data?.result + const programId = config?.account.communityTokenConfig.voterWeightAddin + + return registrar && + walletPk && + programId && + getVoterPDA(registrar.registrar, walletPk, programId); +} + export const useVsrGovpower = () => { const { connection } = useConnection() - const realmPk = useSelectedRealmPubkey() const actingAsWallet = useUserOrDelegator() - return useAsync(async () => { - if (realmPk === undefined || actingAsWallet === undefined) return undefined - return getVsrGovpower(connection, realmPk, actingAsWallet) - }, [connection, realmPk, actingAsWallet]) + const config = useRealmConfigQuery().data?.result + const pluginId = config?.account.communityTokenConfig.voterWeightAddin + const voterPk = useVoterPk(actingAsWallet)?.voter + const registrarPk = useRegistrarPk()?.registrar + const pluginName = findPluginName(pluginId) + + const enabled = + pluginName === 'VSR' && + !( + pluginId === undefined || + registrarPk === undefined || + voterPk === undefined + ) + return useQuery({ + enabled, + queryKey: enabled + ? vsrQueryKeys.votingPower(connection, pluginId, registrarPk, voterPk) + : undefined, + queryFn: () => { + if (!enabled) throw new Error() + return votingPowerQueryFn(connection, pluginId, registrarPk, voterPk) + }, + }) } /** @@ -148,49 +218,59 @@ const voterPowerLogQueryFn = async ( return [...parser.parseLogs(sim.value.logs)] } -export const useVsrGovpowerMulti = (wallets: PublicKey[]) => { +// TODO, use batshit to batch this, i guess. +export const useVsrGovpowerMulti = (wallets: PublicKey[] | undefined) => { const { connection } = useConnection() const realm = useRealmQuery().data?.result + const { vsrClient } = useVsrClient(); - return useAsync(async () => { - if (realm === undefined) return undefined - const config = await fetchRealmConfigQuery(connection, realm.pubkey) - const programId = - config.result?.account.communityTokenConfig.voterWeightAddin - if (programId === undefined) return undefined - - const client = { - program: new Program(IDL, programId, { - connection, - }), - } - - const x = await getLockTokensVotingPowerPerWallet( - wallets, - realm, - client, - connection - ) - - const { registrar: registrarPk } = await getRegistrarPDA( - realm.pubkey, - realm.account.communityMint, - programId - ) + return useQuery({ + enabled: wallets !== undefined && wallets.length > 0, + queryKey: [ + vsrQueryKeys.all(connection), + 'VSR', + 'voting power', + 'simulation', + 'multi', + wallets?.map((x) => x.toString()).toString(), + ], + queryFn: async () => { + console.log('vsr multi govpower CALLED') + if (realm === undefined) return undefined + if (wallets === undefined) return undefined + if (wallets.length === 0) return {} + const config = await fetchRealmConfigQuery(connection, realm.pubkey) + const programId = + config.result?.account.communityTokenConfig.voterWeightAddin + if (!vsrClient || programId === undefined) return undefined - for (const [key, power] of Object.entries(x)) { - const { voter: voterPk } = await getVoterPDA( - registrarPk, - new PublicKey(key), - programId + const x = await getLockTokensVotingPowerPerWallet( + wallets, + realm, + vsrClient, + connection ) - queryClient.setQueryData( - vsrQueryKeys.votingPower(connection, registrarPk, voterPk), - power + const { registrar: registrarPk } = getRegistrarPDA( + realm.pubkey, + realm.account.communityMint, + programId ) - } - return x - }, [connection, realm, wallets]) + for (const [key, power] of Object.entries(x)) { + const { voter: voterPk } = getVoterPDA( + registrarPk, + new PublicKey(key), + programId + ) + + queryClient.setQueryData( + vsrQueryKeys.votingPower(connection, programId, registrarPk, voterPk), + { found: true, result: power } + ) + } + + return x + }, + }) } diff --git a/hooks/queries/realm.ts b/hooks/queries/realm.ts index b750a1c51b..c55762c6e2 100644 --- a/hooks/queries/realm.ts +++ b/hooks/queries/realm.ts @@ -6,6 +6,7 @@ import { getNetworkFromEndpoint } from '@utils/connection' import asFindable from '@utils/queries/asFindable' import queryClient from './queryClient' import { useConnection } from '@solana/wallet-adapter-react' +import { HIDDEN_REALMS } from '@components/instructions/tools' export const realmQueryKeys = { all: (endpoint: string) => [endpoint, 'Realm'], @@ -30,7 +31,10 @@ export const useRealmsByProgramQuery = (program: PublicKey) => { : undefined, queryFn: async () => { if (!enabled) throw new Error() - return getRealms(connection, program) + const realms = (await getRealms(connection, program)).filter( + (x) => !HIDDEN_REALMS.includes(x.pubkey.toBase58()) + ) + return realms }, staleTime: 3600000, // 1 hour cacheTime: 3600000 * 24 * 10, diff --git a/hooks/queries/tokenAccount.ts b/hooks/queries/tokenAccount.ts index 126ae1539f..f2952c19e5 100644 --- a/hooks/queries/tokenAccount.ts +++ b/hooks/queries/tokenAccount.ts @@ -1,10 +1,57 @@ import { Connection, PublicKey } from '@solana/web3.js' import { useQuery } from '@tanstack/react-query' -import { getOwnedTokenAccounts, tryGetTokenAccount } from '@utils/tokens' import queryClient from './queryClient' import asFindable from '@utils/queries/asFindable' import { useConnection } from '@solana/wallet-adapter-react' import useWalletOnePointOh from '@hooks/useWalletOnePointOh' +import { AccountInfo, TOKEN_PROGRAM_ID } from '@solana/spl-token' +import { parseTokenAccountData } from '@utils/parseTokenAccountData' + +type TokenAccount = AccountInfo + +type TokenProgramAccount = { + publicKey: PublicKey + account: T +} + +async function getOwnedTokenAccounts( + connection: Connection, + publicKey: PublicKey +): Promise[]> { + const result = await connection.getTokenAccountsByOwner(publicKey, { + programId: TOKEN_PROGRAM_ID, + }) + + return result.value.map((r) => { + const publicKey = r.pubkey + const data = Buffer.from(r.account.data) + const account = parseTokenAccountData(publicKey, data) + return { publicKey, account } + }) +} + +async function tryGetTokenAccount( + connection: Connection, + publicKey: PublicKey +): Promise | undefined> { + try { + const result = await connection.getAccountInfo(publicKey) + + if (!result?.owner.equals(TOKEN_PROGRAM_ID)) { + return undefined + } + + const data = Buffer.from(result!.data) + const account = parseTokenAccountData(publicKey, data) + return { + publicKey, + account, + } + } catch (ex) { + // This is Try method and is expected to fail and hence logging is uneccesery + // console.error(`Can't fetch token account ${publicKey?.toBase58()}`, ex) + } +} export const tokenAccountQueryKeys = { all: (endpoint: string) => [endpoint, 'TokenAccount'], diff --git a/hooks/queries/tokenOwnerRecord.ts b/hooks/queries/tokenOwnerRecord.ts index 092a438153..8a1a9c11f7 100644 --- a/hooks/queries/tokenOwnerRecord.ts +++ b/hooks/queries/tokenOwnerRecord.ts @@ -3,6 +3,7 @@ import { getGovernanceAccounts, getTokenOwnerRecord, pubkeyFilter, + booleanFilter, } from '@solana/spl-governance' import { Connection, PublicKey } from '@solana/web3.js' import { useQuery } from '@tanstack/react-query' @@ -12,11 +13,11 @@ import { useAddressQuery_CouncilTokenOwner, } from './addresses/tokenOwnerRecord' import { useRealmQuery } from './realm' -import { useMemo } from 'react' import useLegacyConnectionContext from '@hooks/useLegacyConnectionContext' import useWalletOnePointOh from '@hooks/useWalletOnePointOh' import queryClient from './queryClient' import mainnetBetaRealms from 'public/realms/mainnet-beta.json' +import { determineVotingPowerType } from './governancePower' export const tokenOwnerRecordQueryKeys = { all: (endpoint: string) => [endpoint, 'TokenOwnerRecord'], @@ -24,17 +25,28 @@ export const tokenOwnerRecordQueryKeys = { ...tokenOwnerRecordQueryKeys.all(endpoint), k.toString(), ], - byRealm: (endpoint: string, realm: PublicKey) => [ + byRealm: (endpoint: string, realm: PublicKey, type: string) => [ ...tokenOwnerRecordQueryKeys.all(endpoint), 'by Realm', - realm, + realm.toString(), + type ], + byRealmXDelegate: ( + endpoint: string, + realm: PublicKey, + delegate: PublicKey + ) => [ + ...tokenOwnerRecordQueryKeys.byRealm(endpoint, realm, 'all'), + 'by Delegate', + delegate.toString(), + ], + byProgramXOwner: (endpoint: string, program: PublicKey, owner: PublicKey) => [ ...tokenOwnerRecordQueryKeys.all(endpoint), 'by Program', - program, + program.toString(), 'by Owner', - owner, + owner.toString(), ], } @@ -88,7 +100,8 @@ export const useTokenOwnerRecordsForRealmQuery = () => { queryKey: enabled ? tokenOwnerRecordQueryKeys.byRealm( connection.current.rpcEndpoint, - realm.pubkey + realm.pubkey, + 'all' ) : undefined, queryFn: async () => { @@ -121,23 +134,109 @@ export const useTokenOwnerRecordsForRealmQuery = () => { return query } +export const useCouncilTokenOwnerRecordsForRealmQuery = () => { + const connection = useLegacyConnectionContext() + const realm = useRealmQuery().data?.result + + const enabled = realm !== undefined + const query = useQuery({ + queryKey: enabled + ? tokenOwnerRecordQueryKeys.byRealm( + connection.current.rpcEndpoint, + realm.pubkey, + 'council' + ) + : undefined, + queryFn: async () => { + if (!enabled) throw new Error() + + const filter = pubkeyFilter(1, realm.pubkey) + if (!filter) throw new Error() // unclear why this would ever happen, probably it just cannot + + const votingType = await determineVotingPowerType(connection.current, realm.pubkey, "community") + const councilOnly = !(votingType === 'vanilla' || votingType === 'NFT') + const councilMint = realm.account.config.councilMint + + const mintFilter = councilOnly && councilMint ? + pubkeyFilter(33, councilMint) : + null + + const results = await getGovernanceAccounts( + connection.current, + realm.owner, + TokenOwnerRecord, + mintFilter ? [filter, mintFilter] : [filter] + ) + + // This may or may not be resource intensive for big DAOs, and is not too useful + /* + results.forEach((x) => { + queryClient.setQueryData( + tokenOwnerRecordQueryKeys.byPubkey(connection.cluster, x.pubkey), + { found: true, result: x } + ) + }) */ + + return results + }, + enabled, + }) + + return query +} + +// 1 + 32 + 32 + 32 + 8 + 4 + 4 + 1 + 1 + 6 // TODO filter in the gPA (would need rpc to also index) export const useTokenOwnerRecordsDelegatedToUser = () => { - const { data: tors } = useTokenOwnerRecordsForRealmQuery() + const connection = useLegacyConnectionContext() + const realm = useRealmQuery().data?.result const wallet = useWalletOnePointOh() - const delagatingTors = useMemo( - () => - tors?.filter( - (x) => - wallet?.publicKey !== undefined && - wallet?.publicKey !== null && - x.account.governanceDelegate !== undefined && - x.account.governanceDelegate.equals(wallet.publicKey) - ), - [tors, wallet?.publicKey] - ) + const walletPk = wallet?.publicKey ?? undefined + const enabled = realm !== undefined && walletPk !== undefined + const query = useQuery({ + queryKey: enabled + ? tokenOwnerRecordQueryKeys.byRealmXDelegate( + connection.current.rpcEndpoint, + realm.pubkey, + walletPk + ) + : undefined, + queryFn: async () => { + if (!enabled) throw new Error() + + const realmFilter = pubkeyFilter(1, realm.pubkey) + const hasDelegateFilter = booleanFilter( + 1 + 32 + 32 + 32 + 8 + 4 + 4 + 1 + 1 + 6, + true + ) + const delegatedToUserFilter = pubkeyFilter( + 1 + 32 + 32 + 32 + 8 + 4 + 4 + 1 + 1 + 6 + 1, + walletPk + ) + if (!realmFilter || !delegatedToUserFilter) throw new Error() // unclear why this would ever happen, probably it just cannot + + const results = await getGovernanceAccounts( + connection.current, + realm.owner, + TokenOwnerRecord, + [realmFilter, hasDelegateFilter, delegatedToUserFilter] + ) - return delagatingTors + // This may or may not be resource intensive for big DAOs, and is not too useful + /* + results.forEach((x) => { + queryClient.setQueryData( + tokenOwnerRecordQueryKeys.byPubkey(connection.cluster, x.pubkey), + { found: true, result: x } + ) + }) */ + + return results + }, + enabled, + }) + + return query } const queryFn = (connection: Connection, pubkey: PublicKey) => diff --git a/hooks/queries/useProgramVersionQuery.ts b/hooks/queries/useProgramVersionQuery.ts index bea6e40779..c8ad64441b 100644 --- a/hooks/queries/useProgramVersionQuery.ts +++ b/hooks/queries/useProgramVersionQuery.ts @@ -1,4 +1,4 @@ -import { getGovernanceProgramVersion } from '@solana/spl-governance' +import { getGovernanceProgramVersion } from '@realms-today/spl-governance' import { useConnection } from '@solana/wallet-adapter-react' import { Connection, PublicKey } from '@solana/web3.js' import { useQuery } from '@tanstack/react-query' diff --git a/hooks/selectedRealm/useSelectedRealmPubkey.ts b/hooks/selectedRealm/useSelectedRealmPubkey.ts index c6d2edb191..e4fc3301ed 100644 --- a/hooks/selectedRealm/useSelectedRealmPubkey.ts +++ b/hooks/selectedRealm/useSelectedRealmPubkey.ts @@ -8,7 +8,13 @@ import MAINNET_REALMS from 'public/realms/mainnet-beta.json' import { useMemo } from 'react' const useSelectedRealmPubkey = () => { - const { symbol, cluster } = useRouter().query + const { symbol } = useRouter().query + + return useRealmPubkeyByPkOrSymbol(symbol as string) +} + +export const useRealmPubkeyByPkOrSymbol = (symbol: string | undefined) => { + const { cluster } = useRouter().query const parsed = useMemo( () => (typeof symbol === 'string' ? tryParsePublicKey(symbol) : undefined), diff --git a/hooks/useAccountInvestments/staticInvestments.ts b/hooks/useAccountInvestments/staticInvestments.ts index 7c620067dd..f50325ec9e 100644 --- a/hooks/useAccountInvestments/staticInvestments.ts +++ b/hooks/useAccountInvestments/staticInvestments.ts @@ -46,17 +46,17 @@ export const getTokenInvestments = (tokenImg: string) => [ createProposalFcn: () => null, noProtocol: true, }, - // { - // liquidity: 0, - // protocolSymbol: '', - // apy: '', - // protocolName: 'Mango', - // handledMint: '', - // handledTokenSymbol: '', - // handledTokenImgSrc: tokenImg, - // protocolLogoSrc: 'https://alpha.mango.markets/logos/logo-mark.svg', - // strategyName: 'Trade', - // strategyDescription: '', - // createProposalFcn: () => null, - // }, + { + liquidity: 0, + protocolSymbol: '', + apy: '', + protocolName: 'Mango', + handledMint: '', + handledTokenSymbol: '', + handledTokenImgSrc: tokenImg, + protocolLogoSrc: 'https://mango.markets/logos/logo-mark.svg', + strategyName: 'Trade', + strategyDescription: '', + createProposalFcn: () => null, + }, ] diff --git a/hooks/useCreatePostMessageIx.ts b/hooks/useCreatePostMessageIx.ts index 6de921fc71..0771a41046 100644 --- a/hooks/useCreatePostMessageIx.ts +++ b/hooks/useCreatePostMessageIx.ts @@ -6,12 +6,13 @@ import { GOVERNANCE_CHAT_PROGRAM_ID, withPostChatMessage, } from '@solana/spl-governance' -import useVotePluginsClientStore from 'stores/useVotePluginsClientStore' import useWalletOnePointOh from '@hooks/useWalletOnePointOh' import { useRealmQuery } from '@hooks/queries/realm' import { useRouteProposalQuery } from '@hooks/queries/proposal' import { Keypair, TransactionInstruction } from '@solana/web3.js' import useUserOrDelegator from './useUserOrDelegator' +import {useRealmVoterWeightPlugins} from "@hooks/useRealmVoterWeightPlugins"; +import {convertTypeToVoterWeightAction} from "../VoterWeightPlugins"; /** This is WIP and shouldn't be used * @deprecated @@ -21,9 +22,8 @@ const useCreatePostMessageIx = () => { const realm = useRealmQuery().data?.result const walletPk = useWalletOnePointOh()?.publicKey ?? undefined const actingWalletPk = useUserOrDelegator() - const votingPluginClient = useVotePluginsClientStore( - (s) => s.state.currentRealmVotingClient - ) + const { updateVoterWeightRecords, voterWeightPks, voterWeightPkForWallet } = useRealmVoterWeightPlugins(); + const voterWeightPk = actingWalletPk ? voterWeightPkForWallet(actingWalletPk) : undefined; const proposal = useRouteProposalQuery().data?.result return useCallback( @@ -47,13 +47,10 @@ const useCreatePostMessageIx = () => { proposal.account.governingTokenMint, actingWalletPk ) + const updateVWRInstructions = await updateVoterWeightRecords(actingWalletPk, convertTypeToVoterWeightAction('commentProposal')); - const plugin = await votingPluginClient?.withUpdateVoterWeightRecord( - instructions, - userTorPk, - 'commentProposal', - createNftTicketsIxs - ) + instructions.push(...updateVWRInstructions.pre); + createNftTicketsIxs.push(...updateVWRInstructions.post); const body = new ChatMessageBody({ type: ChatMessageBodyType.Text, @@ -73,10 +70,10 @@ const useCreatePostMessageIx = () => { walletPk, undefined, body, - plugin?.voterWeightPk + voterWeightPk ) }, - [actingWalletPk, proposal, realm, votingPluginClient, walletPk] + [actingWalletPk, proposal, realm, voterWeightPks, walletPk] ) } diff --git a/hooks/useCreateProposal.ts b/hooks/useCreateProposal.ts index 45337a8e6d..434bdfbbe2 100644 --- a/hooks/useCreateProposal.ts +++ b/hooks/useCreateProposal.ts @@ -2,8 +2,6 @@ import { InstructionDataWithHoldUpTime, createProposal, } from 'actions/createProposal' -import useVotePluginsClientStore from 'stores/useVotePluginsClientStore' -import useRealm from './useRealm' import useRpcContext from './useRpcContext' import { fetchGovernanceByPubkey } from './queries/governance' import { PublicKey } from '@solana/web3.js' @@ -18,12 +16,9 @@ import queryClient from './queries/queryClient' import { proposalQueryKeys } from './queries/proposal' import { createLUTProposal } from 'actions/createLUTproposal' import { useLegacyVoterWeight } from './queries/governancePower' +import {useVotingClients} from "@hooks/useVotingClients"; export default function useCreateProposal() { - const client = useVotePluginsClientStore( - (s) => s.state.currentRealmVotingClient - ) - const connection = useLegacyConnectionContext() const realm = useRealmQuery().data?.result const config = useRealmConfigQuery().data?.result @@ -31,8 +26,8 @@ export default function useCreateProposal() { const councilMint = useRealmCouncilMintInfoQuery().data?.result const { result: ownVoterWeight } = useLegacyVoterWeight() - const { canChooseWhoVote } = useRealm() const { getRpcContext } = useRpcContext() + const votingClients = useVotingClients(); /** @deprecated because the api is goofy, use `propose` */ const handleCreateProposal = async ({ @@ -56,13 +51,24 @@ export default function useCreateProposal() { connection.current, governance.pubkey ) + const minCouncilTokensToCreateProposal = selectedGovernance?.account.config.minCouncilTokensToCreateProposal + const councilPower = ownVoterWeight?.councilTokenRecord?.account.governingTokenDepositAmount + + const ownTokenRecord = + minCouncilTokensToCreateProposal && councilPower && councilPower >= minCouncilTokensToCreateProposal ? + ownVoterWeight?.councilTokenRecord : + ownVoterWeight?.communityTokenRecord + + if (!ownTokenRecord) throw new Error('token owner record does not exist') if (!selectedGovernance) throw new Error('governance not found') if (!realm) throw new Error() - const ownTokenRecord = ownVoterWeight?.getTokenRecordToCreateProposal( - selectedGovernance.account.config, - voteByCouncil - ) // TODO just get the token record the normal way + // this is somewhat confusing - the basic idea is: + // although a vote may be by community vote, the proposer may create it with their council token + // The choice of which token to use is made when the token record is selected + const proposeByCouncil = ownVoterWeight?.councilTokenRecord?.pubkey.toBase58() === (ownTokenRecord?.pubkey.toBase58() ?? ""); + // now we can we identify whether we are using the community or council voting client (to decide which (if any) plugins to use) + const votingClient = votingClients(proposeByCouncil ? 'council' : 'community'); const defaultProposalMint = !mint?.supply.isZero() || @@ -73,7 +79,7 @@ export default function useCreateProposal() { : undefined const proposalMint = - canChooseWhoVote && voteByCouncil + voteByCouncil ? realm?.account.config.councilMint : defaultProposalMint @@ -83,12 +89,13 @@ export default function useCreateProposal() { const rpcContext = getRpcContext() if (!rpcContext) throw new Error() + const create = utilizeLookupTable ? createLUTProposal : createProposal const proposalAddress = await create( rpcContext, realm, governance.pubkey, - ownTokenRecord!, + ownTokenRecord, title, description, proposalMint, @@ -96,7 +103,7 @@ export default function useCreateProposal() { instructionsData, isDraft, ['Approve'], - client + votingClient ) queryClient.invalidateQueries({ queryKey: proposalQueryKeys.all(connection.endpoint), @@ -134,13 +141,25 @@ export default function useCreateProposal() { connection.current, governance ) + + const minCouncilTokensToCreateProposal = selectedGovernance?.account.config.minCouncilTokensToCreateProposal + const councilPower = ownVoterWeight?.councilTokenRecord?.account.governingTokenDepositAmount + + const ownTokenRecord = + minCouncilTokensToCreateProposal && councilPower && councilPower >= minCouncilTokensToCreateProposal ? + ownVoterWeight?.councilTokenRecord : + ownVoterWeight?.communityTokenRecord + + if (!ownTokenRecord) throw new Error('token owner record does not exist') if (!selectedGovernance) throw new Error('governance not found') if (!realm) throw new Error() - const ownTokenRecord = ownVoterWeight?.getTokenRecordToCreateProposal( - selectedGovernance.account.config, - voteByCouncil - ) + // this is somewhat confusing - the basic idea is: + // although a vote may be by community vote, the proposer may create it with their council token + // The choice of which token to use is made when the token record is selected + const proposeByCouncil = ownVoterWeight?.councilTokenRecord?.pubkey.toBase58() === (ownTokenRecord?.pubkey.toBase58() ?? ""); + // now we can we identify whether we are using the community or council voting client (to decide which (if any) plugins to use) + const votingClient = votingClients(proposeByCouncil ? 'council' : 'community'); const defaultProposalMint = !mint?.supply.isZero() || @@ -151,7 +170,7 @@ export default function useCreateProposal() { : undefined const proposalMint = - canChooseWhoVote && voteByCouncil + voteByCouncil ? realm?.account.config.councilMint : defaultProposalMint @@ -165,7 +184,7 @@ export default function useCreateProposal() { rpcContext, realm, governance, - ownTokenRecord!, + ownTokenRecord, title, description, proposalMint, @@ -173,7 +192,7 @@ export default function useCreateProposal() { instructionsData, isDraft, options, - client + votingClient ) queryClient.invalidateQueries({ queryKey: proposalQueryKeys.all(connection.endpoint), diff --git a/hooks/useDelegatorAwareVoterWeight.ts b/hooks/useDelegatorAwareVoterWeight.ts new file mode 100644 index 0000000000..86ea749215 --- /dev/null +++ b/hooks/useDelegatorAwareVoterWeight.ts @@ -0,0 +1,37 @@ +import {GovernanceRole} from "../@types/types"; +import {CalculatedWeight} from "../VoterWeightPlugins/lib/types"; +import useWalletOnePointOh from "@hooks/useWalletOnePointOh"; +import {useSelectedDelegatorStore} from "../stores/useSelectedDelegatorStore"; +import {useRealmVoterWeightPlugins} from "@hooks/useRealmVoterWeightPlugins"; +import {DELEGATOR_BATCH_VOTE_SUPPORT_BY_PLUGIN} from "@constants/flags"; + +export const useDelegatorAwareVoterWeight = (role: GovernanceRole): CalculatedWeight | undefined => { + const wallet = useWalletOnePointOh(); + // these hooks return different results depending on whether batch delegator voting is supported + // if batch is on, and these are undefined, it means "yourself + all delegators" + // if batch is off, and these are undefined, it means "yourself only" + // if batch is on, and yourself only is picked, the selectedDelegator will be the current logged-in wallet + const selectedCommunityDelegator = useSelectedDelegatorStore((s) => s.communityDelegator) + const selectedCouncilDelegator = useSelectedDelegatorStore((s) => s.councilDelegator) + const selectedDelegatorForRole = role === 'community' ? selectedCommunityDelegator : selectedCouncilDelegator; + const votingWallet = (role === 'community' ? selectedCommunityDelegator : selectedCouncilDelegator) ?? wallet?.publicKey + + const {plugins, totalCalculatedVoterWeight, voterWeightForWallet} = useRealmVoterWeightPlugins(role) + + // if the plugin supports delegator batch voting (or no plugins exist on the dao), + // and no delegator is selected, we can use totalCalculatedVoterWeight + // otherwise, use the voterWeightForWallet for the correct delegator or the wallet itself + const lastPlugin = plugins?.voterWeight[plugins.voterWeight.length - 1]; + const supportsBatchVoting = !lastPlugin || DELEGATOR_BATCH_VOTE_SUPPORT_BY_PLUGIN[lastPlugin?.name] + + // the user has selected "yourself + all delegators" and the plugin supports batch voting + if (supportsBatchVoting && !selectedDelegatorForRole) { + return totalCalculatedVoterWeight; + } + + // there is no wallet to calculate voter weight for + if (!votingWallet) return undefined; + + // the user has selected a specific delegator or "yourself only" + return voterWeightForWallet(votingWallet); +} \ No newline at end of file diff --git a/hooks/useGovernanceAssets.ts b/hooks/useGovernanceAssets.ts index 08a8052c97..f0f320b684 100644 --- a/hooks/useGovernanceAssets.ts +++ b/hooks/useGovernanceAssets.ts @@ -4,10 +4,10 @@ import useGovernanceAssetsStore from 'stores/useGovernanceAssetsStore' import { HELIUM_VSR_PLUGINS_PKS, VSR_PLUGIN_PKS } from '../constants/plugins' import { useRealmQuery } from './queries/realm' import { useRealmConfigQuery } from './queries/realmConfig' -import { useRouter } from 'next/router' import { useRealmGovernancesQuery } from './queries/governance' import { useMemo } from 'react' -import { useLegacyVoterWeight } from './queries/governancePower' +import { useRealmVoterWeights } from '@hooks/useRealmVoterWeightPlugins' +import { GovernanceConfig } from '@solana/spl-governance' type Package = { name: string @@ -43,8 +43,19 @@ export type InstructionType = { export default function useGovernanceAssets() { const realm = useRealmQuery().data?.result const config = useRealmConfigQuery().data?.result - const { symbol } = useRouter().query - const { result: ownVoterWeight } = useLegacyVoterWeight() + const { communityWeight, councilWeight } = useRealmVoterWeights() + const ownVoterWeights = { + community: communityWeight?.value, + council: councilWeight?.value, + } + + const canCreateProposal = (config: GovernanceConfig) => { + return ( + ownVoterWeights.community?.gte( + config.minCommunityTokensToCreateProposal + ) || ownVoterWeights.council?.gte(config.minCouncilTokensToCreateProposal) + ) + } const governedTokenAccounts: AssetAccount[] = useGovernanceAssetsStore( (s) => s.governedTokenAccounts @@ -67,12 +78,9 @@ export default function useGovernanceAssets() { realm && assetAccounts .filter((x) => types.find((t) => t === x.type)) - .some((govAcc) => - ownVoterWeight?.canCreateProposal(govAcc.governance.account.config) - ) + .some((govAcc) => canCreateProposal(govAcc.governance.account.config)) ) } - const canMintRealmCouncilToken = () => { return !!assetAccounts.find( (x) => @@ -83,10 +91,7 @@ export default function useGovernanceAssets() { const governance = governancesArray.find( (x) => acc.governance.pubkey.toBase58() === x.pubkey.toBase58() ) - return ( - governance && - ownVoterWeight?.canCreateProposal(governance?.account?.config) - ) + return governance && canCreateProposal(governance?.account?.config) }) const canUseProgramUpgradeInstruction = canUseGovernanceForInstruction([ @@ -99,9 +104,7 @@ export default function useGovernanceAssets() { const canUseAnyInstruction = realm && - governancesArray.some((gov) => - ownVoterWeight?.canCreateProposal(gov.account.config) - ) + governancesArray.some((gov) => canCreateProposal(gov.account.config)) const realmAuth = realm && @@ -109,7 +112,7 @@ export default function useGovernanceAssets() { (x) => x.pubkey.toBase58() === realm.account.authority?.toBase58() ) const canUseAuthorityInstruction = - realmAuth && ownVoterWeight?.canCreateProposal(realmAuth?.account.config) + realmAuth && canCreateProposal(realmAuth?.account.config) const governedSPLTokenAccounts = governedTokenAccounts.filter( (x) => x.type === AccountType.TOKEN @@ -125,10 +128,7 @@ export default function useGovernanceAssets() { const governance = governancesArray.find( (x) => acc.governance.pubkey.toBase58() === x.pubkey.toBase58() ) - return ( - governance && - ownVoterWeight?.canCreateProposal(governance?.account?.config) - ) + return governance && canCreateProposal(governance?.account?.config) } ) @@ -150,14 +150,8 @@ export default function useGovernanceAssets() { [PackageEnum.Distribution]: { name: 'Distribution Program', }, - [PackageEnum.Foresight]: { - name: 'Foresight', - isVisible: symbol === 'FORE', - image: '/img/foresight.png', - }, - [PackageEnum.GatewayPlugin]: { - name: 'Gateway Plugin', + name: 'Civic Plugin', image: '/img/civic.svg', }, [PackageEnum.Identity]: { @@ -179,6 +173,10 @@ export default function useGovernanceAssets() { name: 'PsyFinance', image: '/img/psyfinance.png', }, + [PackageEnum.Pyth]: { + name: 'Pyth', + image: '/img/pyth.svg', + }, [PackageEnum.Serum]: { name: 'Serum', image: '/img/serum.png', @@ -190,6 +188,14 @@ export default function useGovernanceAssets() { name: 'Solend', image: '/img/solend.png', }, + [PackageEnum.Symmetry]: { + name: 'Symmetry', + image: '/img/symmetry.png', + }, + [PackageEnum.Squads]: { + name: 'Squads', + image: '/img/squads.png', + }, [PackageEnum.Switchboard]: { name: 'Switchboard', image: '/img/switchboard.png', @@ -265,6 +271,10 @@ export default function useGovernanceAssets() { name: 'Delegate Stake Account', packageId: PackageEnum.Common, }, + [Instructions.RemoveStakeLock]: { + name: 'Stake Account Remove Lock', + packageId: PackageEnum.Common, + }, [Instructions.DifferValidatorStake]: { name: 'Differ validator stake', // Not to be used for now @@ -292,9 +302,7 @@ export default function useGovernanceAssets() { name: 'None', isVisible: realm && - governancesArray.some((g) => - ownVoterWeight?.canCreateProposal(g.account.config) - ), + governancesArray.some((g) => canCreateProposal(g.account.config)), packageId: PackageEnum.Common, }, [Instructions.ProgramUpgrade]: { @@ -311,11 +319,24 @@ export default function useGovernanceAssets() { name: 'Stake A Validator', packageId: PackageEnum.Common, }, + [Instructions.SanctumDepositStake]: { + name: 'Sanctum Deposit Stake', + packageId: PackageEnum.Common, + }, + [Instructions.SanctumWithdrawStake]: { + name: 'Sanctum Withdraw Stake', + packageId: PackageEnum.Common, + }, [Instructions.Transfer]: { name: 'Transfer Tokens', isVisible: canUseTokenTransferInstruction, packageId: PackageEnum.Common, }, + [Instructions.Burn]: { + name: 'Burn Tokens', + isVisible: canUseTokenTransferInstruction, + packageId: PackageEnum.Common, + }, [Instructions.TransferDomainName]: { name: 'SNS Transfer Out Domain Name', packageId: PackageEnum.Common, @@ -337,6 +358,21 @@ export default function useGovernanceAssets() { name: 'Split Stake Validator', packageId: PackageEnum.Common, }, + [Instructions.DaoVote]: { + name: 'Vote in another DAO', + isVisible: canUseTransferInstruction, + packageId: PackageEnum.Common, + }, + [Instructions.DualFinanceDelegateWithdraw]: { + name: 'Withdraw Vote Deposit', + isVisible: canUseTransferInstruction, + packageId: PackageEnum.Common, + }, + [Instructions.DualFinanceVoteDeposit]: { + name: 'Join a VSR DAO', + isVisible: canUseTransferInstruction, + packageId: PackageEnum.Common, + }, /* ██████ ██ ██ █████ ██ ███████ ██ ███ ██ █████ ███ ██ ██████ ███████ ██ ██ ██ ██ ██ ██ ██ ██ ██ ████ ██ ██ ██ ████ ██ ██ ██ @@ -390,54 +426,6 @@ export default function useGovernanceAssets() { isVisible: canUseTransferInstruction, packageId: PackageEnum.Dual, }, - [Instructions.DualFinanceDelegateWithdraw]: { - name: 'Withdraw Vote Deposit', - isVisible: canUseTransferInstruction, - packageId: PackageEnum.Dual, - }, - [Instructions.DualFinanceVoteDeposit]: { - name: 'Vote Deposit', - isVisible: canUseTransferInstruction, - packageId: PackageEnum.Dual, - }, - [Instructions.DualFinanceVote]: { - name: 'Vote', - isVisible: canUseTransferInstruction, - packageId: PackageEnum.Dual, - }, - - /* - ███████ ██████ ██████ ███████ ███████ ██ ██████ ██ ██ ████████ - ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ - █████ ██ ██ ██████ █████ ███████ ██ ██ ███ ███████ ██ - ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ - ██ ██████ ██ ██ ███████ ███████ ██ ██████ ██ ██ ██ - */ - - [Instructions.ForesightAddMarketListToCategory]: { - name: 'Add Market List To Category', - packageId: PackageEnum.Foresight, - }, - [Instructions.ForesightInitCategory]: { - name: 'Init Category', - packageId: PackageEnum.Foresight, - }, - [Instructions.ForesightInitMarket]: { - name: 'Init Market', - packageId: PackageEnum.Foresight, - }, - [Instructions.ForesightInitMarketList]: { - name: 'Init Market List', - packageId: PackageEnum.Foresight, - }, - [Instructions.ForesightResolveMarket]: { - name: 'Resolve Market', - packageId: PackageEnum.Foresight, - }, - [Instructions.ForesightSetMarketMetadata]: { - name: 'Set Market Metadata', - packageId: PackageEnum.Foresight, - }, /* ██ ██████ ███████ ███ ██ ████████ ██ ████████ ██ ██ @@ -649,6 +637,21 @@ export default function useGovernanceAssets() { packageId: PackageEnum.PsyFinance, }, + /* + ██████ ██ ██ ████████ ██ ██ + ██ ██ ██ ██ ██ ██ ██ + ██████ ████ ██ ███████ + ██ ██ ██ ██ ██ + ██ ██ ██ ██ ██ + */ + [Instructions.PythRecoverAccount]: { + name: 'Recover Account', + packageId: PackageEnum.Pyth, + }, + [Instructions.PythUpdatePoolAuthority]: { + name: 'Update Pool Authority', + packageId: PackageEnum.Pyth, + }, /* ███████ ███████ ██████ ██ ██ ███ ███ ██ ██ ██ ██ ██ ██ ████ ████ @@ -719,6 +722,48 @@ export default function useGovernanceAssets() { packageId: PackageEnum.Solend, }, + /* + ███████ ██ ██ ███ ███ ███ ███ ███████ ████████ ██████ ██ ██ + ██ ██ ██ ████ ████ ████ ████ ██ ██ ██ ██ ██ ██ + ███████ ████ ██ ████ ██ ██ ████ ██ █████ ██ ██████ ████ + ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ + ███████ ██ ██ ██ ██ ██ ███████ ██ ██ ██ ██ + */ + [Instructions.SymmetryCreateBasket]: { + name: 'Create Basket', + packageId: PackageEnum.Symmetry, + }, + [Instructions.SymmetryEditBasket]: { + name: 'Edit Basket', + packageId: PackageEnum.Symmetry, + }, + [Instructions.SymmetryDeposit]: { + name: 'Deposit into Basket', + packageId: PackageEnum.Symmetry, + }, + [Instructions.SymmetryWithdraw]: { + name: 'Withdraw from Basket', + packageId: PackageEnum.Symmetry, + }, + /* + ███████ ██████ ██ ██ █████ ██████ ███████ + ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ + ███████ ██ ██ ██ ██ ███████ ██ ██ ███████ + ██ ██ ▄▄ ██ ██ ██ ██ ██ ██ ██ ██ + ███████ ██████ ██████ ██ ██ ██████ ███████ + */ + [Instructions.SquadsMeshAddMember]: { + name: 'Mesh Add Member', + packageId: PackageEnum.Squads, + }, + [Instructions.SquadsMeshChangeThresholdMember]: { + name: 'Mesh Change Threshold', + packageId: PackageEnum.Squads, + }, + [Instructions.SquadsMeshRemoveMember]: { + name: 'Mesh Remove Member', + packageId: PackageEnum.Squads, + }, /* ███████ ██ ██ ██ ████████ ██████ ██ ██ ██████ ██████ █████ ██████ ██████ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ diff --git a/hooks/useJoinRealm.ts b/hooks/useJoinRealm.ts new file mode 100644 index 0000000000..8effc5c9c5 --- /dev/null +++ b/hooks/useJoinRealm.ts @@ -0,0 +1,63 @@ +import {useUserCommunityTokenOwnerRecord} from "@hooks/queries/tokenOwnerRecord"; +import { withCreateTokenOwnerRecord } from '@solana/spl-governance' +import {useRealmQuery} from "@hooks/queries/realm"; +import useWalletOnePointOh from "@hooks/useWalletOnePointOh"; +import useProgramVersion from "@hooks/useProgramVersion"; +import {useRealmVoterWeightPlugins} from "@hooks/useRealmVoterWeightPlugins"; +import {useCallback, useEffect, useState} from "react"; +import {TransactionInstruction} from "@solana/web3.js"; + +type UseJoinRealmReturnType = { + // use this to decide whether the join button should be displayed + userNeedsTokenOwnerRecord: boolean, + userNeedsVoterWeightRecords: boolean, + // returns an array of instructions that should be added to a transaction when the join button is clicked. + // will create the Token Owner Record if needed unless includeTokenOwnerRecord is false + // (this allows it to be chained with deposit instructions) + handleRegister: (includeTokenOwnerRecord?: boolean) => Promise +} + +export const useJoinRealm = (): UseJoinRealmReturnType => { + const tokenOwnerRecord = useUserCommunityTokenOwnerRecord().data?.result + const { createVoterWeightRecords } = useRealmVoterWeightPlugins(); + const realm = useRealmQuery().data?.result + const wallet = useWalletOnePointOh() + const programVersion = useProgramVersion() + const [userNeedsVoterWeightRecords, setUserNeedsVoterWeightRecords] = useState(false) + + // A user needs a token owner record if they don't have one already and either + // there are no plugins (vanilla realm) or + // the first plugin in the chain requires an input voter weight + const userNeedsTokenOwnerRecord = !tokenOwnerRecord; + useEffect(() => { + if (!wallet?.publicKey) return; + createVoterWeightRecords(wallet.publicKey) + .then((ixes) => setUserNeedsVoterWeightRecords(ixes.length > 0)); + }, [createVoterWeightRecords]) + + const handleRegister = useCallback(async (includeTokenOwnerRecord = true) => { + if (!wallet?.publicKey) return []; + + const onboardUserIxes = [] + if (includeTokenOwnerRecord && userNeedsTokenOwnerRecord && realm && programVersion) { + await withCreateTokenOwnerRecord(onboardUserIxes, + realm.owner, + programVersion, + realm.pubkey, + wallet.publicKey, + realm.account.communityMint, + wallet.publicKey) + } + + const createVoterWeightRecordIxes = await createVoterWeightRecords(wallet.publicKey); + return [...createVoterWeightRecordIxes, ...onboardUserIxes] + }, [realm?.pubkey.toBase58(), wallet?.publicKey?.toBase58(), programVersion, userNeedsTokenOwnerRecord]) + + return { + // use these to decide whether the join button should be displayed + userNeedsTokenOwnerRecord, + userNeedsVoterWeightRecords, + // returns an array of instructions that should be added to a transaction when the join button is clicked. + handleRegister + } +} \ No newline at end of file diff --git a/hooks/useMangoAccountsTreasury.ts b/hooks/useMangoAccountsTreasury.ts new file mode 100644 index 0000000000..73eb098a16 --- /dev/null +++ b/hooks/useMangoAccountsTreasury.ts @@ -0,0 +1,78 @@ +import useProgramSelector from '@components/Mango/useProgramSelector' +import BigNumber from 'bignumber.js' +import { useState, useEffect } from 'react' +import UseMangoV4 from './useMangoV4' +import { fetchMangoAccounts } from './useTreasuryInfo/assembleWallets' +import { convertAccountToAsset } from './useTreasuryInfo/convertAccountToAsset' +import { AccountType, AssetAccount } from '@utils/uiTypes/assets' +import { Asset } from '@models/treasury/Asset' +import { useRealmQuery } from './queries/realm' + +export function useMangoAccountsTreasury(assetAccounts: AssetAccount[]) { + const programSelectorHook = useProgramSelector() + const { mangoClient, mangoGroup } = UseMangoV4( + programSelectorHook.program?.val, + programSelectorHook.program?.group + ) + const [mangoAccountsValue, setMangoAccountsValue] = useState(new BigNumber(0)) + const [isFetching, setIsFetching] = useState(true) + const [isFetchingMangoAcc, setIsFetchingMangoAcc] = useState(false) + const [mangoAccWasFetched, setMangoAccWasFetched] = useState(false) + const realm = useRealmQuery().data?.result + + useEffect(() => { + async function fetchMangoValue() { + setMangoAccWasFetched(false) + setIsFetchingMangoAcc(true) + setMangoAccountsValue(new BigNumber(0)) + const filteredAssetAccounts = assetAccounts.filter( + (a) => a.type !== AccountType.PROGRAM && a.type !== AccountType.NFT + ) + const assets = ( + await Promise.all( + filteredAssetAccounts.map((a) => convertAccountToAsset(a)) + ) + ).filter((asset): asset is Asset => asset !== null) + const { mangoAccountsValue: newMangoValue } = await fetchMangoAccounts( + assets!, + mangoClient!, + mangoGroup + ) + + setIsFetchingMangoAcc(false) + setMangoAccWasFetched(true) + setMangoAccountsValue(newMangoValue) + } + + if ( + assetAccounts.length > 0 && + isFetching && + mangoClient && + mangoGroup && + realm && + !isFetchingMangoAcc && + !mangoAccWasFetched + ) { + fetchMangoValue().finally(() => { + setIsFetchingMangoAcc(false) + setIsFetching(false) + setMangoAccWasFetched(true) + }) + } + }, [ + assetAccounts, + isFetching, + mangoClient, + mangoGroup, + realm, + isFetchingMangoAcc, + mangoAccWasFetched, + ]) + + return { + isFetching, + mangoAccountsValue, + setMangoAccountsValue, + setIsFetching, + } +} diff --git a/hooks/useMangoV4.tsx b/hooks/useMangoV4.tsx index 9f2cbf3c91..db5277f83a 100644 --- a/hooks/useMangoV4.tsx +++ b/hooks/useMangoV4.tsx @@ -2,16 +2,19 @@ import { AnchorProvider } from '@coral-xyz/anchor' import { PublicKey } from '@solana/web3.js' import { ConnectionContext } from '@utils/connection' import { WalletSigner } from '@solana/spl-governance' +import Decimal from 'decimal.js' import { Group, MangoClient, MANGO_V4_ID, + Bank, + MangoAccount, } from '@blockworks-foundation/mango-v4' import { useEffect, useState } from 'react' import useWalletOnePointOh from './useWalletOnePointOh' import useLegacyConnectionContext from './useLegacyConnectionContext' -export default function UseMangoV4() { +export default function UseMangoV4(programId?: PublicKey, group?: PublicKey) { const connection = useLegacyConnectionContext() const cluster = connection.cluster const wallet = useWalletOnePointOh() @@ -24,7 +27,14 @@ export default function UseMangoV4() { '78b8f4cGCwmZ9ysPFMWLaLTkkaYnUjwMJYStWe5RTSSX' ) const clientCluster = cluster === 'devnet' ? 'devnet' : 'mainnet-beta' - const GROUP = cluster === 'devnet' ? DEVNET_GROUP : MAINNET_GROUP + const GROUP = group + ? group + : cluster === 'devnet' + ? DEVNET_GROUP + : MAINNET_GROUP + + const program = programId ? programId : MANGO_V4_ID[clientCluster] + const isMainMangoProgram = program.equals(MANGO_V4_ID[clientCluster]) const [mangoClient, setMangoClient] = useState(null) const [mangoGroup, setMangoGroup] = useState(null) const getClient = async ( @@ -41,7 +51,10 @@ export default function UseMangoV4() { const client = await MangoClient.connect( adminProvider, clientCluster, - MANGO_V4_ID[clientCluster] + program, + { + idsSource: isMainMangoProgram ? 'api' : undefined, + } ) return client @@ -54,11 +67,16 @@ export default function UseMangoV4() { setMangoClient(client) setMangoGroup(group) } - if (wallet?.publicKey && connection) { + if (wallet && connection) { console.log('SET NEW CLIENT') handleSetClient() } - }, [connection.cluster, wallet?.publicKey?.toBase58()]) + }, [ + connection.cluster, + wallet?.publicKey?.toBase58(), + GROUP.toBase58(), + program.toBase58(), + ]) const docs = mangoClient?.program.idl.accounts .flatMap((x) => x.type.fields as any) @@ -76,6 +94,40 @@ export default function UseMangoV4() { } } + const getMaxBorrowForBank = ( + group: Group, + bank: Bank, + mangoAccount: MangoAccount + ) => { + try { + const maxBorrow = new Decimal( + mangoAccount.getMaxWithdrawWithBorrowForTokenUi(group, bank.mint) + ) + return maxBorrow + } catch (e) { + console.log(`failed to get max borrow for ${bank.name}`, e) + return new Decimal(0) + } + } + + const getMaxWithdrawForBank = ( + group: Group, + bank: Bank, + mangoAccount: MangoAccount, + allowBorrow = false + ): Decimal => { + const accountBalance = new Decimal(mangoAccount.getTokenBalanceUi(bank)) + const vaultBalance = group.getTokenVaultBalanceByMintUi(bank.mint) + const maxBorrow = getMaxBorrowForBank(group, bank, mangoAccount) + const maxWithdraw = allowBorrow + ? Decimal.min(vaultBalance, maxBorrow) + : bank.initAssetWeight.toNumber() === 0 + ? Decimal.min(accountBalance, vaultBalance) + : Decimal.min(accountBalance, vaultBalance, maxBorrow) + + return Decimal.max(0, maxWithdraw) + } + return { ADMIN_PK, GROUP_NUM, @@ -84,5 +136,17 @@ export default function UseMangoV4() { mangoClient, mangoGroup, getAdditionalLabelInfo, + getMaxWithdrawForBank, } } + +export const MANGO_BOOST_PROGRAM_ID = new PublicKey( + 'zF2vSz6V9g1YHGmfrzsY497NJzbRr84QUrPry4bLQ25' +) +export const BOOST_MAINNET_GROUP = new PublicKey( + 'AKeMSYiJekyKfwCc3CUfVNDVAiqk9FfbQVMY3G7RUZUf' +) + +export const MANGO_V4_MAINNET_GROUP = new PublicKey( + '78b8f4cGCwmZ9ysPFMWLaLTkkaYnUjwMJYStWe5RTSSX' +) diff --git a/hooks/useMaxVoteRecord.ts b/hooks/useMaxVoteRecord.ts index dddf667526..f701fc4657 100644 --- a/hooks/useMaxVoteRecord.ts +++ b/hooks/useMaxVoteRecord.ts @@ -1,15 +1,17 @@ -import { useMemo } from 'react' -import { MaxVoterWeightRecord, ProgramAccount } from '@solana/spl-governance' -import useNftPluginStore from 'NftVotePlugin/store/nftPluginStore' -import useHeliumVsrStore from 'HeliumVotePlugin/hooks/useHeliumVsrStore' +import {getMaxVoterWeightRecord} from '@solana/spl-governance' +import {useRealmVoterWeightPlugins} from "@hooks/useRealmVoterWeightPlugins"; +import {useConnection} from "@solana/wallet-adapter-react"; +import {useAsync} from "react-async-hook"; export const useMaxVoteRecord = () => { - const nftMaxVoteRecord = useNftPluginStore((s) => s.state.maxVoteRecord) - const heliumMaxVoteRecord = useHeliumVsrStore((s) => s.state.maxVoteRecord) - const maxVoteWeightRecord: ProgramAccount | null = useMemo( - () => nftMaxVoteRecord || heliumMaxVoteRecord || null, - [nftMaxVoteRecord, heliumMaxVoteRecord] - ) + const { connection } = useConnection() + const { maxVoterWeightPk } = useRealmVoterWeightPlugins(); - return maxVoteWeightRecord + const maxVoteWeightRecord = useAsync(async () => + maxVoterWeightPk && + getMaxVoterWeightRecord(connection, maxVoterWeightPk), + [maxVoterWeightPk?.toBase58()] + ); + + return maxVoteWeightRecord.result } diff --git a/hooks/useNftRegistrar.ts b/hooks/useNftRegistrar.ts new file mode 100644 index 0000000000..f0e25bc7d6 --- /dev/null +++ b/hooks/useNftRegistrar.ts @@ -0,0 +1,8 @@ +import {useNftClient} from "../VoterWeightPlugins/useNftClient"; +import {NftVoter} from "../idls/nft_voter"; +import { IdlAccounts } from '@coral-xyz/anchor'; + +export const useNftRegistrar = () => { + const { plugin } = useNftClient(); + return plugin?.params as IdlAccounts['registrar'] | null; +} diff --git a/hooks/useNftRegistrarCollection.ts b/hooks/useNftRegistrarCollection.ts index b86475c243..4a860dc63e 100644 --- a/hooks/useNftRegistrarCollection.ts +++ b/hooks/useNftRegistrarCollection.ts @@ -1,14 +1,12 @@ -import useVotePluginsClientStore from 'stores/useVotePluginsClientStore' import { useRealmConfigQuery } from './queries/realmConfig' import { useMemo } from 'react' import { NFT_PLUGINS_PKS } from '@constants/plugins' +import {useNftRegistrar} from "@hooks/useNftRegistrar"; export const useNftRegistrarCollection = () => { const config = useRealmConfigQuery().data?.result + const nftMintRegistrar = useNftRegistrar(); const currentPluginPk = config?.account.communityTokenConfig.voterWeightAddin - const [nftMintRegistrar] = useVotePluginsClientStore((s) => [ - s.state.nftMintRegistrar, - ]) return useMemo( () => diff --git a/hooks/useProposalVotes.tsx b/hooks/useProposalVotes.tsx index 5113b42bd9..d83283b679 100644 --- a/hooks/useProposalVotes.tsx +++ b/hooks/useProposalVotes.tsx @@ -10,21 +10,28 @@ import { useRealmCouncilMintInfoQuery, } from './queries/mintInfo' import { useGovernanceByPubkeyQuery } from './queries/governance' +import usePythScalingFactor from './PythNetwork/useScalingFactor' +import useParclScalingFactor from './parcl/useScalingFactor' // TODO support council plugins export default function useProposalVotes(proposal?: Proposal) { + const realm = useRealmQuery().data?.result const mint = useRealmCommunityMintInfoQuery().data?.result const councilMint = useRealmCouncilMintInfoQuery().data?.result const maxVoteRecord = useMaxVoteRecord() const governance = useGovernanceByPubkeyQuery(proposal?.governance).data ?.result?.account + // This is always undefined except for Pyth + const pythScalingFactor: number | undefined = usePythScalingFactor(); + // This is always undefined except for parcl + const parclScalingFactor: number | undefined = useParclScalingFactor(); const programVersion = useProgramVersion() const proposalMint = proposal?.governingTokenMint.toBase58() === - realm?.account.communityMint.toBase58() + realm?.account.communityMint.toBase58() ? mint : councilMint // TODO: optimize using memo @@ -52,8 +59,8 @@ export default function useProposalVotes(proposal?: Proposal) { ? governance.config.communityVoteThreshold.value : 0 : programVersion > 2 - ? governance.config.councilVoteThreshold.value || 0 - : governance.config.communityVoteThreshold.value || 0 + ? governance.config.councilVoteThreshold.value || 0 + : governance.config.communityVoteThreshold.value || 0 if (voteThresholdPct === undefined) throw new Error( @@ -100,12 +107,12 @@ export default function useProposalVotes(proposal?: Proposal) { voteThresholdPct, yesVotePct, yesVoteProgress, - yesVoteCount, - noVoteCount, + yesVoteCount: Math.floor(yesVoteCount * (pythScalingFactor || parclScalingFactor || 1)), + noVoteCount: Math.floor(noVoteCount * (pythScalingFactor || parclScalingFactor || 1)), relativeYesVotes, relativeNoVotes, minimumYesVotes, - yesVotesRequired, + yesVotesRequired: yesVotesRequired * (pythScalingFactor || parclScalingFactor || 1), } // @asktree: you may be asking yourself, "is this different from the more succinct way to write this?" @@ -166,11 +173,11 @@ export default function useProposalVotes(proposal?: Proposal) { const vetoMaxVoteWeight = isPluginCommunityVeto ? maxVoteRecord.account.maxVoterWeight : getProposalMaxVoteWeight( - realm.account, - proposal, - vetoMintInfo, - vetoMintPk - ) + realm.account, + proposal, + vetoMintInfo, + vetoMintPk + ) const vetoVoteProgress = calculatePct( proposal.vetoVoteWeight, diff --git a/hooks/useRealm.tsx b/hooks/useRealm.tsx index 14b4153f42..8840ee1183 100644 --- a/hooks/useRealm.tsx +++ b/hooks/useRealm.tsx @@ -8,10 +8,6 @@ import { useUserCouncilTokenOwnerRecord, } from './queries/tokenOwnerRecord' import { useRealmConfigQuery } from './queries/realmConfig' -import { - useRealmCommunityMintInfoQuery, - useRealmCouncilMintInfoQuery, -} from './queries/mintInfo' import { useSelectedRealmInfo } from './selectedRealm/useSelectedRealmRegistryEntry' import { useUserTokenAccountsQuery } from './queries/tokenAccount' @@ -27,9 +23,6 @@ export default function useRealm() { const realmInfo = useSelectedRealmInfo() const config = useRealmConfigQuery().data?.result - const mint = useRealmCommunityMintInfoQuery().data?.result - const councilMint = useRealmCouncilMintInfoQuery().data?.result - const currentPluginPk = config?.account?.communityTokenConfig.voterWeightAddin const ownTokenRecord = useUserCommunityTokenOwnerRecord().data?.result @@ -55,13 +48,6 @@ export default function useRealm() { [realm, tokenAccounts] ) - const canChooseWhoVote = - realm?.account.communityMint && - (!mint?.supply.isZero() || - config?.account.communityTokenConfig.voterWeightAddin) && - realm.account.config.councilMint && - !councilMint?.supply.isZero() - //TODO take from realm config when available const realmCfgMaxOutstandingProposalCount = 10 const toManyCommunityOutstandingProposalsForUser = @@ -100,7 +86,6 @@ export default function useRealm() { /** @deprecated just use the token owner record directly, ok? */ //ownVoterWeight, //realmDisplayName: realmInfo?.displayName ?? realm?.account?.name, - canChooseWhoVote, //councilTokenOwnerRecords, toManyCouncilOutstandingProposalsForUse, toManyCommunityOutstandingProposalsForUser, @@ -111,7 +96,6 @@ export default function useRealm() { isNftMode, }), [ - canChooseWhoVote, councilTokenAccount, currentPluginPk, isNftMode, diff --git a/hooks/useRealmVoterWeightPlugins.ts b/hooks/useRealmVoterWeightPlugins.ts new file mode 100644 index 0000000000..46b593b8e1 --- /dev/null +++ b/hooks/useRealmVoterWeightPlugins.ts @@ -0,0 +1,149 @@ +// Exposes a 'realms-friendly' version of the generic useVoterWeightPlugins hook, +// which knows how to get the current realm, governance mint, and wallet public keys +// this simplifies usage across the realms codebase +import { useVoterWeightPlugins } from '../VoterWeightPlugins' +import { useRealmQuery } from '@hooks/queries/realm' +import useWalletOnePointOh from '@hooks/useWalletOnePointOh' +import { GovernanceRole } from '../@types/types' +import { useSelectedDelegatorStore } from '../stores/useSelectedDelegatorStore' +import { UseVoterWeightPluginsReturnType } from '../VoterWeightPlugins/useVoterWeightPlugins' +import { PublicKey } from '@solana/web3.js' +import { CalculatedWeight } from '../VoterWeightPlugins/lib/types' +import useDelegators from '@components/VotePanel/useDelegators' +import {BN_ZERO} from "@solana/spl-governance"; +import {TokenOwnerRecord} from "@solana/spl-governance/lib/governance/accounts"; +import {SignerWalletAdapter} from "@solana/wallet-adapter-base"; + +export type UseRealmVoterWeightPluginsReturnType = UseVoterWeightPluginsReturnType & { + totalCalculatedVoterWeight: CalculatedWeight | undefined, + ownVoterWeight: CalculatedWeight | undefined + voterWeightForWallet: (walletPublicKey: PublicKey) => CalculatedWeight | undefined + voterWeightPkForWallet: (walletPublicKey: PublicKey) => PublicKey | undefined +} + +/** + * Select the wallets to determine the voter weights for as follows: + * - If a delegator is selected, use it only + * - If delegators are available, use them and the connected wallet (in first position) + * - If no delegators are available, use the connected wallet only + * @param selectedDelegator + * @param delegators + * @param wallet + */ +const getWalletList = ( + selectedDelegator: PublicKey | undefined, + delegators: TokenOwnerRecord[] | undefined, + wallet: SignerWalletAdapter | undefined +): PublicKey[] => { + if (!wallet?.publicKey) return []; + + // if selectedDelegator is not set, this means "yourself + all delegators" + if (selectedDelegator) { + return [selectedDelegator] + } + + if (delegators) { + const delegatorOwners = delegators.map((d) => d.governingTokenOwner) + + return [ + wallet.publicKey, + ...delegatorOwners + ] + } + + return [wallet.publicKey]; +} + +export const useRealmVoterWeightPlugins = ( + role: GovernanceRole = 'community' +): UseRealmVoterWeightPluginsReturnType => { + const realm = useRealmQuery().data?.result + const wallet = useWalletOnePointOh() + const governanceMintPublicKey = + role === 'community' + ? realm?.account.communityMint + : realm?.account.config.councilMint + const selectedDelegator = useSelectedDelegatorStore((s) => + role === 'community' ? s.communityDelegator : s.councilDelegator + ) + + const mainWalletPk = selectedDelegator || wallet?.publicKey + + const delegators = useDelegators(role) + const walletPublicKeys = getWalletList( + selectedDelegator, + delegators?.map(programAccount => programAccount.account), + wallet + ); + + // if a delegator is selected, use it, otherwise use the currently connected wallet + const nonAggregatedResult = useVoterWeightPlugins({ + realmPublicKey: realm?.pubkey, + governanceMintPublicKey, + walletPublicKeys, + realmConfig: realm?.account.config, + }) + + const totalCalculatedVoterWeight = nonAggregatedResult.calculatedVoterWeights?.length ? nonAggregatedResult.calculatedVoterWeights?.reduce( + (acc, weight) => { + if (!acc) return weight; + + const initialValue = weight.initialValue === null ? (acc.initialValue === null ? null : acc.initialValue) : weight.initialValue.add(acc.initialValue ?? BN_ZERO); + const value = weight.value === null ? (acc.value === null ? null : acc.value) : weight.value.add(acc.value ?? BN_ZERO); + // Note - voter-specific details (e.g. plugin weights) are not aggregated and just use the first one + const details = acc.details + + return { + details, + initialValue, + value, + } + } + ) : undefined; + + + // This requires that the index of the wallet in the list of wallets remains consistent with the output voter weights, + // while not ideal, this is simpler than the alternative, which would be to return a map of wallet public keys to voter weights + // or something similar. + const voterWeightForWallet = (walletPublicKey: PublicKey): CalculatedWeight | undefined => { + const walletIndex = walletPublicKeys.findIndex((pk) => pk.equals(walletPublicKey)) + if (walletIndex === -1) return undefined; // the wallet is not one of the ones passed in + return nonAggregatedResult.calculatedVoterWeights?.[walletIndex] + } + + const voterWeightPkForWallet = (walletPublicKey: PublicKey): PublicKey | undefined => { + const walletIndex = walletPublicKeys.findIndex((pk) => pk.equals(walletPublicKey)) + if (walletIndex === -1) return undefined; // the wallet is not one of the ones passed in + return nonAggregatedResult.voterWeightPks?.[walletIndex] + } + + const ownVoterWeight = mainWalletPk ? voterWeightForWallet(mainWalletPk) : undefined + + return { + ...nonAggregatedResult, + totalCalculatedVoterWeight, + ownVoterWeight, + voterWeightForWallet, + voterWeightPkForWallet, + } +} + +// Get the current weights for the community and council governances - should be used in cases where the realm is known but the choice of governance is not, +// e.g. when creating a proposal +export const useRealmVoterWeights = () => { + const { + calculatedMaxVoterWeight: communityMaxWeight, + totalCalculatedVoterWeight: communityWeight, + } = useRealmVoterWeightPlugins('community') + const { + calculatedMaxVoterWeight: councilMaxWeight, + totalCalculatedVoterWeight: councilWeight, + } = useRealmVoterWeightPlugins('council') + + return { + communityMaxWeight, + communityWeight, + councilMaxWeight, + councilWeight, + } +} diff --git a/hooks/useSubmitVote.ts b/hooks/useSubmitVote.ts index 7b2fd28789..2571fe2a50 100644 --- a/hooks/useSubmitVote.ts +++ b/hooks/useSubmitVote.ts @@ -1,6 +1,5 @@ import useWalletOnePointOh from './useWalletOnePointOh' import useRealm from './useRealm' -import useVotePluginsClientStore from 'stores/useVotePluginsClientStore' import useNftProposalStore from 'NftVotePlugin/NftProposalStore' import { useAsyncCallback } from 'react-async-hook' import { @@ -22,34 +21,29 @@ import { castVote } from 'actions/castVote' import { NftVoterClient } from '@utils/uiTypes/NftVoterClient' import { notify } from '@utils/notifications' import { useRealmQuery } from './queries/realm' -import { useRealmConfigQuery } from './queries/realmConfig' import { proposalQueryKeys, useRouteProposalQuery } from './queries/proposal' import useLegacyConnectionContext from './useLegacyConnectionContext' -import { NFT_PLUGINS_PKS } from '@constants/plugins' import { TransactionInstruction } from '@solana/web3.js' import useProgramVersion from './useProgramVersion' import useVotingTokenOwnerRecords from './useVotingTokenOwnerRecords' import { useMemo } from 'react' -import { useTokenOwnerRecordsDelegatedToUser } from './queries/tokenOwnerRecord' import { useSelectedDelegatorStore } from 'stores/useSelectedDelegatorStore' +import { useBatchedVoteDelegators } from '@components/VotePanel/useDelegators' +import { useVotingClients } from '@hooks/useVotingClients' +import { useNftClient } from '../VoterWeightPlugins/useNftClient' +import { useRealmVoterWeightPlugins } from './useRealmVoterWeightPlugins' export const useSubmitVote = () => { const wallet = useWalletOnePointOh() const connection = useLegacyConnectionContext() const realm = useRealmQuery().data?.result - const config = useRealmConfigQuery().data?.result const proposal = useRouteProposalQuery().data?.result const { realmInfo } = useRealm() const { closeNftVotingCountingModal } = useNftProposalStore.getState() - const client = useVotePluginsClientStore( - (s) => s.state.currentRealmVotingClient - ) + const votingClients = useVotingClients() // TODO this should be passed the role + const { nftClient } = useNftClient() - const isNftPlugin = - config?.account.communityTokenConfig.voterWeightAddin && - NFT_PLUGINS_PKS.includes( - config?.account.communityTokenConfig.voterWeightAddin?.toBase58() - ) + const isNftPlugin = !!nftClient const selectedCommunityDelegator = useSelectedDelegatorStore( (s) => s.communityDelegator @@ -57,7 +51,15 @@ export const useSubmitVote = () => { const selectedCouncilDelegator = useSelectedDelegatorStore( (s) => s.councilDelegator ) - const delegators = useTokenOwnerRecordsDelegatedToUser() + const communityDelegators = useBatchedVoteDelegators('community') + const councilDelegators = useBatchedVoteDelegators('council') + + const { + voterWeightForWallet: voterWeightForWalletCommunity, + } = useRealmVoterWeightPlugins('community') + const { + voterWeightForWallet: voterWeightForWalletCouncil, + } = useRealmVoterWeightPlugins('council') const { error, loading, execute } = useAsyncCallback( async ({ @@ -125,15 +127,22 @@ export const useSubmitVote = () => { actingAsWalletPk ) - const relevantDelegators = - // if the user is manually selecting a delegator, don't auto-vote for the rest of the delegators - // ("delegator" is a slight misnomer here since you can select yourself, so that you dont vote with your other delegators) - relevantSelectedDelegator !== undefined - ? [] - : delegators - ?.filter((x) => x.account.governingTokenMint.equals(relevantMint)) - .map((x) => x.pubkey) + const relevantDelegators = (role === 'community' + ? communityDelegators + : councilDelegators + )?.map((x) => x.pubkey) + const voterWeightForWallet = + role === 'community' + ? voterWeightForWalletCommunity + : voterWeightForWalletCouncil + const ownVoterWeight = relevantSelectedDelegator + ? voterWeightForWallet(relevantSelectedDelegator) + : wallet?.publicKey + ? voterWeightForWallet(wallet?.publicKey) + : undefined + + const votingClient = votingClients(role) try { await castVote( rpcContext, @@ -142,10 +151,11 @@ export const useSubmitVote = () => { tokenOwnerRecordPk, vote, msg, - role === 'community' ? client : undefined, // NOTE: currently FE doesn't support council plugins fully + votingClient, confirmationCallback, voteWeights, - relevantDelegators + relevantDelegators, + ownVoterWeight?.value ) queryClient.invalidateQueries({ queryKey: proposalQueryKeys.all(connection.current.rpcEndpoint), @@ -157,10 +167,13 @@ export const useSubmitVote = () => { } catch (e) { console.error(e) notify({ type: 'error', message: e.message }) + if (msg) { + throw e + } } finally { if (isNftPlugin) { closeNftVotingCountingModal( - client.client as NftVoterClient, + votingClient.client as NftVoterClient, proposal!, wallet!.publicKey! ) @@ -191,10 +204,8 @@ export const useCreateVoteIxs = () => { const programVersion = useProgramVersion() const realm = useRealmQuery().data?.result const wallet = useWalletOnePointOh() - const votingPluginClient = useVotePluginsClientStore( - (s) => s.state.currentRealmVotingClient - ) const getVotingTokenOwnerRecords = useVotingTokenOwnerRecords() + const votingClients = useVotingClients() // get delegates @@ -207,7 +218,6 @@ export const useCreateVoteIxs = () => { walletPk !== undefined ? // eslint-disable-next-line @typescript-eslint/no-unused-vars async ({ voteKind, governingBody, proposal, comment }: VoteArgs) => { - //const signers: Keypair[] = [] const instructions: TransactionInstruction[] = [] const governingTokenMint = @@ -217,12 +227,13 @@ export const useCreateVoteIxs = () => { if (governingTokenMint === undefined) throw new Error(`no mint for ${governingBody} governing body`) + const votingClient = votingClients(governingBody) const vote = formatVote(voteKind) const votingTors = await getVotingTokenOwnerRecords(governingBody) for (const torPk of votingTors) { //will run only if any plugin is connected with realm - const votingPluginHelpers = await votingPluginClient?.withCastPluginVote( + const votingPluginHelpers = await votingClient.withCastPluginVote( instructions, proposal, torPk @@ -249,13 +260,7 @@ export const useCreateVoteIxs = () => { } } : undefined, - [ - getVotingTokenOwnerRecords, - programVersion, - realm, - votingPluginClient, - walletPk, - ] + [getVotingTokenOwnerRecords, programVersion, realm, votingClients, walletPk] ) } diff --git a/hooks/useTotalTreasuryPrice.ts b/hooks/useTotalTreasuryPrice.ts index 9615116dcb..e56ba44bb7 100644 --- a/hooks/useTotalTreasuryPrice.ts +++ b/hooks/useTotalTreasuryPrice.ts @@ -6,6 +6,7 @@ import { useJupiterPricesByMintsQuery } from './queries/jupiterPrice' import { PublicKey } from '@metaplex-foundation/js' import { WSOL_MINT } from '@components/instructions/tools' import { AccountType } from '@utils/uiTypes/assets' +import { useMangoAccountsTreasury } from './useMangoAccountsTreasury' export function useTotalTreasuryPrice() { const { @@ -26,6 +27,10 @@ export function useTotalTreasuryPrice() { new PublicKey(WSOL_MINT), ]) + const { mangoAccountsValue, isFetching } = useMangoAccountsTreasury( + assetAccounts + ) + const totalTokensPrice = [ ...governedTokenAccountsWithoutNfts, ...auxiliaryTokenAccounts, @@ -57,11 +62,13 @@ export function useTotalTreasuryPrice() { const totalPrice = totalTokensPrice + stakeAccountsTotalPrice - const totalPriceFormatted = governedTokenAccountsWithoutNfts.length - ? new BigNumber(totalPrice).toFormat(0) - : '' + const totalPriceFormatted = (governedTokenAccountsWithoutNfts.length + ? new BigNumber(totalPrice) + : new BigNumber(0) + ).plus(mangoAccountsValue) return { + isFetching, totalPriceFormatted, } } diff --git a/hooks/useTreasuryInfo/assembleWallets.tsx b/hooks/useTreasuryInfo/assembleWallets.tsx index 95ec2a22aa..bf6c72f61b 100644 --- a/hooks/useTreasuryInfo/assembleWallets.tsx +++ b/hooks/useTreasuryInfo/assembleWallets.tsx @@ -9,7 +9,14 @@ import { import { SparklesIcon } from '@heroicons/react/outline' import { AssetAccount, AccountType } from '@utils/uiTypes/assets' -import { AssetType, Token, RealmAuthority } from '@models/treasury/Asset' +import { + AssetType, + Token, + RealmAuthority, + Asset, + Mango, + Sol, +} from '@models/treasury/Asset' import { AuxiliaryWallet, Wallet } from '@models/treasury/Wallet' import { getAccountName } from '@components/instructions/tools' import { RealmInfo } from '@models/registry/api' @@ -26,16 +33,79 @@ import { Domain } from '@models/treasury/Domain' import { groupDomainsByWallet } from './groupDomainsByWallet' import { ConnectionContext } from '@utils/connection' import { PublicKey } from '@solana/web3.js' +import { + MangoClient, + Group, + MangoAccount, + I80F48, + toUiDecimals, +} from '@blockworks-foundation/mango-v4' +import { BN } from '@coral-xyz/anchor' function isNotNull(x: T | null): x is T { return x !== null } +export async function fetchMangoAccounts( + assets: Asset[], + mangoClient: MangoClient | null, + mangoGroup: Group | null +) { + if (!mangoClient || !mangoGroup) { + return { + mangoAccountsValue: new BigNumber(0), + mangoAccounts: [], + } + } + + const tokenAccountOwners = Array.from( + new Set( + assets + .filter((a) => a.type === AssetType.Token) + .filter((a: Token) => a.raw.extensions.token) + .filter((a: Token) => a.raw.extensions.token!.account.owner) + .map((a: Token) => a.raw.extensions.token!.account.owner.toString()) + ) + ).map((o) => new PublicKey(o)) + + const mangoAccounts: MangoAccount[] = [] + if (tokenAccountOwners.length <= 2) { + for (const tokenAccountOwner of tokenAccountOwners) { + const accounts = await mangoClient.getMangoAccountsForOwner( + mangoGroup, + tokenAccountOwner + ) + + if (accounts) { + mangoAccounts.push(...accounts) + } + } + } + + const mangoAccountsValue = mangoAccounts.reduce((acc: I80F48, account) => { + try { + const value = account.getAssetsValue(mangoGroup!) + acc = acc.add(value) + return acc + } catch (e) { + console.log(e) + return acc + } + }, new I80F48(new BN(0))) + + return { + mangoAccountsValue: new BigNumber(toUiDecimals(mangoAccountsValue, 6)), + mangoAccounts, + } +} + export const assembleWallets = async ( connection: ConnectionContext, accounts: AssetAccount[], domains: Domain[], programId: PublicKey, + mangoGroup: Group | null, + mangoClient: MangoClient | null, councilMintAddress?: string, communityMintAddress?: string, councilMint?: MintInfo, @@ -172,7 +242,7 @@ export const assembleWallets = async ( } } - walletMap[walletAddress].assets.push({ + walletMap[walletAddress].assets.unshift({ type: AssetType.Domain, id: 'domain-list', count: new BigNumber(domainList.length), @@ -180,8 +250,23 @@ export const assembleWallets = async ( }) } - const allWallets = Object.values(walletMap) - .map((wallet) => ({ + const allWallets: any[] = [] + for (const wallet of Object.values(walletMap)) { + const { mangoAccountsValue } = await fetchMangoAccounts( + wallet.assets, + mangoClient, + mangoGroup + ) + + if (mangoAccountsValue.gt(0)) { + wallet.assets.push({ + id: 'mango', + type: AssetType.Mango, + value: mangoAccountsValue, + }) + } + + allWallets.push({ ...wallet, name: wallet.governanceAddress ? getAccountName(wallet.governanceAddress) @@ -191,27 +276,29 @@ export const assembleWallets = async ( 'value' in asset ? asset.value : new BigNumber(0) ) ), - })) - .sort((a, b) => { - if (a.totalValue.isZero() && b.totalValue.isZero()) { - const aContainsSortable = a.assets.some( - (asset) => asset.type === AssetType.Programs - ) - const bContainsSortable = b.assets.some( - (asset) => asset.type === AssetType.Programs - ) + }) + } - if (aContainsSortable && !bContainsSortable) { - return -1 - } else if (!aContainsSortable && bContainsSortable) { - return 1 - } else { - return b.assets.length - a.assets.length - } + allWallets.sort((a, b) => { + if (a.totalValue.isZero() && b.totalValue.isZero()) { + const aContainsSortable = a.assets.some( + (asset) => asset.type === AssetType.Programs + ) + const bContainsSortable = b.assets.some( + (asset) => asset.type === AssetType.Programs + ) + + if (aContainsSortable && !bContainsSortable) { + return -1 + } else if (!aContainsSortable && bContainsSortable) { + return 1 + } else { + return b.assets.length - a.assets.length } + } - return b.totalValue.comparedTo(a.totalValue) - }) + return b.totalValue.comparedTo(a.totalValue) + }) const auxiliaryAssets = ( await Promise.all( @@ -220,11 +307,23 @@ export const assembleWallets = async ( convertAccountToAsset({ ...account, type: AccountType.TOKEN, - }) as Promise + }) as Promise ) ) ).filter(isNotNull) + const { + mangoAccountsValue: auxMangoAccountsValue, + } = await fetchMangoAccounts(auxiliaryAssets, mangoClient!, mangoGroup!) + + if (auxMangoAccountsValue.gt(0)) { + auxiliaryAssets.unshift({ + id: 'mango', + type: AssetType.Mango, + value: auxMangoAccountsValue, + }) + } + const auxiliaryWallets: AuxiliaryWallet[] = auxiliaryAssets.length ? [ { diff --git a/hooks/useTreasuryInfo/getDomains.ts b/hooks/useTreasuryInfo/getDomains.ts index 3d3133ed64..06be01ee72 100644 --- a/hooks/useTreasuryInfo/getDomains.ts +++ b/hooks/useTreasuryInfo/getDomains.ts @@ -10,19 +10,23 @@ const getAccountDomains = async ( account: AssetAccount, connection: Connection ): Promise => { - const domains = await getAllDomains(connection, account.pubkey) + try { + const domains = await getAllDomains(connection, account.pubkey) - if (!domains.length) { - return [] - } + if (!domains.length) { + return [] + } - const reverse = await performReverseLookupBatch(connection, domains) + const reverse = await performReverseLookupBatch(connection, domains) - return domains.map((domain, index) => ({ - name: reverse[index], - address: domain.toBase58(), - owner: account.pubkey.toBase58(), - })) + return domains.map((domain, index) => ({ + name: reverse[index], + address: domain.toBase58(), + owner: account.pubkey.toBase58(), + })) + } catch (e) { + return [] + } } export const getDomains = async ( diff --git a/hooks/useTreasuryInfo/index.tsx b/hooks/useTreasuryInfo/index.tsx index e0d00b604f..b9f3d8b8a0 100644 --- a/hooks/useTreasuryInfo/index.tsx +++ b/hooks/useTreasuryInfo/index.tsx @@ -17,6 +17,8 @@ import { useRealmCouncilMintInfoQuery, } from '@hooks/queries/mintInfo' import useLegacyConnectionContext from '@hooks/useLegacyConnectionContext' +import UseMangoV4 from '@hooks/useMangoV4' +import useProgramSelector from '@components/Mango/useProgramSelector' interface Data { auxiliaryWallets: AuxiliaryWallet[] @@ -41,6 +43,12 @@ export default function useTreasuryInfo( const connection = useLegacyConnectionContext() const accounts = useGovernanceAssetsStore((s) => s.assetAccounts) + const programSelectorHook = useProgramSelector() + const { mangoClient, mangoGroup } = UseMangoV4( + programSelectorHook.program?.val, + programSelectorHook.program?.group + ) + const loadingGovernedAccounts = useGovernanceAssetsStore( (s) => s.loadGovernedAccounts ) @@ -84,6 +92,8 @@ export default function useTreasuryInfo( accounts, domains, realmInfo.programId, + mangoGroup, + mangoClient, realm?.account.config.councilMint?.toBase58(), realm?.account.communityMint?.toBase58(), councilMint, diff --git a/hooks/useVoteByCouncilToggle.ts b/hooks/useVoteByCouncilToggle.ts new file mode 100644 index 0000000000..7c17104d0b --- /dev/null +++ b/hooks/useVoteByCouncilToggle.ts @@ -0,0 +1,65 @@ +// The voteByCouncil toggle UI is avaiable on a number of views in Realms. +// Whether it is available, or enabled, is determined by the realm's config and the user's tokens. +// This hook encapsulates this logic +import { useEffect, useState } from 'react' +import { useRealmVoterWeights } from '@hooks/useRealmVoterWeightPlugins' +import BN from 'bn.js' +import { GovernanceRole } from '../@types/types' + +const onlyGovernanceAvailable = ( + availableVoteGovernanceOptions: GovernanceRole[], + role: GovernanceRole +) => + availableVoteGovernanceOptions.length === 1 && + availableVoteGovernanceOptions[0] === role + +type UseVoteByCouncilToggleValue = { + // Allow the UI to decide whether the toggle should be shown + // False if there is only one option + // NOTE: ignore this if you always want to show the toggle if the user has council tokens (even if they don't have community tokens) + shouldShowVoteByCouncilToggle: boolean + // The value of the UI toggle + // True if this proposal should be voted by council + voteByCouncil: boolean + // Set the value of the UI toggle + setVoteByCouncil: (value: boolean) => void +} + +export const useVoteByCouncilToggle = (): UseVoteByCouncilToggleValue => { + const { communityMaxWeight, councilMaxWeight } = useRealmVoterWeights() + const availableVoteGovernanceOptions = [ + communityMaxWeight?.value?.gt(new BN(0)) ? 'community' : undefined, + councilMaxWeight?.value?.gt(new BN(0)) ? 'council' : undefined, + ].filter(Boolean) as GovernanceRole[] // filter out undefined + const [voteByCouncil, setVoteByCouncil] = useState(false) + + // once availableVoteGovernanceOptions is available, we set the default vote level (if there is only one option) + useEffect(() => { + if (onlyGovernanceAvailable(availableVoteGovernanceOptions, 'council')) { + setVoteByCouncil(true) + } else if ( + onlyGovernanceAvailable(availableVoteGovernanceOptions, 'community') + ) { + setVoteByCouncil(false) + } + }, [availableVoteGovernanceOptions]) + + // the proposal will use the council if: + // - that is the only option + // - the proposer chooses it + const updateVoteByCouncilToggle = (value: boolean) => { + // only set it to false if there is another option + if ( + value || + !onlyGovernanceAvailable(availableVoteGovernanceOptions, 'council') + ) { + setVoteByCouncil(value) + } + } + + return { + shouldShowVoteByCouncilToggle: availableVoteGovernanceOptions.length > 1, + voteByCouncil, + setVoteByCouncil: updateVoteByCouncilToggle, + } +} diff --git a/hooks/useVoteRecords.ts b/hooks/useVoteRecords.ts index dcb1599442..6eeb2c1500 100644 --- a/hooks/useVoteRecords.ts +++ b/hooks/useVoteRecords.ts @@ -14,7 +14,6 @@ import useRpcContext from '@hooks/useRpcContext' import { getVoteRecords, getTokenOwnerRecords } from '@models/proposal' import useRealm from '@hooks/useRealm' import { buildTopVoters } from '@models/proposal' -import useVotePluginsClientStore from 'stores/useVotePluginsClientStore' import { getLockTokensVotingPowerPerWallet } from 'VoteStakeRegistry/tools/deposits' import { BN } from '@coral-xyz/anchor' import useGovernanceAssetsStore from 'stores/useGovernanceAssetsStore' @@ -27,6 +26,8 @@ import { getNetworkFromEndpoint } from '@utils/connection' import { fetchDigitalAssetsByOwner } from './queries/digitalAssets' import { useNftRegistrarCollection } from './useNftRegistrarCollection' import { useAsync } from 'react-async-hook' +import {useVsrClient} from "../VoterWeightPlugins/useVsrClient"; +import {useNftRegistrar} from "@hooks/useNftRegistrar"; export default function useVoteRecords(proposal?: ProgramAccount) { const { getRpcContext } = useRpcContext() @@ -52,7 +53,7 @@ export default function useVoteRecords(proposal?: ProgramAccount) { /// const [context, setContext] = useState(null) - const client = useVotePluginsClientStore((s) => s.state.vsrClient) + const { vsrClient } = useVsrClient(); const connection = useLegacyConnectionContext() const governingTokenMintPk = proposal?.account.governingTokenMint @@ -61,9 +62,7 @@ export default function useVoteRecords(proposal?: ProgramAccount) { // In buildTopVoters.ts, it checks whether the token_owner_record is in the vote_record. // If not, the function use record.account.governingTokenDepositAmount as the undecided vote weight, where nft-voter should be 0. // Thus, pre-calculating the undecided weight for each nft voter is necessary. - const [nftMintRegistrar] = useVotePluginsClientStore((s) => [ - s.state.nftMintRegistrar, - ]) + const nftMintRegistrar = useNftRegistrar(); const usedCollectionsPks: string[] = useNftRegistrarCollection() const { result: undecidedNftsByVoteRecord } = useAsync(async () => { @@ -165,7 +164,7 @@ export default function useVoteRecords(proposal?: ProgramAccount) { const nftVoterPluginTotalWeight = nftMintRegistrar?.collectionConfigs.reduce( (prev, curr) => { const size = curr.size - const weight = curr.weight + const weight = curr.weight.toNumber() if (typeof size === 'undefined' || typeof weight === 'undefined') return prev return prev + size * weight @@ -177,7 +176,7 @@ export default function useVoteRecords(proposal?: ProgramAccount) { tokenOwnerRecords, mint, undecidedNftsByVoteRecord ?? {}, - new BN(nftVoterPluginTotalWeight) + new BN(nftVoterPluginTotalWeight ?? 0) ) } return [] @@ -196,12 +195,12 @@ export default function useVoteRecords(proposal?: ProgramAccount) { useEffect(() => { //VSR only const handleGetVsrVotingPowers = async (walletsPks: PublicKey[]) => { - if (!realm || !client) throw new Error() + if (!realm || !vsrClient) throw new Error() const votingPerWallet = await getLockTokensVotingPowerPerWallet( walletsPks, realm, - client, + vsrClient, connection.current ) setUndecidedDepositByVoteRecord(votingPerWallet) @@ -221,7 +220,7 @@ export default function useVoteRecords(proposal?: ProgramAccount) { tokenOwnerRecord.account.governingTokenOwner.toBase58() ) ) - if (undecidedData.length && mintsUsedInRealm.length && realm && client) { + if (undecidedData.length && mintsUsedInRealm.length && realm && vsrClient) { handleGetVsrVotingPowers( undecidedData.map((x) => x.account.governingTokenOwner) ) @@ -235,7 +234,7 @@ export default function useVoteRecords(proposal?: ProgramAccount) { tokenOwnerRecords, voteRecords, realm, - client, + vsrClient, connection, mintsUsedInRealm, ]) diff --git a/hooks/useVotingClients.ts b/hooks/useVotingClients.ts new file mode 100644 index 0000000000..e870a45058 --- /dev/null +++ b/hooks/useVotingClients.ts @@ -0,0 +1,60 @@ +import {useRealmVoterWeightPlugins} from "@hooks/useRealmVoterWeightPlugins"; +import {useRealmQuery} from "@hooks/queries/realm"; +import useWalletOnePointOh from "@hooks/useWalletOnePointOh"; +import {VotingClient} from "@utils/uiTypes/VotePlugin"; +import {GovernanceRole} from "../@types/types"; +import {PublicKey} from "@solana/web3.js"; +import {useSelectedDelegatorStore} from "../stores/useSelectedDelegatorStore"; + +/** + * The Voting Client encapsulates plugin-specific voting logic not currently encapsulated in the individual plugins, and exposed by the + * useVoterWeightPlugins hook. + * As such, it should be used only if the useVoterWeightPlugins hook is insufficient, or in places where access to hooks is not available. + * Since in the latter cases, it is not always clear which governance role to use, it exposes a callback to get the correct client for a given role. + */ +export const useVotingClients = () => { + const voterWeightPluginDetailsForCommunity = useRealmVoterWeightPlugins('community'); + const voterWeightPluginDetailsForCouncil = useRealmVoterWeightPlugins('council'); + const realm = useRealmQuery().data?.result + const wallet = useWalletOnePointOh() + + const selectedCouncilDelegator = useSelectedDelegatorStore( + (s) => s.councilDelegator + ) + const selectedCommunityDelegator = useSelectedDelegatorStore( + (s) => s.communityDelegator + ) + const councilWallet = selectedCouncilDelegator ?? wallet?.publicKey; + const communityWallet = selectedCommunityDelegator ?? wallet?.publicKey; + + + // This is not cached at present, but should be efficient, as the contents (plugins) are cached. + // If this becomes a performance issue, we should add react-query here. + return (kind: GovernanceRole) => { + // messy logic to get the "legacy" client out of the plugins. + // if there's more than one, use the first one. + // this only works if the legacy plugins don't support chaining anyway. + // if they did, then we would have to call relinquish on whichever plugin supported it + const voterWeightPluginDetails = kind === 'community' ? voterWeightPluginDetailsForCommunity : voterWeightPluginDetailsForCouncil; + const client = voterWeightPluginDetails.plugins?.voterWeight.length ? voterWeightPluginDetails.plugins.voterWeight[0].client : undefined; + const wallet = kind === 'community' ? communityWallet : councilWallet; + + return new VotingClient({ + client: client, + realm: realm, + walletPk: wallet, + voterWeightPluginDetails + }); + }; +} + +// If we know the governingTokenMint, we can deduce the role. +// This is a little convoluted, but necessary in places, until we decommission the voting client. +export const useVotingClientForGoverningTokenMint = (governingTokenMint: PublicKey | undefined) => { + const clients = useVotingClients(); + const realm = useRealmQuery().data?.result + // default to community if there is no council or the realm or governingTokenMint are not yet loaded + const kind = governingTokenMint && realm?.account.config.councilMint?.equals(governingTokenMint) ? 'council' : 'community'; + + return clients(kind); +} \ No newline at end of file diff --git a/hooks/useVotingPlugins.ts b/hooks/useVotingPlugins.ts deleted file mode 100644 index 9cafeae390..0000000000 --- a/hooks/useVotingPlugins.ts +++ /dev/null @@ -1,383 +0,0 @@ -import { useCallback, useEffect, useMemo } from 'react' -import useNftPluginStore from 'NftVotePlugin/store/nftPluginStore' -import useVotePluginsClientStore from 'stores/useVotePluginsClientStore' -import { getMaxVoterWeightRecord } from '@solana/spl-governance' -import { getMaxVoterWeightRecord as getPluginMaxVoterWeightRecord } from '@utils/plugin/accounts' -import { notify } from '@utils/notifications' - -import useGatewayPluginStore from '../GatewayPlugin/store/gatewayPluginStore' -import { getGatekeeperNetwork } from '../GatewayPlugin/sdk/accounts' -import { DasNftObject } from '@hooks/queries/digitalAssets' -import useHeliumVsrStore from 'HeliumVotePlugin/hooks/useHeliumVsrStore' -import * as heliumVsrSdk from '@helium/voter-stake-registry-sdk' -import useWalletOnePointOh from './useWalletOnePointOh' -import { useRealmQuery } from './queries/realm' -import { useRealmConfigQuery } from './queries/realmConfig' -import useLegacyConnectionContext from './useLegacyConnectionContext' -import { - NFT_PLUGINS_PKS, - HELIUM_VSR_PLUGINS_PKS, - VSR_PLUGIN_PKS, - GATEWAY_PLUGINS_PKS, -} from '../constants/plugins' -import useUserOrDelegator from './useUserOrDelegator' -import { getNetworkFromEndpoint } from '@utils/connection' -import { fetchDigitalAssetsByOwner } from './queries/digitalAssets' -import { ON_NFT_VOTER_V2, SUPPORT_CNFTS } from '@constants/flags' - -export function useVotingPlugins() { - const realm = useRealmQuery().data?.result - const config = useRealmConfigQuery().data?.result - const currentPluginPk = config?.account.communityTokenConfig.voterWeightAddin - const voterPk = useUserOrDelegator() - - const { - handleSetVsrRegistrar, - handleSetVsrClient, - handleSetHeliumVsrRegistrar, - handleSetHeliumVsrClient, - handleSetNftClient, - handleSetGatewayClient, - handleSetNftRegistrar, - handleSetGatewayRegistrar, - handleSetCurrentRealmVotingClient, - } = useVotePluginsClientStore() - - const [ - setIsLoadingNfts, - setNftMaxVoterWeight, - setVotingNfts, - ] = useNftPluginStore((s) => [ - s.setIsLoadingNfts, - s.setMaxVoterWeight, - s.setVotingNfts, - ]) - - // @asktree: you should select what you need from stores, not use entire thing - const heliumStore = useHeliumVsrStore() - const gatewayStore = useGatewayPluginStore() - const wallet = useWalletOnePointOh() - const connection = useLegacyConnectionContext() - const connected = !!wallet?.connected - - const [ - currentClient, - vsrClient, - gatewayClient, - nftClient, - nftMintRegistrar, - heliumVsrClient, - ] = useVotePluginsClientStore((s) => [ - s.state.currentRealmVotingClient, - s.state.vsrClient, - s.state.gatewayClient, - s.state.nftClient, - s.state.nftMintRegistrar, - s.state.heliumVsrClient, - s.state.heliumVsrRegistrar, - ]) - - const usedCollectionsPks: string[] = useMemo( - () => - (currentPluginPk && - NFT_PLUGINS_PKS.includes(currentPluginPk?.toBase58()) && - nftMintRegistrar?.collectionConfigs.map((x) => - x.collection.toBase58() - )) || - [], - [currentPluginPk, nftMintRegistrar?.collectionConfigs] - ) - - const handleRegisterGatekeeperNetwork = useCallback(async () => { - if (realm && gatewayClient) { - gatewayStore.setIsLoadingGatewayToken(true) - - try { - const gatekeeperNetwork = await getGatekeeperNetwork( - gatewayClient, - realm - ) - - gatewayStore.setGatekeeperNetwork(gatekeeperNetwork) - } catch (e) { - console.log(e) - notify({ - message: 'Error fetching gateway token', - type: 'error', - }) - } - gatewayStore.setIsLoadingGatewayToken(false) - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [ - gatewayClient, - //gatewayStore, - realm, - ]) - - const getIsFromCollection = useCallback( - (nft: DasNftObject) => { - const collection = nft.grouping.find((x) => x.group_key === 'collection') - return ( - (SUPPORT_CNFTS || !nft.compression.compressed) && - collection && - usedCollectionsPks.includes(collection.group_value) && - nft.creators?.filter((x) => x.verified).length > 0 - ) - }, - [usedCollectionsPks] - ) - - useEffect(() => { - if (wallet && connection) { - if (currentPluginPk) { - if (VSR_PLUGIN_PKS.includes(currentPluginPk.toBase58())) { - handleSetVsrClient(wallet, connection, currentPluginPk) - } - if (HELIUM_VSR_PLUGINS_PKS.includes(currentPluginPk.toBase58())) { - handleSetHeliumVsrClient(wallet, connection, currentPluginPk) - } - } - handleSetNftClient(wallet, connection) - handleSetGatewayClient(wallet, connection) - } - }, [ - connection, - currentPluginPk, - handleSetGatewayClient, - handleSetHeliumVsrClient, - handleSetNftClient, - handleSetVsrClient, - wallet, - ]) - - useEffect(() => { - const handleVsrPlugin = () => { - if ( - vsrClient && - currentPluginPk && - VSR_PLUGIN_PKS.includes(currentPluginPk.toBase58()) - ) { - handleSetVsrRegistrar(vsrClient, realm) - if (voterPk) { - handleSetCurrentRealmVotingClient({ - client: vsrClient, - realm, - walletPk: voterPk, - }) - } - } - } - - const handleHeliumVsrPlugin = () => { - if ( - heliumVsrClient && - currentPluginPk && - HELIUM_VSR_PLUGINS_PKS.includes(currentPluginPk.toBase58()) - ) { - handleSetHeliumVsrRegistrar(heliumVsrClient, realm) - if (voterPk) { - handleSetCurrentRealmVotingClient({ - client: heliumVsrClient, - realm, - walletPk: voterPk, - }) - } - } - } - - const handleNftplugin = () => { - if ( - nftClient && - currentPluginPk && - NFT_PLUGINS_PKS.includes(currentPluginPk.toBase58()) - ) { - handleSetNftRegistrar(nftClient, realm) - if (voterPk) { - handleSetCurrentRealmVotingClient({ - client: nftClient, - realm, - walletPk: voterPk, - }) - } - } - } - - // If the current realm uses Civic Pass - // register the gatekeeper network (the "type" of Civic) - // in the Civic GatewayProvider. - // This updates the UI to show if the user has a gateway token - const handleGatewayPlugin = () => { - if ( - gatewayClient && - currentPluginPk && - GATEWAY_PLUGINS_PKS.includes(currentPluginPk.toBase58()) - ) { - handleSetGatewayRegistrar(gatewayClient, realm) - if (voterPk) { - handleSetCurrentRealmVotingClient({ - client: gatewayClient, - realm, - walletPk: voterPk, - }) - } - - handleRegisterGatekeeperNetwork() - } - } - - if ( - realm && - (!currentClient || - currentClient.realm?.pubkey.toBase58() !== realm.pubkey.toBase58() || - (voterPk && currentClient.walletPk?.toBase58() !== voterPk.toBase58())) - ) { - console.log( - 'setting plugin; if this is getting spammed, this store just needs to be refactored away' - ) - handleNftplugin() - handleGatewayPlugin() - handleVsrPlugin() - handleHeliumVsrPlugin() - } - }, [ - currentClient, - currentPluginPk, - gatewayClient, - handleRegisterGatekeeperNetwork, - handleSetCurrentRealmVotingClient, - handleSetGatewayRegistrar, - handleSetHeliumVsrRegistrar, - handleSetNftRegistrar, - handleSetVsrRegistrar, - heliumVsrClient, - nftClient, - voterPk, - realm, - vsrClient, - ]) - - const handleMaxVoterWeight = useCallback(async () => { - if (!realm || !nftClient) return - - const { maxVoterWeightRecord } = await getPluginMaxVoterWeightRecord( - realm.pubkey, - realm.account.communityMint, - nftClient.program.programId - ) - try { - const existingMaxVoterRecord = await getMaxVoterWeightRecord( - connection.current, - maxVoterWeightRecord - ) - setNftMaxVoterWeight(existingMaxVoterRecord) - } catch (e) { - console.log(e) - setNftMaxVoterWeight(null) - } - }, [connection, nftClient, setNftMaxVoterWeight, realm]) - - const handleGetHeliumVsrVoting = useCallback(async () => { - if ( - realm && - currentPluginPk && - HELIUM_VSR_PLUGINS_PKS.includes(currentPluginPk.toBase58()) - ) { - const [maxVoterRecord] = heliumVsrSdk.maxVoterWeightRecordKey( - realm.pubkey, - realm.account.communityMint, - currentPluginPk - ) - try { - const mvwr = await getMaxVoterWeightRecord( - connection.current, - maxVoterRecord - ) - heliumStore.setMaxVoterWeight(mvwr) - } catch (_e) { - console.log("Couldn't get max voter weight record. Setting to null.") - heliumStore.setMaxVoterWeight(null) - } - - if (currentClient.walletPk && heliumVsrClient) { - try { - await heliumStore.getPositions({ - realmPk: realm.pubkey, - communityMintPk: realm.account.communityMint, - walletPk: currentClient.walletPk, - connection: connection.current, - client: heliumVsrClient, - votingClient: currentClient, - }) - } catch (e) { - console.log(e) - } - } - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [ - connection, - currentClient, - currentPluginPk, - //heliumStore, - heliumVsrClient, - realm, - ]) - - const handleGetNfts = useCallback(async () => { - setIsLoadingNfts(true) - if (!wallet?.publicKey) return - try { - // const nfts = await getNfts(wallet.publicKey, connection) - const network = getNetworkFromEndpoint(connection.endpoint) - if (network === 'localnet') throw new Error() - const nfts = await fetchDigitalAssetsByOwner(network, wallet.publicKey) - const votingNfts = nfts - .filter(getIsFromCollection) - .filter((x) => ON_NFT_VOTER_V2 || !x.compression.compressed) - const nftsWithMeta = votingNfts - setVotingNfts(nftsWithMeta, currentClient, nftMintRegistrar) - } catch (e) { - console.log(e) - notify({ - message: `Something went wrong can't fetch nfts: ${e}`, - type: 'error', - }) - } - setIsLoadingNfts(false) - }, [ - connection, - currentClient, - getIsFromCollection, - nftMintRegistrar, - setIsLoadingNfts, - setVotingNfts, - wallet?.publicKey, - ]) - - useEffect(() => { - if (usedCollectionsPks.length && realm) { - if (connected && currentClient.walletPk?.toBase58()) { - handleGetNfts() - } - handleMaxVoterWeight() - } else if (realm) { - handleGetHeliumVsrVoting() - } else { - setVotingNfts([], currentClient, nftMintRegistrar) - setNftMaxVoterWeight(null) - } - }, [ - connected, - currentClient, - currentPluginPk, - handleGetHeliumVsrVoting, - handleGetNfts, - handleMaxVoterWeight, - nftMintRegistrar, - realm, - setNftMaxVoterWeight, - setVotingNfts, - usedCollectionsPks.length, - ]) -} diff --git a/hooks/useVotingTokenOwnerRecords.ts b/hooks/useVotingTokenOwnerRecords.ts index d5712f1979..891b07e8ad 100644 --- a/hooks/useVotingTokenOwnerRecords.ts +++ b/hooks/useVotingTokenOwnerRecords.ts @@ -9,7 +9,7 @@ import { getTokenOwnerRecordAddress } from '@solana/spl-governance' * Namely, this would be the user + any delegates that are enabled (by default, they all are) */ const useVotingTokenOwnerRecords = () => { - const delegated = useTokenOwnerRecordsDelegatedToUser() + const { data: delegated } = useTokenOwnerRecordsDelegatedToUser() const realm = useRealmQuery().data?.result const wallet = useWalletOnePointOh() diff --git a/hooks/useVsrMode.ts b/hooks/useVsrMode.ts index 39005bf032..7134513c48 100644 --- a/hooks/useVsrMode.ts +++ b/hooks/useVsrMode.ts @@ -1,8 +1,8 @@ import { useMemo } from 'react' -import { HELIUM_VSR_PLUGINS_PKS, VSR_PLUGIN_PKS } from '../constants/plugins' +import { HELIUM_VSR_PLUGINS_PKS, PYTH_PLUGIN_PK, VSR_PLUGIN_PKS } from '../constants/plugins' import { useRealmConfigQuery } from './queries/realmConfig' -export const useVsrMode = (): undefined | 'default' | 'helium' => { +export const useVsrMode = (): undefined | 'default' | 'helium' | 'pyth' => { const config = useRealmConfigQuery().data?.result const mode = useMemo(() => { const currentPluginPk = @@ -11,6 +11,7 @@ export const useVsrMode = (): undefined | 'default' | 'helium' => { if (VSR_PLUGIN_PKS.includes(currentPluginPk?.toBase58())) return 'default' if (HELIUM_VSR_PLUGINS_PKS.includes(currentPluginPk?.toBase58())) return 'helium' + if (PYTH_PLUGIN_PK.includes(currentPluginPk?.toBase58())) return 'pyth' }, [config?.account?.communityTokenConfig]) return mode diff --git a/hub/components/EditMetadata/EditForms/common/FieldIconPreview.tsx b/hub/components/EditMetadata/EditForms/common/FieldIconPreview.tsx index c5e87709b8..1038dc26e9 100644 --- a/hub/components/EditMetadata/EditForms/common/FieldIconPreview.tsx +++ b/hub/components/EditMetadata/EditForms/common/FieldIconPreview.tsx @@ -18,8 +18,10 @@ export function FieldIconPreview(props: Props) { useEffect(() => { setIsValid(true); }, [setIsValid, props.url]); - - if (props.url?.includes('www.youtube.com') && props.allowYoutube) { + if ( + props.url?.startsWith('youtube.com/') || + (props.url?.startsWith('youtu.be/') && props.allowYoutube) + ) { return (

diff --git a/hub/components/EditMetadata/EditForms/index.tsx b/hub/components/EditMetadata/EditForms/index.tsx index 2d7d7c1522..b84af1b6c2 100644 --- a/hub/components/EditMetadata/EditForms/index.tsx +++ b/hub/components/EditMetadata/EditForms/index.tsx @@ -134,7 +134,10 @@ async function enhanceData(data: Realm): Promise { newRealm.gallery = await Promise.all( newRealm.gallery.map(async (image) => { - if (image.url.includes('youtube.com')) { + if ( + image.url.startsWith('youtube.com/') || + image.url.startsWith('youtu.be/') + ) { return removeTypename({ ...image, height: 448, width: 800 }); } diff --git a/hub/components/EditRealmConfig/CommunityStructure/index.tsx b/hub/components/EditRealmConfig/CommunityStructure/index.tsx index d1395f9328..5c34732493 100644 --- a/hub/components/EditRealmConfig/CommunityStructure/index.tsx +++ b/hub/components/EditRealmConfig/CommunityStructure/index.tsx @@ -1,5 +1,6 @@ import EventsIcon from '@carbon/icons-react/lib/Events'; import WarningFilledIcon from '@carbon/icons-react/lib/WarningFilled'; +import { Coefficients } from '@solana/governance-program-library'; import { GoverningTokenType } from '@solana/spl-governance'; import type { PublicKey } from '@solana/web3.js'; import BigNumber from 'bignumber.js'; @@ -9,6 +10,7 @@ import { produce } from 'immer'; import { Config } from '../fetchConfig'; import { TokenTypeSelector } from '../TokenTypeSelector'; import { VotingStructureSelector } from '../VotingStructureSelector'; +import { useRealmVoterWeightPlugins } from '@hooks/useRealmVoterWeightPlugins'; import { ButtonToggle } from '@hub/components/controls/ButtonToggle'; import { Input } from '@hub/components/controls/Input'; import { MAX_NUM } from '@hub/components/EditWalletRules/constants'; @@ -25,16 +27,23 @@ interface Props nftCollection?: PublicKey; nftCollectionSize: number; nftCollectionWeight: BN; + civicPassType: Config['civicPassType']; + qvCoefficients?: Coefficients; + chainingEnabled: boolean; }> { currentConfigAccount: Config['configAccount']; currentNftCollection?: PublicKey; currentNftCollectionSize: number; currentNftCollectionWeight: BN; + currentCivicPassType: Config['civicPassType']; communityMint: Config['communityMint']; className?: string; } export function CommunityStructure(props: Props) { + const { plugins } = useRealmVoterWeightPlugins(); + const inOrderPlugins = plugins?.voterWeight.reverse(); + const currentVotingStructure = { votingProgramId: props.currentConfigAccount.communityTokenConfig.voterWeightAddin, @@ -43,6 +52,8 @@ export function CommunityStructure(props: Props) { nftCollection: props.currentNftCollection, nftCollectionSize: props.currentNftCollectionSize, nftCollectionWeight: props.currentNftCollectionWeight, + civicPassType: props.currentCivicPassType, + qvCoefficients: props.qvCoefficients, }; const votingStructure = { @@ -52,6 +63,9 @@ export function CommunityStructure(props: Props) { nftCollection: props.nftCollection, nftCollectionSize: props.nftCollectionSize, nftCollectionWeight: props.nftCollectionWeight, + civicPassType: props.civicPassType, + qvCoefficients: props.qvCoefficients, + chainingEnabled: props.chainingEnabled, }; const minTokensToManage = new BigNumber( @@ -183,6 +197,7 @@ export function CommunityStructure(props: Props) { )} )} + {props.configAccount.communityTokenConfig.tokenType !== GoverningTokenType.Dormant && ( + {inOrderPlugins && + inOrderPlugins?.length > 1 && + inOrderPlugins.slice(0, -1).map((plugin) => { + return ( + <> + +
+ + ); + })}
{ const newConfig = produce( { ...props.configAccount }, @@ -246,6 +283,29 @@ export function CommunityStructure(props: Props) { ) { props.onNftCollectionWeightChange?.(nftCollectionWeight); } + + if ( + typeof civicPassType !== 'undefined' && + !props.civicPassType?.equals(civicPassType) + ) { + props.onCivicPassTypeChange?.(civicPassType); + } + + if ( + typeof qvCoefficients !== 'undefined' && + !props?.qvCoefficients?.every((value) => + qvCoefficients.includes(value), + ) + ) { + props.onQvCoefficientsChange?.(qvCoefficients); + } + + if ( + typeof chainingEnabled !== 'undefined' && + props.chainingEnabled !== chainingEnabled + ) { + props.onChainingEnabledChange?.(chainingEnabled); + } }, 0); }} /> diff --git a/hub/components/EditRealmConfig/Form/index.tsx b/hub/components/EditRealmConfig/Form/index.tsx index bd008aae9a..d3fe929082 100644 --- a/hub/components/EditRealmConfig/Form/index.tsx +++ b/hub/components/EditRealmConfig/Form/index.tsx @@ -67,9 +67,13 @@ export function Form(props: Props) { currentNftCollection={props.currentConfig.nftCollection} currentNftCollectionSize={props.currentConfig.nftCollectionSize} currentNftCollectionWeight={props.currentConfig.nftCollectionWeight} + currentCivicPassType={props.currentConfig.civicPassType} nftCollection={props.config.nftCollection} nftCollectionSize={props.config.nftCollectionSize} nftCollectionWeight={props.config.nftCollectionWeight} + civicPassType={props.config.civicPassType} + qvCoefficients={props.config.qvCoefficients} + chainingEnabled={props.config.chainingEnabled} onConfigChange={(config) => { const newConfig = produce(props.config, (data) => { data.config = config; @@ -103,6 +107,27 @@ export function Form(props: Props) { data.nftCollectionWeight = nftCollectionWeight; }); + props.onConfigChange?.(newConfig); + }} + onCivicPassTypeChange={(civicPassType) => { + const newConfig = produce(props.config, (data) => { + data.civicPassType = civicPassType; + }); + + props.onConfigChange?.(newConfig); + }} + onQvCoefficientsChange={(coeffcients) => { + const newConfig = produce(props.config, (data) => { + data.qvCoefficients = coeffcients; + }); + + props.onConfigChange?.(newConfig); + }} + onChainingEnabledChange={(chainingEnabled) => { + const newConfig = produce(props.config, (data) => { + data.chainingEnabled = chainingEnabled; + }); + props.onConfigChange?.(newConfig); }} /> diff --git a/hub/components/EditRealmConfig/UpdatesList/index.tsx b/hub/components/EditRealmConfig/UpdatesList/index.tsx index 489e4eb219..1f8b48c2c0 100644 --- a/hub/components/EditRealmConfig/UpdatesList/index.tsx +++ b/hub/components/EditRealmConfig/UpdatesList/index.tsx @@ -2,6 +2,7 @@ import EditIcon from '@carbon/icons-react/lib/Edit'; import EventsIcon from '@carbon/icons-react/lib/Events'; import RuleIcon from '@carbon/icons-react/lib/Rule'; import ScaleIcon from '@carbon/icons-react/lib/Scale'; +import { Coefficients } from '@solana/governance-program-library'; import { MintMaxVoteWeightSourceType, MintMaxVoteWeightSource, @@ -10,12 +11,16 @@ import { PublicKey } from '@solana/web3.js'; import { BigNumber } from 'bignumber.js'; import BN from 'bn.js'; +import { FC } from 'react'; + +import { availablePasses } from '../../../../GatewayPlugin/config'; import { Config } from '../fetchConfig'; import { getLabel } from '../TokenTypeSelector'; import { DEFAULT_NFT_CONFIG, DEFAULT_VSR_CONFIG, DEFAULT_CIVIC_CONFIG, + DEFAULT_QV_CONFIG, } from '../VotingStructureSelector'; import { SectionBlock } from '@hub/components/EditWalletRules/SectionBlock'; import { SectionHeader } from '@hub/components/EditWalletRules/SectionHeader'; @@ -45,6 +50,9 @@ export function buildUpdates(config: Config) { nftCollection: config.nftCollection, nftCollectionSize: config.nftCollectionSize, nftCollectionWeight: config.nftCollectionWeight, + civicPassType: config.civicPassType, + qvCoefficients: config.qvCoefficients, + chainingEnabled: config.chainingEnabled, }; } @@ -81,6 +89,37 @@ export function diff( return diffs; } +const civicPassTypeLabel = (civicPassType: PublicKey | undefined): string => { + if (!civicPassType) return 'None'; + const foundPass = availablePasses.find( + (pass) => pass.value === civicPassType?.toBase58(), + ); + + if (!foundPass) return 'Other (' + abbreviateAddress(civicPassType) + ')'; + return foundPass.name; +}; + +// display QV coefficients inline with A, B, C as labels +const QvCoefficientsDisplay: FC<{ + qvCoefficients: Coefficients | undefined; +}> = ({ qvCoefficients }) => { + if (!qvCoefficients) return null; + const coefficient = (label: string, value: number) => ( +
+
{label}
+
{value}
+
+ ); + + return ( + <> + {coefficient('A', qvCoefficients[0])} + {coefficient('B', qvCoefficients[1])} + {coefficient('C', qvCoefficients[2])} + + ); +}; + function votingStructureText( votingPluginDiff: [PublicKey | undefined, PublicKey | undefined], maxVotingPluginDiff: [PublicKey | undefined, PublicKey | undefined], @@ -103,6 +142,11 @@ function votingStructureText( typeof maxVotingPluginDiff[0] === 'undefined' ) { existingText = 'Civic'; + } else if ( + votingPluginDiff[0]?.equals(DEFAULT_QV_CONFIG.votingProgramId) && + typeof maxVotingPluginDiff[0] === 'undefined' + ) { + existingText = 'QV'; } else if (votingPluginDiff[0] || maxVotingPluginDiff[0]) { existingText = 'Custom'; } @@ -122,6 +166,11 @@ function votingStructureText( typeof maxVotingPluginDiff[1] === 'undefined' ) { newText = 'Civic'; + } else if ( + votingPluginDiff[1]?.equals(DEFAULT_QV_CONFIG.votingProgramId) && + typeof maxVotingPluginDiff[1] === 'undefined' + ) { + newText = 'QV'; } else if (votingPluginDiff[1] || maxVotingPluginDiff[1]) { newText = 'Custom'; } @@ -156,7 +205,8 @@ export function UpdatesList(props: Props) { 'communityMaxVotingPlugin' in updates || 'nftCollection' in updates || 'nftCollectionSize' in updates || - 'nftCollectionWeight' in updates; + 'nftCollectionWeight' in updates || + 'civicPassType' in updates; const hasCouncilUpdates = 'councilTokenType' in updates || @@ -302,14 +352,25 @@ export function UpdatesList(props: Props) { )[1] }
-
- { - votingStructureText( - updates.communityVotingPlugin || [], - updates.communityMaxVotingPlugin || [], - )[0] - } -
+ {updates.chainingEnabled ? ( +
+ {'⬅ ' + + votingStructureText( + updates.communityVotingPlugin || [], + updates.communityMaxVotingPlugin || [], + )[0] + + ' (Chaining)'} +
+ ) : ( +
+ { + votingStructureText( + updates.communityVotingPlugin || [], + updates.communityMaxVotingPlugin || [], + )[0] + } +
+ )}
} /> @@ -420,6 +481,19 @@ export function UpdatesList(props: Props) { } /> )} + {'civicPassType' in updates && ( + +
{civicPassTypeLabel(updates.civicPassType[1])}
+
+ {civicPassTypeLabel(updates.civicPassType[0])} +
+
+ } + /> + )}
)} @@ -621,6 +695,25 @@ export function UpdatesList(props: Props) {
)} + {'qvCoefficients' in updates && ( + +
+ +
+
+ +
+
+ } + /> + )} ); } diff --git a/hub/components/EditRealmConfig/VotingStructureSelector/ChainToggle.tsx b/hub/components/EditRealmConfig/VotingStructureSelector/ChainToggle.tsx new file mode 100644 index 0000000000..f07c774a68 --- /dev/null +++ b/hub/components/EditRealmConfig/VotingStructureSelector/ChainToggle.tsx @@ -0,0 +1,70 @@ +import React, { FC } from 'react'; + +import { ButtonToggle } from '@hub/components/controls/ButtonToggle'; +import { PLUGIN_DISPLAY_NAMES } from '@hub/components/EditRealmConfig/VotingStructureSelector/index'; +import { ValueBlock } from '@hub/components/EditWalletRules/ValueBlock'; +import cx from '@hub/lib/cx'; + +interface Props { + className?: string; + previousPlugin: string; + chainingEnabled: boolean; + onChange(value: boolean): void; +} + +const pluginProgramIdToName = (plugin: string) => + PLUGIN_DISPLAY_NAMES[plugin] ?? plugin; + +// A dropdown of all the available Civic Passes +const ChainToggle: FC<{ + className?: string; + previousPlugin: string; + chainingEnabled: boolean; + onChange(value: boolean): void; +}> = (props) => { + return ( + + { + props.onChange(value); + }} + /> + + ); +}; + +export function ChainToggleConfigurator(props: Props) { + return ( +
+
+
+
+
+
+ +
+
+
+
+ ); +} diff --git a/hub/components/EditRealmConfig/VotingStructureSelector/CivicConfigurator.tsx b/hub/components/EditRealmConfig/VotingStructureSelector/CivicConfigurator.tsx new file mode 100644 index 0000000000..7c8ec56f18 --- /dev/null +++ b/hub/components/EditRealmConfig/VotingStructureSelector/CivicConfigurator.tsx @@ -0,0 +1,212 @@ +import ChevronDownIcon from '@carbon/icons-react/lib/ChevronDown'; +import * as DropdownMenu from '@radix-ui/react-dropdown-menu'; + +import { PublicKey } from '@solana/web3.js'; +import React, { FC, useRef, useState } from 'react'; + +import { + availablePasses, + CivicPass, + defaultPass, +} from '../../../../GatewayPlugin/config'; +import Input from '@components/inputs/Input'; +import cx from '@hub/lib/cx'; + +const itemStyles = cx( + 'border', + 'cursor-pointer', + 'gap-x-4', + 'grid-cols-[150px,1fr,20px]', + 'grid', + 'h-14', + 'items-center', + 'px-4', + 'w-full', + 'rounded-md', + 'text-left', + 'transition-colors', + 'dark:bg-neutral-800', + 'dark:border-neutral-700', + 'dark:hover:bg-neutral-700', +); + +const labelStyles = cx('font-700', 'dark:text-neutral-50', 'w-full'); +const descriptionStyles = cx('dark:text-neutral-400 text-sm'); +const iconStyles = cx('fill-neutral-500', 'h-5', 'transition-transform', 'w-4'); + +const isOther = (pass: CivicPass | undefined): boolean => + pass?.name === 'Other'; +const other = availablePasses.find(isOther) as CivicPass; + +// If Other is selected, allow the user to enter a custom pass address here. +const ManualPassEntry: FC<{ + manualPassType?: PublicKey; + onChange: (newManualPassType?: PublicKey) => void; +}> = ({ manualPassType, onChange }) => { + const [error, setError] = useState(); + const [inputValue, setInputValue] = useState( + manualPassType?.toBase58() || '', + ); + + return ( +
+
+
+
+
+ { + const value = evt.target.value; + setInputValue(value); + try { + const pk = new PublicKey(value); + onChange(pk); + setError(undefined); + } catch { + setError('Invalid address'); + } + }} + error={error} + /> +
+
+
+ ); +}; + +// A dropdown of all the available Civic Passes +const CivicPassDropdown: FC<{ + className?: string; + previousSelected?: PublicKey; + onPassTypeChange(value: PublicKey | undefined): void; +}> = (props) => { + const [open, setOpen] = useState(false); + const trigger = useRef(null); + const [selectedPass, setSelectedPass] = useState( + !!props.previousSelected + ? availablePasses.find( + (pass) => pass.value === props.previousSelected?.toBase58(), + ) ?? other + : defaultPass, + ); + + return ( + +
+ +
+ {selectedPass?.name || 'Select a Civic Pass'} +
+
+ {selectedPass?.description || ''} +
+ +
+ + + {availablePasses.map((config, i) => ( + { + setSelectedPass(config); + props.onPassTypeChange( + config?.value ? new PublicKey(config.value) : undefined, + ); + }} + > +
{config.name}
+
{config.description}
+
+ ))} +
+
+
+ {isOther(selectedPass) && ( + { + setSelectedPass(other); + props.onPassTypeChange(manualPassType); + }} + manualPassType={ + props.previousSelected && selectedPass !== other + ? props.previousSelected + : undefined + } + /> + )} +
+ ); +}; + +interface Props { + className?: string; + currentPassType?: PublicKey; + onPassTypeChange(value: PublicKey | undefined): void; +} + +export function CivicConfigurator(props: Props) { + return ( +
+
+
+
+
+ What type of verification? +
+
+
+ +
+
+
+
+ ); +} diff --git a/hub/components/EditRealmConfig/VotingStructureSelector/QVConfigurator.tsx b/hub/components/EditRealmConfig/VotingStructureSelector/QVConfigurator.tsx new file mode 100644 index 0000000000..2d20030500 --- /dev/null +++ b/hub/components/EditRealmConfig/VotingStructureSelector/QVConfigurator.tsx @@ -0,0 +1,129 @@ +import { Coefficients } from '@solana/governance-program-library'; +import { useConnection } from '@solana/wallet-adapter-react'; +import React, { useState, useEffect } from 'react'; + +import { getCoefficients } from '../../../../actions/addPlugins/addQVPlugin'; +import { useRealmQuery } from '@hooks/queries/realm'; + +import { Input } from '@hub/components/controls/Input'; +import cx from '@hub/lib/cx'; +import { preventNegativeNumberInput } from '@utils/helpers'; + +interface Props { + className?: string; + onCoefficientsChange(value: Coefficients | undefined): void; +} + +export function QVConfigurator({ className, onCoefficientsChange }: Props) { + const [coefficients, setCoefficients] = useState([1000, 0, 0]); + const { connection } = useConnection(); + const realm = useRealmQuery().data?.result; + + useEffect(() => { + const fetchCoefficients = async () => { + const coefficients = await getCoefficients( + undefined, + realm?.account.communityMint, + connection, + ); + + const coefficientA = Number(coefficients[0].toFixed(2)); + + setCoefficients([coefficientA, coefficients[1], coefficients[2]]); + onCoefficientsChange([coefficientA, coefficients[1], coefficients[2]]); + }; + + // If the user wants to use a pre-existing token, we need to adjust the coefficients ot match the decimals of that token + if (realm?.account.communityMint) { + fetchCoefficients(); + } + }, [connection, realm?.account.communityMint]); + + return ( +
+
+
+
+
+ Quadratic Coefficients +
+
+
+
+
+ { + preventNegativeNumberInput(ev); + const newCoefficients = [...coefficients]; + newCoefficients[0] = Number(ev.target.value); + setCoefficients(newCoefficients as Coefficients); + onCoefficientsChange(newCoefficients as Coefficients); + }} + /> +
+ A +
+
+
+ { + preventNegativeNumberInput(ev); + const newCoefficients = [...coefficients]; + newCoefficients[1] = Number(ev.target.value); + setCoefficients(newCoefficients as Coefficients); + onCoefficientsChange(newCoefficients as Coefficients); + }} + /> +
+ B +
+
+
+ { + preventNegativeNumberInput(ev); + const newCoefficients = [...coefficients]; + newCoefficients[2] = Number(ev.target.value); + setCoefficients(newCoefficients as Coefficients); + onCoefficientsChange(newCoefficients as Coefficients); + }} + /> +
+ C +
+
+
+
+
+ Advanced Option: Defaults have been set to an appropriate curve + based on the community token. Please take a look at the Realms + documentation to understand how changing the quadratic formula will + affect voting. +
+
+
+
+ ); +} diff --git a/hub/components/EditRealmConfig/VotingStructureSelector/index.tsx b/hub/components/EditRealmConfig/VotingStructureSelector/index.tsx index a8706d676d..ead8388aef 100644 --- a/hub/components/EditRealmConfig/VotingStructureSelector/index.tsx +++ b/hub/components/EditRealmConfig/VotingStructureSelector/index.tsx @@ -1,17 +1,24 @@ import ChevronDownIcon from '@carbon/icons-react/lib/ChevronDown'; +import { GATEWAY_PLUGINS_PKS, QV_PLUGINS_PKS } from '@constants/plugins'; import * as DropdownMenu from '@radix-ui/react-dropdown-menu'; +import { Coefficients } from '@solana/governance-program-library'; import { PublicKey } from '@solana/web3.js'; import BN from 'bn.js'; import { produce } from 'immer'; + import { useEffect, useRef, useState } from 'react'; +import { defaultPass } from '../../../../GatewayPlugin/config'; import { Config } from '../fetchConfig'; +import { ChainToggleConfigurator } from '@hub/components/EditRealmConfig/VotingStructureSelector/ChainToggle'; import cx from '@hub/lib/cx'; import { DEFAULT_NFT_VOTER_PLUGIN } from '@tools/constants'; +import { CivicConfigurator } from './CivicConfigurator'; import { Custom } from './Custom'; import { NFT } from './NFT'; +import { QVConfigurator } from './QVConfigurator'; export const DEFAULT_NFT_CONFIG = { votingProgramId: new PublicKey(DEFAULT_NFT_VOTER_PLUGIN), @@ -24,12 +31,22 @@ export const DEFAULT_VSR_CONFIG = { }; export const DEFAULT_CIVIC_CONFIG = { - votingProgramId: new PublicKey( - 'GgathUhdrCWRHowoRKACjgWhYHfxCEdBi5ViqYN6HVxk', - ), + votingProgramId: new PublicKey(GATEWAY_PLUGINS_PKS[0]), maxVotingProgramId: undefined, }; +export const DEFAULT_QV_CONFIG = { + votingProgramId: new PublicKey(QV_PLUGINS_PKS[0]), + maxVotingProgramId: undefined, // the QV plugin does not use a max voting weight record. +}; + +export const PLUGIN_DISPLAY_NAMES = { + [DEFAULT_NFT_VOTER_PLUGIN]: 'NFT Plugin', + [DEFAULT_VSR_CONFIG.votingProgramId.toBase58() || '']: 'VSR Plugin', + [DEFAULT_CIVIC_CONFIG.votingProgramId.toBase58() || '']: 'Civic Plugin', + [DEFAULT_QV_CONFIG.votingProgramId.toBase58() || '']: 'QV Plugin', +}; + const itemStyles = cx( 'border', 'cursor-pointer', @@ -51,36 +68,39 @@ const labelStyles = cx('font-700', 'dark:text-neutral-50'); const descriptionStyles = cx('dark:text-neutral-400'); const iconStyles = cx('fill-neutral-500', 'h-5', 'transition-transform', 'w-4'); +type VotingStructure = { + votingProgramId?: PublicKey; + maxVotingProgramId?: PublicKey; + nftCollection?: PublicKey; + nftCollectionSize?: number; + nftCollectionWeight?: BN; + civicPassType?: PublicKey; + chainingEnabled?: boolean; + qvCoefficients?: Coefficients; +}; + interface Props { allowNFT?: boolean; allowCivic?: boolean; allowVSR?: boolean; + allowQV?: boolean; className?: string; communityMint: Config['communityMint']; - currentStructure: { - votingProgramId?: PublicKey; - maxVotingProgramId?: PublicKey; - nftCollection?: PublicKey; - nftCollectionSize?: number; - nftCollectionWeight?: BN; - }; - structure: { - votingProgramId?: PublicKey; - maxVotingProgramId?: PublicKey; - nftCollection?: PublicKey; - nftCollectionSize?: number; - nftCollectionWeight?: BN; - }; - onChange?(value: { - votingProgramId?: PublicKey; - maxVotingProgramId?: PublicKey; - nftCollection?: PublicKey; - nftCollectionSize?: number; - nftCollectionWeight?: BN; - }): void; + currentStructure: VotingStructure; + structure: VotingStructure; + onChange?(value: VotingStructure): void; } function areConfigsEqual(a: Props['structure'], b: Props['structure']) { + if ( + Object.hasOwnProperty.call(a, 'maxVotingProgramId') !== + Object.hasOwnProperty.call(b, 'maxVotingProgramId') || + Object.hasOwnProperty.call(a, 'votingProgramId') !== + Object.hasOwnProperty.call(b, 'votingProgramId') + ) { + return false; + } + if ( (a.maxVotingProgramId && !b.maxVotingProgramId) || (!a.maxVotingProgramId && b.maxVotingProgramId) @@ -126,8 +146,22 @@ function isCivicConfig(config: Props['structure']) { return areConfigsEqual(config, DEFAULT_CIVIC_CONFIG); } +function isQVConfig(config: Props['structure']) { + return areConfigsEqual(config, DEFAULT_QV_CONFIG); +} + +// true if this plugin supports chaining with other plugins +function isChainablePlugin(config: Props['structure']) { + return isCivicConfig(config) || isQVConfig(config); +} + function isCustomConfig(config: Props['structure']) { - return !isNFTConfig(config) && !isVSRConfig(config) && !isCivicConfig(config); + return ( + !isNFTConfig(config) && + !isVSRConfig(config) && + !isCivicConfig(config) && + !isQVConfig(config) + ); } export function getLabel(value: Props['structure']): string { @@ -143,9 +177,26 @@ export function getLabel(value: Props['structure']): string { return 'Civic'; } + if (isQVConfig(value)) { + return 'QV'; + } + return 'Custom'; } +const getDefaults = (value: Props['structure']): Partial => { + let result: Partial = {}; + + if (isCivicConfig(value)) { + result = { + ...result, + civicPassType: new PublicKey(defaultPass.value), + }; + } + + return result; +}; + function getDescription(value: Props['structure']): string { if (isNFTConfig(value)) { return 'Voting enabled and weighted based on NFTs owned'; @@ -159,6 +210,10 @@ function getDescription(value: Props['structure']): string { return 'Governance based on Civic verification'; } + if (isQVConfig(value)) { + return 'Quadratic voting'; + } + return 'Add a custom program ID for governance structure'; } @@ -171,6 +226,15 @@ export function VotingStructureSelector(props: Props) { ); const trigger = useRef(null); + // only show the chain toggle if the plugin supports chaining + // and there is a previous plugin to chain with + // and the current plugin is not the same as the previous plugin + const shouldShowChainToggle = + isChainablePlugin(props.structure) && + !!props.currentStructure.votingProgramId && + props.structure.votingProgramId?.toBase58() !== + props.currentStructure.votingProgramId?.toBase58(); + useEffect(() => { if (trigger.current) { setWidth(trigger.current.clientWidth); @@ -191,12 +255,10 @@ export function VotingStructureSelector(props: Props) { ref={trigger} >
- {areConfigsEqual({}, props.structure) && isDefault - ? 'Default' - : getLabel(props.structure)} + {isDefault ? 'Default' : getLabel(props.structure)}
- {areConfigsEqual({}, props.structure) && isDefault + {isDefault ? 'Governance is based on token ownership' : getDescription(props.structure)}
@@ -211,6 +273,7 @@ export function VotingStructureSelector(props: Props) { const newConfig = produce({ ...props.structure }, (data) => { data.votingProgramId = value || undefined; data.nftCollection = undefined; + data.chainingEnabled = isChainablePlugin(data); }); props.onChange?.(newConfig); @@ -258,6 +321,43 @@ export function VotingStructureSelector(props: Props) { }} /> )} + {isQVConfig(props.structure) && ( + { + const newConfig = produce({ ...props.structure }, (data) => { + data.qvCoefficients = value ?? undefined; + }); + props.onChange?.(newConfig); + }} + /> + )} + {isCivicConfig(props.structure) && ( + { + const newConfig = produce({ ...props.structure }, (data) => { + data.civicPassType = value ?? undefined; + }); + props.onChange?.(newConfig); + }} + /> + )} + {shouldShowChainToggle && ( + { + const newConfig = produce({ ...props.structure }, (data) => { + data.chainingEnabled = value; + }); + props.onChange?.(newConfig); + }} + /> + )} { if (typeof config === 'string') { - return !areConfigsEqual({}, props.structure); + return !isDefault; } - return !areConfigsEqual(config, props.structure); }) .map((config, i) => ( @@ -295,7 +395,11 @@ export function VotingStructureSelector(props: Props) { props.onChange?.({}); setIsDefault(true); } else { - props.onChange?.(config); + const changes = { + ...getDefaults(config), // add any default values (e.g. chainingEnabled) + ...config, + }; + props.onChange?.(changes); setIsDefault(false); } }} diff --git a/hub/components/EditRealmConfig/createTransaction.ts b/hub/components/EditRealmConfig/createTransaction.ts index e48a21410b..e0458af446 100644 --- a/hub/components/EditRealmConfig/createTransaction.ts +++ b/hub/components/EditRealmConfig/createTransaction.ts @@ -1,20 +1,37 @@ -import { AnchorProvider, Wallet } from '@project-serum/anchor'; +import { AnchorProvider, Wallet } from '@coral-xyz/anchor'; +import { + GatewayClient, + QuadraticClient, +} from '@solana/governance-program-library'; import { createSetRealmConfig, GoverningTokenType, GoverningTokenConfigAccountArgs, tryGetRealmConfig, getRealm, - getGovernanceProgramVersion, SYSTEM_PROGRAM_ID, } from '@solana/spl-governance'; +import { + getGovernanceProgramVersion +} from "@realms-today/spl-governance" import type { Connection, PublicKey, TransactionInstruction, } from '@solana/web3.js'; +import { + configureCivicRegistrarIx, + createCivicRegistrarIx, +} from '../../../GatewayPlugin/sdk/api'; +import { + coefficientsEqual, + configureQuadraticRegistrarIx, + createQuadraticRegistrarIx, + DEFAULT_COEFFICIENTS, +} from '../../../QuadraticPlugin/sdk/api'; +import { DEFAULT_QV_CONFIG } from '@hub/components/EditRealmConfig/VotingStructureSelector'; import { getMaxVoterWeightRecord, getRegistrarPDA, @@ -44,6 +61,9 @@ function shouldAddConfigInstruction(config: Config, currentConfig: Config) { return false; } +const configUsesVoterWeightPlugin = (config: Config, plugin: PublicKey) => + config.configAccount.communityTokenConfig.voterWeightAddin?.equals(plugin); + export async function createTransaction( realmPublicKey: PublicKey, governance: PublicKey, @@ -67,15 +87,7 @@ export async function createTransaction( programId, ); - if ( - realmAccount.account.authority && - wallet && - config.nftCollection && - (!currentConfig.nftCollection || - !currentConfig.nftCollection.equals(config.nftCollection) || - currentConfig.nftCollectionSize !== config.nftCollectionSize || - !currentConfig.nftCollectionWeight.eq(config.nftCollectionWeight)) - ) { + if (realmAccount.account.authority && wallet) { const defaultOptions = AnchorProvider.defaultOptions(); const anchorProvider = new AnchorProvider( connection, @@ -83,62 +95,168 @@ export async function createTransaction( defaultOptions, ); - const nftClient = await NftVoterClient.connect(anchorProvider, isDevnet); - const { registrar } = await getRegistrarPDA( - realmPublicKey, - config.communityMint.publicKey, - nftClient.program.programId, - ); - const { maxVoterWeightRecord } = await getMaxVoterWeightRecord( - realmPublicKey, - config.communityMint.publicKey, - nftClient.program.programId, - ); + if ( + config.nftCollection && + (!currentConfig.nftCollection || + !currentConfig.nftCollection.equals(config.nftCollection) || + currentConfig.nftCollectionSize !== config.nftCollectionSize || + !currentConfig.nftCollectionWeight.eq(config.nftCollectionWeight)) + ) { + const nftClient = await NftVoterClient.connect( + anchorProvider, + undefined, + isDevnet, + ); + const { registrar } = getRegistrarPDA( + realmPublicKey, + config.communityMint.publicKey, + nftClient.program.programId, + ); + const { maxVoterWeightRecord } = await getMaxVoterWeightRecord( + realmPublicKey, + config.communityMint.publicKey, + nftClient.program.programId, + ); - instructions.push( - await nftClient.program.methods - .createRegistrar(10) - .accounts({ - registrar, - realm: realmPublicKey, - governanceProgramId: programId, - realmAuthority: realmAccount.account.authority, - governingTokenMint: config.communityMint.publicKey, - payer: wallet.publicKey, - systemProgram: SYSTEM_PROGRAM_ID, - }) - .instruction(), - ); + instructions.push( + await nftClient.program.methods + .createRegistrar(10) + .accounts({ + registrar, + realm: realmPublicKey, + governanceProgramId: programId, + realmAuthority: realmAccount.account.authority, + governingTokenMint: config.communityMint.publicKey, + payer: wallet.publicKey, + systemProgram: SYSTEM_PROGRAM_ID, + }) + .instruction(), + ); - instructions.push( - await nftClient.program.methods - .createMaxVoterWeightRecord() - .accounts({ - maxVoterWeightRecord, - realm: realmPublicKey, - governanceProgramId: programId, - realmGoverningTokenMint: config.communityMint.publicKey, - payer: wallet.publicKey, - systemProgram: SYSTEM_PROGRAM_ID, - }) - .instruction(), - ); + instructions.push( + await nftClient.program.methods + .createMaxVoterWeightRecord() + .accounts({ + maxVoterWeightRecord, + realm: realmPublicKey, + governanceProgramId: programId, + realmGoverningTokenMint: config.communityMint.publicKey, + payer: wallet.publicKey, + systemProgram: SYSTEM_PROGRAM_ID, + }) + .instruction(), + ); - instructions.push( - await nftClient.program.methods - .configureCollection( - config.nftCollectionWeight, - config.nftCollectionSize, - ) - .accounts({ - registrar, - realm: realmPublicKey, - maxVoterWeightRecord, - realmAuthority: realmAccount.account.authority, - collection: config.nftCollection, - }) - .instruction(), - ); + instructions.push( + await nftClient.program.methods + .configureCollection( + config.nftCollectionWeight, + config.nftCollectionSize, + ) + .accounts({ + registrar, + realm: realmPublicKey, + maxVoterWeightRecord, + realmAuthority: realmAccount.account.authority, + collection: config.nftCollection, + }) + .instruction(), + ); + } else if ( + config.civicPassType && + (!currentConfig.civicPassType || + !currentConfig.civicPassType.equals(config.civicPassType)) + ) { + // If this DAO uses Civic, we need to either create or configure the Civic gateway plugin registrar. + const gatewayClient = await GatewayClient.connect( + anchorProvider, + isDevnet, + ); + + const predecessorPlugin = config.chainingEnabled + ? currentConfig.configAccount.communityTokenConfig.voterWeightAddin + : undefined; + + const existingRegistrarAccount = await gatewayClient.getRegistrarAccount( + realmPublicKey, + config.communityMint.publicKey, + ); + + const instruction = existingRegistrarAccount + ? await configureCivicRegistrarIx( + realmAccount, + gatewayClient, + config.civicPassType, + ) + : await createCivicRegistrarIx( + realmAccount, + wallet.publicKey, + gatewayClient, + config.civicPassType, + predecessorPlugin, + ); + + instructions.push(instruction); + } else if ( + (config.qvCoefficients && + !coefficientsEqual( + config.qvCoefficients, + currentConfig.qvCoefficients, + )) || + (configUsesVoterWeightPlugin(config, DEFAULT_QV_CONFIG.votingProgramId) && + !configUsesVoterWeightPlugin( + currentConfig, + DEFAULT_QV_CONFIG.votingProgramId, + )) + ) { + // Configure the registrar for the quadratic voting plugin for the DAO + // Since QV needs to be paired up with some other plugin that protects against sybil attacks, + // it will typically have a predecessor plugin (e.g. the Civic Gateway plugin) + const predecessorPlugin = config.chainingEnabled + ? currentConfig.configAccount.communityTokenConfig.voterWeightAddin + : undefined; + + const quadraticClient = await QuadraticClient.connect( + anchorProvider, + isDevnet, + ); + + const existingRegistrarAccount = await quadraticClient.getRegistrarAccount( + realmPublicKey, + config.communityMint.publicKey, + ); + + // if the update is a simple coefficient update, do not change the predecessor unless also set specifically + // Note - the UI is somewhat overloaded here and it would be nicer differentiate + // between updates and new plugins being added to the chain + const isCoefficientUpdate = + existingRegistrarAccount && + config.qvCoefficients && + !coefficientsEqual(config.qvCoefficients, currentConfig.qvCoefficients); + const previousVoterWeightPluginProgramId = + predecessorPlugin ?? + (isCoefficientUpdate + ? existingRegistrarAccount.previousVoterWeightPluginProgramId + : undefined); + + const instruction = existingRegistrarAccount + ? await configureQuadraticRegistrarIx( + realmAccount, + quadraticClient, + config.qvCoefficients || DEFAULT_COEFFICIENTS, + // keep the existing predecessor when updating the coefficients + previousVoterWeightPluginProgramId, + ) + : await createQuadraticRegistrarIx( + realmAccount, + wallet.publicKey, + quadraticClient, + config.qvCoefficients || DEFAULT_COEFFICIENTS, + predecessorPlugin, + ); + + instructions.push(instruction); + } } if (shouldAddConfigInstruction(config, currentConfig)) { diff --git a/hub/components/EditRealmConfig/fetchConfig.ts b/hub/components/EditRealmConfig/fetchConfig.ts index fd313af135..f108922fbe 100644 --- a/hub/components/EditRealmConfig/fetchConfig.ts +++ b/hub/components/EditRealmConfig/fetchConfig.ts @@ -1,24 +1,28 @@ -import { NFT_PLUGINS_PKS } from '@constants/plugins'; -import { AnchorProvider, Wallet } from '@project-serum/anchor'; - import { - RealmConfig, - RealmConfigAccount, + Coefficients, + GatewayClient, +} from '@solana/governance-program-library'; +import { QuadraticClient } from '@solana/governance-program-library/dist/quadraticVoter/client'; +import { getRealm, getRealmConfigAddress, - ProgramAccount, GovernanceAccountParser, - GoverningTokenType, GoverningTokenConfig, + GoverningTokenType, + ProgramAccount, + RealmConfig, + RealmConfigAccount, } from '@solana/spl-governance'; import { Connection, PublicKey } from '@solana/web3.js'; import BN from 'bn.js'; +import { QuadraticPluginParams } from 'VoterWeightPlugins/useQuadraticVoterWeightPlugin'; import { tryGetNftRegistrar } from 'VoteStakeRegistry/sdk/api'; -import { getNetworkFromEndpoint } from '@utils/connection'; +import { AnchorParams } from '../../../QuadraticPlugin/sdk/api'; +import { VoterWeightPluginInfo } from '../../../VoterWeightPlugins/lib/types'; import { getRegistrarPDA as getPluginRegistrarPDA } from '@utils/plugin/accounts'; -import { parseMintAccountData, MintAccount } from '@utils/tokens'; +import { MintAccount, parseMintAccountData } from '@utils/tokens'; import { NftVoterClient } from '@utils/uiTypes/NftVoterClient'; export interface Config { @@ -32,12 +36,15 @@ export interface Config { nftCollectionSize: number; nftCollectionWeight: BN; realmAuthority?: PublicKey; + civicPassType?: PublicKey; + chainingEnabled: boolean; + qvCoefficients?: Coefficients; } export async function fetchConfig( connection: Connection, realmPublicKey: PublicKey, - wallet: Pick, + currentPlugins: VoterWeightPluginInfo[], ): Promise { const realm = await getRealm(connection, realmPublicKey); @@ -82,37 +89,54 @@ export async function fetchConfig( let nftCollection: PublicKey | undefined = undefined; let nftCollectionSize = 0; let nftCollectionWeight = new BN(0); - const defaultOptions = AnchorProvider.defaultOptions(); - const anchorProvider = new AnchorProvider(connection, wallet, defaultOptions); - - const isDevnet = getNetworkFromEndpoint(connection.rpcEndpoint) === 'devnet'; - const nftClient = await NftVoterClient.connect(anchorProvider, isDevnet); - const pluginPublicKey = - configProgramAccount.account.communityTokenConfig.voterWeightAddin; - - if (pluginPublicKey && NFT_PLUGINS_PKS.includes(pluginPublicKey.toBase58())) { - if (nftClient && realm.account.communityMint) { - const programId = nftClient.program.programId; - const registrarPDA = ( - await getPluginRegistrarPDA( - realmPublicKey, - realm.account.communityMint, - programId, - ) - ).registrar; - - const registrar: any = await tryGetNftRegistrar(registrarPDA, nftClient); - - const collections = registrar?.collectionConfigs || []; - - if (collections[0]) { - nftCollection = new PublicKey(collections[0].collection); - nftCollectionSize = collections[0].size; - nftCollectionWeight = collections[0].weight; - } + let civicPassType: PublicKey | undefined = undefined; + let qvCoefficients: Coefficients | undefined = undefined; + + const nftClient = currentPlugins.find((plugin) => plugin.name === 'NFT') + ?.client as NftVoterClient | undefined; + const gatewayClient = currentPlugins.find( + (plugin) => plugin.name === 'gateway', + )?.client as GatewayClient | undefined; + const quadraticPlugin = currentPlugins.find((plugin) => plugin.name === 'QV'); + + if (nftClient && realm.account.communityMint) { + const programId = nftClient.program.programId; + const registrarPDA = ( + await getPluginRegistrarPDA( + realmPublicKey, + realm.account.communityMint, + programId, + ) + ).registrar; + + const registrar: any = await tryGetNftRegistrar(registrarPDA, nftClient); + + const collections = registrar?.collectionConfigs || []; + + if (collections[0]) { + nftCollection = new PublicKey(collections[0].collection); + nftCollectionSize = collections[0].size; + nftCollectionWeight = collections[0].weight; } } + if (gatewayClient && realm.account.communityMint) { + const registrar = await gatewayClient.getRegistrarAccount( + realm.pubkey, + realm.account.communityMint, + ); + civicPassType = registrar?.gatekeeperNetwork; + } + + if (quadraticPlugin && realm.account.communityMint) { + const anchorCoefficients = (quadraticPlugin?.params as + | AnchorParams + | undefined)?.quadraticCoefficients; + qvCoefficients = anchorCoefficients + ? QuadraticClient.convertCoefficientsFromAnchorType(anchorCoefficients) + : undefined; + } + const mintPkStr = realm.account.communityMint.toBase58(); const communityMint = await fetch(connection.rpcEndpoint, { method: 'POST', @@ -161,8 +185,11 @@ export async function fetchConfig( nftCollection, nftCollectionSize, nftCollectionWeight, + civicPassType, config: realmConfig, configAccount: configProgramAccount.account, realmAuthority: realm.account.authority, + chainingEnabled: false, + qvCoefficients, }; } diff --git a/hub/components/EditRealmConfig/index.tsx b/hub/components/EditRealmConfig/index.tsx index b3b8fa6002..8f1864f08b 100644 --- a/hub/components/EditRealmConfig/index.tsx +++ b/hub/components/EditRealmConfig/index.tsx @@ -17,6 +17,7 @@ import { useRealmQuery } from '@hooks/queries/realm'; import useCreateProposal from '@hooks/useCreateProposal'; import useLegacyConnectionContext from '@hooks/useLegacyConnectionContext'; import useQueryContext from '@hooks/useQueryContext'; +import { useRealmVoterWeightPlugins } from '@hooks/useRealmVoterWeightPlugins'; import useWalletOnePointOh from '@hooks/useWalletOnePointOh'; import { Primary, Secondary } from '@hub/components/controls/Button'; import { useQuery } from '@hub/hooks/useQuery'; @@ -79,6 +80,7 @@ export function EditRealmConfig(props: Props) { realmUrlId: props.realmUrlId, }, }); + const { plugins } = useRealmVoterWeightPlugins(); const { propose } = useCreateProposal(); const [governance, setGovernance] = useState(null); @@ -111,15 +113,12 @@ export function EditRealmConfig(props: Props) { }, [step]); useEffect(() => { - if (RE.isOk(result) && wallet?.publicKey) { - Promise.resolve(wallet.publicKey) // :-) - .then((publicKey) => - fetchConfig(connection.current, result.data.realmByUrlId.publicKey, { - publicKey, - signAllTransactions: wallet.signAllTransactions, - signTransaction: wallet.signTransaction, - }), - ) + if (RE.isOk(result)) { + fetchConfig( + connection.current, + result.data.realmByUrlId.publicKey, + plugins?.voterWeight ?? [], + ) .then((config) => { setConfig({ ...config }); setProposalTitle( @@ -320,7 +319,6 @@ export function EditRealmConfig(props: Props) { 24 * governance.minInstructionHoldupDays, prerequisiteInstructions: [], - chunkBy: 3, })), governance: governance.governanceAddress, }); diff --git a/hub/components/EditWalletRules/RulesDetailsInputs.tsx b/hub/components/EditWalletRules/RulesDetailsInputs.tsx index 940729cd40..6f1abe5492 100644 --- a/hub/components/EditWalletRules/RulesDetailsInputs.tsx +++ b/hub/components/EditWalletRules/RulesDetailsInputs.tsx @@ -1,3 +1,4 @@ +import { NFT_PLUGINS_PKS } from '@constants/plugins'; import { BN } from '@coral-xyz/anchor'; import { BigNumber } from 'bignumber.js'; import { produce } from 'immer'; @@ -6,6 +7,7 @@ import { useRealmCommunityMintInfoQuery, useRealmCouncilMintInfoQuery, } from '@hooks/queries/mintInfo'; +import { useRealmConfigQuery } from '@hooks/queries/realmConfig'; import { ButtonToggle } from '@hub/components/controls/ButtonToggle'; import { Input } from '@hub/components/controls/Input'; import { Slider } from '@hub/components/controls/Slider'; @@ -120,6 +122,7 @@ export function CanVote(props: Props) { export function VotingPowerToCreateProposals(props: Props) { const communityTokenInfo = useRealmCommunityMintInfoQuery(); const councilTokenInfo = useRealmCouncilMintInfoQuery(); + const config = useRealmConfigQuery().data?.result; const tokenInfoQuery = props.rules.tokenType === GovernanceTokenType.Community @@ -140,11 +143,27 @@ export function VotingPowerToCreateProposals(props: Props) { .multipliedBy(100) : undefined; + const isNftPlugin = + config?.account.communityTokenConfig.voterWeightAddin && + NFT_PLUGINS_PKS.includes( + config?.account.communityTokenConfig.voterWeightAddin?.toBase58(), + ); + + const isNftGovernanceWithoutCouncil = + isNftPlugin && props.govPop === 'community' && !councilTokenInfo.data; + return ( + {isNftGovernanceWithoutCouncil ? ( +
+ The council is not added. Max tokens can be 5 +
+ ) : ( + '' + )}
{ const text = e.currentTarget.value.replaceAll(/[^\d.-]/g, ''); - const value = text ? new BigNumber(text) : new BigNumber(0); + let value = text ? new BigNumber(text) : new BigNumber(0); + + if ( + isNftGovernanceWithoutCouncil && + value.isGreaterThan(new BigNumber(5)) + ) { + value = new BigNumber(5); + } const newRules = produce(props.rules, (data) => { data.votingPowerToCreateProposals = value; }); diff --git a/hub/components/EditWalletRules/createTransaction.ts b/hub/components/EditWalletRules/createTransaction.ts index aa74d6cdfa..0e58a3c038 100644 --- a/hub/components/EditWalletRules/createTransaction.ts +++ b/hub/components/EditWalletRules/createTransaction.ts @@ -6,12 +6,10 @@ import { VoteTipping, } from '@solana/spl-governance'; import type { Connection, PublicKey } from '@solana/web3.js'; -import BigNumber from 'bignumber.js'; import BN from 'bn.js'; import { fetchGovernanceAccountByPubkey } from '@hooks/queries/governanceAccount'; import { fetchMintInfoByPubkey } from '@hooks/queries/mintInfo'; -import queryClient from '@hooks/queries/queryClient'; import { GovernanceVoteTipping } from '@hub/types/GovernanceVoteTipping'; import { MAX_NUM } from './constants'; diff --git a/hub/components/GlobalFooter/index.tsx b/hub/components/GlobalFooter/index.tsx index 00dd0ab813..1aef768a90 100644 --- a/hub/components/GlobalFooter/index.tsx +++ b/hub/components/GlobalFooter/index.tsx @@ -42,7 +42,7 @@ export function GlobalFooter(props: Props) { 'sm:text-sm', )} > -
© 2022 Solana Technology Services LLC
+
© 2024 Realms Today Ltd
|
Terms diff --git a/hub/components/GlobalHeader/User/DialectNotifications/DialectNotifications.tsx b/hub/components/GlobalHeader/User/DialectNotifications/DialectNotifications.tsx index 272be8c1cc..449311ae1e 100644 --- a/hub/components/GlobalHeader/User/DialectNotifications/DialectNotifications.tsx +++ b/hub/components/GlobalHeader/User/DialectNotifications/DialectNotifications.tsx @@ -1,20 +1,12 @@ -import { - DialectSolanaSdk, - DialectSolanaWalletAdapter, - SolanaConfigProps, -} from '@dialectlabs/react-sdk-blockchain-solana'; -import { - ConfigProps, - DialectThemeProvider, - DialectUiManagementProvider, - NotificationsButton, -} from '@dialectlabs/react-ui'; +import { DialectSolanaSdk } from '@dialectlabs/react-sdk-blockchain-solana'; +import { NotificationsButton } from '@dialectlabs/react-ui'; import * as NavigationMenu from '@radix-ui/react-navigation-menu'; import { useWallet } from '@solana/wallet-adapter-react'; -import React, { useEffect, useMemo, useState } from 'react'; +import { PublicKey } from '@solana/web3.js'; -import { REALMS_PUBLIC_KEY, themeVariables } from './Notifications.constants'; -import { solanaWalletToDialectWallet } from './solanaWalletToDialectWallet'; +export const REALMS_PUBLIC_KEY = new PublicKey( + 'BUxZD6aECR5B5MopyvvYqJxwSKDBhx2jSSo1U32en6mj', +); interface Props { className?: string; @@ -23,51 +15,6 @@ interface Props { export const DialectNotifications = (props: Props) => { const wallet = useWallet(); - const [ - dialectSolanaWalletAdapter, - setDialectSolanaWalletAdapter, - ] = useState(null); - - useEffect(() => { - setDialectSolanaWalletAdapter(solanaWalletToDialectWallet(wallet)); - }, [wallet]); - - const dialectConfig = useMemo( - (): ConfigProps => ({ - environment: 'production', - dialectCloud: { - tokenStore: 'local-storage', - }, - }), - [], - ); - - const solanaConfig: SolanaConfigProps = useMemo( - () => ({ - wallet: dialectSolanaWalletAdapter, - }), - [dialectSolanaWalletAdapter], - ); - - // Uncomment when theme will be available for hub components - // const [theme, setTheme] = useState('light'); - // useEffect(() => { - // if ( - // window.matchMedia && - // window.matchMedia('(prefers-color-scheme: dark)').matches - // ) { - // setTheme('dark'); - // } else { - // setTheme('light'); - // } - // window - // .matchMedia('(prefers-color-scheme: dark)') - // .addEventListener('change', (event) => { - // const newColorScheme = event.matches ? 'dark' : 'light'; - // setTheme(newColorScheme); - // }); - // }, []); - return ( { } }} > - - - - - - + + ); diff --git a/hub/components/GlobalHeader/User/DialectNotifications/Notifications.constants.ts b/hub/components/GlobalHeader/User/DialectNotifications/Notifications.constants.ts deleted file mode 100644 index 5bf6e1ef29..0000000000 --- a/hub/components/GlobalHeader/User/DialectNotifications/Notifications.constants.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { - defaultVariables, - IncomingThemeVariables, -} from '@dialectlabs/react-ui'; -import { PublicKey } from '@solana/web3.js'; - -import cx from '@hub/lib/cx'; - -export const REALMS_PUBLIC_KEY = new PublicKey( - 'BUxZD6aECR5B5MopyvvYqJxwSKDBhx2jSSo1U32en6mj', -); - -export const themeVariables: IncomingThemeVariables = { - dark: { - bellButton: `${defaultVariables.dark.bellButton} bg-transparent !shadow-none text-neutral-300 h-10 rounded-full w-10 hover:bg-bkg-3`, - iconButton: `${defaultVariables.dark.iconButton} hover:opacity-100 bg-transparent`, - adornmentButton: `${defaultVariables.dark.adornmentButton} bg-sky-500 hover:!bg-sky-400 active:bg-sky-500 rounded transition-colors`, - buttonLoading: `${defaultVariables.dark.buttonLoading} rounded-full min-h-[40px]`, - colors: { - ...defaultVariables.dark.colors, - label: 'text-white/80', - toggleThumb: 'bg-white', - toggleBackground: 'bg-zinc-300', - toggleBackgroundActive: 'bg-sky-500', - }, - textStyles: { - ...defaultVariables.dark.textStyles, - input: `${defaultVariables.dark.textStyles.input}`, - }, - outlinedInput: `${defaultVariables.dark.outlinedInput} h-12 rounded focus-within:border-sky-500`, - disabledButton: `${defaultVariables.dark.disabledButton} border-sky-500 font-bold rounded-full border-fgd-3 text-fgd-3 cursor-not-allowed`, - modal: `${defaultVariables.dark.modal} bg-bkg-1 sm:border sm:border-fgd-4 shadow-md sm:rounded-md`, - modalWrapper: `${defaultVariables.dark.modalWrapper} sm:top-14 rounded-md`, - secondaryDangerButton: `${defaultVariables.dark.secondaryDangerButton} rounded-full`, - }, - light: { - bellButton: cx( - defaultVariables.light.bellButton, - 'bg-transparent', - 'border-none', - 'h-10', - 'shadow-none', - 'text-neutral-600', - 'w-10', - 'active:bg-neutral-300', - 'hover:bg-neutral-200', - 'dark:text-neutral-400', - 'dark:hover:text-neutral-200', - 'dark:active:bg-neutral-600', - 'dark:hover:bg-neutral-700', - ), - iconButton: `${defaultVariables.light.iconButton} hover:opacity-100 bg-transparent`, - buttonLoading: `${defaultVariables.light.buttonLoading} rounded-full min-h-[40px]`, - adornmentButton: `${defaultVariables.light.adornmentButton} bg-sky-500 hover:!bg-sky-400 active:bg-sky-500 rounded transition-colors`, - colors: { - ...defaultVariables.light.colors, - label: 'text-neutral-900', - toggleThumb: 'bg-white', - toggleBackground: 'bg-zinc-300', - toggleBackgroundActive: 'bg-sky-500', - }, - textStyles: { - input: cx( - defaultVariables.light.textStyles.input, - 'text-neutral-900', - 'placeholder:text-fgd-3', - ), - body: `${defaultVariables.light.textStyles.body} text-neutral-900`, - small: `${defaultVariables.light.textStyles.small} text-neutral-900`, - xsmall: `${defaultVariables.light.textStyles.xsmall} text-neutral-900`, - label: `${defaultVariables.light.textStyles.label} dt-text-sm dt-font-bold`, - }, - outlinedInput: `${defaultVariables.light.outlinedInput} h-12 rounded text-neutral-900 focus-within:border-sky-500`, - modal: `${defaultVariables.light.modal} sm:border sm:rounded-md sm:shadow-md`, - modalWrapper: `${defaultVariables.light.modalWrapper} sm:top-14`, - secondaryDangerButton: `${defaultVariables.light.secondaryDangerButton} rounded-full`, - }, -}; diff --git a/hub/components/GlobalHeader/User/DialectNotifications/solanaWalletToDialectWallet.ts b/hub/components/GlobalHeader/User/DialectNotifications/solanaWalletToDialectWallet.ts deleted file mode 100644 index 3d59bc64b5..0000000000 --- a/hub/components/GlobalHeader/User/DialectNotifications/solanaWalletToDialectWallet.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { DialectSolanaWalletAdapter } from '@dialectlabs/react-sdk-blockchain-solana'; -import { WalletContextState as SolanaWalletContextState } from '@solana/wallet-adapter-react'; - -export function solanaWalletToDialectWallet( - wallet: SolanaWalletContextState, -): DialectSolanaWalletAdapter | null { - if ( - !wallet.connected || - wallet.connecting || - wallet.disconnecting || - !wallet.publicKey - ) { - return null; - } - - return { - publicKey: wallet.publicKey, - signMessage: wallet.signMessage, - signTransaction: wallet.signTransaction, - signAllTransactions: wallet.signAllTransactions, - // @ts-ignore - diffieHellman: wallet.wallet?.adapter?._wallet?.diffieHellman - ? async (pubKey: any) => { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - return wallet.wallet?.adapter?._wallet?.diffieHellman(pubKey); - } - : undefined, - }; -} diff --git a/hub/components/GlobalStats/NumVoteRecords/index.tsx b/hub/components/GlobalStats/NumVoteRecords/index.tsx index 4d76cbc44b..485336a38c 100644 --- a/hub/components/GlobalStats/NumVoteRecords/index.tsx +++ b/hub/components/GlobalStats/NumVoteRecords/index.tsx @@ -24,7 +24,7 @@ export function NumVoteRecords(props: Props) { props.className, )} > - Number of Votes + Number of votes {formatNumber(props.voteRecords.length, undefined, { maximumFractionDigits: 0, diff --git a/hub/components/Hub/Gallery/index.tsx b/hub/components/Hub/Gallery/index.tsx index 599ea608bd..4e7e6204ab 100644 --- a/hub/components/Hub/Gallery/index.tsx +++ b/hub/components/Hub/Gallery/index.tsx @@ -4,15 +4,21 @@ import * as AspectRatio from '@radix-ui/react-aspect-ratio'; import cx from '@hub/lib/cx'; export function getYoutubeEmbedUrl(url: string) { - const regExp = /^.*((youtu.be\/)|(v\/)|(\/u\/\w\/)|(embed\/)|(watch\?))\??v?=?([^#&?]*).*/; + if (!url.startsWith('youtube.com/') && !url.startsWith('youtu.be/')) { + return null; + } + + const regExp = /^(?:https?:\/\/)?(?:www\.)?(?:youtube\.com\/(?:[^\/\n\s]+\/\S+\/|(?:v|e(?:mbed)?)\/|\S*?[?&]v=)|youtu\.be\/)([a-zA-Z0-9_-]{11})/; const match = url.match(regExp); - const id = match && match[7].length == 11 ? match[7] : null; + + const id = match && /^[a-zA-Z0-9_-]{11}$/.test(match[1]) ? match[1] : null; if (id) { - return `https://www.youtube.com/embed/${id}`; + const safeId = encodeURIComponent(id); + return `https://www.youtube.com/embed/${safeId}`; } - return url; + return null; } interface Props { @@ -57,65 +63,68 @@ export function Gallery(props: Props) { width: props.items[0].width / 2, }} /> - {props.items.map((item, i) => ( -
- {item.url.includes('www.youtube.com') ? ( -
- -