[PERA-4490] PQ-017: external signing PQ fee override on dApp transaction groups (WC/deeplinks)#1004
Conversation
…-group [PERA-4490]
…t [PERA-4490] Removes assertNoQuantumSignedTransactions and its call in buildSourceMetadata's transaction-approve wrapper — the callback transport (WalletConnect / webview / deeplink / local-callback) now forwards a QuantumSignedTransaction pqsig carrier to the requester unchanged instead of throwing, since encodeSignedTransaction is already carrier-aware. Widens TransactionSignRequest.approve to Nullable<PeraSignedTxnResult>[] to match the real runtime contract, and updates every consumer surfaced by the type change (useSignAndSubmitGroup, requestRekeySignatures, useFeeDelegation, and the swap module's requestSwapSignatures, which defensively excludes quantum carriers since swaps don't route quantum accounts today).
requestSwapSignatures' approve callback silently filtered out any QuantumSignedTransaction carrier alongside the defensive null-filter. Quantum accounts are only kept out of swap by a feature flag today, not a structural guard, so a silent drop would vanish signed slots and corrupt the group into an opaque downstream crash. Throw instead, mirroring the fail-loud contract of assertNoQuantumSignedTransactions (the callback-transport gate PQ-017 removed from the signing machine).
…essage [PERA-4490]
…t [PERA-4490] Build the QuantumFeeDeliveryError message with a template literal against QUANTUM_FEE_DELIVERY_MESSAGE_MARKER instead of a hardcoded phrase, so a future rewording can't silently break the marker-based detection used for WalletConnect's connectionError rewrap. Adds a test asserting the rejected error's message contains the marker.
…ck [PERA-4490] useDeepLink.test.ts re-mocks wallet-core-blockchain/-signing for the keyreg builder assertions; without useMinimumFeeConfig/getSuggestedParams/ resolveMinFeeForSender the hook throws when rendered after task 6's fee-floor change.
| const { addSignRequest } = useSigningRequest() | ||
| const allAccounts = useAllAccounts() | ||
| const showError = useDeeplinkErrorHandler() | ||
| const { minTxnFee, pqMultiplier } = useMinimumFeeConfig() |
There was a problem hiding this comment.
I think I've mentioned this before but just in case not I think we're going to want this to be something more like useMinimumFeeCalculator which returns an "assignFeeToGroup" and it computes the actual fee requirements for the whole group. In this first implementation it can just look for pq accounts and change the fee, but in a future implementation it's going to have to do a bunch of conditional stuff based on different factors (or, probably more likely, just send the tx group to the simulate endpoint).
I think it's worth preparing an interface now that makes sense for that future.
| const dAppFee = data.fee ? BigInt(data.fee) : undefined | ||
|
|
||
| // Mirrors PQ-007's useTransactionSendFlow.buildNormalTxs: the | ||
| // dApp-set (or default) fee is honored verbatim unless the | ||
| // sender is PQ-signed, in which case `resolvedMin` exceeds | ||
| // algod's suggested minimum and the fee is floored up to it | ||
| // — never lowered — so a quantum sender's keyreg isn't | ||
| // rejected as underfunded. | ||
| const suggestedParams = await withTimeout( | ||
| 'keyregSuggestedParams', | ||
| KEYREG_BUILD_TIMEOUT_MS, | ||
| algorandClient.getSuggestedParams(), | ||
| ) | ||
| const suggestedMinFee = BigInt(suggestedParams.minFee) | ||
| const resolvedMin = resolveMinFeeForSender({ | ||
| senderAddress: data.senderAddress, | ||
| accounts: allAccounts, | ||
| suggestedMinFee, | ||
| configMinTxnFee: minTxnFee, | ||
| pqMultiplier, | ||
| }) | ||
| const staticFee = | ||
| resolvedMin > suggestedMinFee | ||
| ? microAlgo( | ||
| dAppFee !== undefined && dAppFee > resolvedMin | ||
| ? dAppFee | ||
| : resolvedMin, | ||
| ) | ||
| : dAppFee !== undefined | ||
| ? microAlgo(dAppFee) | ||
| : undefined |
There was a problem hiding this comment.
If you change the above thing to a fee calculator, this can all pretty much go back to the previous version I think?
| export const useQuantumFeeAdjustment = ( | ||
| transaction?: PeraDisplayableTransaction, | ||
| ): UseQuantumFeeAdjustmentResult => { |
There was a problem hiding this comment.
Same comment as before, I think we can have a general function that just sets the right fee for any tx group and we just always call that.
| expect(outcome).toEqual({ | ||
| kind: 'error', | ||
| phase: 'signing', | ||
| message: 'Quantum accounts are not supported in swap flows yet', | ||
| }) |
There was a problem hiding this comment.
Why not support PQ in swap?
| signableIndices: number[], | ||
| sourceMetadata: TransactionSignRequest['sourceMetadata'], | ||
| ): Promise<PeraSignedTransaction[]> => | ||
| ): Promise<PeraSignedTxnResult[]> => |
There was a problem hiding this comment.
Why do we need this type change. Shoudn't PeraSignedTransaction just change shape to accommodate PQ if necessary?
I think making all these pervasive intermediate changes is making the whole train way more complicated and brittle. Totally cool to stub out functionality for a future PR but I think adding new flows and changing signatures to temporary structures feels like it's making things more complicated rather than less so?
There was a problem hiding this comment.
Shouldn't need changes here at all
There was a problem hiding this comment.
Shouldn't need to touch this
There was a problem hiding this comment.
If this is really legacy, can we just delete it?
There was a problem hiding this comment.
I think the adjustFeeIfNecessary function could just return a list of adjusted txns and an object describing the fee adjustments and then you don't need separate concepts.
…culator [PERA-4490]
Review follow-ups (wjbeau):
- useMinimumFeeCalculator().assignFeeToGroup(group) is now the single seam
that assigns required minimum fees to any tx group (quantum precheck +
suggested-params fetch live inside; free no-op for non-quantum groups).
PQ-022's simulate()-based fees swap in behind this interface.
- Core renamed applyQuantumFeeOverride -> assignMinimumFeesToGroup; records
are scheme-agnostic FeeAdjustment { index, originalFee, adjustedFee,
reason } ('quantum-minimum' is just today's only rule).
- useEnqueueArc0001SignRequest collapses its inline quantum branching into
one assignFeeToGroup call; useKeyregDeeplink builds with the dApp fee
verbatim again (previous shape) and floors post-build via the calculator —
non-quantum keyreg deeplinks no longer fetch suggested params and their
fees are never floored (restores zero-behavior-change for non-quantum).
- QuantumFeeDeliveryError -> FeeAdjustmentDeliveryError (marker
'fee-adjusted'); UI hook useQuantumFeeAdjustment -> useFeeAdjustment.
- Deleted the runtime-dead legacy createWalletConnectSource DataSource +
spec (live path is the actor machine + enqueue hook).
wjbeau
left a comment
There was a problem hiding this comment.
Looks much better, just one small comment in there.
| let suggestedMinFee = 0n | ||
| try { | ||
| suggestedMinFee = BigInt( | ||
| (await algokit.getSuggestedParams()).minFee, |
There was a problem hiding this comment.
I might extract this out of the useCallback into a useSuggestedParams or something (I think we might have that already). The reason is that if you do this it will cache it indefinitely (since accounts, algokit, minTxnFee and pqMultiplier may likely not change), but the suggested params can change if, for example, the network becomes congested. I think our existing hook caches it for like 15s or something.
…ms cache; centralize signed-result narrowing [PERA-4490] Review follow-ups (wjbeau, approval round): - useMinimumFeeCalculator now fetches params via useFetchSuggestedParameters, a fetch-through-cache companion to useSuggestedParametersQuery (same query key, 10s staleness, networkMode 'always') — congestion-driven minFee changes propagate on the app-wide contract while staying lazy: non-quantum groups still fetch nothing. - compactSignedResults (blockchain models) replaces the four hand-rolled null-filter sites (useSignAndSubmitGroup, useFeeDelegation, requestRekeySignatures, swapExecutionHelpers) — one narrowing seam for PQ-023 to update when the carrier collapses into the signed model.
…uggested-min-fee source [PERA-4490] Follows the shape wjbeau suggested on the calculator: the min-fee acquisition is a named, reusable function outside the useCallback. useFetchSuggestedMinFee (blockchain) wraps the fetch-through-cache params fetcher and returns the network minFee as bigint, with an optional fallback for never-block callers. Consumers migrated off private BigInt((await algokit.getSuggestedParams()).minFee) acquisitions onto the shared query-cache entry: - useMinimumFeeCalculator (fallback: 0n — config base applies offline) - useSubmitRekeyMutation (submit-side fee now reads the same cache entry as the useRekeyTransactionFeeQuery display) - useTransactionSendFlow buildNormalTxs + buildExpressTxs Render-time consumers (useMinFeeForSender, useRekeyTransactionFeeQuery) stay on the eager useSuggestedParametersQuery — same key, same entry.
…n/removeScripts advisories Unblocks the Audit CI gate. Two fresh high advisories on fast-uri (GHSA-4c8g-83qw-93j6, GHSA-v2hh-gcrm-f6hx — host confusion via failed IDN canonicalization / literal backslash authority delimiter) flag >=4.0.0. We were on 4.0.0 because the previous override target (">=3.1.2") was unbounded and let the resolver escape ajv's declared ^3.0.1 range into the 4.x line. Re-pin exact INSIDE the range: 3.1.4, upstream's 3.x backport of both fixes (same advisory ids on the releases). 3.1.4 is 5 days old, so fast-uri gets a TEMPORARY minimumReleaseAgeExclude entry (maintainer continuity verified: same 11 maintainers, no install scripts) — remove after 2026-07-26 when it clears the 7-day window. Also pins svgo 3.3.4 (GHSA-2p49-hgcm-8545, dev-only via react-native-svg-transformer > @svgr/plugin-svgo; 13 days old, no exclude needed) — pre-existing on main, fixed here since the lockfile is already being touched. Both CI audit commands verified clean locally; lockfile diff is the two pins only. Pre-existing on main (this PR doesn't otherwise touch deps).
…confusion/removeScripts advisories" This reverts commit 333f393.
Description
Implements PQ-017: when an external (WalletConnect / webview) transaction group will be signed by a quantum account and the dApp-set fee is below the post-quantum minimum, the wallet raises the fee before the user reviews and signs, re-assigns the group ID across the full group, and returns the modified signed transactions to the dApp.
applyQuantumFeeOverride(packages/signing) — pure core: raises quantum-signed txn fees tomax(algod suggestedMinFee, fee_min_txn_fee) × fee_pq_multiplier(defaults 1000 × 3 = 3,000 µAlgo), validates the incoming group as received, then re-derivesgrpover each affected partition in full — including txns Pera doesn't sign. Fees are never lowered; groups with no quantum signer are returned byte-identical (same references).useEnqueueArc0001SignRequest(the real runtime WC/webview convergence point — the ticket'screateWalletConnectSource.tsis a runtime-dead legacy layer, its pass-through regression comment now points here), so the review sheet shows final fees. Non-quantum requests take a fast path with no suggested-params fetch and zero behavior change.feeAdjustmentsis recorded on the request; invalid incoming groups reject to the dApp without enqueueing.assertNoQuantumSignedTransactionsremoved; pqsig carriers now flow back to the dApp via the carrier-aware encoder (nulls at unsigned slots per ARC-0001). All widenedapproveconsumers audited; swap flows fail loud on unexpected quantum carriers instead of silently dropping them.useQuantumFeeAdjustmenthook, newtransactions.quantum_fee.adjusted_*keys).QuantumFeeDeliveryError→ i18n toast noting the dApp may not support Quantum account fees (walletconnect.request.quantum_fee_delivery_failed), through the existing WC error handling.Related Issues
Checklist
Additional Notes
wc-sign-quantum-fee.test.tsx) driving the real WC connector → review sheet → real Falcon signing → callback, including the byte-identical (rawTransactionsMatch) non-quantum regression.pnpm pre-pushfully green; fullpnpm testmatches the pre-existing local integration baseline exactly (no regressions).docs/QUANTUM_PQ017_DAPP_COMPATIBILITY_TEST_PLAN.md— behavior contract, dApp test matrix, per-dApp procedure, limitations. dApps that hash-check returned bytes will reject modified groups by design; that is a per-dApp compatibility gap to record, not a wallet defect.🤖 Generated with Claude Code