From 90d8d63a0475976a86eced55715634e2f2399c10 Mon Sep 17 00:00:00 2001 From: Dummy Tester 123 Date: Mon, 15 Mar 2021 17:33:25 -0500 Subject: [PATCH] WIP commit on re-engineering the front end to take into account governance. Not nearly done, and none of it works, but dont want to lose work, so making intermediate commit just in case. --- packages/common/src/contexts/accounts.tsx | 2 +- packages/common/src/contexts/connection.tsx | 73 +++++++ .../proposals/src/actions/createProposal.ts | 205 ++++++++++-------- ...VotingTokens.ts => depositVotingTokens.ts} | 70 +++--- .../src/actions/mintGovernanceTokens.ts | 103 +++++++++ .../src/actions/registerProgramGovernance.ts | 139 ++++++++++++ packages/proposals/src/actions/vote.ts | 21 +- .../src/actions/withdrawVotingTokens.ts | 156 +++++++++++++ ...{AddVotes.tsx => MintGovernanceTokens.tsx} | 193 ++++++++++------- .../Proposal/NewInstructionCard.tsx | 140 ++---------- .../components/Proposal/RegisterToVote.tsx | 104 +++++++++ .../src/components/Proposal/Vote.tsx | 61 ++++-- .../components/Proposal/WithdrawTokens.tsx | 0 packages/proposals/src/constants/labels.ts | 28 ++- packages/proposals/src/contexts/proposals.tsx | 54 +++-- .../addCustomSingleSignerTransaction.ts | 4 +- .../src/models/depositVotingTokens.ts | 74 +++++++ .../src/models/initTimelockConfig.ts | 72 ++++++ .../proposals/src/models/initTimelockSet.ts | 64 +++--- .../proposals/src/models/mintVotingTokens.ts | 69 ------ packages/proposals/src/models/timelock.ts | 145 +++++++++---- packages/proposals/src/models/vote.ts | 43 +++- .../src/models/withdrawVotingTokens.ts | 87 ++++++++ .../proposals/src/views/proposal/index.tsx | 56 +++-- 24 files changed, 1410 insertions(+), 553 deletions(-) rename packages/proposals/src/actions/{mintVotingTokens.ts => depositVotingTokens.ts} (69%) create mode 100644 packages/proposals/src/actions/mintGovernanceTokens.ts create mode 100644 packages/proposals/src/actions/registerProgramGovernance.ts create mode 100644 packages/proposals/src/actions/withdrawVotingTokens.ts rename packages/proposals/src/components/Proposal/{AddVotes.tsx => MintGovernanceTokens.tsx} (50%) create mode 100644 packages/proposals/src/components/Proposal/RegisterToVote.tsx create mode 100644 packages/proposals/src/components/Proposal/WithdrawTokens.tsx create mode 100644 packages/proposals/src/models/depositVotingTokens.ts create mode 100644 packages/proposals/src/models/initTimelockConfig.ts delete mode 100644 packages/proposals/src/models/mintVotingTokens.ts create mode 100644 packages/proposals/src/models/withdrawVotingTokens.ts diff --git a/packages/common/src/contexts/accounts.tsx b/packages/common/src/contexts/accounts.tsx index 1e81616..1334cfe 100644 --- a/packages/common/src/contexts/accounts.tsx +++ b/packages/common/src/contexts/accounts.tsx @@ -629,7 +629,7 @@ export const deserializeAccount = (data: Buffer) => { }; // TODO: expose in spl package -const deserializeMint = (data: Buffer) => { +export const deserializeMint = (data: Buffer) => { if (data.length !== MintLayout.span) { throw new Error('Not a valid Mint'); } diff --git a/packages/common/src/contexts/connection.tsx b/packages/common/src/contexts/connection.tsx index 18b8509..9b91afc 100644 --- a/packages/common/src/contexts/connection.tsx +++ b/packages/common/src/contexts/connection.tsx @@ -236,6 +236,79 @@ export const getErrorForTransaction = async ( return errors; }; +export const sendTransactions = async ( + connection: Connection, + wallet: any, + instructionSet: TransactionInstruction[][], + signersSet: Account[][], + awaitConfirmation = true, + commitment = 'singleGossip', + successCallback: (txid: string, ind: number) => void = (txid, ind) => {}, + failCallback: (txid: string, ind: number) => boolean = (txid, ind) => false, +) => { + const unsignedTxns: Transaction[] = []; + for (let i = 0; i < instructionSet.length; i++) { + const instructions = instructionSet[i]; + const signers = signersSet[i]; + let transaction = new Transaction(); + instructions.forEach(instruction => transaction.add(instruction)); + transaction.recentBlockhash = ( + await connection.getRecentBlockhash('max') + ).blockhash; + transaction.setSigners( + // fee payied by the wallet owner + wallet.publicKey, + ...signers.map(s => s.publicKey), + ); + if (signers.length > 0) { + transaction.partialSign(...signers); + } + unsignedTxns.push(transaction); + } + const signedTxns = await wallet.signTransactions(unsignedTxns); + const rawTransactions = signedTxns.map((t: Transaction) => t.serialize()); + let options = { + skipPreflight: true, + commitment, + }; + + for (let i = 0; i < rawTransactions.length; i++) { + const rawTransaction = rawTransactions[i]; + const txid = await connection.sendRawTransaction(rawTransaction, options); + + if (awaitConfirmation) { + const status = ( + await connection.confirmTransaction( + txid, + options && (options.commitment as any), + ) + ).value; + + if (status?.err && !failCallback(txid, i)) { + const errors = await getErrorForTransaction(connection, txid); + notify({ + message: 'Transaction failed...', + description: ( + <> + {errors.map(err => ( +
{err}
+ ))} + + + ), + type: 'error', + }); + + throw new Error( + `Raw transaction ${txid} failed (${JSON.stringify(status)})`, + ); + } else { + successCallback(txid, i); + } + } + } +}; + export const sendTransaction = async ( connection: Connection, wallet: any, diff --git a/packages/proposals/src/actions/createProposal.ts b/packages/proposals/src/actions/createProposal.ts index 20e3eb0..e4d648b 100644 --- a/packages/proposals/src/actions/createProposal.ts +++ b/packages/proposals/src/actions/createProposal.ts @@ -5,18 +5,13 @@ import { SystemProgram, TransactionInstruction, } from '@solana/web3.js'; -import { contexts, utils, actions } from '@oyster/common'; +import { contexts, utils, actions, ParsedAccount } from '@oyster/common'; import { AccountLayout, MintLayout } from '@solana/spl-token'; import { initTimelockSetInstruction } from '../models/initTimelockSet'; -import { - ConsensusAlgorithm, - ExecutionType, - TimelockSetLayout, - TimelockType, -} from '../models/timelock'; +import { TimelockConfig, TimelockSetLayout } from '../models/timelock'; -const { sendTransaction } = contexts.Connection; +const { sendTransactions } = contexts.Connection; const { createMint, createTokenAccount } = actions; const { notify } = utils; @@ -25,9 +20,7 @@ export const createProposal = async ( wallet: any, name: string, description: string, - timelockType: TimelockType, - consensusAlgorithm: ConsensusAlgorithm, - executionType: ExecutionType, + timelockConfig: ParsedAccount, ): Promise => { const PROGRAM_IDS = utils.programIds(); @@ -44,18 +37,25 @@ export const createProposal = async ( const { sigMint, voteMint, + yesVoteMint, + noVoteMint, adminMint, voteValidationAccount, sigValidationAccount, adminValidationAccount, adminDestinationAccount, sigDestinationAccount, + yesVoteDumpAccount, + noVoteDumpAccount, + governanceHoldingAccount, authority, - } = await createValidationAccountsAndMints( - connection, + instructions: associatedInstructions, + signers: associatedSigners, + } = await getAssociatedAccountsAndInstructions( wallet, accountRentExempt, mintRentExempt, + timelockConfig, ); const timelockRentExempt = await connection.getMinimumBalanceForRentExemption( @@ -80,17 +80,19 @@ export const createProposal = async ( sigMint, adminMint, voteMint, + yesVoteMint, + noVoteMint, sigValidationAccount, adminValidationAccount, voteValidationAccount, adminDestinationAccount, sigDestinationAccount, + yesVoteDumpAccount, + noVoteDumpAccount, + governanceHoldingAccount, + timelockConfig.info.governanceMint, + timelockConfig.pubkey, authority, - { - timelockType, - consensusAlgorithm, - executionType, - }, description, name, ), @@ -103,11 +105,11 @@ export const createProposal = async ( }); try { - let tx = await sendTransaction( + let tx = await sendTransactions( connection, wallet, - instructions, - signers, + [...associatedInstructions, instructions], + [...associatedSigners, signers], true, ); @@ -127,165 +129,192 @@ export const createProposal = async ( interface ValidationReturn { sigMint: PublicKey; voteMint: PublicKey; + yesVoteMint: PublicKey; + noVoteMint: PublicKey; adminMint: PublicKey; voteValidationAccount: PublicKey; sigValidationAccount: PublicKey; adminValidationAccount: PublicKey; adminDestinationAccount: PublicKey; sigDestinationAccount: PublicKey; + yesVoteDumpAccount: PublicKey; + noVoteDumpAccount: PublicKey; + governanceHoldingAccount: PublicKey; authority: PublicKey; + signers: [Account[], Account[], Account[]]; + instructions: [ + TransactionInstruction[], + TransactionInstruction[], + TransactionInstruction[], + ]; } -async function createValidationAccountsAndMints( - connection: Connection, + +async function getAssociatedAccountsAndInstructions( wallet: any, accountRentExempt: number, mintRentExempt: number, + timelockConfig: ParsedAccount, ): Promise { const PROGRAM_IDS = utils.programIds(); - notify({ - message: `Creating mints...`, - type: 'warn', - description: `Please wait...`, - }); const [authority] = await PublicKey.findProgramAddress( [PROGRAM_IDS.timelock.programAccountId.toBuffer()], PROGRAM_IDS.timelock.programId, ); - let signers: Account[] = []; - let instructions: TransactionInstruction[] = []; + let mintSigners: Account[] = []; + let mintInstructions: TransactionInstruction[] = []; const adminMint = createMint( - instructions, + mintInstructions, wallet.publicKey, mintRentExempt, 0, authority, authority, - signers, + mintSigners, ); const sigMint = createMint( - instructions, + mintInstructions, wallet.publicKey, mintRentExempt, 0, authority, authority, - signers, + mintSigners, ); const voteMint = createMint( - instructions, + mintInstructions, wallet.publicKey, mintRentExempt, 0, authority, authority, - signers, + mintSigners, ); - try { - let tx = await sendTransaction( - connection, - wallet, - instructions, - signers, - true, - ); + const yesVoteMint = createMint( + mintInstructions, + wallet.publicKey, + mintRentExempt, + 0, + authority, + authority, + mintSigners, + ); - notify({ - message: `Mints created.`, - type: 'success', - description: `Transaction - ${tx}`, - }); - } catch (ex) { - console.error(ex); - throw new Error(); - } + const noVoteMint = createMint( + mintInstructions, + wallet.publicKey, + mintRentExempt, + 0, + authority, + authority, + mintSigners, + ); - notify({ - message: `Creating validation accounts...`, - type: 'warn', - description: `Please wait...`, - }); - - signers = []; - instructions = []; + let validationSigners: Account[] = []; + let validationInstructions: TransactionInstruction[] = []; const adminValidationAccount = createTokenAccount( - instructions, + validationInstructions, wallet.publicKey, accountRentExempt, adminMint, authority, - signers, + validationSigners, ); const sigValidationAccount = createTokenAccount( - instructions, + validationInstructions, wallet.publicKey, accountRentExempt, sigMint, authority, - signers, + validationSigners, ); const voteValidationAccount = createTokenAccount( - instructions, + validationInstructions, wallet.publicKey, accountRentExempt, voteMint, authority, - signers, + validationSigners, ); + let destinationSigners: Account[] = []; + let destinationInstructions: TransactionInstruction[] = []; + const adminDestinationAccount = createTokenAccount( - instructions, + destinationInstructions, wallet.publicKey, accountRentExempt, adminMint, wallet.publicKey, - signers, + destinationSigners, ); const sigDestinationAccount = createTokenAccount( - instructions, + destinationInstructions, wallet.publicKey, accountRentExempt, sigMint, wallet.publicKey, - signers, + destinationSigners, ); - try { - let tx = await sendTransaction( - connection, - wallet, - instructions, - signers, - true, - ); + let holdingSigners: Account[] = []; + let holdingInstructions: TransactionInstruction[] = []; - notify({ - message: `Admin and signatory accounts created.`, - type: 'success', - description: `Transaction - ${tx}`, - }); - } catch (ex) { - console.error(ex); - throw new Error(); - } + const yesVoteDumpAccount = createTokenAccount( + holdingInstructions, + wallet.publicKey, + accountRentExempt, + yesVoteMint, + wallet.publicKey, + holdingSigners, + ); + + const noVoteDumpAccount = createTokenAccount( + holdingInstructions, + wallet.publicKey, + accountRentExempt, + noVoteMint, + wallet.publicKey, + holdingSigners, + ); + + const governanceHoldingAccount = createTokenAccount( + holdingInstructions, + wallet.publicKey, + accountRentExempt, + timelockConfig.info.governanceMint, + wallet.publicKey, + holdingSigners, + ); return { sigMint, voteMint, adminMint, + yesVoteMint, + noVoteMint, voteValidationAccount, sigValidationAccount, adminValidationAccount, adminDestinationAccount, sigDestinationAccount, + yesVoteDumpAccount, + noVoteDumpAccount, + governanceHoldingAccount, authority, + signers: [mintSigners, validationSigners, destinationSigners], + instructions: [ + mintInstructions, + validationInstructions, + destinationInstructions, + ], }; } diff --git a/packages/proposals/src/actions/mintVotingTokens.ts b/packages/proposals/src/actions/depositVotingTokens.ts similarity index 69% rename from packages/proposals/src/actions/mintVotingTokens.ts rename to packages/proposals/src/actions/depositVotingTokens.ts index 222246b..0ff0cee 100644 --- a/packages/proposals/src/actions/mintVotingTokens.ts +++ b/packages/proposals/src/actions/depositVotingTokens.ts @@ -14,20 +14,21 @@ import { import { TimelockSet } from '../models/timelock'; import { AccountLayout } from '@solana/spl-token'; -import { mintVotingTokensInstruction } from '../models/mintVotingTokens'; +import { depositVotingTokensInstruction } from '../models/depositVotingTokens'; import { LABELS } from '../constants'; const { createTokenAccount } = actions; const { sendTransaction } = contexts.Connection; const { notify } = utils; const { approve } = models; -export const mintVotingTokens = async ( +export const depositVotingTokens = async ( connection: Connection, wallet: any, proposal: ParsedAccount, - signatoryAccount: PublicKey, - newVotingAccountOwner: PublicKey, existingVoteAccount: PublicKey | undefined, + existingYesVoteAccount: PublicKey | undefined, + existingNoVoteAccount: PublicKey | undefined, + sourceAccount: PublicKey, votingTokenAmount: number, ) => { const PROGRAM_IDS = utils.programIds(); @@ -45,37 +46,31 @@ export const mintVotingTokens = async ( wallet.publicKey, accountRentExempt, proposal.info.votingMint, - newVotingAccountOwner, + wallet.publicKey, signers, ); + } - notify({ - message: LABELS.ADDING_NEW_VOTE_ACCOUNT, - description: LABELS.PLEASE_WAIT, - type: 'warn', - }); + if (!existingYesVoteAccount) { + createTokenAccount( + instructions, + wallet.publicKey, + accountRentExempt, + proposal.info.yesVotingMint, + wallet.publicKey, + signers, + ); + } - try { - let tx = await sendTransaction( - connection, - wallet, - instructions, - signers, - true, - ); - - notify({ - message: LABELS.NEW_VOTED_ACCOUNT_ADDED, - type: 'success', - description: LABELS.TRANSACTION + ` ${tx}`, - }); - } catch (ex) { - console.error(ex); - throw new Error(); - } - - signers = []; - instructions = []; + if (!existingNoVoteAccount) { + createTokenAccount( + instructions, + wallet.publicKey, + accountRentExempt, + proposal.info.noVotingMint, + wallet.publicKey, + signers, + ); } const [mintAuthority] = await PublicKey.findProgramAddress( @@ -86,20 +81,21 @@ export const mintVotingTokens = async ( const transferAuthority = approve( instructions, [], - signatoryAccount, + sourceAccount, wallet.publicKey, - 1, + votingTokenAmount, ); signers.push(transferAuthority); instructions.push( - mintVotingTokensInstruction( - proposal.pubkey, + depositVotingTokensInstruction( existingVoteAccount, + sourceAccount, + proposal.info.governanceHolding, proposal.info.votingMint, - signatoryAccount, - proposal.info.signatoryValidation, + proposal.pubkey, + proposal.info.config, transferAuthority.publicKey, mintAuthority, votingTokenAmount, diff --git a/packages/proposals/src/actions/mintGovernanceTokens.ts b/packages/proposals/src/actions/mintGovernanceTokens.ts new file mode 100644 index 0000000..2eb68d4 --- /dev/null +++ b/packages/proposals/src/actions/mintGovernanceTokens.ts @@ -0,0 +1,103 @@ +import { + Account, + Connection, + PublicKey, + TransactionInstruction, +} from '@solana/web3.js'; +import { + contexts, + utils, + models, + ParsedAccount, + actions, +} from '@oyster/common'; + +import { TimelockConfig } from '../models/timelock'; +import { AccountLayout, Token } from '@solana/spl-token'; +import { LABELS } from '../constants'; +const { createTokenAccount } = actions; +const { sendTransactions } = contexts.Connection; +const { notify } = utils; +export interface GovernanceEntryInterface { + owner: PublicKey; + governanceAccount: PublicKey | undefined; + tokenAmount: number; +} +export const mintGovernanceTokens = async ( + connection: Connection, + wallet: any, + timelockConfig: ParsedAccount, + entries: GovernanceEntryInterface[], + setSavePerc: (num: number) => void, + onFailedTxn: (index: number) => void, +) => { + const PROGRAM_IDS = utils.programIds(); + + let allSigners: Account[][] = []; + let allInstructions: TransactionInstruction[][] = []; + + const accountRentExempt = await connection.getMinimumBalanceForRentExemption( + AccountLayout.span, + ); + + entries.forEach(e => { + const signers: Account[] = []; + const instructions: TransactionInstruction[] = []; + if (!e.governanceAccount) + e.governanceAccount = createTokenAccount( + instructions, + wallet.publicKey, + accountRentExempt, + timelockConfig.info.governanceMint, + e.owner, + signers, + ); + + instructions.push( + Token.createMintToInstruction( + PROGRAM_IDS.token, + timelockConfig.info.governanceMint, + e.governanceAccount, + wallet.publicKey, + [], + e.tokenAmount, + ), + ); + + allSigners.push(signers); + allInstructions.push(instructions); + }); + + notify({ + message: LABELS.ADDING_GOVERNANCE_TOKENS, + description: LABELS.PLEASE_WAIT, + type: 'warn', + }); + + try { + await sendTransactions( + connection, + wallet, + allInstructions, + allSigners, + true, + 'singleGossip', + (_txId: string, index: number) => { + setSavePerc(Math.round(100 * ((index + 1) / allInstructions.length))); + }, + (_txId: string, index: number) => { + setSavePerc(Math.round(100 * ((index + 1) / allInstructions.length))); + onFailedTxn(index); + return true; // keep going even on failed save + }, + ); + + notify({ + message: LABELS.GOVERNANCE_TOKENS_ADDED, + type: 'success', + }); + } catch (ex) { + console.error(ex); + throw new Error(); + } +}; diff --git a/packages/proposals/src/actions/registerProgramGovernance.ts b/packages/proposals/src/actions/registerProgramGovernance.ts new file mode 100644 index 0000000..8ae77f7 --- /dev/null +++ b/packages/proposals/src/actions/registerProgramGovernance.ts @@ -0,0 +1,139 @@ +import { + Account, + Connection, + PublicKey, + SystemProgram, + TransactionInstruction, +} from '@solana/web3.js'; +import { contexts, utils, actions, models } from '@oyster/common'; + +import { AccountLayout, MintLayout, Token } from '@solana/spl-token'; +import { TimelockConfig, TimelockConfigLayout } from '../models/timelock'; +import { initTimelockConfigInstruction } from '../models/initTimelockConfig'; + +const { sendTransaction } = contexts.Connection; +const { createMint, createTokenAccount } = actions; +const { notify } = utils; +const { approve } = models; + +export const registerProgramGovernance = async ( + connection: Connection, + wallet: any, + uninitializedTimelockConfig: TimelockConfig, +): Promise => { + const PROGRAM_IDS = utils.programIds(); + let signers: Account[] = []; + let instructions: TransactionInstruction[] = []; + + const mintRentExempt = await connection.getMinimumBalanceForRentExemption( + MintLayout.span, + ); + const accountRentExempt = await connection.getMinimumBalanceForRentExemption( + AccountLayout.span, + ); + + if (!uninitializedTimelockConfig.governanceMint) { + // Initialize the mint, an account for the admin, and give them one governance token + // to start their lives with. + uninitializedTimelockConfig.governanceMint = createMint( + instructions, + wallet.publicKey, + mintRentExempt, + 0, + wallet.publicKey, + wallet.publicKey, + signers, + ); + + const adminsGovernanceToken = createTokenAccount( + instructions, + wallet.publicKey, + accountRentExempt, + uninitializedTimelockConfig.governanceMint, + wallet.publicKey, + signers, + ); + + const addAuthority = approve( + instructions, + [], + adminsGovernanceToken, + wallet.publicKey, + 1, + ); + + instructions.push( + Token.createMintToInstruction( + PROGRAM_IDS.token, + uninitializedTimelockConfig.governanceMint, + adminsGovernanceToken, + addAuthority.publicKey, + [], + 1, + ), + ); + signers.push(addAuthority); + } + + const timelockRentExempt = await connection.getMinimumBalanceForRentExemption( + TimelockConfigLayout.span, + ); + const [timelockConfigKey] = await PublicKey.findProgramAddress( + [ + PROGRAM_IDS.timelock.programAccountId.toBuffer(), + uninitializedTimelockConfig.governanceMint.toBuffer(), + uninitializedTimelockConfig.program.toBuffer(), + ], + PROGRAM_IDS.timelock.programId, + ); + + const uninitializedTimelockConfigInstruction = SystemProgram.createAccount({ + fromPubkey: wallet.publicKey, + newAccountPubkey: timelockConfigKey, + lamports: timelockRentExempt, + space: TimelockConfigLayout.span, + programId: PROGRAM_IDS.timelock.programId, + }); + + instructions.push(uninitializedTimelockConfigInstruction); + + instructions.push( + initTimelockConfigInstruction( + timelockConfigKey, + uninitializedTimelockConfig.program, + uninitializedTimelockConfig.governanceMint, + uninitializedTimelockConfig.consensusAlgorithm, + uninitializedTimelockConfig.executionType, + uninitializedTimelockConfig.timelockType, + uninitializedTimelockConfig.votingEntryRule, + uninitializedTimelockConfig.minimumSlotWaitingPeriod, + ), + ); + + notify({ + message: 'Initializing governance of program...', + description: 'Please wait...', + type: 'warn', + }); + + try { + let tx = await sendTransaction( + connection, + wallet, + instructions, + signers, + true, + ); + + notify({ + message: 'Program is now governed.', + type: 'success', + description: `Transaction - ${tx}`, + }); + + return timelockConfigKey; + } catch (ex) { + console.error(ex); + throw new Error(); + } +}; diff --git a/packages/proposals/src/actions/vote.ts b/packages/proposals/src/actions/vote.ts index 8496160..ae080b7 100644 --- a/packages/proposals/src/actions/vote.ts +++ b/packages/proposals/src/actions/vote.ts @@ -12,9 +12,7 @@ import { actions, } from '@oyster/common'; -import { TimelockSet } from '../models/timelock'; -import { AccountLayout } from '@solana/spl-token'; -import { mintVotingTokensInstruction } from '../models/mintVotingTokens'; +import { TimelockConfig, TimelockSet } from '../models/timelock'; import { LABELS } from '../constants'; import { voteInstruction } from '../models/vote'; const { createTokenAccount } = actions; @@ -26,8 +24,12 @@ export const vote = async ( connection: Connection, wallet: any, proposal: ParsedAccount, + timelockConfig: ParsedAccount, votingAccount: PublicKey, - votingTokenAmount: number, + yesVotingAccount: PublicKey, + noVotingAccount: PublicKey, + yesVotingTokenAmount: number, + noVotingTokenAmount: number, ) => { const PROGRAM_IDS = utils.programIds(); @@ -44,7 +46,7 @@ export const vote = async ( [], votingAccount, wallet.publicKey, - votingTokenAmount, + yesVotingTokenAmount + noVotingTokenAmount, ); signers.push(transferAuthority); @@ -53,10 +55,17 @@ export const vote = async ( voteInstruction( proposal.pubkey, votingAccount, + yesVotingAccount, + noVotingAccount, proposal.info.votingMint, + proposal.info.yesVotingMint, + proposal.info.noVotingMint, + timelockConfig.info.governanceMint, + timelockConfig.pubkey, transferAuthority.publicKey, mintAuthority, - votingTokenAmount, + yesVotingTokenAmount, + noVotingTokenAmount, ), ); diff --git a/packages/proposals/src/actions/withdrawVotingTokens.ts b/packages/proposals/src/actions/withdrawVotingTokens.ts new file mode 100644 index 0000000..8595453 --- /dev/null +++ b/packages/proposals/src/actions/withdrawVotingTokens.ts @@ -0,0 +1,156 @@ +import { + Account, + Connection, + PublicKey, + TransactionInstruction, +} from '@solana/web3.js'; +import { + contexts, + utils, + models, + ParsedAccount, + actions, +} from '@oyster/common'; + +import { TimelockSet } from '../models/timelock'; +import { AccountLayout } from '@solana/spl-token'; +import { withdrawVotingTokensInstruction } from '../models/withdrawVotingTokens'; +import { LABELS } from '../constants'; +const { createTokenAccount } = actions; +const { sendTransaction } = contexts.Connection; +const { notify } = utils; +const { approve } = models; + +export const withdrawVotingTokens = async ( + connection: Connection, + wallet: any, + proposal: ParsedAccount, + newVotingAccountOwner: PublicKey, + existingVoteAccount: PublicKey | undefined, + existingYesVoteAccount: PublicKey | undefined, + existingNoVoteAccount: PublicKey | undefined, + destinationAccount: PublicKey, + votingTokenAmount: number, +) => { + const PROGRAM_IDS = utils.programIds(); + + let signers: Account[] = []; + let instructions: TransactionInstruction[] = []; + + const accountRentExempt = await connection.getMinimumBalanceForRentExemption( + AccountLayout.span, + ); + + if (!existingVoteAccount) { + existingVoteAccount = createTokenAccount( + instructions, + wallet.publicKey, + accountRentExempt, + proposal.info.votingMint, + newVotingAccountOwner, + signers, + ); + } + + if (!existingYesVoteAccount) { + existingYesVoteAccount = createTokenAccount( + instructions, + wallet.publicKey, + accountRentExempt, + proposal.info.yesVotingMint, + newVotingAccountOwner, + signers, + ); + } + + if (!existingNoVoteAccount) { + existingNoVoteAccount = createTokenAccount( + instructions, + wallet.publicKey, + accountRentExempt, + proposal.info.noVotingMint, + newVotingAccountOwner, + signers, + ); + } + + const [mintAuthority] = await PublicKey.findProgramAddress( + [PROGRAM_IDS.timelock.programAccountId.toBuffer()], + PROGRAM_IDS.timelock.programId, + ); + + // We dont know in this scope how much is in each account so we just ask for all in each. + // Should be alright, this is just permission, not actual moving. + const transferAuthority = approve( + instructions, + [], + existingVoteAccount, + wallet.publicKey, + votingTokenAmount, + ); + + const yesTransferAuthority = approve( + instructions, + [], + existingYesVoteAccount, + wallet.publicKey, + votingTokenAmount, + ); + + const noTransferAuthority = approve( + instructions, + [], + existingNoVoteAccount, + wallet.publicKey, + votingTokenAmount, + ); + + signers.push(transferAuthority); + signers.push(yesTransferAuthority); + signers.push(noTransferAuthority); + + instructions.push( + withdrawVotingTokensInstruction( + existingVoteAccount, + existingYesVoteAccount, + existingNoVoteAccount, + destinationAccount, + proposal.info.governanceHolding, + proposal.info.yesVotingDump, + proposal.info.noVotingDump, + proposal.info.votingMint, + proposal.pubkey, + proposal.info.config, + transferAuthority.publicKey, + yesTransferAuthority.publicKey, + noTransferAuthority.publicKey, + mintAuthority, + votingTokenAmount, + ), + ); + + notify({ + message: LABELS.WITHDRAWING_VOTING_TOKENS, + description: LABELS.PLEASE_WAIT, + type: 'warn', + }); + + try { + let tx = await sendTransaction( + connection, + wallet, + instructions, + signers, + true, + ); + + notify({ + message: LABELS.TOKENS_WITHDRAWN, + type: 'success', + description: LABELS.TRANSACTION + ` ${tx}`, + }); + } catch (ex) { + console.error(ex); + throw new Error(); + } +}; diff --git a/packages/proposals/src/components/Proposal/AddVotes.tsx b/packages/proposals/src/components/Proposal/MintGovernanceTokens.tsx similarity index 50% rename from packages/proposals/src/components/Proposal/AddVotes.tsx rename to packages/proposals/src/components/Proposal/MintGovernanceTokens.tsx index 1ca4fa7..bcc8cce 100644 --- a/packages/proposals/src/components/Proposal/AddVotes.tsx +++ b/packages/proposals/src/components/Proposal/MintGovernanceTokens.tsx @@ -1,137 +1,165 @@ import { ParsedAccount } from '@oyster/common'; import { Button, Modal, Input, Form, Progress, InputNumber, Radio } from 'antd'; -import React, { useState } from 'react'; -import { TimelockSet } from '../../models/timelock'; +import React, { useEffect, useState } from 'react'; +import { TimelockConfig, TimelockSet } from '../../models/timelock'; import { utils, contexts, hooks } from '@oyster/common'; import { PublicKey } from '@solana/web3.js'; import { LABELS } from '../../constants'; -import { mintVotingTokens } from '../../actions/mintVotingTokens'; +import { + GovernanceEntryInterface, + mintGovernanceTokens, +} from '../../actions/mintGovernanceTokens'; const { notify } = utils; const { TextArea } = Input; const { useWallet } = contexts.Wallet; const { useConnection } = contexts.Connection; -const { useAccountByMint } = hooks; -const { deserializeAccount } = contexts.Accounts; +const { deserializeAccount, useMint } = contexts.Accounts; const layout = { labelCol: { span: 5 }, wrapperCol: { span: 19 }, }; -export default function AddVotes({ - proposal, +export default function MintGovernanceTokens({ + timelockConfig, }: { - proposal: ParsedAccount; + timelockConfig: ParsedAccount; }) { const PROGRAM_IDS = utils.programIds(); const wallet = useWallet(); const connection = useConnection(); - const sigAccount = useAccountByMint(proposal.info.signatoryMint); + const governanceMint = useMint(timelockConfig.info.governanceMint); + const [saving, setSaving] = useState(false); + const [isModalVisible, setIsModalVisible] = useState(false); const [bulkModeVisible, setBulkModeVisible] = useState(false); const [savePerc, setSavePerc] = useState(0); - const [failedVoters, setFailedVoters] = useState([]); + const [failedGovernances, setFailedGovernances] = useState([]); const [form] = Form.useForm(); const onSubmit = async (values: { - voters: string; - failedVoters: string; - singleVoter: string; - singleVoteCount: number; + governanceHolders: string; + failedGovernances: string; + singleGovernanceHolder: string; + singleGovernanceCount: number; }) => { - const { singleVoter, singleVoteCount } = values; - const votersAndCounts = values.voters - ? values.voters.split(',').map(s => s.trim()) + const { singleGovernanceHolder, singleGovernanceCount } = values; + const governanceHoldersAndCounts = values.governanceHolders + ? values.governanceHolders.split(',').map(s => s.trim()) : []; - const voters: any[] = []; - votersAndCounts.forEach((value: string, index: number) => { - if (index % 2 == 0) voters.push([value, 0]); - else voters[voters.length - 1][1] = parseInt(value); + const governanceHolders: GovernanceEntryInterface[] = []; + let failedGovernancesHold: GovernanceEntryInterface[] = []; + const zeroKey = new PublicKey('0'); + governanceHoldersAndCounts.forEach((value: string, index: number) => { + if (index % 2 == 0) + governanceHolders.push({ + owner: value ? new PublicKey(value) : zeroKey, + tokenAmount: 0, + governanceAccount: undefined, + }); + else + governanceHolders[governanceHolders.length - 1].tokenAmount = parseInt( + value, + ); }); - console.log('Voters', votersAndCounts); - if (singleVoter) voters.push([singleVoter, singleVoteCount]); - if (!sigAccount) { - notify({ - message: LABELS.SIG_ACCOUNT_NOT_DEFINED, - type: 'error', + if (singleGovernanceHolder) + governanceHolders.push({ + owner: singleGovernanceHolder + ? new PublicKey(singleGovernanceHolder) + : zeroKey, + tokenAmount: singleGovernanceCount, + governanceAccount: undefined, }); - return; - } - if (!voters.find(v => v[0])) { + + if (!governanceHolders.find(v => v.owner != zeroKey)) { notify({ message: LABELS.ENTER_AT_LEAST_ONE_PUB_KEY, type: 'error', }); return; } - setSaving(true); - if (voters.find(v => v[1] === 0)) { + if (governanceHolders.find(v => v.tokenAmount === 0)) { notify({ - message: LABELS.CANT_GIVE_ZERO_VOTES, + message: LABELS.CANT_GIVE_ZERO_TOKENS, type: 'error', }); setSaving(false); return; } - const failedVotersHold: any[] = []; + setSaving(true); - for (let i = 0; i < voters.length; i++) { + const failedGovernanceCatch = (index: number, error: any) => { + if (error) console.error(error); + failedGovernancesHold.push(governanceHolders[index]); + notify({ + message: + governanceHolders[index].owner?.toBase58() + LABELS.PUB_KEY_FAILED, + type: 'error', + }); + }; + + const governanceHoldersToRun = []; + for (let i = 0; i < governanceHolders.length; i++) { try { - const tokenAccounts = await connection.getTokenAccountsByOwner( - new PublicKey(voters[i][0]), - { - programId: PROGRAM_IDS.token, - }, - ); - const specificToThisMint = tokenAccounts.value.find( - a => - deserializeAccount(a.account.data).mint.toBase58() === - proposal.info.votingMint.toBase58(), - ); - await mintVotingTokens( - connection, - wallet.wallet, - proposal, - sigAccount.pubkey, - new PublicKey(voters[i][0]), - specificToThisMint?.pubkey, - voters[i][1], - ); - setSavePerc(Math.round(100 * ((i + 1) / voters.length))); + if (governanceHolders[i].owner) { + const tokenAccounts = await connection.getTokenAccountsByOwner( + governanceHolders[i].owner || new PublicKey('0'), + { + programId: PROGRAM_IDS.token, + }, + ); + const specificToThisMint = tokenAccounts.value.find( + a => + deserializeAccount(a.account.data).mint.toBase58() === + timelockConfig.info.governanceMint, + ); + governanceHolders[i].governanceAccount = specificToThisMint?.pubkey; + governanceHoldersToRun.push(governanceHolders[i]); + } } catch (e) { - console.error(e); - failedVotersHold.push(voters[i]); - notify({ - message: voters[i][0] + LABELS.PUB_KEY_FAILED, - type: 'error', - }); + failedGovernanceCatch(i, e); } } - setFailedVoters(failedVotersHold); + + try { + await mintGovernanceTokens( + connection, + wallet.wallet, + timelockConfig, + governanceHoldersToRun, + setSavePerc, + index => failedGovernanceCatch(index, null), + ); + } catch (e) { + console.error(e); + failedGovernancesHold = governanceHolders; + } + + setFailedGovernances(failedGovernancesHold); setSaving(false); setSavePerc(0); - setIsModalVisible(failedVotersHold.length > 0); - if (failedVotersHold.length === 0) form.resetFields(); + setIsModalVisible(failedGovernancesHold.length > 0); + if (failedGovernancesHold.length === 0) form.resetFields(); }; return ( <> - {sigAccount ? ( + {governanceMint?.mintAuthority == wallet.wallet?.publicKey ? ( ) : null}
@@ -170,15 +198,15 @@ export default function AddVotes({ {!bulkModeVisible && ( <> @@ -188,12 +216,11 @@ export default function AddVotes({ )} {bulkModeVisible && (