taler-typescript-core

Wallet core logic and WebUIs for various components
Log | Files | Refs | Submodules | README | LICENSE

commit 062357e318d578aa024595cd5bd384fbfca520fa
parent 973a12881b05086b7a4f68ea903cebdf11ad5a0c
Author: Florian Dold <florian@dold.me>
Date:   Tue, 19 Aug 2025 19:16:50 +0200

wallet-core: prepare for contracts v1 feature flag, clean up

Diffstat:
Mpackages/taler-util/src/contract-terms.ts | 56++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mpackages/taler-util/src/errors.ts | 2+-
Mpackages/taler-util/src/types-taler-wallet.ts | 39+++++++++++++++++++++------------------
Mpackages/taler-wallet-core/src/deposits.ts | 36+++++++++++++++++-------------------
Mpackages/taler-wallet-core/src/pay-merchant.ts | 568++++++++++++++++++++++++++++++++++++-------------------------------------------
Mpackages/taler-wallet-core/src/wallet-api-types.ts | 44++++++++++++++++++++++++++++----------------
Mpackages/taler-wallet-core/src/wallet.ts | 5+++--
Mpackages/taler-wallet-webextension/src/hooks/useSettings.ts | 14+++-----------
Mpackages/taler-wallet-webextension/src/platform/api.ts | 1+
9 files changed, 390 insertions(+), 375 deletions(-)

