Feature/create proposal cli (#1519)
* create base64 proposal cli * fix * fix
This commit is contained in:
parent
0df64cd16b
commit
b601b97db6
|
@ -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()
|
|
@ -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
|
||||
}
|
|
@ -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",
|
||||
|
|
39
yarn.lock
39
yarn.lock
|
@ -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==
|
||||
|
|
Loading…
Reference in New Issue