diff --git a/.gitignore b/.gitignore index 97a8637876..55713a92f9 100644 --- a/.gitignore +++ b/.gitignore @@ -43,3 +43,5 @@ yarn-error.log* # Sentry .sentryclirc + +.vscode/settings.json \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index 92a022a112..0000000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "typescript.tsdk": "node_modules/typescript/lib", - "cSpell.words": [ - "Addin", - "blockworks", - "lamports", - "solana", - "VERCEL", - "WSOL" - ], - "editor.formatOnSave": true -} diff --git a/actions/castVote.ts b/actions/castVote.ts index 0dd54fbb2c..111470eb2c 100644 --- a/actions/castVote.ts +++ b/actions/castVote.ts @@ -1,7 +1,7 @@ import { + Connection, Keypair, PublicKey, - Transaction, TransactionInstruction, } from '@solana/web3.js' import { @@ -14,6 +14,8 @@ import { VoteKind, VoteType, withPostChatMessage, + withCreateTokenOwnerRecord, + getVoteRecordAddress, } from '@solana/spl-governance' import { ProgramAccount } from '@solana/spl-governance' import { RpcContext } from '@solana/spl-governance' @@ -28,12 +30,17 @@ import { SequenceType, txBatchesToInstructionSetWithSigners, } from '@utils/sendTransactions' -import { sendTransaction } from '@utils/send' import { calcCostOfNftVote, checkHasEnoughSolToVote } from '@tools/nftVoteCalc' import useNftProposalStore from 'NftVotePlugin/NftProposalStore' import { HeliumVsrClient } from 'HeliumVotePlugin/sdk/client' import { NftVoterClient } from '@utils/uiTypes/NftVoterClient' +import { fetchRealmByPubkey } from '@hooks/queries/realm' +import { fetchProposalByPubkeyQuery } from '@hooks/queries/proposal' +import { findPluginName } from '@hooks/queries/governancePower' +import { DELEGATOR_BATCH_VOTE_SUPPORT_BY_PLUGIN } from '@constants/flags' +import { fetchTokenOwnerRecordByPubkey } from '@hooks/queries/tokenOwnerRecord' import { fetchProgramVersion } from '@hooks/queries/useProgramVersionQuery' +import { fetchVoteRecordByPubkey } from '@hooks/queries/voteRecord' const getVetoTokenMint = ( proposal: ProgramAccount, @@ -50,6 +57,86 @@ const getVetoTokenMint = ( return vetoTokenMint } +const createDelegatorVote = async ({ + connection, + realmPk, + proposalPk, + tokenOwnerRecordPk, + userPk, + vote, +}: { + connection: Connection + realmPk: PublicKey + proposalPk: PublicKey + tokenOwnerRecordPk: PublicKey + userPk: PublicKey + vote: Vote +}) => { + // + const realm = (await fetchRealmByPubkey(connection, realmPk)).result + if (!realm) throw new Error() + const proposal = (await fetchProposalByPubkeyQuery(connection, proposalPk)) + .result + if (!proposal) throw new Error() + + const programVersion = await fetchProgramVersion(connection, realm.owner) + + const castVoteIxs: TransactionInstruction[] = [] + await withCastVote( + castVoteIxs, + realm.owner, + programVersion, + realm.pubkey, + proposal.account.governance, + proposal.pubkey, + proposal.account.tokenOwnerRecord, + tokenOwnerRecordPk, + userPk, + proposal.account.governingTokenMint, + vote, + userPk + //plugin?.voterWeightPk, + //plugin?.maxVoterWeightRecord + ) + return castVoteIxs +} + +const createTokenOwnerRecordIfNeeded = async ({ + connection, + realmPk, + tokenOwnerRecordPk, + payer, + governingTokenMint, +}: { + connection: Connection + realmPk: PublicKey + tokenOwnerRecordPk: PublicKey + payer: PublicKey + governingTokenMint: PublicKey +}) => { + const realm = await fetchRealmByPubkey(connection, realmPk) + if (!realm.result) throw new Error() + const version = await fetchProgramVersion(connection, realm.result.owner) + + const tokenOwnerRecord = await fetchTokenOwnerRecordByPubkey( + connection, + tokenOwnerRecordPk + ) + if (tokenOwnerRecord.result) return [] + // create token owner record + const ixs: TransactionInstruction[] = [] + await withCreateTokenOwnerRecord( + ixs, + realm.result.owner, + version, + realmPk, + payer, + governingTokenMint, + payer + ) + return ixs +} + export async function castVote( { connection, wallet, programId, walletPubkey }: RpcContext, realm: ProgramAccount, @@ -60,9 +147,9 @@ export async function castVote( votingPlugin?: VotingClient, runAfterConfirmation?: (() => void) | null, voteWeights?: number[], - _additionalTokenOwnerRecords?: [] + additionalTokenOwnerRecords?: PublicKey[] ) { - const signers: Keypair[] = [] + const chatMessageSigners: Keypair[] = [] const createCastNftVoteTicketIxs: TransactionInstruction[] = [] const createPostMessageTicketIxs: TransactionInstruction[] = [] @@ -81,7 +168,6 @@ export async function castVote( tokenOwnerRecord, createCastNftVoteTicketIxs ) - console.log('PLUGIN IXS', pluginCastVoteIxs) const isMulti = proposal.account.voteType !== VoteType.SINGLE_CHOICE && @@ -153,6 +239,39 @@ export async function castVote( plugin?.maxVoterWeightRecord ) + const delegatorCastVoteAtoms = + additionalTokenOwnerRecords && + DELEGATOR_BATCH_VOTE_SUPPORT_BY_PLUGIN[ + findPluginName(votingPlugin?.client?.program.programId) + ] + ? ( + await Promise.all( + additionalTokenOwnerRecords.map(async (tokenOwnerRecordPk) => { + // Skip vote if already voted + const voteRecordPk = await getVoteRecordAddress( + realm.owner, + proposal.pubkey, + tokenOwnerRecordPk + ) + const voteRecord = await fetchVoteRecordByPubkey( + connection, + voteRecordPk + ) + if (voteRecord.found) return undefined + + return createDelegatorVote({ + connection, + realmPk: realm.pubkey, + proposalPk: proposal.pubkey, + tokenOwnerRecordPk, + userPk: walletPubkey, + vote, + }) + }) + ) + ).filter((x): x is NonNullable => x !== undefined) + : [] + const pluginPostMessageIxs: TransactionInstruction[] = [] const postMessageIxs: TransactionInstruction[] = [] if (message) { @@ -165,7 +284,7 @@ export async function castVote( await withPostChatMessage( postMessageIxs, - signers, + chatMessageSigners, GOVERNANCE_CHAT_PROGRAM_ID, programId, realm.pubkey, @@ -182,22 +301,48 @@ export async function castVote( const isNftVoter = votingPlugin?.client instanceof NftVoterClient const isHeliumVoter = votingPlugin?.client instanceof HeliumVsrClient + const tokenOwnerRecordIxs = await createTokenOwnerRecordIfNeeded({ + connection, + realmPk: realm.pubkey, + tokenOwnerRecordPk: tokenOwnerRecord, + payer, + governingTokenMint: tokenMint, + }) if (!isNftVoter && !isHeliumVoter) { - const transaction = new Transaction() - transaction.add( - ...[ - ...pluginCastVoteIxs, - ...castVoteIxs, - ...pluginPostMessageIxs, - ...postMessageIxs, - ] + const batch1 = [ + ...tokenOwnerRecordIxs, + ...pluginCastVoteIxs, + ...castVoteIxs, + ...pluginPostMessageIxs, + ...postMessageIxs, + ] + // chunk size chosen conservatively. "Atoms" refers to atomic clusters of instructions (namely, updatevoterweight? + vote) + const delegatorBatches = chunks(delegatorCastVoteAtoms, 2).map((x) => + x.flat() ) + const actions = [batch1, ...delegatorBatches].map((ixs) => ({ + instructionsSet: ixs.map((ix) => ({ + transactionInstruction: ix, + signers: chatMessageSigners.filter((kp) => + ix.keys.find((key) => key.isSigner && key.pubkey.equals(kp.publicKey)) + ), + })), + sequenceType: SequenceType.Parallel, + })) - await sendTransaction({ transaction, wallet, connection, signers }) - if (runAfterConfirmation) { - runAfterConfirmation() - } + await sendTransactionsV3({ + connection, + wallet, + transactionInstructions: actions, + callbacks: { + afterAllTxConfirmed: () => { + if (runAfterConfirmation) { + runAfterConfirmation() + } + }, + }, + }) } // we need to chunk instructions @@ -217,7 +362,7 @@ export async function castVote( return { instructionsSet: txBatchesToInstructionSetWithSigners( txBatch, - message ? [[], signers] : [], + message ? [[], chatMessageSigners] : [], // seeing signer related bugs when posting chat? This is likely culprit batchIdx ), sequenceType: SequenceType.Sequential, @@ -249,16 +394,18 @@ export async function castVote( [...createCastNftVoteTicketIxs, ...createPostMessageTicketIxs], 1 ) - const otherChunks = chunks( - [ - ...pluginCastVoteIxs, - ...castVoteIxs, - ...pluginPostMessageIxs, - ...postMessageIxs, - ], - 2 + + // last element of pluginCastVoteIxs + const last = pluginCastVoteIxs[pluginCastVoteIxs.length - 1] + // everything except last element of pluginCastVoteIxs + const nftCountingChunks = pluginCastVoteIxs.slice(0, -1) + const voteChunk = [last, ...castVoteIxs] // the final nft-voter.CastNftVote instruction has to in same tx as the vote + const chunkedIxs = [...chunks(nftCountingChunks, 2), voteChunk].filter( + (x) => x.length > 0 ) + // note that we are not chunking postMessageIxs, not yet supported (somehow) + const instructionsChunks = [ ...createNftVoteTicketsChunks.map((txBatch, batchIdx) => { return { @@ -270,11 +417,11 @@ export async function castVote( sequenceType: SequenceType.Parallel, } }), - ...otherChunks.map((txBatch, batchIdx) => { + ...chunkedIxs.map((txBatch, batchIdx) => { return { instructionsSet: txBatchesToInstructionSetWithSigners( txBatch, - message ? [[], signers] : [], + message ? [[], chatMessageSigners] : [], // seeing signer related bugs when posting chat? This is likely culprit batchIdx ), sequenceType: SequenceType.Sequential, diff --git a/components/GovernancePower/GovernancePowerCard.tsx b/components/GovernancePower/GovernancePowerCard.tsx index f5d54b0d7f..1465eceebf 100644 --- a/components/GovernancePower/GovernancePowerCard.tsx +++ b/components/GovernancePower/GovernancePowerCard.tsx @@ -29,6 +29,20 @@ const GovernancePowerTitle = () => { ) } +/* +// TODO: refactor deposit components to their own generic DepositForRole component +const VanillaDeposit = ({ role }: { role: 'community' | 'council' }) => { + const { connection } = useConnection() + + const realmPk = useSelectedRealmPubkey() + + const { result: kind } = useAsync(async () => { + if (realmPk === undefined) return undefined + return determineVotingPowerType(connection, realmPk, role) + }, [connection, realmPk, role]) + + return kind === 'vanilla' ? : <> +} */ const GovernancePowerCard = () => { const connected = useWalletOnePointOh()?.connected ?? false @@ -38,12 +52,6 @@ const GovernancePowerCard = () => { const bothLoading = communityPower.loading && councilPower.loading - const bothZero = - communityPower.result !== undefined && - councilPower.result !== undefined && - communityPower.result.isZero() && - councilPower.result.isZero() - const realmConfig = useRealmConfigQuery().data?.result return ( @@ -58,10 +66,6 @@ const GovernancePowerCard = () => {
- ) : bothZero ? ( -
- You do not have any governance power in this dao -
) : (
{realmConfig?.account.communityTokenConfig.tokenType === diff --git a/components/GovernancePower/GovernancePowerForRole.tsx b/components/GovernancePower/GovernancePowerForRole.tsx index 6315343556..173d5083bc 100644 --- a/components/GovernancePower/GovernancePowerForRole.tsx +++ b/components/GovernancePower/GovernancePowerForRole.tsx @@ -26,7 +26,6 @@ export default function GovernancePowerForRole({ const { result: kind } = useAsync(async () => { if (realmPk === undefined) return undefined - return determineVotingPowerType(connection, realmPk, role) }, [connection, realmPk, role]) @@ -40,9 +39,10 @@ export default function GovernancePowerForRole({ <> {role === 'community' ? ( kind === 'vanilla' ? ( - +
+ - +
) : kind === 'VSR' ? ( ) : kind === 'NFT' ? ( @@ -51,9 +51,10 @@ export default function GovernancePowerForRole({ ) : null ) : kind === 'vanilla' ? ( - +
+ - +
) : null} ) diff --git a/components/GovernancePower/Vanilla/useDepositCallback.tsx b/components/GovernancePower/Vanilla/useDepositCallback.tsx index 3d8e2dfa8e..08b47f7bde 100644 --- a/components/GovernancePower/Vanilla/useDepositCallback.tsx +++ b/components/GovernancePower/Vanilla/useDepositCallback.tsx @@ -38,24 +38,30 @@ export const useDepositCallback = ( ASSOCIATED_TOKEN_PROGRAM_ID, TOKEN_PROGRAM_ID, mint, - walletPk // owner + walletPk, // owner + true ) const instructions: TransactionInstruction[] = [] const signers: Keypair[] = [] - const transferAuthority = approveTokenTransfer( - instructions, - [], - userAtaPk, - wallet!.publicKey!, - amount - ) + // Checks if the connected wallet is the Squads Multisig extension (or any PDA wallet for future reference). If it is the case, it will not use an ephemeral signer. + const transferAuthority = wallet?.name == "SquadsX" + ? undefined + : approveTokenTransfer(instructions, [], userAtaPk, wallet!.publicKey!, amount); - signers.push(transferAuthority) + if (transferAuthority) { + signers.push(transferAuthority); + } const programVersion = await fetchProgramVersion(connection, realm.owner) + const publicKeyToUse = transferAuthority != undefined && wallet?.publicKey != null ? transferAuthority.publicKey : wallet?.publicKey; + + if (!publicKeyToUse) { + throw new Error() + } + await withDepositGoverningTokens( instructions, realm.owner, @@ -64,7 +70,7 @@ export const useDepositCallback = ( userAtaPk, mint, walletPk, - transferAuthority.publicKey, + publicKeyToUse, walletPk, amount ) diff --git a/components/SelectPrimaryDelegators.tsx b/components/SelectPrimaryDelegators.tsx index 89e4bbb077..b30f259482 100644 --- a/components/SelectPrimaryDelegators.tsx +++ b/components/SelectPrimaryDelegators.tsx @@ -9,7 +9,8 @@ import { ProgramAccount, TokenOwnerRecord } from '@solana/spl-governance' import { capitalize } from '@utils/helpers' import { ProfileName } from './Profile/ProfileName' -const YOUR_WALLET_VALUE = 'Your wallet' +const YOUR_WALLET_VALUE = 'Yourself + all delegators' +const JUST_YOUR_WALLET = 'Yourself only' const SelectPrimaryDelegators = () => { const wallet = useWalletOnePointOh() @@ -49,42 +50,42 @@ const SelectPrimaryDelegators = () => { setCouncilDelegator, } = useSelectedDelegatorStore() - const handleCouncilSelect = (councilTokenRecord: string | undefined) => { + const handleCouncilSelect = (councilWalletPk: string | undefined) => { setCouncilDelegator( - councilTokenRecord !== undefined - ? new PublicKey(councilTokenRecord) - : undefined + councilWalletPk !== undefined ? new PublicKey(councilWalletPk) : undefined ) } - const handleCommunitySelect = (communityPubKey?: string) => { + const handleCommunitySelect = (communityWalletPk: string | undefined) => { setCommunityDelegator( - communityPubKey ? new PublicKey(communityPubKey) : undefined + communityWalletPk ? new PublicKey(communityWalletPk) : undefined ) } return ( <> - {walletId && + {((walletId && communityTorsDelegatedToUser && - communityTorsDelegatedToUser.length > 0 && ( - - )} - {walletId && + communityTorsDelegatedToUser.length > 0) || + communityDelegator) && ( + + )} + {((walletId && councilTorsDelegatedToUser && - councilTorsDelegatedToUser.length > 0 && ( - - )} + councilTorsDelegatedToUser.length > 0) || + councilDelegator) && ( + + )} ) } @@ -103,6 +104,8 @@ function PrimaryDelegatorSelect({ kind: 'community' | 'council' tors: ProgramAccount[] }) { + const wallet = useWalletOnePointOh() + const walletPk = wallet?.publicKey ?? undefined return (
@@ -115,15 +118,19 @@ function PrimaryDelegatorSelect({ onChange={handleSelect} componentLabel={ selectedDelegator ? ( -
- -
-
+ walletPk && selectedDelegator.equals(walletPk) ? ( + JUST_YOUR_WALLET + ) : ( +
+ +
+
+ ) ) : ( YOUR_WALLET_VALUE ) @@ -132,6 +139,16 @@ function PrimaryDelegatorSelect({ {YOUR_WALLET_VALUE} + {walletPk ? ( + + {JUST_YOUR_WALLET} + + ) : ( + <> + )} {tors.map((delegatedTor) => ( { const getMangoAccounts = async () => { const accounts = await mangoClient?.getMangoAccountsForOwner( mangoGroup!, - account.extensions.transferAddress! + account.extensions.token!.account.owner! ) if (accounts) { setMangoAccounts(accounts) @@ -46,23 +48,96 @@ const MangoModal = ({ account }: { account: AssetAccount }) => { try { setIsProposing(true) const newAccountNum = getNextAccountNumber(mangoAccounts) - const ix = await mangoClient?.program.methods + const bank = mangoGroup!.getFirstBankByMint( + account.extensions.mint!.publicKey! + ) + const createAccIx = await mangoClient!.program.methods .accountCreate( newAccountNum, 8, - 8, - 8, - 8, + 4, + 4, + 32, mangoAccName || `Account ${newAccountNum + 1}` ) .accounts({ group: mangoGroup!.publicKey, - owner: account.extensions.transferAddress, - payer: account.extensions.transferAddress, + 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) + ) + ) + .instruction() + + const delegateIx = await mangoClient!.program.methods + .accountEdit( + null, + new PublicKey('EsWMqyaEDoAqMgiWG9McSmpetBiYjL4VkHPkfevxKu4D'), + null, + null + ) + .accounts({ + group: mangoGroup!.publicKey, + account: mangoAccount, + owner: account.extensions.token!.account.owner!, }) .instruction() - const instructionData = { - data: getInstructionDataFromBase64(serializeInstructionToBase64(ix!)), + + const createAccInstData = { + data: getInstructionDataFromBase64( + serializeInstructionToBase64(createAccIx!) + ), + holdUpTime: + account?.governance.account?.config.minInstructionHoldUpTime, + prerequisiteInstructions: [], + } + const depositAccInstData = { + data: getInstructionDataFromBase64( + serializeInstructionToBase64(depositIx!) + ), + holdUpTime: + account?.governance.account?.config.minInstructionHoldUpTime, + prerequisiteInstructions: [], + } + const delegateAccInstData = { + data: getInstructionDataFromBase64( + serializeInstructionToBase64(delegateIx!) + ), holdUpTime: account?.governance.account?.config.minInstructionHoldUpTime, prerequisiteInstructions: [], @@ -71,7 +146,11 @@ const MangoModal = ({ account }: { account: AssetAccount }) => { title: proposalTitle, description: proposalDescription, voteByCouncil, - instructionsData: [instructionData], + instructionsData: [ + createAccInstData, + depositAccInstData, + delegateAccInstData, + ], governance: account.governance!, }) const url = fmtUrlWithCluster( @@ -87,6 +166,7 @@ const MangoModal = ({ account }: { account: AssetAccount }) => {

Mango

+ {console.log(mangoAccount)} {mangoGroup && (