Prerequisites
- Node.js installed
- Solana CLI installed (installation guide)
- Basic TypeScript knowledge
Quick example
import * as multisig from "@sqds/multisig";
import { Connection, Keypair, LAMPORTS_PER_SOL } from "@solana/web3.js";
const { Permission, Permissions } = multisig.types;
const connection = new Connection("http://localhost:8899", "confirmed");
Full example
Find the complete code for this guide here.1
Set up your workspace
Create a new directory:Create Create Create
mkdir squads_quickstart
cd squads_quickstart
tsconfig.json:{
"compilerOptions": {
"module": "commonjs",
"target": "es6",
"esModuleInterop": true
}
}
package.json:{
"scripts": {
"test": "npx mocha -r ts-node/register 'main.ts' --timeout 10000"
},
"dependencies": {
"@solana/web3.js": "^1.73.0",
"@sqds/multisig": "^2.1.3"
},
"devDependencies": {
"@types/chai": "^4.3.3",
"@types/mocha": "^10.0.6",
"chai": "^4.3.6",
"mocha": "^10.3.0",
"ts-mocha": "^10.0.0",
"typescript": "^4.8.3"
}
}
main.ts:import * as multisig from "@sqds/multisig";
import {
Connection,
Keypair,
LAMPORTS_PER_SOL,
SystemProgram,
TransactionMessage,
VersionedTransaction,
clusterApiUrl,
} from "@solana/web3.js";
const { Permission, Permissions } = multisig.types;
// Use 'https://api.devnet.solana.com' if there are struggles running locally
const connection = new Connection("http://localhost:8899", "confirmed");
2
Create a multisig
describe("Interacting with the Squads V4 SDK", () => {
const creator = Keypair.generate();
const secondMember = Keypair.generate();
before(async () => {
const airdropSignature = await connection.requestAirdrop(
creator.publicKey,
1 * LAMPORTS_PER_SOL
);
await connection.confirmTransaction(airdropSignature);
});
const createKey = Keypair.generate();
// Derive the multisig account PDA
const [multisigPda] = multisig.getMultisigPda({
createKey: createKey.publicKey,
});
it("Create a new multisig", async () => {
const programConfigPda = multisig.getProgramConfigPda({})[0];
console.log("Program Config PDA: ", programConfigPda.toBase58());
const programConfig =
await multisig.accounts.ProgramConfig.fromAccountAddress(
connection,
programConfigPda
);
const configTreasury = programConfig.treasury;
// Create the multisig
const signature = await multisig.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 },
});
await connection.confirmTransaction(signature);
console.log("Multisig created: ", signature);
});
3
Create a transaction proposal
Create a transaction proposal to send 0.1 SOL from the multisig vault to the creator.
it("Create a transaction proposal", async () => {
const [vaultPda] = multisig.getVaultPda({
multisigPda,
index: 0,
});
// Fund the vault so it can pay for the transfer
const fundVaultTx = new VersionedTransaction(
new TransactionMessage({
payerKey: creator.publicKey,
recentBlockhash: (await connection.getLatestBlockhash()).blockhash,
instructions: [
SystemProgram.transfer({
fromPubkey: creator.publicKey,
toPubkey: vaultPda,
lamports: 0.2 * LAMPORTS_PER_SOL,
}),
],
}).compileToV0Message()
);
fundVaultTx.sign([creator]);
const fundSignature = await connection.sendTransaction(fundVaultTx);
await connection.confirmTransaction(fundSignature);
const instruction = SystemProgram.transfer({
// The transfer is being signed from the Squads Vault, that is why we use the VaultPda
fromPubkey: vaultPda,
toPubkey: creator.publicKey,
lamports: 0.1 * LAMPORTS_PER_SOL,
});
// This message contains the instructions that the transaction is going to execute
const transferMessage = new TransactionMessage({
payerKey: vaultPda,
recentBlockhash: (await connection.getLatestBlockhash()).blockhash,
instructions: [instruction],
});
// Get the current multisig transaction index
const multisigInfo = await multisig.accounts.Multisig.fromAccountAddress(
connection,
multisigPda
);
const currentTransactionIndex = Number(multisigInfo.transactionIndex);
const newTransactionIndex = BigInt(currentTransactionIndex + 1);
const signature1 = await multisig.rpc.vaultTransactionCreate({
connection,
feePayer: creator,
multisigPda,
transactionIndex: newTransactionIndex,
creator: creator.publicKey,
vaultIndex: 0,
ephemeralSigners: 0,
transactionMessage: transferMessage,
memo: "Transfer 0.1 SOL to creator",
});
await connection.confirmTransaction(signature1);
console.log("Transaction created: ", signature1);
const signature2 = await multisig.rpc.proposalCreate({
connection,
feePayer: creator,
multisigPda,
transactionIndex: newTransactionIndex,
creator,
});
await connection.confirmTransaction(signature2);
console.log("Transaction proposal created: ", signature2);
});
4
Vote on the proposal
it("Vote on the created proposal", async () => {
const transactionIndex =
await multisig.accounts.Multisig.fromAccountAddress(
connection,
multisigPda
).then((info) => Number(info.transactionIndex));
const signature1 = await multisig.rpc.proposalApprove({
connection,
feePayer: creator,
multisigPda,
transactionIndex: BigInt(transactionIndex),
member: creator,
});
await connection.confirmTransaction(signature1);
const signature2 = await multisig.rpc.proposalApprove({
connection,
feePayer: creator,
multisigPda,
transactionIndex: BigInt(transactionIndex),
member: secondMember,
});
await connection.confirmTransaction(signature2);
});
5
Execute the transaction
it("Execute the proposal", async () => {
const transactionIndex =
await multisig.accounts.Multisig.fromAccountAddress(
connection,
multisigPda
).then((info) => Number(info.transactionIndex));
const [proposalPda] = multisig.getProposalPda({
multisigPda,
transactionIndex: BigInt(transactionIndex),
});
const signature = await multisig.rpc.vaultTransactionExecute({
connection,
feePayer: creator,
multisigPda,
transactionIndex: BigInt(transactionIndex),
member: creator.publicKey,
signers: [creator],
sendOptions: { skipPreflight: true },
});
await connection.confirmTransaction(signature);
console.log("Transaction executed: ", signature);
});
// Don't forget to close the describe block here
});
6
Start a local validator
Start a local validator with the Squads V4 program preloaded:
solana-test-validator --url m --clone-upgradeable-program SQDS4ep65T869zMMBKyuUq6aD6EgTu8psMjkvj52pCf -c BSTq9w3kZwNwpBXJEvTZz2G9ZTNyKBvoSeXMvwb4cNZr -c Fy3YMJCvwbAXUgUM5b91ucUVA3jYzwWLHL3MwBqKsh8n
7
Run the script
yarn test
Visualize transactions
View transactions on Solana Explorer by pasting the signature and selecting the appropriate cluster endpoint.Using devnet instead of local validator
Using devnet instead of local validator
If local validator issues occur, switch to devnet by updating the connection:For devnet rate limit workarounds:Add this at the beginning of your first test:
const connection = new Connection("https://api.devnet.solana.com", "confirmed");
// Add these two dependencies
import { readFileSync } from "fs";
import path from "path";
import dotenv from "dotenv";
dotenv.config();
// Add this before you define `describe`
const keyFileContents = JSON.parse(
readFileSync(path.join(process.env.HOME, ".config/solana/id.json")).toString()
);
const creator = Keypair.fromSecretKey(new Uint8Array(keyFileContents));
const secondMember = Keypair.generate();
try {
// Attempt an airdrop
await connection.requestAirdrop(
secondMember.publicKey,
1 * LAMPORTS_PER_SOL
);
} catch (e) {
// If it fails due to rate limits, send from the filesystem keypair
console.log("airdrop failed");
const tx = new VersionedTransaction(
new TransactionMessage({
payerKey: creator.publicKey,
recentBlockhash: await (
await connection.getLatestBlockhash()
).blockhash,
instructions: [
SystemProgram.transfer({
fromPubkey: creator.publicKey,
toPubkey: secondMember.publicKey,
lamports: 1_000_000,
}),
],
}).compileToV0Message()
);
tx.sign([creator]);
console.log("Sending SOL...");
await connection.sendTransaction(tx);
console.log("SOL sent.");
}
Troubleshooting
Troubleshooting
| Issue | Solution |
|---|---|
| ”fetch failed” | Ensure local validator is running |
| ESM resolution issues | Try SDK versions 2.1.1 or 2.1.0 |
| Node.js errors | Use Node.js version 22.x or above |