Let's set up a testing flow in which we're going to create a multisig, propose a new transaction, vote on that transaction and execute it.
First, let's add our first step: Setting up the multisig members and creating the multisig.
describe("Interacting with the Squads V4 SDK", () => {constcreator=Keypair.generate();constsecondMember=Keypair.generate();before(async () => {constairdropSignature=awaitconnection.requestAirdrop(creator.publicKey,1*LAMPORTS_PER_SOL );awaitconnection.confirmTransaction(airdropSignature); });constcreateKey=Keypair.generate();// Derive the multisig account PDAconst [multisigPda] =multisig.getMultisigPda({ createKey:createKey.publicKey, });it("Create a new multisig",async () => {constprogramConfigPda=multisig.getProgramConfigPda({})[0];console.log("Program Config PDA: ",programConfigPda.toBase58());constprogramConfig=awaitmultisig.accounts.ProgramConfig.fromAccountAddress( connection, programConfigPda );constconfigTreasury=programConfig.treasury;// Create the multisigconstsignature=awaitmultisig.rpc.multisigCreateV2({ connection,// One time random Key createKey,// The creator & fee payer creator, multisigPda, configAuthority:null, timeLock:0, members: [ { key:creator.publicKey, permissions:Permissions.all(), }, { key:secondMember.publicKey,// This permission means that the user will only be able to vote on transactions permissions:Permissions.fromPermissions([Permission.Vote]), }, ],// This means that there needs to be 2 votes for a transaction proposal to be approved threshold:2, rentCollector:null, treasury: configTreasury, sendOptions: { skipPreflight:true }, });awaitconnection.confirmTransaction(signature);console.log("Multisig created: ", signature); });
Create transaction proposal (2 minutes)
Now, let's create a transaction proposal. We want the multisig to send 0.1 SOL to the creator.
For purposes of this tutorial, we first have to send that amount to the multisig, and can then create a message containing the instruction that needs to be executed.
it("Create a transaction proposal",async () => {const [vaultPda] =multisig.getVaultPda({ multisigPda, index:0, });constinstruction=SystemProgram.transfer({// The transfer is being signed from the Squads Vault, that is why we use the VaultPda fromPubkey: vaultPda, toPubkey:creator.publicKey, lamports:1*LAMPORTS_PER_SOL, });// This message contains the instructions that the transaction is going to executeconsttransferMessage=newTransactionMessage({ payerKey: vaultPda, recentBlockhash: (awaitconnection.getLatestBlockhash()).blockhash, instructions: [instruction], });// Get the current multisig transaction indexconstmultisigInfo=awaitmultisig.accounts.Multisig.fromAccountAddress( connection, multisigPda );constcurrentTransactionIndex=Number(multisigInfo.transactionIndex);constnewTransactionIndex=BigInt(currentTransactionIndex +1);constsignature1=awaitmultisig.rpc.vaultTransactionCreate({ connection, feePayer: creator, multisigPda, transactionIndex: newTransactionIndex, creator:creator.publicKey, vaultIndex:0, ephemeralSigners:0, transactionMessage: transferMessage, memo:"Transfer 0.1 SOL to creator", });awaitconnection.confirmTransaction(signature1);console.log("Transaction created: ", signature1);constsignature2=awaitmultisig.rpc.proposalCreate({ connection, feePayer: creator, multisigPda, transactionIndex: newTransactionIndex, creator, });awaitconnection.confirmTransaction(signature2);console.log("Transaction proposal created: ", signature2); });
Vote on the transaction proposal (2 minutes)
Let's now vote on the transaction proposal we just made using the two Keypairs we created at the start of this tutorial: creator and secondMember.
it("Vote on the created proposal",async () => {consttransactionIndex=awaitmultisig.accounts.Multisig.fromAccountAddress( connection, multisigPda ).then((info) =>Number(info.transactionIndex));constsignature1=awaitmultisig.rpc.proposalApprove({ connection, feePayer: creator, multisigPda, transactionIndex:BigInt(transactionIndex), member: creator, });awaitconnection.confirmTransaction(signature1);constsignature2=awaitmultisig.rpc.proposalApprove({ connection, feePayer: creator, multisigPda, transactionIndex:BigInt(transactionIndex), member: secondMember, });awaitconnection.confirmTransaction(signature2); });
Execute the transaction (2 minutes)
Now the most important part, actually executing that transaction we proposed.
it("Execute the proposal",async () => {consttransactionIndex=awaitmultisig.accounts.Multisig.fromAccountAddress( connection, multisigPda ).then((info) =>Number(info.transactionIndex));const [proposalPda] =multisig.getProposalPda({ multisigPda, transactionIndex:BigInt(transactionIndex), });constsignature=awaitmultisig.rpc.vaultTransactionExecute({ connection, feePayer: creator, multisigPda, transactionIndex:BigInt(transactionIndex), member:creator.publicKey, signers: [creator], sendOptions: { skipPreflight:true }, });awaitconnection.confirmTransaction(signature);console.log("Transaction executed: ", signature); });// Don't forget to close the describe block here});
Start a local validator (1 minute)
Now that you have a completed flow, let's actually execute these transactions on chain.
For the purpose of this tutorial, we are going to do so on a local Solana instance.
If you do not yet have the Solana CLI installed, please do so by reading the following guide.
Now, start up a local validator with the Squads V4 program preloaded.
Note:You will also have to clone the program config account from mainnet.
solana-test-validator --url m -c SQDS4ep65T869zMMBKyuUq6aD6EgTu8psMjkvj52pCf -c BSTq9w3kZwNwpBXJEvTZz2G9ZTNyKBvoSeXMvwb4cNZr -c Fy3YMJCvwbAXUgUM5b91ucUVA3jYzwWLHL3MwBqKsh8n
Execute the script
Okay, let's execute the script and see what happens.
yarn test
If you are encountering any issues here, try using another version of Node.js (above 20.xx).
If you get a "fetch failed" error, make sure your local validator is running.
Visualize your transactions
Once the tests have passed, you can go to the Solana Explorer and visualize your transactions by pasting their signature in the search bar and modifying the cluster endpoint to the one you want (mainnet, devnet, localnet...).