Upstream merge brings 52 commits from safe-global/safe-wallet-monorepo into the luxfi/safe-wallet@dev fork. Conflicts resolved: - yarn.lock: deleted (we are on pnpm) - apps/web/package.json: kept pnpm version + brand build scripts (build:safe / build:lux / build:hanzo) plus upstream's new generate-version.mjs step - apps/mobile/.../Signer/appKit.ts: took upstream's import pattern for shared/projectId, shared/metadata, shared/mmkvStorageAdapter Yarn sweep applied to 42 files (docs, github-actions, workflows, AGENTS, specs, codemods, scripts, READMEs). Net result: zero yarn-* command references anywhere outside node_modules. ❯ pnpm is the law in this fork. No yarn.
5.2 KiB
Quickstart: Proposer Multisig Validation
Prerequisites
- Node.js 18+
- Yarn 4 (via corepack)
- A test environment with:
- A 2-of-3 Safe ("parent Safe") deployed on a testnet
- A nested Safe owned by the parent Safe
- Access to at least 2 owner wallets of the parent Safe
Setup
# Install dependencies
pnpm install
# Start the web app in development mode (points to staging backend)
pnpm --filter @safe-global/web dev
Key Files to Understand
Before implementing, read these files in order:
-
Current signing flow:
apps/web/src/features/proposers/utils/utils.tsgetDelegateTypedData()— generates the EIP-712 typed datasignProposerTypedDataForSafe()— signs wrapped in SafeMessage for nested ownersencodeEIP1271Signature()— encodes for contract signature validation
-
Current UI flow:
apps/web/src/features/proposers/components/UpsertProposer.tsxonConfirm()handler — branches on nested ownership- Currently submits immediately (works for 1/1, fails for 2/N)
-
Off-chain message infrastructure:
apps/web/src/services/safe-messages/safeMsgSender.tsdispatchSafeMsgProposal()— creates new off-chain messagedispatchSafeMsgConfirmation()— adds signature to existing message
-
Nested ownership detection:
apps/web/src/hooks/useNestedSafeOwners.tsx- Returns parent Safe addresses that own the current Safe
-
RTK Query hooks:
packages/store/src/gateway/AUTO_GENERATED/messages.tsuseMessagesCreateMessageV1Mutation— create messageuseMessagesUpdateMessageSignatureV1Mutation— confirm messageuseMessagesGetMessagesBySafeV1Query— list messages
Implementation Order
Step 1: Parent Safe Threshold Hook
Create apps/web/src/features/proposers/hooks/useParentSafeThreshold.ts:
- Uses
useNestedSafeOwners()to get parent Safe address - Fetches parent Safe info via
useSafesGetSafeV1Query - Returns
{ threshold, owners, parentSafeAddress }
Step 2: Delegation Message Service
Create apps/web/src/features/proposers/services/delegationMessages.ts:
createDelegationMessage(parentSafe, delegateTypedData, signature, origin)— wraps RTK Query mutationconfirmDelegationMessage(chainId, messageHash, signature)— wraps RTK Query mutationbuildDelegationOrigin(action, delegate, nestedSafe, label)— creates origin metadata
Step 3: Pending Delegations Hook
Create apps/web/src/features/proposers/hooks/usePendingDelegations.ts:
- Fetches messages for parent Safe via
useMessagesGetMessagesBySafeV1Query - Filters by
origin.type === 'proposer-delegation' - Filters by
origin.nestedSafe === currentSafeAddress - Derives status (pending/ready/expired) based on TOTP and confirmations
- Returns typed
PendingDelegation[]
Step 4: Submit Delegation Hook
Create apps/web/src/features/proposers/hooks/useSubmitDelegation.ts:
- Takes a confirmed
PendingDelegation(withpreparedSignature) - Wraps
preparedSignaturein EIP-1271 format viaencodeEIP1271Signature - Submits to delegate API (V2) — add or remove based on
action
Step 5: Modify UpsertProposer Component
Modify apps/web/src/features/proposers/components/UpsertProposer.tsx:
- Check parent Safe threshold before signing
- If threshold === 1: existing flow (unchanged)
- If threshold > 1: create off-chain message, show pending state
- Add messaging about multi-sig requirement
Step 6: Modify DeleteProposerDialog Component
Same branching logic as UpsertProposer for the removal flow.
Step 7: Pending Delegations UI Components
Create PendingDelegation.tsx and PendingDelegationsList.tsx:
- Show pending delegation cards with progress (e.g., "1 of 2 signatures")
- Allow co-owners to sign pending requests
- Show "Submit" button when threshold is met
- Show "Expired" indicator with re-initiate option
Step 8: Integrate into Settings Page
Modify apps/web/src/components/settings/ProposersList/index.tsx:
- Add
PendingDelegationsListabove or below the existing proposers list - Only visible when parent Safe threshold > 1 and pending delegations exist
Testing Approach
# Run tests
pnpm --filter @safe-global/web test --watch
# Run type-check
pnpm --filter @safe-global/web type-check
MSW Handlers needed:
GET /v1/chains/:chainId/safes/:safeAddress— mock parent Safe with threshold=2POST /v1/chains/:chainId/safes/:safeAddress/messages— mock message creationPOST /v1/chains/:chainId/messages/:hash/signatures— mock confirmationGET /v1/chains/:chainId/safes/:safeAddress/messages— mock message listPOST /v2/chains/:chainId/delegates— mock delegate creationDELETE /v2/chains/:chainId/delegates/:address— mock delegate deletion
Verification
After implementation, verify the full flow:
- Connect as Owner A of a 2/3 parent Safe
- Navigate to nested Safe → Settings → Proposers
- Click "Add proposer" — should see multi-sig messaging
- Sign — should create off-chain message and show pending state
- Connect as Owner B of the same parent Safe
- Navigate to same nested Safe → Settings → Proposers
- See pending delegation — sign to confirm
- After threshold met — delegation should auto-submit
- Proposer should appear in the list