Coordinate Squads approvals
Implement an optional coordinator that collects member signatures for one shared approval transaction.
Choose the approval path, implement the contract, then track partial approvals. See Need Help for support.
Prerequisites: an existing multisig integration and a backend able to construct, persist and distribute the exact compiled Solana transaction. Coordination is optional; ordinary on-chain votes work without it. The package supplies the interface, not a hosted service or transaction-builder backend.
Choose the Approval Path
Without a coordinator, approveProposal() submits one member's vote as a transaction. A coordinator can gather several signatures over a single approval bundle; the member whose signature completes it broadcasts it.
Configure coordinator as a factory receiving { signerAddress }. It receives the member address, not its key. Every participant must receive the same compiled message and the correct signature slots. The coordinator chooses the bundle's fee payer, fee settings and lifetime; confirm these independently before signing.
Implement the Contract
In TypeScript, implement the interface structurally; the declaration does not expose a constructor even though JavaScript exports a base class. Implement the two IMultisigCoordinator methods:
| Method | Responsibility |
|---|---|
getProposal(proposalId) | Return the compiled Solana transaction for this member to sign, or null to use an ordinary on-chain vote. |
confirmProposal(proposalId, signature) | Verify this member's base58 signature against signerAddress and the held bundle's exact messageBytes before storing it in that member's slot. |
Your backend must decode and verify each received signature before changing the held bundle. Reject invalid signatures, including signatures over different message bytes, without merging them. The package provides the coordinator interface; it does not implement this backend verification for you.
The account validates that the bundle includes its own approval exactly once and limits supported instructions around the same proposal/multisig. That validation is not a replacement for reviewing the stored payment payload. Use access controls, immutable payload storage and an application review screen. A durable-nonce signature can remain usable until its nonce advances, so blockhash expiry alone is not a universal revocation mechanism.
The compiled bundle fixes its actions: memo, autoExecute and vaultIndex passed to approveProposal() do not rewrite it. approveMaxFee checks the bundle quote before the member signs. A rejection always uses its own on-chain transaction.
Track Partial Approvals
Implement and configure your backend client in approval-coordinator.mjs, exporting a createCoordinator({ signerAddress }) factory that returns both methods described above. This is an application-owned module, not an SDK export; it must exist before running this example. Return a compiled bundle to use coordinated approval. Returning null submits an ordinary on-chain vote instead.
Create a separate WalletManagerMultisigSquads with that factory; the shared squads-account.mjs setup has no coordinator. Use your member's configured derivation path, shown here as 0'/0', and inspect approveProposal() results without treating all signatures as votes already recorded on-chain:
import WalletManagerMultisigSquads from '@tetherto/wdk-wallet-multisig-squads'
import { createCoordinator } from './approval-coordinator.mjs'
function requiredEnv(name) {
const value = process.env[name]
if (!value) throw new Error(`Missing ${name}`)
return value
}
const wallet = new WalletManagerMultisigSquads(requiredEnv('WDK_SEED_PHRASE'), {
provider: requiredEnv('SOLANA_RPC_URL'),
multisigPdaOrCreateKey: requiredEnv('MULTISIG_ADDRESS'),
commitment: 'confirmed',
coordinator: createCoordinator
})
try {
const account = await wallet.getAccountByPath("0'/0'")
const result = await account.approveProposal(requiredEnv('PROPOSAL_ID'))
if (result.transaction === undefined) {
console.log('Collected signatures awaiting broadcast:', result.pendingConfirmations)
} else {
const receipt = await account.waitForTransaction(result.transaction.hash)
if (!receipt.success) throw new Error('Approval transaction failed')
}
} finally {
wallet.dispose()
}A partial result reports transaction: undefined, leaves confirmations at the on-chain count, and places collected signatures in pendingConfirmations. No on-chain transaction has been broadcast for that partial approval. A broadcast result resets pending confirmations to zero. When a bundle includes execution, its returned status can be executed; still verify the receipt and proposal state. Follow the cleanup guidance for caller-held secrets after the signing session ends.