Feature/create proposal cli (#1519)

* create base64 proposal cli

* fix

* fix
This commit is contained in:
Adrian Brzeziński 2023-04-04 18:09:14 +02:00 committed by GitHub
parent 0df64cd16b
commit b601b97db6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 384 additions and 12 deletions

185
cli/createProposalScript.ts Normal file
View File

@ -0,0 +1,185 @@
import { EndpointTypes } from '@models/types'
import { Connection, Keypair, PublicKey } from '@solana/web3.js'
import { VsrClient } from 'VoteStakeRegistry/sdk/client'
import chalk from 'chalk'
import figlet from 'figlet'
import promptly from 'promptly'
import { readFileSync } from 'fs'
import { homedir } from 'os'
import { join } from 'path'
import NodeWallet from '@coral-xyz/anchor/dist/cjs/nodewallet'
import { AnchorProvider, Wallet } from '@coral-xyz/anchor'
import { createBase64Proposal } from './helpers/createBase64Proposal'
import {
getAllProposals,
getInstructionDataFromBase64,
getTokenOwnerRecord,
getTokenOwnerRecordAddress,
} from '@solana/spl-governance'
import { tryParseKey } from '@tools/validators/pubkey'
const loadWalletFromFile = (walletPath: string): Keypair => {
const walletJSON = readFileSync(walletPath, 'utf-8')
const walletData = JSON.parse(walletJSON)
return Keypair.fromSecretKey(new Uint8Array(walletData))
}
const VSR_PROGRAM_ID = '4Q6WW2ouZ6V3iaNm56MTd5n2tnTm4C5fiH8miFHnAFHo'
const ENDPOINT_URL = 'https://api.mainnet-beta.solana.com/'
const CLUSTER = 'mainnet'
const REALM = new PublicKey('DPiH3H3c7t47BMxqTxLsuPQpEC6Kne8GA9VXbxpnZxFE')
const GOVERNANCE_PROGRAM = new PublicKey(
'GqTPL6qRf5aUuqscLh8Rg2HTxPUXfhhAXDptTLhp1t2J'
)
const PROPOSAL_MINT = new PublicKey(
'MangoCzJ36AjZyKwVj3VnYU4GTonjfVEnJmvvWaxLac'
)
class GovernanceCli {
#connectionContext = {
cluster: CLUSTER as EndpointTypes,
current: new Connection(ENDPOINT_URL, 'recent'),
endpoint: ENDPOINT_URL,
}
wallet: NodeWallet
walletKeyPair: Keypair
vsrClient: VsrClient
constructor(walletKeyPair: Keypair) {
this.walletKeyPair = walletKeyPair
}
async setupKeyPairWallet() {
console.log('Setting up wallet...')
const tempPayerWallet = Keypair.fromSecretKey(this.walletKeyPair.secretKey)
const tempWallet = new NodeWallet(tempPayerWallet)
this.wallet = tempWallet
console.log('Wallet ready')
}
async setupVoterClient() {
console.log('Setting up vsr...')
const options = AnchorProvider.defaultOptions()
const provider = new AnchorProvider(
this.#connectionContext.current,
(this.wallet as unknown) as Wallet,
options
)
const vsrClient = await VsrClient.connect(
provider,
new PublicKey(VSR_PROGRAM_ID),
this.#connectionContext.cluster === 'devnet'
)
this.vsrClient = vsrClient
console.log('Vsr ready')
}
async createProposal() {
const instructionsCount = await promptly.prompt(
'How many instructions you want to use?'
)
if (isNaN(instructionsCount)) {
console.log('Error instruction count is not a number')
return
}
const governancePk = await promptly.prompt(
'Provide governance address for proposal: '
)
if (!tryParseKey(governancePk)) {
console.log('Error invalid publickey')
return
}
const delegatedWallet = await promptly.prompt(
'Enter the address that delegated the token to you: '
)
if (!tryParseKey(delegatedWallet)) {
console.log('Error invalid publickey')
return
}
const title = await promptly.prompt('Title: ')
const description = await promptly.prompt('Description: ')
const instructions: string[] = []
for (let i = 0; i < instructionsCount; i++) {
const instructionNumber = i + 1
const inst = await promptly.prompt(
`Instruction ${instructionNumber} Base64: `
)
try {
getInstructionDataFromBase64(inst)
} catch (e) {
console.log('Error while parsing instruction: ', e)
}
instructions.push(inst)
}
let tokenOwnerRecordPk: PublicKey | null = null
if (delegatedWallet) {
tokenOwnerRecordPk = await getTokenOwnerRecordAddress(
GOVERNANCE_PROGRAM,
REALM,
PROPOSAL_MINT,
new PublicKey(delegatedWallet)
)
} else {
tokenOwnerRecordPk = await getTokenOwnerRecordAddress(
GOVERNANCE_PROGRAM,
REALM,
PROPOSAL_MINT,
this.wallet.publicKey
)
}
const [tokenOwnerRecord, proposals] = await Promise.all([
getTokenOwnerRecord(this.#connectionContext.current, tokenOwnerRecordPk),
getAllProposals(
this.#connectionContext.current,
GOVERNANCE_PROGRAM,
REALM
),
])
const proposalIndex = proposals.flatMap((x) => x).length
try {
const address = await createBase64Proposal(
this.#connectionContext.current,
this.wallet,
tokenOwnerRecord,
new PublicKey(governancePk),
REALM,
GOVERNANCE_PROGRAM,
PROPOSAL_MINT,
title,
description,
proposalIndex,
[...instructions],
this.vsrClient
)
console.log(
`Success proposal created url: https://realms.today/dao/${REALM.toBase58()}/proposal/${address.toBase58()}`
)
} catch (e) {
console.log('ERROR: ', e)
}
}
async init() {
await Promise.all([this.setupKeyPairWallet(), this.setupVoterClient()])
}
}
async function run() {
console.log(
chalk.red(
figlet.textSync('spl-governance-cli', { horizontalLayout: 'full' })
)
)
;(async () => {
//Load wallet from file system assuming its in default direction /Users/-USERNAME-/.config/solana/id.json
const walletPath = join(homedir(), '.config', 'solana', 'id.json')
const wallet = loadWalletFromFile(walletPath)
const cli = new GovernanceCli(wallet)
await cli.init()
await cli.createProposal()
})()
}
run()

View File

@ -0,0 +1,165 @@
import {
getGovernanceProgramVersion,
getInstructionDataFromBase64,
getSignatoryRecordAddress,
ProgramAccount,
SYSTEM_PROGRAM_ID,
TokenOwnerRecord,
VoteType,
WalletSigner,
withAddSignatory,
withCreateProposal,
withInsertTransaction,
withSignOffProposal,
} from '@solana/spl-governance'
import { Connection, PublicKey, TransactionInstruction } from '@solana/web3.js'
import { chunk } from 'lodash'
import { sendSignAndConfirmTransactions } from '@blockworks-foundation/mangolana/lib/transactions'
import { SequenceType } from '@blockworks-foundation/mangolana/lib/globalTypes'
import { VsrClient } from 'VoteStakeRegistry/sdk/client'
import {
getRegistrarPDA,
getVoterPDA,
getVoterWeightPDA,
} from 'VoteStakeRegistry/sdk/accounts'
export const createBase64Proposal = async (
connection: Connection,
wallet: WalletSigner,
tokenOwnerRecord: ProgramAccount<TokenOwnerRecord>,
governance: PublicKey,
realm: PublicKey,
governanceProgram: PublicKey,
proposalMint: PublicKey,
name: string,
descriptionLink: string,
proposalIndex: number,
base64Instructions: string[],
client?: VsrClient
) => {
const instructions: TransactionInstruction[] = []
const walletPk = wallet.publicKey!
const governanceAuthority = walletPk
const signatory = walletPk
const payer = walletPk
console.log(wallet)
// Changed this because it is misbehaving on my local validator setup.
const programVersion = await getGovernanceProgramVersion(
connection,
governanceProgram
)
// V2 Approve/Deny configuration
const voteType = VoteType.SINGLE_CHOICE
const options = ['Approve']
const useDenyOption = true
let voterWeightPluginPk: PublicKey | undefined = undefined
if (client) {
const { registrar } = await getRegistrarPDA(
realm,
proposalMint,
client.program.programId
)
const { voter } = await getVoterPDA(
registrar,
walletPk,
client.program.programId
)
const { voterWeightPk } = await getVoterWeightPDA(
registrar,
walletPk,
client.program.programId
)
voterWeightPluginPk = voterWeightPk
const updateVoterWeightRecordIx = await client.program.methods
.updateVoterWeightRecord()
.accounts({
registrar,
voter,
voterWeightRecord: voterWeightPk,
systemProgram: SYSTEM_PROGRAM_ID,
})
.instruction()
instructions.push(updateVoterWeightRecordIx)
}
const proposalAddress = await withCreateProposal(
instructions,
governanceProgram,
programVersion,
realm,
governance,
tokenOwnerRecord.pubkey,
name,
descriptionLink,
proposalMint,
governanceAuthority,
proposalIndex,
voteType,
options,
useDenyOption,
payer,
voterWeightPluginPk
)
await withAddSignatory(
instructions,
governanceProgram,
programVersion,
proposalAddress,
tokenOwnerRecord.pubkey,
governanceAuthority,
signatory,
payer
)
const signatoryRecordAddress = await getSignatoryRecordAddress(
governanceProgram,
proposalAddress,
signatory
)
const insertInstructions: TransactionInstruction[] = []
for (const i in base64Instructions) {
const instruction = getInstructionDataFromBase64(base64Instructions[i])
await withInsertTransaction(
insertInstructions,
governanceProgram,
programVersion,
governance,
proposalAddress,
tokenOwnerRecord.pubkey,
governanceAuthority,
Number(i),
0,
0,
[instruction],
payer
)
}
withSignOffProposal(
insertInstructions, // SingOff proposal needs to be executed after inserting instructions hence we add it to insertInstructions
governanceProgram,
programVersion,
realm,
governance,
proposalAddress,
signatory,
signatoryRecordAddress,
undefined
)
const txChunks = chunk([...instructions, ...insertInstructions], 2)
await sendSignAndConfirmTransactions({
connection,
wallet,
transactionInstructions: txChunks.map((txChunk) => ({
instructionsSet: txChunk.map((tx) => ({
signers: [],
transactionInstruction: tx,
})),
sequenceType: SequenceType.Sequential,
})),
})
return proposalAddress
}

View File

@ -18,7 +18,8 @@
"notifier": "ts-node scripts/governance-notifier.ts",
"setup": "yarn install && yarn allow-scripts",
"deduplicate": "npx yarn-deduplicate",
"postinstall": "echo '\\033[35mIf you just added a package, consider running `\\033[1m\\033[36;1myarn deduplicate` \\033[0m\\033[35mto check for duplicates!\\033[00m\n \\033[35malso make sure scripts run by new packages are reviewed and added in the allowScripts section. Then run `\\033[1m\\033[36;1myarn allow-scripts\\033[0m\\033[35m`!\\033[00m'"
"postinstall": "echo '\\033[35mIf you just added a package, consider running `\\033[1m\\033[36;1myarn deduplicate` \\033[0m\\033[35mto check for duplicates!\\033[00m\n \\033[35malso make sure scripts run by new packages are reviewed and added in the allowScripts section. Then run `\\033[1m\\033[36;1myarn allow-scripts\\033[0m\\033[35m`!\\033[00m'",
"create-proposal": "ts-node cli/createProposalScript.ts"
},
"lint-staged": {
"*.@(ts|tsx|js|jsx)": [
@ -119,13 +120,16 @@
"bignumber.js": "9.0.2",
"borsh": "0.7.0",
"buffer-layout": "1.2.2",
"chalk": "4.1.2",
"classnames": "2.3.1",
"clear": "0.1.0",
"clsx": "1.2.1",
"d3": "7.4.4",
"date-fns": "2.29.2",
"dayjs": "1.11.1",
"did-jwt": "6.11.0",
"draft-js": "0.11.7",
"figlet": "1.5.2",
"fp-ts": "2.12.2",
"goblingold-sdk": "1.2.35",
"graphql": "16.5.0",
@ -140,6 +144,7 @@
"node-fetch": "2.6.7",
"numbro": "2.3.6",
"papaparse": "5.3.2",
"promptly": "3.2.0",
"psyfi-euros-test": "0.0.1-rc.33",
"pyth-staking-api": "1.3.2",
"qr-code-styling": "1.6.0-rc.1",

View File

@ -4527,14 +4527,14 @@
bs58 "^4.0.1"
superstruct "^0.15.2"
"@solana/spl-token-registry@0.2.3775", "@solana/spl-token-registry@^0.2.1107":
"@solana/spl-token-registry@0.2.3775":
version "0.2.3775"
resolved "https://registry.yarnpkg.com/@solana/spl-token-registry/-/spl-token-registry-0.2.3775.tgz#96abffc351fe156917aedb8bba7db94600abed6e"
integrity sha512-3a6wn6LZ1ZdCt50p9Bz2s8tIh1cJvJue4vvlPlzZ1n2j/0H2Coy4Xx92nIsYot399TjtO9NeLU2NowBDH4vtpg==
dependencies:
cross-fetch "3.0.6"
"@solana/spl-token-registry@0.2.4574":
"@solana/spl-token-registry@0.2.4574", "@solana/spl-token-registry@^0.2.1107":
version "0.2.4574"
resolved "https://registry.yarnpkg.com/@solana/spl-token-registry/-/spl-token-registry-0.2.4574.tgz#13f4636b7bec90d2bb43bbbb83512cd90d2ce257"
integrity sha512-JzlfZmke8Rxug20VT/VpI2XsXlsqMlcORIUivF+Yucj7tFi7A0dXG7h+2UnD0WaZJw8BrUz2ABNkUnv89vbv1A==
@ -7801,6 +7801,14 @@ chai@^4.3.7:
pathval "^1.1.1"
type-detect "^4.0.5"
chalk@4.1.2, chalk@^4.0.0, chalk@^4.1.0, chalk@^4.1.1, chalk@^4.1.2:
version "4.1.2"
resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01"
integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==
dependencies:
ansi-styles "^4.1.0"
supports-color "^7.1.0"
chalk@^1.0.0, chalk@^1.1.3:
version "1.1.3"
resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98"
@ -7829,14 +7837,6 @@ chalk@^3.0.0:
ansi-styles "^4.1.0"
supports-color "^7.1.0"
chalk@^4.0.0, chalk@^4.1.0, chalk@^4.1.1, chalk@^4.1.2:
version "4.1.2"
resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01"
integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==
dependencies:
ansi-styles "^4.1.0"
supports-color "^7.1.0"
chan@^0.6.1:
version "0.6.1"
resolved "https://registry.yarnpkg.com/chan/-/chan-0.6.1.tgz#ec0ad132e5bc62c27ef10ccbfc4d8dcd8ca00640"
@ -7939,6 +7939,11 @@ clean-stack@^3.0.0:
dependencies:
escape-string-regexp "4.0.0"
clear@0.1.0:
version "0.1.0"
resolved "https://registry.yarnpkg.com/clear/-/clear-0.1.0.tgz#b81b1e03437a716984fd7ac97c87d73bdfe7048a"
integrity sha512-qMjRnoL+JDPJHeLePZJuao6+8orzHMGP04A8CdwCNsKhRbOnKRjefxONR7bwILT3MHecxKBjHkKL/tkZ8r4Uzw==
cli-columns@^3.1.2:
version "3.1.2"
resolved "https://registry.yarnpkg.com/cli-columns/-/cli-columns-3.1.2.tgz#6732d972979efc2ae444a1f08e08fa139c96a18e"
@ -10147,6 +10152,11 @@ fetch-retry-ts@^1.1.24:
resolved "https://registry.yarnpkg.com/fetch-retry-ts/-/fetch-retry-ts-1.1.25.tgz#25849a87a5a016cc772f90986d95dd5049eeb5ac"
integrity sha512-kjJcfBYDbajnxMBfBa85hrS0Z+A0bDKuZWuzh5uHpaLSm2qZ3eAND5sDVfFUuIJg51sTd91cdB+Ayqo3magB/g==
figlet@1.5.2:
version "1.5.2"
resolved "https://registry.yarnpkg.com/figlet/-/figlet-1.5.2.tgz#dda34ff233c9a48e36fcff6741aeb5bafe49b634"
integrity sha512-WOn21V8AhyE1QqVfPIVxe3tupJacq1xGkPTB4iagT6o+P2cAgEOOwIxMftr4+ZCTI6d551ij9j61DFr0nsP2uQ==
figures@^1.7.0:
version "1.7.0"
resolved "https://registry.yarnpkg.com/figures/-/figures-1.7.0.tgz#cbe1e3affcf1cd44b80cadfed28dc793a9701d2e"
@ -14816,6 +14826,13 @@ prompt-sync@^4.1.6:
dependencies:
strip-ansi "^5.0.0"
promptly@3.2.0:
version "3.2.0"
resolved "https://registry.yarnpkg.com/promptly/-/promptly-3.2.0.tgz#a5517fbbf59bd31c1751d4e1d9bef1714f42b9d8"
integrity sha512-WnR9obtgW+rG4oUV3hSnNGl1pHm3V1H/qD9iJBumGSmVsSC5HpZOLuu8qdMb6yCItGfT7dcRszejr/5P3i9Pug==
dependencies:
read "^1.0.4"
prompts@^2.0.1, prompts@^2.4.1:
version "2.4.2"
resolved "https://registry.yarnpkg.com/prompts/-/prompts-2.4.2.tgz#7b57e73b3a48029ad10ebd44f74b01722a4cb069"
@ -15470,7 +15487,7 @@ read-package-json@^4.1.1:
normalize-package-data "^3.0.0"
npm-normalize-package-bin "^1.0.0"
read@1, read@^1.0.7, read@~1.0.1, read@~1.0.7:
read@1, read@^1.0.4, read@^1.0.7, read@~1.0.1, read@~1.0.7:
version "1.0.7"
resolved "https://registry.yarnpkg.com/read/-/read-1.0.7.tgz#b3da19bd052431a97671d44a42634adf710b40c4"
integrity sha512-rSOKNYUmaxy0om1BNjMN4ezNT6VKK+2xF4GBhc81mkH7L60i6dp8qPYrkndNLT3QPphoII3maL9PVC9XmhHwVQ==