Safe{Wallet} Management Interface Development
Safe{Wallet} (formerly Gnosis Safe)—de facto standard for corporate and DAO treasuries. The standard interface at app.safe.global covers 80% of use cases, but as soon as protocol-specific requirements emerge—custom Safe Apps, batch transactions via Safe Transaction Builder, governance module integration, custom guards—you need your own interface.
Safe SDK: Integration Architecture
Safe Protocol Kit
Main tool for managing Safe via TypeScript:
import Safe, { EthersAdapter } from '@safe-global/protocol-kit';
import { ethers } from 'ethers';
const provider = new ethers.JsonRpcProvider(RPC_URL);
const signer = new ethers.Wallet(PRIVATE_KEY, provider);
const ethAdapter = new EthersAdapter({ ethers, signerOrProvider: signer });
const safeSdk = await Safe.create({
ethAdapter,
safeAddress: SAFE_ADDRESS
});
// Create transaction
const safeTransaction = await safeSdk.createTransaction({
transactions: [{
to: TOKEN_CONTRACT,
value: '0',
data: encodeFunctionData({
abi: erc20Abi,
functionName: 'transfer',
args: [recipient, amount]
})
}]
});
// Sign
const signedTransaction = await safeSdk.signTransaction(safeTransaction);
// Propose to Safe Transaction Service (for other signers)
const apiKit = new SafeApiKit({ chainId: BigInt(1) });
await apiKit.proposeTransaction({
safeAddress: SAFE_ADDRESS,
safeTransactionData: signedTransaction.data,
safeTxHash: await safeSdk.getTransactionHash(signedTransaction),
senderAddress: await signer.getAddress(),
senderSignature: signedTransaction.signatures.get(signer.address.toLowerCase())!.data
});
Safe Transaction Service stores pending transactions off-chain, allowing other owners to find and sign them without direct coordination.
Batch Transactions via MultiSend
One of the main reasons to use Safe—batching: multiple operations in one transaction. In the standard interface, this is Transaction Builder. In custom:
// Batch: approve + stake in one transaction
const batchTransactions = [
{
to: USDC_ADDRESS,
value: '0',
data: encodeFunctionData({
abi: erc20Abi,
functionName: 'approve',
args: [STAKING_CONTRACT, parseUnits('10000', 6)]
})
},
{
to: STAKING_CONTRACT,
value: '0',
data: encodeFunctionData({
abi: stakingAbi,
functionName: 'deposit',
args: [parseUnits('10000', 6)]
})
}
];
const safeTransaction = await safeSdk.createTransaction({ transactions: batchTransactions });
MultiSend contract (deployed by Safe team, addresses fixed per network) executes all operations atomically. If one reverts—entire batch rolls back.
Key UI Components
Pending Transactions List
Central interface element. Each transaction shows:
- Operation type (transfer, contract interaction, batch)
- Decoded calldata—not raw hex, but human-readable (e.g., "Transfer 5,000 USDC to 0x1234...")
- Signature status:
2/3 confirmationswith signer avatars - Gas estimate
- Buttons: Sign / Execute (if threshold met) / Reject
Calldata decoding—via viem decodeFunctionData + ABI repository (4byte.directory or local registry of known ABIs). Unrecognized calls shown as hex with warning.
Transaction Creation Form
For non-technical users (e.g., DAO treasurer) a form abstracting raw calldata is critical:
// Form for DeFi operations—without manual calldata entry
function TransactionForm() {
const [operation, setOperation] = useState<'transfer' | 'stake' | 'vote'>();
return (
<form>
<Select onValueChange={setOperation}>
<SelectItem value="transfer">Transfer Tokens</SelectItem>
<SelectItem value="stake">Stake in Protocol</SelectItem>
<SelectItem value="vote">Governance Vote</SelectItem>
</Select>
{operation === 'transfer' && <TransferForm />}
{operation === 'stake' && <StakingForm />}
{operation === 'vote' && <VotingForm />}
</form>
);
}
Each operation-specific module knows the contract ABI and builds calldata. User enters intuitive values (address, token amount).
Owner and Threshold Management
Changing owners or threshold—also a Safe transaction (addOwnerWithThreshold, removeOwner, changeThreshold calls). Interface should show this explicitly:
- Current owners with ENS names (if resolved)
- Current threshold
- Form to add/remove owner—creates Safe transaction requiring M-of-N signature
- History of owner changes from on-chain events
Safe Apps iframe Integration
Safe App—web application running inside Safe interface iframe. For custom interface, either embed existing Safe Apps (Uniswap, Aave, Compound) or create your own:
import { useSafeAppsSDK } from '@safe-global/safe-apps-react-sdk';
// Inside Safe App iframe
function SafeAppComponent() {
const { sdk, safe } = useSafeAppsSDK();
async function sendTransaction() {
// Transaction doesn't need wallet—sent via Safe SDK
const { safeTxHash } = await sdk.txs.send({
txs: [{
to: CONTRACT_ADDRESS,
value: '0',
data: calldata
}]
});
console.log('Proposed:', safeTxHash);
}
}
Delegates and WalletConnect
Delegates—addresses to which Safe delegates proposal rights (but not signing). Useful for automated systems creating transactions on schedule (grant payouts, rebalancing).
WalletConnect v2 in Safe context: Safe can act as WalletConnect peer—connects to external dApp and signs transactions via Safe flow. Useful for protocols without Safe Apps.
Tech Stack
Next.js 14 + TypeScript, @safe-global/protocol-kit, @safe-global/api-kit, @safe-global/safe-apps-react-sdk, wagmi 2.x + viem for wallet connection and chain interaction, @tanstack/react-query for caching Safe Transaction Service data.
Deployment: Vercel or static hosting. For internal DAO tool—self-hosted on own domain with authentication (Privy or custom JWT).
Development Timeline
Custom interface for specific Safe with batch transactions, pending list, calldata decoding for known contracts—3-4 days. With Safe Apps iframe, delegation management, governance integration, full operation history—1-2 weeks.







