WDK logoWDK documentation

Manage Safe proposals

Quote, propose, approve and submit payments with explicit receipt and fee handling.

Prepare a proposal, collect approvals, then submit and confirm. See Need Help for support.

Prerequisites: a deployed Safe, configured owner accounts, a shared coordinator and enough funds for the payload and fees. The examples use native gas and a native transfer. RECIPIENT_ADDRESS and SEND_WEI must be values reviewed by the owners.

Prepare a Proposal

  1. Verify the Safe, recipient, chain and value in your application's review screen.
  2. Estimate the operation with quoteSendTransaction().
  3. Submit the reviewed request using propose().

Prepare the request and its estimate:

Review a native transfer
import { account, requiredEnv } from './safe-account.mjs'

const tx = {
  to: requiredEnv('RECIPIENT_ADDRESS'),
  value: BigInt(requiredEnv('SEND_WEI'))
}
const quote = await account.quoteSendTransaction(tx)
console.log('Estimated native wei:', quote.fee)

After application confirmation, create the proposal:

Propose the reviewed request
const proposal = await account.propose(tx)
console.log('Proposal ID:', proposal.proposalId)

A proposal is not a transfer receipt. For ERC-20 transfers, use proposeTransfer() with token, recipient and amount in token base units. Token-paymaster fees and native execution-cost estimates are different quantities.

Collect Approvals

Each other owner must independently inspect the proposed operation in the coordinator or application. The compact getProposal() result contains status/counts, not the full payment payload; it is insufficient as a payment-review screen.

From another owner's process, use approveProposal() with the same Safe/coordinator configuration:

Approve a reviewed proposal
const approved = await account.approveProposal(requiredEnv('PROPOSAL_ID'))
console.log(approved.confirmations, approved.threshold)

The account must represent a different approving owner. Approval returns status: 'pending', even when the threshold is reached. It does not auto-execute. rejectProposal() creates a competing zero-value proposal at the same nonce; rejection also needs approvals and execution.

Submit and Confirm

Check the stored proposal, then call executeProposal() only after owners approve its contents:

Submit the approved operation
const proposalId = requiredEnv('PROPOSAL_ID')
if (!await account.isReadyToExecute(proposalId)) {
  throw new Error('Approval threshold has not been met')
}
const submitted = await account.executeProposal(proposalId)
console.log('UserOperation hash:', submitted.hash)

submitted.fee is the stored operation's maximum native gas cost, not a measured token charge. Readiness checks count confirmations; they do not guarantee successful execution or fresh coordinator state.

Use getTransactionReceipt() to inspect inclusion:

Inspect submission receipt
const receipt = await account.getTransactionReceipt(submitted.hash)
console.log(receipt)

A missing receipt is not success. Apply a bounded retry policy and inspect the returned UserOperation success and underlying receipt; an EVM receipt uses its EVM status. The method first queries the EVM provider, so provider errors can interrupt its bundler fallback. Do not use inherited waitForTransaction() in beta.1.

autoExecute: true on propose() or proposeTransfer() submits only when the initial proposal already meets the threshold. Its returned status: 'executed' means this submission branch ran, not that a successful receipt was observed. Always confirm receipts.

Next Steps


Need Help?

On this page