3434use chia_protocol:: CoinSpend ;
3535use dig_wallet_backend:: types:: TransactionSummary ;
3636
37- use crate :: auth:: provider:: SpendDecision ;
37+ use crate :: auth:: provider:: { AuthProvider , SpendConfirmRequest , SpendDecision } ;
3838use crate :: error:: { AccountError , Result } ;
39+ use crate :: id:: { AccountId , ProfileIx } ;
3940use crate :: wallet:: summary:: SpendSummary ;
4041
4142/// The spends a ruling was made about, together with the single derivation made from them.
@@ -48,9 +49,14 @@ struct AuthorizedSpend {
4849 coin_spends : Vec < CoinSpend > ,
4950 /// This crate's tiered, human-renderable view of those spends.
5051 summary : SpendSummary ,
51- /// `dig-wallet-backend`'s own verified re-parse of the same spends, carried so the signer can
52- /// hand it back for the pre-signing cross-check without re-deriving it (one derivation, one
53- /// answer — two derivations could disagree).
52+ /// The hinted-only summary `dig-wallet-backend`'s signer takes as a required PARAMETER, derived by
53+ /// the gate alongside `summary` so the signer never has to re-parse the spend.
54+ ///
55+ /// The signer does compare it against its own re-derivation, but this crate does not treat that as
56+ /// a check and does not rely on it: both sides come from these same bytes, so it can only agree. A
57+ /// genuine second opinion would need an INDEPENDENT derivation — exactly the two-answers-can-
58+ /// disagree shape this whole type exists to remove. What protects the caller is that `coin_spends`
59+ /// below is the same `Vec` the gate judged.
5460 verified : TransactionSummary ,
5561}
5662
@@ -129,29 +135,61 @@ impl PendingApproval {
129135 & self . inner . summary
130136 }
131137
132- /// Convert the user's ruling into a signable approval.
138+ /// Run the confirm ceremony through `provider`, and convert the user's ruling into a signable
139+ /// approval.
140+ ///
141+ /// **This is the ONLY route from "needs a human" to a signature**, and it is a route THROUGH the
142+ /// consent seam rather than past it. A host cannot mint an approval by asserting consent it never
143+ /// obtained: it must implement [`AuthProvider::confirm_spend`], which is the seam that exists to
144+ /// render the ceremony. A host that cannot render one MUST return
145+ /// [`Decline`](SpendDecision::Decline) — never `Approve` — and `SPEC.md` §6.3 states that as a MUST.
133146 ///
134- /// Consumes `self`, so a ceremony cannot be run once and converted twice .
147+ /// Consumes `self`, so one prompt yields at most one approval .
135148 ///
136149 /// # A decline is terminal, not a retry
137150 ///
138- /// [`Decline`](SpendDecision::Decline) yields [`PolicyDenied`](AccountError::PolicyDenied) rather
139- /// than an escalatable refusal: the human has already been asked, so no further ceremony may
140- /// permit this spend, and a caller that treated the decline as "ask again" would turn a refusal
141- /// into a prompt-until-mis-click.
151+ /// [`Decline`](SpendDecision::Decline) yields [`UserDeclined`](AccountError::UserDeclined) — a
152+ /// distinct variant from the structural [`PolicyDenied`](AccountError::PolicyDenied), so a host can
153+ /// report "you said no" separately from "the rules say no" instead of collapsing them. Either way no
154+ /// further ceremony may permit this spend: a caller that treated a decline as "ask again" would turn
155+ /// a refusal into a prompt-until-mis-click.
142156 ///
143157 /// # A confirmed spend does not consume the auto-send allowance
144158 ///
145- /// The rolling period cap bounds what may move *unattended*. This spend moved because a human
146- /// said so, so it is charged to nothing — and, symmetrically, a declined spend leaves the
147- /// allowance untouched (a refusal must never cost the user their allowance). That is why this
148- /// method holds no reference to the gate's ledger: it structurally cannot charge it.
149- pub fn confirmed ( self , decision : SpendDecision ) -> Result < SpendApproval > {
159+ /// The rolling period cap bounds what may move *unattended*. This spend moves because a human said
160+ /// so, so it is charged to nothing — and, symmetrically, a declined spend leaves the allowance
161+ /// untouched. Charging a confirmed spend would let anything that can raise a prompt drain the user's
162+ /// unattended allowance without a single approval, turning the cap into a weapon against them.
163+ /// `SPEC.md` §6.4 records the reasoning; this method holds no reference to the gate's ledger, so it
164+ /// structurally cannot charge it either way.
165+ pub async fn confirm_with (
166+ self ,
167+ provider : & dyn AuthProvider ,
168+ account : AccountId ,
169+ profile : ProfileIx ,
170+ ) -> Result < SpendApproval > {
171+ let request = SpendConfirmRequest :: new ( account, profile, self . inner . summary . clone ( ) ) ;
172+ let decision = provider. confirm_spend ( request) . await ?;
173+ self . decided ( decision)
174+ }
175+
176+ /// Convert an already-collected decision.
177+ ///
178+ /// `pub(crate)`: this is [`confirm_with`](Self::confirm_with)'s tail, and it is deliberately not a
179+ /// public door. A public `confirmed(SpendDecision)` would let a host write
180+ /// `RequiresConfirmation(p) => p.confirmed(Approve)` — one line, no user asked, no cap charged, no
181+ /// limit re-checked. That is the `Ok(())` authorizer this crate removed, wearing a different name,
182+ /// and with less accounting than the thing it replaced. Consent must come from the seam that exists
183+ /// to obtain it.
184+ fn decided ( self , decision : SpendDecision ) -> Result < SpendApproval > {
150185 match decision {
151186 SpendDecision :: Approve => Ok ( SpendApproval { inner : self . inner } ) ,
152- SpendDecision :: Decline ( reason) => Err ( AccountError :: PolicyDenied ( format ! (
187+ // `{:?}` deliberately: the reason is host-supplied text that may quote a dapp, and an
188+ // error string ends up in logs. Debug-escaping it keeps a newline or a control character
189+ // from forging a log line.
190+ SpendDecision :: Decline ( reason) => Err ( AccountError :: UserDeclined ( format ! (
153191 "the user declined this spend{}" ,
154- reason. map( |r| format!( ": {r}" ) ) . unwrap_or_default( )
192+ reason. map( |r| format!( ": {r:? }" ) ) . unwrap_or_default( )
155193 ) ) ) ,
156194 }
157195 }
0 commit comments