diff --git a/packages/taler-util/src/contract-terms.ts b/packages/taler-util/src/contract-terms.ts @@ -31,6 +31,7 @@ import { } from "./types-taler-common.js"; import { MerchantContractTerms, + MerchantContractTermsV0, MerchantContractTokenKind, MerchantContractVersion, } from "./types-taler-merchant.js"; @@ -318,4 +319,59 @@ export namespace ContractTermsUtil { maxFee, }; } + + /** + * Try to downgrade contract terms in the v1 format to the v0 format. + * Returns undefined if downgrading is not possible. This can happen + * when the contract only offers token payments. + */ + export function downgradeContractTerms(terms: MerchantContractTerms): MerchantContractTermsV0 | undefined { + if (terms.version == MerchantContractVersion.V0) { + return terms; + } + if (terms.version !== MerchantContractVersion.V1) { + return undefined; + } + // Select the first choice that doesn't have + // and non-currency inputs. + let firstGood = -1; + for (let i = 0; i < terms.choices.length; i++) { + if (terms.choices[i].inputs.length == 0) { + firstGood = i; + break; + } + } + if (firstGood < 0) { + return undefined; + } + return { + amount: terms.choices[firstGood].amount, + exchanges: terms.exchanges, + h_wire: terms.h_wire, + max_fee: terms.choices[firstGood].max_fee, + merchant: terms.merchant, + merchant_base_url: terms.merchant_base_url, + merchant_pub: terms.merchant_pub, + nonce: terms.nonce, + order_id: terms.order_id, + pay_deadline: terms.pay_deadline, + refund_deadline: terms.refund_deadline, + summary: terms.summary, + timestamp: terms.timestamp, + wire_method: terms.wire_method, + wire_transfer_deadline: terms.wire_transfer_deadline, + auto_refund: terms.auto_refund, + delivery_date: terms.delivery_date, + delivery_location: terms.delivery_location, + extra: terms.extra, + fulfillment_message: terms.fulfillment_message, + fulfillment_message_i18n: terms.fulfillment_message_i18n, + fulfillment_url: terms.fulfillment_url, + minimum_age: terms.minimum_age, + products: terms.products, + public_reorder_url: terms.public_reorder_url, + summary_i18n: terms.summary_i18n, + version: MerchantContractVersion.V0, + } + } } diff --git a/packages/taler-util/src/errors.ts b/packages/taler-util/src/errors.ts @@ -58,7 +58,7 @@ export interface DetailsMap { }; [TalerErrorCode.WALLET_ORDER_ALREADY_PAID]: { orderId: string; - fulfillmentUrl: string; + fulfillmentUrl: string | undefined; }; [TalerErrorCode.WALLET_CONTRACT_TERMS_MALFORMED]: empty; [TalerErrorCode.WALLET_CONTRACT_TERMS_SIGNATURE_INVALID]: { diff --git a/packages/taler-util/src/types-taler-wallet.ts b/packages/taler-util/src/types-taler-wallet.ts @@ -52,6 +52,7 @@ import { CurrencySpecification, DurationUnitSpec, EddsaPrivateKeyString, + HashCode, TalerMerchantApi, TemplateParams, WithdrawalOperationStatusFlag, @@ -445,6 +446,16 @@ export interface WalletRunConfig { */ features: { allowHttp: boolean; + + /** + * If set to true, enable V1 contracts. Otherwise, emulate v0 contracts + * to wallet-core clients. + * + * Will become enabled by default in the future. + * + * Added 2025-08-19. + */ + enableV1Contracts: boolean; }; /** @@ -1024,6 +1035,9 @@ export type PreparePayResult = /** * Payment is possible. + * + * This response is only returned for v0 contracts + * or when v1 are not enabled yet. */ export interface PreparePayResultPaymentPossible { status: PreparePayResultType.PaymentPossible; @@ -1078,7 +1092,7 @@ export interface PreparePayResultAlreadyConfirmed { transactionId: TransactionIdStr; - contractTerms: MerchantContractTermsV0; + contractTerms: MerchantContractTerms; paid: boolean; @@ -2424,7 +2438,7 @@ export type GetChoicesForPaymentResult = { * Data extracted from the contract terms that * is relevant for payment processing in the wallet. */ - contractData: WalletContractData; + contractTerms: MerchantContractTerms; }; export interface SharePaymentRequest { @@ -3611,22 +3625,11 @@ export interface AllowedExchangeInfo { * Data extracted from the contract terms that is relevant for payment * processing in the wallet. */ -export type WalletContractData = MerchantContractTerms & { - /** - * Fulfillment URL, or the empty string if the order has no fulfillment URL. - * - * Stored as a non-nullable string as we use this field for IndexedDB indexing. - */ - fulfillmentUrl: string; - contractTermsHash: string; - merchantSig: string; - - /** - * Amounts are undefined for unfinished v1 orders. - */ - amountRaw?: AmountString; - amountEffective?: AmountString; -}; +export interface DownloadedContractData { + contractTermsRaw: any; + contractTerms: MerchantContractTerms; + contractTermsHash: HashCode; +} export type PayWalletData = { choice_index?: number; diff --git a/packages/taler-wallet-core/src/deposits.ts b/packages/taler-wallet-core/src/deposits.ts @@ -42,6 +42,7 @@ import { HttpStatusCode, KycAuthTransferInfo, Logger, + MerchantContractTerms, MerchantContractTermsV0, MerchantContractVersion, NotificationType, @@ -62,7 +63,7 @@ import { TransactionState, TransactionType, URL, - WalletContractData, + DownloadedContractData, WalletNotification, assertUnreachable, canonicalJson, @@ -70,6 +71,7 @@ import { checkLogicInvariant, codecForBatchDepositSuccess, codecForLegitimizationNeededResponse, + codecForMerchantContractTerms, codecForTackTransactionAccepted, codecForTackTransactionWired, encodeCrock, @@ -131,7 +133,6 @@ import { runKycCheckAlgo, } from "./kyc.js"; import { - extractContractData, generateDepositPermissions, getTotalPaymentCost, } from "./pay-merchant.js"; @@ -1548,7 +1549,7 @@ async function processDepositGroupTrack( async function doCoinSelection( ctx: DepositTransactionContext, depositGroup: DepositGroupRecord, - contractData: WalletContractData, + contractData: MerchantContractTerms, ): Promise<TaskRunResult> { const wex = ctx.wex; const depositGroupId = ctx.depositGroupId; @@ -1643,7 +1644,7 @@ async function doCoinSelection( interface SubmitBatchArgs { exchangeBaseUrl: string; - contractTerms: MerchantContractTermsV0; + contractTerms: MerchantContractTerms; depositPermissions: CoinDepositPermission[]; depositGroup: DepositGroupRecord; merchantSigResp: SignContractTermsHashResponse; @@ -1779,12 +1780,8 @@ async function processDepositGroupPendingDeposit( if (!contractTermsRec) { throw Error("contract terms for deposit not found in database"); } - const contractTerms: MerchantContractTermsV0 = - contractTermsRec.contractTermsRaw; - const contractData = extractContractData( + const contractTerms = codecForMerchantContractTerms().decode( contractTermsRec.contractTermsRaw, - depositGroup.contractTermsHash, - "", ); const ctx = new DepositTransactionContext(wex, depositGroupId); @@ -1794,7 +1791,7 @@ async function processDepositGroupPendingDeposit( if (!depositGroup.payCoinSelection) { logger.info("missing coin selection for deposit group, selecting now"); - return await doCoinSelection(ctx, depositGroup, contractData); + return await doCoinSelection(ctx, depositGroup, contractTerms); } await wex.db.runReadWriteTx({ storeNames: ["depositGroups"] }, async (tx) => { @@ -1813,7 +1810,8 @@ async function processDepositGroupPendingDeposit( const depositPermissions = await generateDepositPermissions( wex, depositGroup.payCoinSelection, - contractData, + contractTerms, + contractTermsRec.h, ); // Exchanges involved in the deposit @@ -2268,17 +2266,17 @@ export async function createDepositGroup( str: canonicalJson(contractTerms), }); - const contractData = extractContractData( - contractTerms, + const contractData: DownloadedContractData = { + contractTerms: contractTerms, + contractTermsRaw: contractTerms, contractTermsHash, - "", - ); + } if ( - contractData.version !== undefined && - contractData.version !== MerchantContractVersion.V0 + contractData.contractTerms.version !== undefined && + contractData.contractTerms.version !== MerchantContractVersion.V0 ) { - throw Error(`unsupported contract version ${contractData.version}`); + throw Error(`unsupported contract version ${contractData.contractTerms.version}`); } const totalDepositCost = await getTotalPaymentCost(wex, currency, coins); @@ -2322,7 +2320,7 @@ export async function createDepositGroup( contractTermsHash, depositGroupId, currency: Amounts.currencyOf(totalDepositCost), - amount: contractData.amount, + amount: contractData.contractTerms.amount, noncePriv: noncePair.priv, noncePub: noncePair.pub, timestampCreated: timestampPreciseToDb( diff --git a/packages/taler-wallet-core/src/pay-merchant.ts b/packages/taler-wallet-core/src/pay-merchant.ts @@ -51,11 +51,13 @@ import { ConfirmPayResultType, ContractTermsUtil, DenomKeyType, + DownloadedContractData, Duration, encodeCrock, ForcedCoinSel, GetChoicesForPaymentResult, getRandomBytes, + HashCodeString, hashPayWalletData, HttpStatusCode, j2s, @@ -110,7 +112,6 @@ import { TransactionState, TransactionType, URL, - WalletContractData, WalletNotification, } from "@gnu-taler/taler-util"; import { @@ -253,7 +254,7 @@ export class PayMerchantTransactionContext implements TransactionContext { tx, purchaseRec, ); - const contractData = download.contractData; + const contractData = download.contractTerms; const payOpId = TaskIdentifiers.forPay(purchaseRec); const payRetryRec = await tx.operationRetries.get(payOpId); @@ -289,11 +290,11 @@ export class PayMerchantTransactionContext implements TransactionContext { orderId: contractData.order_id, summary: contractData.summary, summary_i18n: contractData.summary_i18n, - contractTermsHash: contractData.contractTermsHash, + contractTermsHash: download.contractTermsHash, }; - if (contractData.fulfillmentUrl !== "") { - info.fulfillmentUrl = contractData.fulfillmentUrl; + if (contractData.fulfillment_url !== "") { + info.fulfillmentUrl = contractData.fulfillment_url; } const refunds: RefundInfoShort[] = refundsInfo.map((r) => ({ @@ -540,7 +541,7 @@ export class PayMerchantTransactionContext implements TransactionContext { async abortTransaction(reason?: TalerErrorDetail): Promise<void> { const { wex, proposalId, transactionId } = this; - const transitionInfo = await wex.db.runReadWriteTx( + await wex.db.runReadWriteTx( { storeNames: [ "coinAvailability", @@ -624,7 +625,7 @@ export class PayMerchantTransactionContext implements TransactionContext { async resumeTransaction(): Promise<void> { const { wex, proposalId, transactionId } = this; - const transitionInfo = await wex.db.runReadWriteTx( + await wex.db.runReadWriteTx( { storeNames: ["purchases", "transactionsMeta"] }, async (tx) => { const purchase = await tx.purchases.get(proposalId); @@ -651,7 +652,7 @@ export class PayMerchantTransactionContext implements TransactionContext { async failTransaction(reason?: TalerErrorDetail): Promise<void> { const { wex, proposalId, transactionId } = this; - const transitionInfo = await wex.db.runReadWriteTx( + await wex.db.runReadWriteTx( { storeNames: [ "purchases", @@ -746,9 +747,9 @@ export class RefundTransactionContext implements TransactionContext { if (maybeContractData) { paymentInfo = { - merchant: maybeContractData.merchant, - summary: maybeContractData.summary, - summary_i18n: maybeContractData.summary_i18n, + merchant: maybeContractData.contractTerms.merchant, + summary: maybeContractData.contractTerms.summary, + summary_i18n: maybeContractData.contractTerms.summary_i18n, }; } const purchaseRecord = await tx.purchases.get(refundRecord.proposalId); @@ -782,7 +783,7 @@ export class RefundTransactionContext implements TransactionContext { } async deleteTransaction(): Promise<void> { - const { wex, refundGroupId, transactionId } = this; + const { wex } = this; const res = await wex.db.runReadWriteTx( { @@ -846,8 +847,8 @@ export class RefundTransactionContext implements TransactionContext { async function lookupMaybeContractData( tx: WalletDbReadOnlyTransaction<["purchases", "contractTerms"]>, proposalId: string, -): Promise<WalletContractData | undefined> { - let contractData: WalletContractData | undefined = undefined; +): Promise<DownloadedContractData | undefined> { + let contractData: DownloadedContractData | undefined = undefined; const purchaseTx = await tx.purchases.get(proposalId); if (purchaseTx && purchaseTx.download) { const download = purchaseTx.download; @@ -857,11 +858,13 @@ async function lookupMaybeContractData( if (!contractTermsRecord) { return; } - contractData = extractContractData( - contractTermsRecord?.contractTermsRaw, - download.contractTermsHash, - download.contractTermsMerchantSig, - ); + contractData = { + contractTerms: contractTermsRecord.contractTermsRaw, + contractTermsHash: contractTermsRecord.h, + contractTermsRaw: codecForMerchantContractTerms().decode( + contractTermsRecord.contractTermsRaw, + ), + }; } return contractData; @@ -957,10 +960,7 @@ export async function expectProposalDownloadByIdInTx( wex: WalletExecutionContext, tx: WalletDbReadOnlyTransaction<["contractTerms", "purchases"]>, proposalId: string, -): Promise<{ - contractData: WalletContractData; - contractTermsRaw: any; -}> { +): Promise<DownloadedContractData> { const rec = await tx.purchases.get(proposalId); if (!rec) { throw Error("purchase record not found"); @@ -972,27 +972,25 @@ export async function expectProposalDownloadInTx( wex: WalletExecutionContext, tx: WalletDbReadOnlyTransaction<["contractTerms"]>, p: PurchaseRecord, -): Promise<{ - contractData: WalletContractData; - contractTermsRaw: any; -}> { +): Promise<DownloadedContractData> { if (!p.download) { throw Error("expected proposal to be downloaded"); } const download = p.download; - const contractTerms = await tx.contractTerms.get(download.contractTermsHash); - if (!contractTerms) { + const contractTermsRec = await tx.contractTerms.get( + download.contractTermsHash, + ); + if (!contractTermsRec) { throw Error("contract terms not found"); } return { - contractData: extractContractData( - contractTerms.contractTermsRaw, - download.contractTermsHash, - download.contractTermsMerchantSig, + contractTerms: codecForMerchantContractTerms().decode( + contractTermsRec.contractTermsRaw, ), - contractTermsRaw: contractTerms.contractTermsRaw, + contractTermsRaw: contractTermsRec.contractTermsRaw, + contractTermsHash: contractTermsRec.h, }; } @@ -1002,10 +1000,7 @@ export async function expectProposalDownloadInTx( async function expectProposalDownload( wex: WalletExecutionContext, p: PurchaseRecord, -): Promise<{ - contractData: WalletContractData; - contractTermsRaw: any; -}> { +): Promise<DownloadedContractData> { return await wex.db.runReadOnlyTx( { storeNames: ["contractTerms"] }, async (tx) => { @@ -1014,19 +1009,6 @@ async function expectProposalDownload( ); } -export function extractContractData( - parsedContractTerms: MerchantContractTerms, - contractTermsHash: string, - merchantSig: string, -): WalletContractData { - return { - fulfillmentUrl: parsedContractTerms.fulfillment_url ?? "", - contractTermsHash, - merchantSig, - ...parsedContractTerms, - }; -} - async function processDownloadProposal( wex: WalletExecutionContext, proposalId: string, @@ -1214,11 +1196,13 @@ async function processDownloadProposal( ); } - const contractData = extractContractData( - parsedContractTerms, - contractTermsHash, - proposalResp.sig, - ); + const contractData: DownloadedContractData = { + contractTerms: codecForMerchantContractTerms().decode( + proposalResp.contract_terms, + ), + contractTermsHash: contractTermsHash, + contractTermsRaw: proposalResp.contract_terms, + }; logger.trace(`extracted contract data: ${j2s(contractData)}`); @@ -1240,19 +1224,23 @@ async function processDownloadProposal( // v1: currency is resolved after choice selection let currency: string = "UNKNOWN"; if ( - contractData.version === undefined || - contractData.version === MerchantContractVersion.V0 + contractData.contractTerms.version === undefined || + contractData.contractTerms.version === MerchantContractVersion.V0 + ) { + currency = Amounts.currencyOf(contractData.contractTerms.amount); + } else if ( + contractData.contractTerms.version === MerchantContractVersion.V1 ) { - currency = Amounts.currencyOf(contractData.amount); - } else if (contractData.version === MerchantContractVersion.V1) { // if there is only one choice, or all choices have the same currency - if (contractData.choices.length === 1) { - currency = Amounts.currencyOf(contractData.choices[0].amount); - } else if (contractData.choices.length > 1) { + if (contractData.contractTerms.choices.length === 1) { + currency = Amounts.currencyOf( + contractData.contractTerms.choices[0].amount, + ); + } else if (contractData.contractTerms.choices.length > 1) { const firstCurrency = Amounts.currencyOf( - contractData.choices[0].amount, + contractData.contractTerms.choices[0].amount, ); - const allSame = contractData.choices.every( + const allSame = contractData.contractTerms.choices.every( (c) => Amounts.currencyOf(c.amount) === firstCurrency, ); if (allSame) { @@ -1263,9 +1251,9 @@ async function processDownloadProposal( p.download = { contractTermsHash, - contractTermsMerchantSig: contractData.merchantSig, + contractTermsMerchantSig: proposalResp.sig, currency, - fulfillmentUrl: contractData.fulfillmentUrl, + fulfillmentUrl: contractData.contractTerms.fulfillment_url, }; await tx.contractTerms.put({ h: contractTermsHash, @@ -1456,11 +1444,7 @@ async function createOrReusePurchase( ); if (oldProposal.shared || oldProposal.createdFromShared) { const download = await expectProposalDownload(wex, oldProposal); - const paid = await checkIfOrderIsAlreadyPaid( - wex, - download.contractData, - false, - ); + const paid = await checkIfOrderIsAlreadyPaid(wex, download, false); logger.info(`old proposal paid: ${paid}`); // if this transaction was shared and the order is paid then it // means that another wallet already paid the proposal @@ -1597,12 +1581,10 @@ async function storeFirstPaySuccess( !!contractTermsRecord, `no contract terms found for purchase ${purchase.orderId}`, ); - const contractData = extractContractData( + const contractTerms = codecForMerchantContractTerms().decode( contractTermsRecord.contractTermsRaw, - dl.contractTermsHash, - dl.contractTermsMerchantSig, ); - const protoAr = contractData.auto_refund; + const protoAr = contractTerms.auto_refund; if (protoAr) { const ar = Duration.fromTalerProtocolDuration(protoAr); logger.info("auto_refund present"); @@ -1663,14 +1645,14 @@ async function reselectCoinsTx( return; } - const { contractData } = await expectProposalDownloadByIdInTx( + const contractData = await expectProposalDownloadByIdInTx( ctx.wex, tx, ctx.proposalId, ); const { available, amountRaw, maxFee } = ContractTermsUtil.extractAmounts( - contractData, + contractData.contractTerms, p.choiceIndex, ); if (!available) { @@ -1696,16 +1678,16 @@ async function reselectCoinsTx( const res = await selectPayCoinsInTx(ctx.wex, tx, { restrictExchanges: { auditors: [], - exchanges: contractData.exchanges.map((ex) => ({ + exchanges: contractData.contractTerms.exchanges.map((ex) => ({ exchangeBaseUrl: ex.url, exchangePub: ex.master_pub, })), }, - restrictWireMethod: contractData.wire_method, + restrictWireMethod: contractData.contractTerms.wire_method, contractTermsAmount: Amounts.parseOrThrow(amountRaw), depositFeeLimit: Amounts.parseOrThrow(maxFee), prevPayCoins, - requiredMinimumAge: contractData.minimum_age, + requiredMinimumAge: contractData.contractTerms.minimum_age, }); switch (res.type) { @@ -1727,13 +1709,13 @@ async function reselectCoinsTx( if (p.choiceIndex === undefined) throw Error("assertion failed"); - if (contractData.version !== MerchantContractVersion.V1) + if (contractData.contractTerms.version !== MerchantContractVersion.V1) throw Error("assertion failed"); const res = await selectPayTokensInTx(tx, { proposalId: p.proposalId, choiceIndex: p.choiceIndex, - contractTerms: contractData, + contractTerms: contractData.contractTerms, }); switch (res.type) { @@ -1903,13 +1885,8 @@ async function checkPaymentByProposalId( } } } - const d = await expectProposalDownload(wex, proposal); - const contractData = d.contractData; - - const merchantSig = d.contractData.merchantSig; - if (!merchantSig) { - throw Error("BUG: proposal is in invalid state"); - } + const contractDl = await expectProposalDownload(wex, proposal); + const contractTerms = contractDl.contractTerms; proposalId = proposal.proposalId; @@ -1938,36 +1915,36 @@ async function checkPaymentByProposalId( purchase.purchaseStatus === PurchaseStatus.DialogProposed || purchase.purchaseStatus === PurchaseStatus.DialogShared ) { - if (contractData.version === MerchantContractVersion.V1) { + if (contractTerms.version === MerchantContractVersion.V1) { return { status: PreparePayResultType.ChoiceSelection, transactionId, - contractTerms: d.contractTermsRaw, - contractTermsHash: contractData.contractTermsHash, + contractTerms: contractTerms, + contractTermsHash: contractDl.contractTermsHash, talerUri, }; } - const instructedAmount = Amounts.parseOrThrow(contractData.amount); + const instructedAmount = Amounts.parseOrThrow(contractTerms.amount); // If not already paid, check if we could pay for it. const res = await selectPayCoins(wex, { restrictExchanges: { auditors: [], - exchanges: contractData.exchanges.map((ex) => ({ + exchanges: contractTerms.exchanges.map((ex) => ({ exchangeBaseUrl: ex.url, exchangePub: ex.master_pub, })), }, contractTermsAmount: instructedAmount, - depositFeeLimit: Amounts.parseOrThrow(contractData.max_fee), + depositFeeLimit: Amounts.parseOrThrow(contractTerms.max_fee), prevPayCoins: [], - requiredMinimumAge: contractData.minimum_age, - restrictWireMethod: contractData.wire_method, + requiredMinimumAge: contractTerms.minimum_age, + restrictWireMethod: contractTerms.wire_method, }); let coins: SelectedProspectiveCoin[] | undefined = undefined; - const allowedExchangeUrls = contractData.exchanges.map((x) => x.url); + const allowedExchangeUrls = contractTerms.exchanges.map((x) => x.url); switch (res.type) { case "failure": { @@ -1982,9 +1959,9 @@ async function checkPaymentByProposalId( }); return { status: PreparePayResultType.InsufficientBalance, - contractTerms: d.contractTermsRaw, + contractTerms, transactionId, - amountRaw: Amounts.stringify(contractData.amount), + amountRaw: Amounts.stringify(contractTerms.amount), scopes, talerUri, balanceDetails: res.insufficientBalanceDetails, @@ -2000,7 +1977,7 @@ async function checkPaymentByProposalId( assertUnreachable(res); } - const currency = Amounts.currencyOf(contractData.amount); + const currency = Amounts.currencyOf(contractTerms.amount); const totalCost = await getTotalPaymentCost(wex, currency, coins); logger.trace("costInfo", totalCost); logger.trace("coinsForPayment", res); @@ -2013,18 +1990,18 @@ async function checkPaymentByProposalId( return { status: PreparePayResultType.PaymentPossible, - contractTerms: d.contractTermsRaw, + contractTerms, transactionId, amountEffective: Amounts.stringify(totalCost), amountRaw: Amounts.stringify(instructedAmount), scopes, - contractTermsHash: d.contractData.contractTermsHash, + contractTermsHash: contractDl.contractTermsHash, talerUri, }; } const scopes = await wex.db.runAllStoresReadOnlyTx({}, async (tx) => { - let exchangeUrls = contractData.exchanges.map((x) => x.url); + let exchangeUrls = contractTerms.exchanges.map((x) => x.url); return await getScopeForAllExchanges(tx, exchangeUrls); }); @@ -2049,10 +2026,8 @@ async function checkPaymentByProposalId( await waitPaymentResult(wex, proposalId, sessionId); const download = await expectProposalDownload(wex, purchase); - const contractData = download.contractData; - const { available, amountRaw } = ContractTermsUtil.extractAmounts( - contractData, + download.contractTerms, purchase.choiceIndex, ); if (!available) { @@ -2061,8 +2036,8 @@ async function checkPaymentByProposalId( return { status: PreparePayResultType.AlreadyConfirmed, - contractTerms: download.contractTermsRaw, - contractTermsHash: download.contractData.contractTermsHash, + contractTerms, + contractTermsHash: download.contractTermsHash, paid: true, amountRaw: Amounts.stringify(amountRaw), amountEffective: purchase.payInfo @@ -2076,7 +2051,7 @@ async function checkPaymentByProposalId( const download = await expectProposalDownload(wex, purchase); const { available, amountRaw } = ContractTermsUtil.extractAmounts( - contractData, + contractTerms, purchase.choiceIndex, ); if (!available) { @@ -2085,8 +2060,8 @@ async function checkPaymentByProposalId( return { status: PreparePayResultType.AlreadyConfirmed, - contractTerms: download.contractTermsRaw, - contractTermsHash: download.contractData.contractTermsHash, + contractTerms, + contractTermsHash: download.contractTermsHash, paid: purchase.purchaseStatus === PurchaseStatus.FailedPaidByOther, amountRaw: Amounts.stringify(amountRaw), amountEffective: purchase.payInfo @@ -2101,7 +2076,7 @@ async function checkPaymentByProposalId( const download = await expectProposalDownload(wex, purchase); const { available, amountRaw } = ContractTermsUtil.extractAmounts( - contractData, + contractTerms, purchase.choiceIndex, ); if (!available) { @@ -2110,14 +2085,14 @@ async function checkPaymentByProposalId( return { status: PreparePayResultType.AlreadyConfirmed, - contractTerms: download.contractTermsRaw, - contractTermsHash: download.contractData.contractTermsHash, + contractTerms: contractTerms, + contractTermsHash: download.contractTermsHash, paid, amountRaw: Amounts.stringify(amountRaw), amountEffective: purchase.payInfo ? Amounts.stringify(purchase.payInfo.totalPayCost) : undefined, - ...(paid ? { nextUrl: contractData.order_id } : {}), + ...(paid ? { nextUrl: contractTerms.order_id } : {}), scopes, transactionId, talerUri, @@ -2137,7 +2112,7 @@ function isPurchasePaid(purchase: PurchaseRecord): boolean { export async function getContractTermsDetails( wex: WalletExecutionContext, proposalId: string, -): Promise<WalletContractData> { +): Promise<DownloadedContractData> { const proposal = await wex.db.runReadOnlyTx( { storeNames: ["purchases"] }, async (tx) => { @@ -2149,9 +2124,7 @@ export async function getContractTermsDetails( throw Error(`proposal with id ${proposalId} not found`); } - const d = await expectProposalDownload(wex, proposal); - - return d.contractData; + return await expectProposalDownload(wex, proposal); } /** @@ -2366,7 +2339,8 @@ export async function preparePayForTemplate( export async function generateDepositPermissions( wex: WalletExecutionContext, payCoinSel: DbCoinSelection, - contractData: WalletContractData, + contractData: MerchantContractTerms, + contractTermsHash: HashCodeString, walletData?: PayWalletData, ): Promise<CoinDepositPermission[]> { const depositPermissions: CoinDepositPermission[] = []; @@ -2403,7 +2377,7 @@ export async function generateDepositPermissions( const dp = await wex.cryptoApi.signDepositPermission({ coinPriv: coin.coinPriv, coinPub: coin.coinPub, - contractTermsHash: contractData.contractTermsHash, + contractTermsHash, denomPubHash: coin.denomPubHash, denomKeyType: denom.denomPub.cipher, denomSig: coin.denomSig, @@ -2530,7 +2504,7 @@ export async function getChoicesForPayment( const choices: ChoiceSelectionDetail[] = []; await wex.db.runAllStoresReadOnlyTx({}, async (tx) => { const tokenSels: SelectPayTokensResult[] = []; - const contractTerms: MerchantContractTerms = d.contractData; + const contractTerms: MerchantContractTerms = d.contractTerms; switch (contractTerms.version) { case undefined: case MerchantContractVersion.V0: @@ -2652,8 +2626,8 @@ export async function getChoicesForPayment( return { choices, - contractData: d.contractData, - ...(await calculateDefaultChoice(wex, choices, d.contractData)), + contractTerms: d.contractTerms, + ...(await calculateDefaultChoice(wex, choices, d.contractTerms)), }; } @@ -2828,9 +2802,9 @@ export async function confirmPay( logger.trace("confirmPay: purchase record does not exist yet"); - const contractData = d.contractData; + const contractTerms = d.contractTerms; const { available, amountRaw, maxFee } = ContractTermsUtil.extractAmounts( - contractData, + contractTerms, choiceIndex, ); if (!available) { @@ -2850,155 +2824,147 @@ export async function confirmPay( `recording payment on ${proposal.orderId} with session ID ${sessionId}`, ); - const transitionInfo = await wex.db.runAllStoresReadWriteTx( - {}, - async (tx) => { - const p = await tx.purchases.get(proposal.proposalId); - if (!p) { - return; - } + await wex.db.runAllStoresReadWriteTx({}, async (tx) => { + const p = await tx.purchases.get(proposal.proposalId); + if (!p) { + return; + } - let selectTokensResult: SelectPayTokensResult | undefined; + let selectTokensResult: SelectPayTokensResult | undefined; - if (contractData.version === MerchantContractVersion.V1) { - selectTokensResult = await selectPayTokensInTx(tx, { - proposalId, - choiceIndex: choiceIndex!, - contractTerms: contractData, - }); + if (contractTerms.version === MerchantContractVersion.V1) { + selectTokensResult = await selectPayTokensInTx(tx, { + proposalId, + choiceIndex: choiceIndex!, + contractTerms, + }); - switch (selectTokensResult.type) { - case "failure": { - logger.warn("not confirming payment, insufficient tokens"); - throw Error("insufficient tokens"); - } + switch (selectTokensResult.type) { + case "failure": { + logger.warn("not confirming payment, insufficient tokens"); + throw Error("insufficient tokens"); } - - logger.trace("token selection result", selectTokensResult); } - const selectCoinsResult = await selectPayCoinsInTx(wex, tx, { - restrictExchanges: { - auditors: [], - exchanges: contractData.exchanges.map((ex) => ({ - exchangeBaseUrl: ex.url, - exchangePub: ex.master_pub, - })), - }, - restrictWireMethod: contractData.wire_method, - contractTermsAmount: Amounts.parseOrThrow(amountRaw), - depositFeeLimit: Amounts.parseOrThrow(maxFee), - prevPayCoins: [], - requiredMinimumAge: contractData.minimum_age, - forcedSelection: forcedCoinSel, - }); + logger.trace("token selection result", selectTokensResult); + } - let coins: SelectedProspectiveCoin[] | undefined = undefined; + const selectCoinsResult = await selectPayCoinsInTx(wex, tx, { + restrictExchanges: { + auditors: [], + exchanges: contractTerms.exchanges.map((ex) => ({ + exchangeBaseUrl: ex.url, + exchangePub: ex.master_pub, + })), + }, + restrictWireMethod: contractTerms.wire_method, + contractTermsAmount: Amounts.parseOrThrow(amountRaw), + depositFeeLimit: Amounts.parseOrThrow(maxFee), + prevPayCoins: [], + requiredMinimumAge: contractTerms.minimum_age, + forcedSelection: forcedCoinSel, + }); - switch (selectCoinsResult.type) { - case "failure": { - // Should not happen, since checkPay should be called first - // FIXME: Actually, this should be handled gracefully, - // and the status should be stored in the DB. - logger.warn("not confirming payment, insufficient coins"); - throw Error("insufficient balance"); - } - case "prospective": { - coins = selectCoinsResult.result.prospectiveCoins; - break; - } - case "success": - coins = selectCoinsResult.coinSel.coins; - break; - default: - assertUnreachable(selectCoinsResult); + let coins: SelectedProspectiveCoin[] | undefined = undefined; + + switch (selectCoinsResult.type) { + case "failure": { + // Should not happen, since checkPay should be called first + // FIXME: Actually, this should be handled gracefully, + // and the status should be stored in the DB. + logger.warn("not confirming payment, insufficient coins"); + throw Error("insufficient balance"); + } + case "prospective": { + coins = selectCoinsResult.result.prospectiveCoins; + break; } + case "success": + coins = selectCoinsResult.coinSel.coins; + break; + default: + assertUnreachable(selectCoinsResult); + } - logger.trace("coin selection result", selectCoinsResult); + logger.trace("coin selection result", selectCoinsResult); - const payCostInfo = await getTotalPaymentCostInTx( - wex, - tx, - currency, - coins, - ); + const payCostInfo = await getTotalPaymentCostInTx(wex, tx, currency, coins); - p.choiceIndex = choiceIndex; - if ( - p.download && - choiceIndex !== undefined && - contractData.version === MerchantContractVersion.V1 - ) { - const amount = contractData.choices[choiceIndex].amount; - p.download.currency = Amounts.currencyOf(amount); - } + p.choiceIndex = choiceIndex; + if ( + p.download && + choiceIndex !== undefined && + contractTerms.version === MerchantContractVersion.V1 + ) { + const amount = contractTerms.choices[choiceIndex].amount; + p.download.currency = Amounts.currencyOf(amount); + } - const oldTxState = computePayMerchantTransactionState(p); - switch (p.purchaseStatus) { - case PurchaseStatus.DialogShared: - case PurchaseStatus.DialogProposed: - p.payInfo = { - totalPayCost: Amounts.stringify(payCostInfo), + const oldTxState = computePayMerchantTransactionState(p); + switch (p.purchaseStatus) { + case PurchaseStatus.DialogShared: + case PurchaseStatus.DialogProposed: + p.payInfo = { + totalPayCost: Amounts.stringify(payCostInfo), + }; + if (selectTokensResult?.type === "success") { + const tokens = selectTokensResult.tokens; + p.payInfo.payTokenSelection = { + tokenPubs: tokens.map((t) => t.tokenUsePub), }; - if (selectTokensResult?.type === "success") { - const tokens = selectTokensResult.tokens; - p.payInfo.payTokenSelection = { - tokenPubs: tokens.map((t) => t.tokenUsePub), - }; - } - if (selectCoinsResult.type === "success") { - setCoinSel(p, selectCoinsResult.coinSel); - } - p.lastSessionId = sessionId; - p.timestampAccept = timestampPreciseToDb(TalerPreciseTimestamp.now()); - p.purchaseStatus = PurchaseStatus.PendingPaying; - await tx.purchases.put(p); - await ctx.updateTransactionMeta(tx); - if (p.payInfo.payTokenSelection) { - await spendTokens(tx, { - tokenPubs: p.payInfo.payTokenSelection.tokenPubs, - transactionId: ctx.transactionId, - }); - } - if (p.payInfo.payCoinSelection) { - const sel = p.payInfo.payCoinSelection; - await spendCoins(wex, tx, { - transactionId: transactionId as TransactionIdStr, - coinPubs: sel.coinPubs, - contributions: sel.coinContributions.map((x) => - Amounts.parseOrThrow(x), - ), - refreshReason: RefreshReason.PayMerchant, - }); - } - break; - case PurchaseStatus.Done: - case PurchaseStatus.PendingPaying: - default: - break; - } - const newTxState = computePayMerchantTransactionState(p); - applyNotifyTransition(tx.notify, transactionId, { - oldTxState, - newTxState, - balanceEffect: BalanceEffect.Any, - }); - }, - ); + } + if (selectCoinsResult.type === "success") { + setCoinSel(p, selectCoinsResult.coinSel); + } + p.lastSessionId = sessionId; + p.timestampAccept = timestampPreciseToDb(TalerPreciseTimestamp.now()); + p.purchaseStatus = PurchaseStatus.PendingPaying; + await tx.purchases.put(p); + await ctx.updateTransactionMeta(tx); + if (p.payInfo.payTokenSelection) { + await spendTokens(tx, { + tokenPubs: p.payInfo.payTokenSelection.tokenPubs, + transactionId: ctx.transactionId, + }); + } + if (p.payInfo.payCoinSelection) { + const sel = p.payInfo.payCoinSelection; + await spendCoins(wex, tx, { + transactionId: transactionId as TransactionIdStr, + coinPubs: sel.coinPubs, + contributions: sel.coinContributions.map((x) => + Amounts.parseOrThrow(x), + ), + refreshReason: RefreshReason.PayMerchant, + }); + } + break; + case PurchaseStatus.Done: + case PurchaseStatus.PendingPaying: + default: + break; + } + const newTxState = computePayMerchantTransactionState(p); + applyNotifyTransition(tx.notify, transactionId, { + oldTxState, + newTxState, + balanceEffect: BalanceEffect.Any, + }); + }); // TODO: pre-generate slates based on choice priority! if ( - choiceIndex !== undefined && - contractData.version === MerchantContractVersion.V1 + choiceIndex != null && + contractTerms.version === MerchantContractVersion.V1 ) { - const choice = contractData.choices[choiceIndex]; + const choice = contractTerms.choices[choiceIndex]; for (let j = 0; j < choice.outputs.length; j++) { await generateSlate( wex, proposal, - contractData, + contractTerms, d.contractTermsRaw, - choiceIndex!, + choiceIndex, j, ); } @@ -3131,11 +3097,7 @@ async function processPurchasePay( const download = await expectProposalDownload(wex, purchase); if (purchase.shared) { - const paid = await checkIfOrderIsAlreadyPaid( - wex, - download.contractData, - false, - ); + const paid = await checkIfOrderIsAlreadyPaid(wex, download, false); if (paid) { await ctx.transition(async (p) => { @@ -3147,16 +3109,15 @@ async function processPurchasePay( type: TaskRunResultType.Error, errorDetail: makeErrorDetail(TalerErrorCode.WALLET_ORDER_ALREADY_PAID, { orderId: purchase.orderId, - fulfillmentUrl: download.contractData.fulfillmentUrl, + fulfillmentUrl: download.contractTerms.fulfillment_url, }), }; } } - const contractData = download.contractData; const choiceIndex = purchase.choiceIndex; const { available, amountRaw, maxFee } = ContractTermsUtil.extractAmounts( - contractData, + download.contractTerms, choiceIndex, ); if (!available) { @@ -3169,16 +3130,16 @@ async function processPurchasePay( const selectCoinsResult = await selectPayCoins(wex, { restrictExchanges: { auditors: [], - exchanges: contractData.exchanges.map((ex) => ({ + exchanges: download.contractTerms.exchanges.map((ex) => ({ exchangeBaseUrl: ex.url, exchangePub: ex.master_pub, })), }, - restrictWireMethod: contractData.wire_method, + restrictWireMethod: download.contractTerms.wire_method, contractTermsAmount: Amounts.parseOrThrow(amountRaw), depositFeeLimit: Amounts.parseOrThrow(maxFee), prevPayCoins: [], - requiredMinimumAge: contractData.minimum_age, + requiredMinimumAge: download.contractTerms.minimum_age, }); switch (selectCoinsResult.type) { case "failure": { @@ -3270,14 +3231,14 @@ async function processPurchasePay( if (!purchase.merchantPaySig) { const payUrl = new URL( - `orders/${download.contractData.order_id}/pay`, - download.contractData.merchant_base_url, + `orders/${download.contractTerms.order_id}/pay`, + download.contractTerms.merchant_base_url, ); let slates: SlateRecord[] | undefined = undefined; let wallet_data: PayWalletData | undefined = undefined; if ( - contractData.version === MerchantContractVersion.V1 && + download.contractTerms.version === MerchantContractVersion.V1 && purchase.choiceIndex !== undefined ) { const index = purchase.choiceIndex; @@ -3300,9 +3261,11 @@ async function processPurchasePay( }, ); - if (slates.length !== contractData.choices[index].outputs.length) { + if ( + slates.length !== download.contractTerms.choices[index].outputs.length + ) { throw Error( - `number of slates ${slates.length} doesn't match number of outputs ${contractData.choices[index].outputs.length}`, + `number of slates ${slates.length} doesn't match number of outputs ${download.contractTerms.choices[index].outputs.length}`, ); } } @@ -3312,7 +3275,8 @@ async function processPurchasePay( depositPermissions = await generateDepositPermissions( wex, payInfo.payCoinSelection, - download.contractData, + download.contractTerms, + download.contractTermsHash, wallet_data, ); @@ -3326,7 +3290,7 @@ async function processPurchasePay( reqBody.tokens = await generateTokenSigs( wex, proposalId, - contractData.contractTermsHash, + download.contractTermsHash, encodeCrock(hashPayWalletData(wallet_data)), payInfo.payTokenSelection.tokenPubs, ); @@ -3425,9 +3389,9 @@ async function processPurchasePay( logger.trace("got success from pay URL", merchantResp); - const merchantPub = download.contractData.merchant_pub; + const merchantPub = download.contractTerms.merchant_pub; const { valid } = await wex.cryptoApi.isValidPaymentSignature({ - contractHash: download.contractData.contractTermsHash, + contractHash: download.contractTermsHash, merchantPub, sig: merchantResp.sig, }); @@ -3486,12 +3450,12 @@ async function processPurchasePay( await storeFirstPaySuccess(wex, proposalId, sessionId, merchantResp); } else { const payAgainUrl = new URL( - `orders/${download.contractData.order_id}/paid`, - download.contractData.merchant_base_url, + `orders/${download.contractTerms.order_id}/paid`, + download.contractTerms.merchant_base_url, ); const reqBody = { sig: purchase.merchantPaySig, - h_contract: download.contractData.contractTermsHash, + h_contract: download.contractTermsHash, session_id: sessionId ?? "", }; logger.trace(`/paid request body: ${j2s(reqBody)}`); @@ -4051,12 +4015,12 @@ export async function sharePayment( async function checkIfOrderIsAlreadyPaid( wex: WalletExecutionContext, - contract: WalletContractData, + contract: DownloadedContractData, doLongPolling: boolean, ) { const requestUrl = new URL( - `orders/${contract.order_id}`, - contract.merchant_base_url, + `orders/${contract.contractTerms.order_id}`, + contract.contractTerms.merchant_base_url, ); requestUrl.searchParams.set("h_contract", contract.contractTermsHash); @@ -4094,11 +4058,7 @@ async function processPurchaseDialogShared( const ctx = new PayMerchantTransactionContext(wex, proposalId); - const paid = await checkIfOrderIsAlreadyPaid( - wex, - download.contractData, - true, - ); + const paid = await checkIfOrderIsAlreadyPaid(wex, download, true); if (paid) { await ctx.transition(async (p) => { @@ -4121,9 +4081,8 @@ async function processPurchaseAutoRefund( logger.trace(`processing auto-refund for proposal ${proposalId}`); const download = await expectProposalDownload(wex, purchase); - const contractData = download.contractData; const { available, amountRaw } = ContractTermsUtil.extractAmounts( - contractData, + download.contractTerms, purchase.choiceIndex, ); if (!available) { @@ -4179,13 +4138,10 @@ async function processPurchaseAutoRefund( } const requestUrl = new URL( - `orders/${download.contractData.order_id}`, - download.contractData.merchant_base_url, - ); - requestUrl.searchParams.set( - "h_contract", - download.contractData.contractTermsHash, + `orders/${download.contractTerms.order_id}`, + download.contractTerms.merchant_base_url, ); + requestUrl.searchParams.set("h_contract", download.contractTermsHash); requestUrl.searchParams.set("refund", Amounts.stringify(totalKnownRefund)); @@ -4225,8 +4181,8 @@ async function processPurchaseAbortingRefund( logger.trace(`processing aborting-refund for proposal ${proposalId}`); const requestUrl = new URL( - `orders/${download.contractData.order_id}/abort`, - download.contractData.merchant_base_url, + `orders/${download.contractTerms.order_id}/abort`, + download.contractTerms.merchant_base_url, ); const abortingCoins: AbortingCoin[] = []; @@ -4250,7 +4206,7 @@ async function processPurchaseAbortingRefund( }); const abortReq: AbortRequest = { - h_contract: download.contractData.contractTermsHash, + h_contract: download.contractTermsHash, coins: abortingCoins, }; @@ -4308,7 +4264,7 @@ async function processPurchaseAbortingRefund( rtransaction_id: 0, execution_time: AbsoluteTime.toProtocolTimestamp( AbsoluteTime.addDuration( - AbsoluteTime.fromProtocolTimestamp(download.contractData.timestamp), + AbsoluteTime.fromProtocolTimestamp(download.contractTerms.timestamp), Duration.fromSpec({ seconds: 1 }), ), ), @@ -4327,13 +4283,10 @@ async function processPurchaseQueryRefund( const download = await expectProposalDownload(wex, purchase); const requestUrl = new URL( - `orders/${download.contractData.order_id}`, - download.contractData.merchant_base_url, - ); - requestUrl.searchParams.set( - "h_contract", - download.contractData.contractTermsHash, + `orders/${download.contractTerms.order_id}`, + download.contractTerms.merchant_base_url, ); + requestUrl.searchParams.set("h_contract", download.contractTermsHash); const resp = await cancelableFetch(wex, requestUrl); const orderStatus = await readSuccessResponseJsonOrThrow( @@ -4377,8 +4330,8 @@ async function processPurchaseAcceptRefund( const download = await expectProposalDownload(wex, purchase); const requestUrl = new URL( - `orders/${download.contractData.order_id}/refund`, - download.contractData.merchant_base_url, + `orders/${download.contractTerms.order_id}/refund`, + download.contractTerms.merchant_base_url, ); logger.trace(`making refund request to ${requestUrl.href}`); @@ -4386,7 +4339,7 @@ async function processPurchaseAcceptRefund( const request = await cancelableFetch(wex, requestUrl, { method: "POST", body: { - h_contract: download.contractData.contractTermsHash, + h_contract: download.contractTermsHash, }, }); @@ -4536,9 +4489,8 @@ async function storeRefunds( const now = TalerPreciseTimestamp.now(); const download = await expectProposalDownload(wex, purchase); - const contractData = download.contractData; const { available, amountRaw } = ContractTermsUtil.extractAmounts( - contractData, + download.contractTerms, purchase.choiceIndex, ); if (!available) { diff --git a/packages/taler-wallet-core/src/wallet-api-types.ts b/packages/taler-wallet-core/src/wallet-api-types.ts @@ -170,7 +170,7 @@ import { UserAttentionsResponse, ValidateIbanRequest, ValidateIbanResponse, - WalletContractData, + DownloadedContractData, WalletCoreVersion, WithdrawTestBalanceRequest, WithdrawUriInfoResponse, @@ -242,18 +242,10 @@ export enum WalletApiOperation { DumpCoins = "dumpCoins", SetCoinSuspended = "setCoinSuspended", ForceRefresh = "forceRefresh", - ExportBackup = "exportBackup", - AddBackupProvider = "addBackupProvider", - RemoveBackupProvider = "removeBackupProvider", - RunBackupCycle = "runBackupCycle", - ExportBackupRecovery = "exportBackupRecovery", - ImportBackupRecovery = "importBackupRecovery", - GetBackupInfo = "getBackupInfo", CheckDeposit = "checkDeposit", GetVersion = "getVersion", GenerateDepositGroupTxId = "generateDepositGroupTxId", CreateDepositGroup = "createDepositGroup", - SetWalletDeviceId = "setWalletDeviceId", ImportDb = "importDb", ExportDb = "exportDb", ExportDbToFile = "exportDbToFile", @@ -272,10 +264,6 @@ export enum WalletApiOperation { ApplyDevExperiment = "applyDevExperiment", ValidateIban = "validateIban", GetCurrencySpecification = "getCurrencySpecification", - ListStoredBackups = "listStoredBackups", - CreateStoredBackup = "createStoredBackup", - DeleteStoredBackup = "deleteStoredBackup", - RecoverStoredBackup = "recoverStoredBackup", UpdateExchangeEntry = "updateExchangeEntry", PrepareWithdrawExchange = "prepareWithdrawExchange", GetExchangeResources = "getExchangeResources", @@ -288,12 +276,33 @@ export enum WalletApiOperation { RemoveGlobalCurrencyAuditor = "removeGlobalCurrencyAuditor", ListAssociatedRefreshes = "listAssociatedRefreshes", Shutdown = "shutdown", - HintNetworkAvailability = "hintNetworkAvailability", CanonicalizeBaseUrl = "canonicalizeBaseUrl", GetDepositWireTypes = "getDepositWireTypes", GetDepositWireTypesForCurrency = "getDepositWireTypesForCurrency", GetQrCodesForPayto = "getQrCodesForPayto", + StartExchangeWalletKyc = "startExchangeWalletKyc", GetBankingChoicesForPayto = "getBankingChoicesForPayto", + + // Stored backups + + ListStoredBackups = "listStoredBackups", + CreateStoredBackup = "createStoredBackup", + DeleteStoredBackup = "deleteStoredBackup", + RecoverStoredBackup = "recoverStoredBackup", + + // legacy backup functionality + + SetWalletDeviceId = "setWalletDeviceId", + ExportBackup = "exportBackup", + AddBackupProvider = "addBackupProvider", + RemoveBackupProvider = "removeBackupProvider", + RunBackupCycle = "runBackupCycle", + ExportBackupRecovery = "exportBackupRecovery", + ImportBackupRecovery = "importBackupRecovery", + GetBackupInfo = "getBackupInfo", + + // Testing + TestingWaitTransactionsFinal = "testingWaitTransactionsFinal", TestingWaitRefreshesFinal = "testingWaitRefreshesFinal", TestingWaitTransactionState = "testingWaitTransactionState", @@ -305,9 +314,12 @@ export enum WalletApiOperation { TestingPing = "testingPing", TestingGetReserveHistory = "testingGetReserveHistory", TestingResetAllRetries = "testingResetAllRetries", - StartExchangeWalletKyc = "startExchangeWalletKyc", TestingWaitExchangeWalletKyc = "testingWaitWalletKyc", TestingPlanMigrateExchangeBaseUrl = "testingPlanMigrateExchangeBaseUrl", + + // Hints + + HintNetworkAvailability = "hintNetworkAvailability", HintApplicationResumed = "hintApplicationResumed", /** @@ -634,7 +646,7 @@ export type PreparePayForTemplateOp = { export type GetContractTermsDetailsOp = { op: WalletApiOperation.GetContractTermsDetails; request: GetContractTermsDetailsRequest; - response: WalletContractData; + response: DownloadedContractData; }; /** diff --git a/packages/taler-wallet-core/src/wallet.ts b/packages/taler-wallet-core/src/wallet.ts @@ -133,7 +133,7 @@ import { ValidateIbanRequest, ValidateIbanResponse, WalletBankAccountInfo, - WalletContractData, + DownloadedContractData, WalletCoreVersion, WalletNotification, WalletRunConfig, @@ -1158,7 +1158,7 @@ async function handleGetExchangeTos( async function handleGetContractTermsDetails( wex: WalletExecutionContext, req: GetContractTermsDetailsRequest, -): Promise<WalletContractData> { +): Promise<DownloadedContractData> { const parsedTx = parseTransactionIdentifier(req.transactionId); if (parsedTx?.tag !== TransactionType.Payment) { throw Error("transactionId is not a payment transaction"); @@ -2574,6 +2574,7 @@ function applyRunConfigDefaults(wcp?: PartialWalletRunConfig): WalletRunConfig { }, features: { allowHttp: true, + enableV1Contracts: false, }, testing: { devModeActive: wcp?.testing?.devModeActive ?? false, diff --git a/packages/taler-wallet-webextension/src/hooks/useSettings.ts b/packages/taler-wallet-webextension/src/hooks/useSettings.ts @@ -14,22 +14,13 @@ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ -import { buildStorageKey, useLocalStorage } from "@gnu-taler/web-util/browser"; -import { Settings, defaultSettings } from "../platform/api.js"; import { Codec, buildCodecForObject, codecForBoolean, } from "@gnu-taler/taler-util"; - -function parse_json_or_undefined<T>(str: string | undefined): T | undefined { - if (str === undefined) return undefined; - try { - return JSON.parse(str); - } catch { - return undefined; - } -} +import { buildStorageKey, useLocalStorage } from "@gnu-taler/web-util/browser"; +import { Settings, defaultSettings } from "../platform/api.js"; export const codecForSettings = (): Codec<Settings> => buildCodecForObject<Settings>() @@ -46,6 +37,7 @@ export const codecForSettings = (): Codec<Settings> => .property("showExchangeManagement", codecForBoolean()) .property("selectTosFormat", codecForBoolean()) .property("showWalletActivity", codecForBoolean()) + .property("walletEnableV1Contracts", codecForBoolean()) .build("Settings"); const SETTINGS_KEY = buildStorageKey("wallet-settings", codecForSettings()); diff --git a/packages/taler-wallet-webextension/src/platform/api.ts b/packages/taler-wallet-webextension/src/platform/api.ts @@ -142,6 +142,7 @@ export const defaultSettings: Settings = { walletAllowHttp: false, selectTosFormat: false, showWalletActivity: false, + walletEnableV1Contracts: true, }; /**