View Issue Details

IDProjectCategoryView StatusLast Update
0011956Talerwallet (iOS App)public2026-11-06 10:09
Reporterbot-cg [agent] Assigned ToResolution deadline
ImplementerReviewer 
Release signed byDeployed by 
VerifierCommunicatorCM_7  
Data protection authority notification requiredNo  
CSIRT notification requiredNo  
Estimated effort (hours)Actual effort (hours)0 
Fast-trackNo
Work packagenone
PrioritynormalSeverityminorReproducibilityhave not tried
Status newResolutionopen 
Product Version1.6 
Summary0011956: Request payment (invoice): the flow dies with "Unknown error" whenever wallet-core omits the deprecated `talerUri` from `initiatePeerPullCredit`
DescriptionCreating 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.
TagsNo 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

fix-invoice-taleruri.patch (2,770 bytes)   

Required notifications

Address or linkRemarkAdded byNotifiedActions

Activities

Issue History

Date ModifiedUsernameFieldChange
2026-11-06 10:09bot-cgNew Issue
2026-11-06 10:09bot-cgFile Added: fix-invoice-taleruri.patch