taler-docs

Documentation for GNU Taler components, APIs and protocols
Log | Files | Refs | README | LICENSE

commit 7bb4aba1f97dd15c1d1286394d8db9add557da1d
parent ddb66c6480ad5b3e22c6b90b59ef934066a5d4cf
Author: Florian Dold <dold@taler.net>
Date:   Sat,  5 Sep 2026 18:12:01 +0200

wallet: document DD71 renewal behavior and client APIs

Describe greedy output selection, persistent scheduling, power hints,
expiration warnings, recovery notices, and conditional annual cost bounds.
Document the deliberate deferral of globally optimal output selection.

Diffstat:
Awallet/auto-refresh.rst | 107+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mwallet/index.rst | 1+
Mwallet/wallet-core.md | 137++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------
3 files changed, 233 insertions(+), 12 deletions(-)

diff --git a/wallet/auto-refresh.rst b/wallet/auto-refresh.rst @@ -0,0 +1,107 @@ +Automatic coin renewal +###################### + +Wallet-core implements the September 2026 policy from +:doc:`../design-documents/071-auto-refresh`, with deterministic greedy output +selection. Wallet clients receive renewal information with each scoped balance. + +Selection and eligibility +========================= + +For an input coin, let ``D`` be its deposit expiration, ``W`` its withdrawal +expiration, ``R = D - now``, and ``E = min(90 days, (D - W) / 4)``. Days are fixed +24-hour periods. Invalid or infinite lifetimes are reported; expired coins use +existing expiration and recovery handling. + +The wallet considers compatible, verified, currently withdrawable denomination +keys, excluding revoked and lost keys. It selects outputs greedily in descending +face-value order, breaking ties by lower withdrawal fee, later deposit expiration, +and denomination hash. The refresh protocol permits at most 64 outputs. +This is deliberately best effort: it does not implement DD71's globally optimal +value/lifetime/coin-count selection. Spending existing coins retains DD91's +modified greedy algorithm and earliest-expiration preference. + +The earliest selected output expiration determines the output lifetime. Total +cost includes melting fees, withdrawal fees, and unrepresentable remainder. +The wallet requires positive recovered value and an extension of validity, then +evaluates these strict conditions in order: + +* Output remaining lifetime exceeds four times the input remaining lifetime. +* Input remaining lifetime is below 180 days, output remaining lifetime exceeds + 360 days, total cost is zero, and external power is confirmed. +* Input remaining lifetime is below ``E``. + +Decision, accounting, and execution use the same selected outputs. Inputs reserved +for payments or refreshes are unavailable for another automatic refresh. Before +sending a melt, the wallet revalidates eligibility and selected keys. A request +that might already have reached the exchange retains its original commitment +through retry and recovery. + +Scheduling +========== + +A private random deadline per wallet and input denomination spreads renewal +traffic. Before ``D - E / 10``, the delay is at most the minimum of one day, +``E / 10``, and half the time to that urgent boundary. Urgent inputs are due +immediately. Restarting, updating keys, or reopening the application does not +redraw or postpone a deadline. Temporarily ineligible inputs retain their deadline +while the wallet waits for a valid plan. Retry backoff is randomized and capped +by the time to urgency, or remaining lifetime once urgent. + +Power observations +================== + +``hintPowerState`` accepts ``{ powerSource: "external" | "battery" | "unknown" }`` +and returns an empty object. Observations are transient and expire after 60 +seconds: a host should refresh its current observation at least every 30 seconds. +The browser integration uses supported battery observations and reports unknown +for unsupported or inconclusive readings. Unknown power never enables the free +opportunistic rule; necessary refreshes do not require external power. + +Balance warnings and recovery notices +==================================== + +Each balance may contain ``refreshInfo`` with ``risks``, ``recoveries``, and +``annualCostBound``. Risks identify the exchange, amount and earliest expiration. +They distinguish a pending renewal, a connection failure, an exchange error, +unavailable suitable replacements established from fresh keys, a check in +progress, and invalid validity dates. They do not assert device-wide offline +status. Pending and failing renewals retain expiration indications until success. + +A completed group whose coins were close to expiration before melting supplies +one recovery notice, including the new expiration and next relevant date. +``dismissWalletWarning`` accepts ``{ warningId: string }`` to persist dismissal. +Active expiration risks cannot be dismissed. A renewal notice does not imply the +wallet was previously offline. + +Conditional cost bound +====================== + +The balance screen's expandable cost summary covers keeping the current balance +for 365 days without payments or additional withdrawals. It is a conditional +upper bound, not a predicted debit. It assumes continuously available compatible +families with stable values, fees and lifetime bounds, and reveal completing +before output withdrawal validity ends. Unpublished future offerings are a +projection, not a signed commitment. + +Wallet-core checks all reachable compatible denominations, including small change, +and includes actual greedy-selection losses. It establishes conservative bounds +``Gmin``, ``Hmax``, and loss fraction ``p``, then uses +``Delta = Gmin - Hmax / 4``, ``N = 1 + floor(365 days / Delta)``, and +``B * (1 - (1 - p)**N)``. Group bounds are rounded upward to Taler's supported +amount precision before summing. Missing or stale information, unrefreshable +change, nonpositive ``Delta`` and unsettled holdings make the estimate unavailable. +Changed balances or offerings cause recomputation. + +The estimate excludes initial withdrawal fees, payment-related refreshes, and +losses from expiration or outages. Keeping a wallet offline beyond its coin +validity periods can still lose funds. + +Development experiments +======================= + +With development mode enabled and an existing balance, use +``taler://dev-experiment/dd71-risk``, ``dd71-recovered``, or +``dd71-cost-unavailable`` to exercise the balance presentation. These transient +fixtures do not alter coins. ``dd71-clear`` removes them. The browser demo also +includes a pending risk, a dismissible recovery notice, and a cost bound. diff --git a/wallet/index.rst b/wallet/index.rst @@ -17,3 +17,4 @@ API. :maxdepth: 1 browser-integration + auto-refresh diff --git a/wallet/wallet-core.md b/wallet/wallet-core.md @@ -8,6 +8,8 @@ This file is auto-generated from the [taler-typescript-core](https://git.taler.n * [SetWalletRunConfigOp](#setwalletrunconfigop) * [GetVersionOp](#getversionop) * [HintNetworkAvailabilityOp](#hintnetworkavailabilityop) +* [HintPowerStateOp](#hintpowerstateop) +* [DismissWalletWarningOp](#dismisswalletwarningop) ### Generic request handling * [RetryProgressTokenNowOp](#retryprogresstokennowop) * [CancelProgressTokenOp](#cancelprogresstokenop) @@ -254,6 +256,46 @@ export interface HintNetworkAvailabilityRequest { } ``` +### HintPowerStateOp +```{ts:def} HintPowerStateOp +/** Report a current power observation. Unknown power never enables free opportunistic refresh. */ +export type HintPowerStateOp = { + op: WalletApiOperation.HintPowerState; + request: HintPowerStateRequest; + response: EmptyObject; +}; +// HintPowerState = "hintPowerState" +``` +```{ts:def} HintPowerStateRequest +export interface HintPowerStateRequest { + powerSource: WalletPowerSource; +} +``` +```{ts:def} WalletPowerSource +/** Host observation; unknown never qualifies for an opportunistic refresh. */ +export declare enum WalletPowerSource { + External = "external", + Battery = "battery", + Unknown = "unknown", +} +``` + +### DismissWalletWarningOp +```{ts:def} DismissWalletWarningOp +/** Dismiss a completed renewal notice. Active expiration risks cannot be dismissed. */ +export type DismissWalletWarningOp = { + op: WalletApiOperation.DismissWalletWarning; + request: DismissWalletWarningRequest; + response: EmptyObject; +}; +// DismissWalletWarning = "dismissWalletWarning" +``` +```{ts:def} DismissWalletWarningRequest +export interface DismissWalletWarningRequest { + warningId: string; +} +``` + ### RetryProgressTokenNowOp ```{ts:def} RetryProgressTokenNowOp export type RetryProgressTokenNowOp = { @@ -647,6 +689,8 @@ export interface BalancesResponse { ``` ```{ts:def} WalletBalance export interface WalletBalance { + /** DD71 expiry information and conditional cost of keeping this balance. */ + refreshInfo?: WalletRefreshInfo; scopeInfo: ScopeInfo; available: AmountString; pendingIncoming: AmountString; @@ -667,6 +711,56 @@ export interface WalletBalance { disableDirectDeposits?: boolean; } ``` +```{ts:def} WalletRefreshInfo +export interface WalletRefreshInfo { + risks: CashExpirationRisk[]; + recoveries: CashRenewalNotice[]; + annualCostBound: AnnualRefreshCostBound; +} +``` +```{ts:def} CashExpirationRisk +export interface CashExpirationRisk { + exchangeBaseUrl: string; + exchangeMasterPub: string; + amount: AmountString; + earliestDepositExpiration: TalerProtocolTimestamp; + reason: + | "pending" + | "connectivity" + | "exchange-error" + | "no-replacement" + | "checking" + | "invalid-lifetime"; +} +``` +```{ts:def} CashRenewalNotice +export interface CashRenewalNotice { + warningId: string; + exchangeBaseUrl: string; + exchangeMasterPub: string; + amount: AmountString; + oldDepositExpiration: TalerProtocolTimestamp; + newDepositExpiration: TalerProtocolTimestamp; + /** Earliest emergency threshold of the renewed coins. */ + nextRelevantDate: TalerProtocolTimestamp; +} +``` +```{ts:def} AnnualRefreshCostBound +/** Conditional on stable, continuously available compatible offerings and timely reveal. */ +export type AnnualRefreshCostBound = { + horizonDays: 365; + projection: "stable-current-offerings"; +} & ( + | { + status: "available"; + amount: AmountString; + } + | { + status: "unavailable"; + reasons: string[]; + } +); +``` ```{ts:def} BalanceFlag export declare enum BalanceFlag { IncomingKyc = "incoming-kyc", @@ -1637,7 +1731,7 @@ export type GetChoicesForPaymentResult = { */ choices: ChoiceSelectionDetail[]; /** - * Index of the choice in @e choices array to present + * Index of the choice in the `choices` array to present * to the user as default. * * Won´t be set if no default selection is configured @@ -1646,7 +1740,7 @@ export type GetChoicesForPaymentResult = { */ defaultChoiceIndex?: number; /** - * Whether the choice referenced by @e automaticExecutableIndex + * Whether the choice referenced by `automaticExecutableIndex` * should be confirmed automatically without * user interaction. * @@ -1659,7 +1753,7 @@ export type GetChoicesForPaymentResult = { automaticExecution?: boolean; /** * Index of the choice that would be set to automatically - * execute if the choice was payable. When @e automaticExecution + * execute if the choice was payable. When `automaticExecution` * is set to true, the payment should be confirmed with this * choice index without user interaction. */ @@ -1804,6 +1898,9 @@ export interface WalletTemplateDetailsResponse { template_contract: TemplateContractDetails; editable_defaults?: TemplateContractDetailsDefaults; required_currency?: string; + merchant?: MerchantInfo; + merchant_pub?: EddsaPublicKey; + exchange_candidates?: TemplateExchangeCandidate[]; } ``` ```{ts:def} TemplateContractDetails @@ -1966,6 +2063,14 @@ export interface TemplateContractDetailsDefaults { amount?: string; } ``` +```{ts:def} TemplateExchangeCandidate +export interface TemplateExchangeCandidate { + base_url: string; + currency: string; + master_pub: EddsaPublicKey; + wire_methods: string[]; +} +``` ### ConfirmPayOp ```{ts:def} ConfirmPayOp @@ -3516,7 +3621,7 @@ export interface InitiatePeerPullCreditResponse { * * @deprecated since it's not necessarily valid yet until the tx is in the right state */ - talerUri: string; + talerUri?: string; transactionId: TransactionIdStr; } ``` @@ -4858,6 +4963,8 @@ export enum WalletApiOperation { GetDiagnostics = "getDiagnostics", // Hints HintNetworkAvailability = "hintNetworkAvailability", + HintPowerState = "hintPowerState", + DismissWalletWarning = "dismissWalletWarning", HintApplicationResumed = "hintApplicationResumed", /** * @deprecated (2025-05-07) @@ -5201,14 +5308,21 @@ export interface TalerDevExperimentUri { export interface TalerPayPullUri { type: TalerUriAction.PayPull; exchangeBaseUrl: HostPortPath; - contractPriv: string; + contractPriv: EddsaPrivateKeyString; + /** Exchange-authenticated purse metadata, required before paying. */ + purseCreateProof?: { + totalDeposited: AmountString; + exchangeTimestamp: TalerProtocolTimestamp; + exchangeSig: EddsaSignatureString; + exchangePub: EddsaPublicKeyString; + }; } ``` ```{ts:def} TalerPayPushUri export interface TalerPayPushUri { type: TalerUriAction.PayPush; exchangeBaseUrl: HostPortPath; - contractPriv: string; + contractPriv: EddsaPrivateKeyString; } ``` ```{ts:def} TalerRestoreUri @@ -5259,8 +5373,8 @@ export interface TalerAddContactUri { alias: string; aliasType: string; mailboxBaseUri: string; - mailboxIdentity: HashCodeString; - sourceBaseUrl: string; + mailboxIdentity: EddsaPublicKeyString; + sourceBaseUrl?: string; } ``` ```{ts:def} MailboxMessageRecord @@ -5446,7 +5560,6 @@ export interface TransactionCommon { * This amount is preserved when execution fails, expires or is aborted. */ amountEffective: AmountString; - /** * Settled wallet balance effect, including fees and abort recovery. * Nonnegative; the transaction type determines whether this is a debit or @@ -5455,7 +5568,6 @@ export interface TransactionCommon { * refunds remain separate credits. Associated refreshes have zero effect. */ amountEffectiveFinal?: AmountString; - error?: TalerErrorDetail; abortReason?: TalerErrorDetail; failReason?: TalerErrorDetail; @@ -5729,7 +5841,7 @@ export interface WithdrawalExchangeAccountDetails { */ transferExpiry?: TalerProtocolTimestamp; /** - * Options for transfering funds to the exchange for the withdrawal. + * Options for transferring funds to the exchange for the withdrawal. */ transferOptions: TransferOption[]; } @@ -6599,7 +6711,8 @@ export interface ExchangeListItem { legacyMasterPubs: string[]; /** * Set when the exchange changed its key set and the user has not confirmed - * the change yet. Withdrawals are refused while this is present. + * the change yet. Operations that send money to the exchange or disclose + * coin authorizations are refused while this is present. */ unconfirmedKeyChange?: ExchangeKeyChangeInfo; currency: string;