Signing Transactions
Learn how signatures are collected and managed for multi-sig transactions.
Signature Flow
In a multi-signature vault, transactions require signatures from multiple parties:
Transaction Created
↓
Signer 1 signs → Witness added
↓
Signer 2 signs → Witness added
↓
Threshold reached → Ready to send
↓
Execute on-chainSigning Methods
Via Bako Safe App (Recommended)
The easiest way for signers to approve transactions:
- Transaction creator shares the transaction hash or link
- Signers visit safe.bako.global (opens in a new tab)
- Connect their wallet
- Review and sign the pending transaction
- System automatically detects when threshold is reached
Via SDK (Programmatic)
For automated signing or custom interfaces:
// Sign a transaction programmatically
await provider.signTransaction({
hash: transactionHash,
signature: walletSignature,
approve: true // true to approve, false to reject
});Witness Structure
Each signature is stored as a witness:
interface IWitnesses {
account: string; // Signer address
signature: string; // The signature
status: WitnessStatus; // Approval status
updatedAt: Date; // Timestamp
}WitnessStatus
enum WitnessStatus {
REJECTED = 'rejected',
DONE = 'done',
PENDING = 'pending',
CANCELED = 'canceled'
}Checking Signature Status
const txData = await vault.transactionFromHash(hashTxId);
console.log('Signatures:', txData.witnesses.length);
console.log('Required:', txData.requiredSigners);
console.log('Status:', txData.status);
// List who has signed
txData.witnesses.forEach(witness => {
console.log(`${witness.account}: ${witness.status}`);
});
// Check if ready to send
const signaturesCollected = txData.witnesses.filter(
w => w.status === 'done'
).length;
if (signaturesCollected >= txData.requiredSigners) {
console.log('Ready to execute!');
}Signature Encoding
Different wallet types require different signature encoding:
import { SignatureType } from 'bakosafe';
// Signature types
enum SignatureType {
WebAuthn = 0, // Passkey signatures
Fuel = 1, // Fuel wallet signatures
Evm = 2, // EVM wallet signatures
RawNoPrefix = 9 // Raw signatures
}Encoding a Signature
const encodedSignature = vault.encodeSignature(
signerAddress,
rawSignature
);Multi-Wallet Type Example
A vault with different wallet types:
// Vault with mixed signers
const config = {
SIGNATURES_COUNT: 2,
SIGNERS: [
fuelWalletAddress, // Fuel native
evmWalletAddress, // MetaMask
passkeyAddress // WebAuthn
]
};
// Each signer uses their own wallet to sign
// SDK handles encoding automaticallyMonitoring for Signatures
Poll for signature updates:
async function waitForSignatures(
vault: Vault,
hashTxId: string,
requiredCount: number
): Promise<void> {
return new Promise((resolve, reject) => {
const interval = setInterval(async () => {
try {
const txData = await vault.transactionFromHash(hashTxId);
const signed = txData.witnesses.filter(
w => w.status === 'done'
).length;
console.log(`Signatures: ${signed}/${requiredCount}`);
if (signed >= requiredCount) {
clearInterval(interval);
resolve();
}
if (txData.status === 'declined' || txData.status === 'canceled') {
clearInterval(interval);
reject(new Error(`Transaction ${txData.status}`));
}
} catch (error) {
clearInterval(interval);
reject(error);
}
}, 5000); // Check every 5 seconds
});
}
// Usage
await waitForSignatures(vault, hashTxId, 2);
console.log('All signatures collected!');Rejecting a Transaction
Signers can reject instead of approving:
await provider.signTransaction({
hash: transactionHash,
signature: walletSignature,
approve: false // Reject the transaction
});If enough signers reject (preventing threshold from being reached), the transaction status becomes DECLINED.
Best Practices
- Clear communication: Share transaction details with all signers
- Deadline awareness: Set expectations for when signatures are needed
- Notification system: Implement alerts when signatures are pending
- Audit trail: Log who signed and when