View Issue Details
| ID | Project | Category | View Status | Date Submitted | Last Update |
|---|---|---|---|---|---|
| 0011956 | Taler | wallet (iOS App) | public | 2026-11-06 10:09 | 2026-11-06 10:09 |
| Reporter | bot-cg [agent] | Assigned To | Resolution deadline | ||
| Implementer | Reviewer | ||||
| Release signed by | Deployed by | ||||
| Verifier | Communicator | CM_7 | |||
| Data protection authority notification required | No | ||||
| CSIRT notification required | No | ||||
| Estimated effort (hours) | Actual effort (hours) | 0 | |||
| Fast-track | No | ||||
| Work package | none | ||||
| Priority | normal | Severity | minor | Reproducibility | have not tried |
| Status | new | Resolution | open | ||
| Product Version | 1.6 | ||||
| Summary | 0011956: Request payment (invoice): the flow dies with "Unknown error" whenever wallet-core omits the deprecated `talerUri` from `initiatePeerPullCredit` | ||||
| Description | Creating an invoice ("Actions -> Request") fails with a bare Unknown error [ OK ] and no invoice is created, no QR code is shown. Nothing is wrong with the request: wallet-core created the purse and the transaction exists. The app threw the reply away while decoding it. From the app's own log (subsystem net.taler.gnu): initiatePeerPullCredit failed after 436 ms DecodingError.keyNotFound: Key 'talerUri' not found in keyed decoding container. Cause ----- TalerWallet1/Model/Model+P2P.swift declares struct InitiatePeerPullCreditResponse: Codable { let talerUri: String // <-- not optional let transactionId: String } wallet-core declares that field optional *and deprecated* (taler-util/src/types-taler-wallet.ts): export interface InitiatePeerPullCreditResponse { /** * Taler URI for the other party to make the payment that was requested. * * @deprecated since it's not necessarily valid yet until the tx is in * the right state */ talerUri?: string; transactionId: TransactionIdStr; } and pay-peer-pull-credit.ts fills it in only when the purse record has *already* reached PendingReady by the time the call returns: return { talerUri: currentRecord?.status === PeerPullPaymentCreditStatus.PendingReady ? makePeerPullPaymentUri(currentRecord) : undefined, transactionId: ctx.transactionId, }; `undefined` is not serialised, so the key is absent -- which is the normal case, because the purse is usually still being created when initiate returns. The non-optional Swift property turns that into a thrown DecodingError, which P2PReadyV.swift catches into `initiationError` and renders as "Unknown error". So this is a race whose *likely* outcome is failure: the invoice flow works only when the purse happens to be ready within the duration of the call. It is also why the error has no useful text -- a DecodingError has no localizedDescription of its own. The sibling push struct in the same file already has the field commented out: struct InitiatePeerPushDebitResponse: Codable { ... // let talerUri: String? let transactionId: String } so the pull side looks like an oversight rather than a deliberate requirement. Note that the app does not need the field at all. The URI the QR code shows is taken from the transaction, not from this reply: TransactionSummaryList.QRCodeDetails reads `transaction.detailsToShow()[TALERURI]`, and wallet-core's transaction record always carries `talerUri` (pay-peer-pull-credit.ts lines 316 and 350). Dropping the dependency loses nothing. How to reproduce ---------------- By hand: 1. open the wallet with a non-zero balance 2. Actions -> Request 3. pick any amount (e.g. the "Shortcut KUDOS 10.0" button) 4. type a subject, choose an expiry (e.g. "3 Min.") 5. press "Request KUDOS 10.00" 6. "Unknown error" appears instead of the invoice QR code With the exploration harness, from harness/ on a Mac with Xcode: python3 -m taler_fuzz.cli build python3 -m taler_fuzz.cli --max-steps 24 \ --prefer "ok,actions,request,shortcut,3 min,1 hour,expires" \ replay --label qrE --shots Because it is a race, a build whose purse creation is fast enough against a nearby exchange can pass; the failing run above was against an exchange on the same LAN, i.e. not a pathologically slow one. Patch ----- Attached: fix-invoice-taleruri.patch (one file, one property), against master. It makes `InitiatePeerPullCreditResponse.talerUri` optional, matching wallet-core's own declaration. No call site reads the field -- the only caller, P2PReadyV.swift:68, uses `response.transactionId` -- so the change is confined to the decoder. Estimated severity ------------------ High - the Request/invoice feature is unusable in the common case, and the user is shown an untitled internal failure. Estimated effort to fix ----------------------- Trivial - one property. I checked the other five response structs in Model+P2P.swift against types-taler-wallet.ts: CheckPeerPushDebitResponse, CheckPeerPullCreditResponse, PreparePeerPushCreditResponse, PreparePeerPullDebitResponse and InitiatePeerPushDebitResponse all declare the same optionality as wallet-core, so this is the only mismatch in the file. Verification status, stated plainly ----------------------------------- What is established: * the defect itself, reproduced from a real run -- the app's own log names the thrown DecodingError, quoted verbatim above; * that wallet-core omits the key by design, from its own declaration and from the `PendingReady` condition in pay-peer-pull-credit.ts; * that nothing in the app reads the field, by inspection of the single call site; * that the patched app builds and installs. What is not established: I have **not** watched an invoice succeed on the patched build. The walk that would have pressed the create button again was stopped by issue 11949 -- the app not reporting itself idle -- before it got there, three attempts in a row. So the patch is argued from wallet-core's contract and from the call site, not from a green run. Filed by an agent (bot-cg) on behalf of Christian Grothoff, from an automated UI exploration campaign. Left unassigned deliberately. | ||||
| Tags | No tags attached. | ||||
| Attached Files | fix-invoice-taleruri.patch (2,770 bytes)
From 0b5de877980b64354fa80919f117a5d9de5095d5 Mon Sep 17 00:00:00 2001
From: Christian Grothoff <christian@grothoff.org>
Date: Fri, 6 Nov 2026 07:41:21 +0100
Subject: [PATCH] AI: the invoice flow must not require wallet-core's
deprecated talerUri
Pressing "Request <amount>" showed a bare "Unknown error" and created no
invoice. The app log named the cause:
initiatePeerPullCredit failed after 436 ms
DecodingError.keyNotFound: Key 'talerUri' not found in keyed decoding
container.
InitiatePeerPullCreditResponse declared `talerUri` as a non-optional String.
wallet-core declares it optional and deprecated, and only fills it in when the
purse record has already reached PendingReady by the time the call returns
(pay-peer-pull-credit.ts); otherwise the key is absent, which is the common
case because the purse is usually still being created. So a decoder that
insists on the key turns an ordinary race into a failed flow, and because a
DecodingError carries no localizedDescription of its own the user is told
nothing beyond "Unknown error".
Nothing needs the field: the only caller uses response.transactionId, and the
URI the QR code renders comes from the transaction
(TransactionSummaryList.QRCodeDetails reads detailsToShow()[TALERURI]), which
wallet-core always populates. The sibling push struct already has the field
commented out.
Checked the other five response structs in this file against
types-taler-wallet.ts: this is the only optionality mismatch.
Assisted by LLM
---
TalerWallet1/Model/Model+P2P.swift | 10 +++++++++-
1 file changed, 9 insertions(+), 1 deletion(-)
diff --git a/TalerWallet1/Model/Model+P2P.swift b/TalerWallet1/Model/Model+P2P.swift
index 7cf461fd..b8d6dea9 100644
--- a/TalerWallet1/Model/Model+P2P.swift
+++ b/TalerWallet1/Model/Model+P2P.swift
@@ -154,7 +154,15 @@ extension WalletModel {
// - - - - - -
/// Initiate an outgoing peer pull payment, send a request(invoice)
struct InitiatePeerPullCreditResponse: Codable {
- let talerUri: String
+ /// Optional, and wallet-core marks it deprecated: `initiatePeerPullCredit`
+ /// only fills it in when the purse record has already reached
+ /// `PendingReady` by the time the call returns, so the key is simply absent
+ /// whenever the purse is still being created -- which is the common case.
+ /// Decoding it as non-optional turned that race into a user-visible
+ /// "Unknown error" and aborted the whole invoice flow. The URI the QR code
+ /// shows comes from the transaction (`detailsToShow()[TALERURI]`), which
+ /// always carries it, so nothing here needs this field.
+ let talerUri: String?
let transactionId: String
}
fileprivate struct InitiatePeerPullCredit: WalletBackendFormattedRequest {
--
2.47.3
| ||||