taler-typescript-core

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

commit 7fe0c88e35e3cb9e3d8619f9334c64b71aa1b270
parent 062357e318d578aa024595cd5bd384fbfca520fa
Author: Florian Dold <florian@dold.me>
Date:   Tue, 19 Aug 2025 21:37:40 +0200

wallet-core,cli: feature flag for v1 contracts, cli support for v1 contracts, remove deprecated requests

Diffstat:
Mpackages/taler-harness/src/harness/environments.ts | 201++++++++++++++++++++++++++++++++++++++++++++++---------------------------------
Mpackages/taler-harness/src/integrationtests/test-wallet-tokens.ts | 214++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------------
Mpackages/taler-util/src/types-taler-wallet.ts | 10----------
Mpackages/taler-wallet-cli/src/index.ts | 82+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------------
Mpackages/taler-wallet-core/src/pay-merchant.ts | 70++++++++++++++++++++++++++++++++++++++--------------------------------
Mpackages/taler-wallet-core/src/wallet-api-types.ts | 31-------------------------------
Mpackages/taler-wallet-core/src/wallet.ts | 24+-----------------------
7 files changed, 360 insertions(+), 272 deletions(-)

diff --git a/packages/taler-harness/src/harness/environments.ts b/packages/taler-harness/src/harness/environments.ts @@ -158,6 +158,8 @@ export interface EnvOptions { */ forceLibeufin?: boolean; + walletConfig?: PartialWalletRunConfig; + additionalExchangeConfig?(e: ExchangeService): void; additionalMerchantConfig?(m: MerchantService): void; additionalBankConfig?(b: BankService): void; @@ -291,32 +293,40 @@ export async function useSharedTestkudosEnvironment(t: GlobalTestState) { await merchant.pingUntilAvailable(); if (!prevSetupDone) { - const { accessToken: adminAccessToken } = await merchant.addInstanceWithWireAccount({ - id: "admin", - name: "Default Instance", - paytoUris: [getTestHarnessPaytoForLabel("merchant-default")], - defaultWireTransferDelay: Duration.toTalerProtocolDuration( - Duration.fromSpec({ minutes: 1 }), - ), - }); - - await merchant.addInstanceWithWireAccount({ - id: "minst1", - name: "minst1", - paytoUris: [getTestHarnessPaytoForLabel("minst1")], - defaultWireTransferDelay: Duration.toTalerProtocolDuration( - Duration.fromSpec({ minutes: 1 }), - ), - }, { adminAccessToken }); + const { accessToken: adminAccessToken } = + await merchant.addInstanceWithWireAccount({ + id: "admin", + name: "Default Instance", + paytoUris: [getTestHarnessPaytoForLabel("merchant-default")], + defaultWireTransferDelay: Duration.toTalerProtocolDuration( + Duration.fromSpec({ minutes: 1 }), + ), + }); + + await merchant.addInstanceWithWireAccount( + { + id: "minst1", + name: "minst1", + paytoUris: [getTestHarnessPaytoForLabel("minst1")], + defaultWireTransferDelay: Duration.toTalerProtocolDuration( + Duration.fromSpec({ minutes: 1 }), + ), + }, + { adminAccessToken }, + ); } const merchApi = new TalerMerchantInstanceHttpClient( - merchant.makeInstanceBaseUrl() - ) + merchant.makeInstanceBaseUrl(), + ); const { access_token: merchantAdminAccessToken } = succeedOrThrow( - await merchApi.createAccessToken("admin", MERCHANT_DEFAULT_AUTH.password, MERCHANT_DEFAULT_LOGIN_SCOPE) - ) - return { merchantAdminAccessToken } + await merchApi.createAccessToken( + "admin", + MERCHANT_DEFAULT_AUTH.password, + MERCHANT_DEFAULT_LOGIN_SCOPE, + ), + ); + return { merchantAdminAccessToken }; }; await bankStart(); @@ -328,7 +338,7 @@ export async function useSharedTestkudosEnvironment(t: GlobalTestState) { walletStartProm, ]); - const merchantAdminAccessToken = res[1].merchantAdminAccessToken + const merchantAdminAccessToken = res[1].merchantAdminAccessToken; const walletClient = res[3].walletClient; const walletService = res[3].walletService; @@ -432,23 +442,27 @@ export async function createSimpleTestkudosEnvironmentV2( await merchant.start(); await merchant.pingUntilAvailable(); - const { accessToken: adminAccessToken } = await merchant.addInstanceWithWireAccount({ - id: "admin", - name: "Default Instance", - paytoUris: [getTestHarnessPaytoForLabel("merchant-default")], - defaultWireTransferDelay: Duration.toTalerProtocolDuration( - Duration.fromSpec({ minutes: 1 }), - ), - }); + const { accessToken: adminAccessToken } = + await merchant.addInstanceWithWireAccount({ + id: "admin", + name: "Default Instance", + paytoUris: [getTestHarnessPaytoForLabel("merchant-default")], + defaultWireTransferDelay: Duration.toTalerProtocolDuration( + Duration.fromSpec({ minutes: 1 }), + ), + }); - await merchant.addInstanceWithWireAccount({ - id: "minst1", - name: "minst1", - paytoUris: [getTestHarnessPaytoForLabel("minst1")], - defaultWireTransferDelay: Duration.toTalerProtocolDuration( - Duration.fromSpec({ minutes: 1 }), - ), - }, { adminAccessToken }); + await merchant.addInstanceWithWireAccount( + { + id: "minst1", + name: "minst1", + paytoUris: [getTestHarnessPaytoForLabel("minst1")], + defaultWireTransferDelay: Duration.toTalerProtocolDuration( + Duration.fromSpec({ minutes: 1 }), + ), + }, + { adminAccessToken }, + ); const { walletClient, walletService } = await createWalletDaemonWithClient( t, @@ -589,23 +603,27 @@ export async function createSimpleTestkudosEnvironmentV3( await merchant.start(); await merchant.pingUntilAvailable(); - const { accessToken: merchantAdminAccessToken } = await merchant.addInstanceWithWireAccount({ - id: "admin", - name: "Default Instance", - paytoUris: [getTestHarnessPaytoForLabel("merchant-default")], - defaultWireTransferDelay: Duration.toTalerProtocolDuration( - Duration.fromSpec({ minutes: 1 }), - ), - }); + const { accessToken: merchantAdminAccessToken } = + await merchant.addInstanceWithWireAccount({ + id: "admin", + name: "Default Instance", + paytoUris: [getTestHarnessPaytoForLabel("merchant-default")], + defaultWireTransferDelay: Duration.toTalerProtocolDuration( + Duration.fromSpec({ minutes: 1 }), + ), + }); - await merchant.addInstanceWithWireAccount({ - id: "minst1", - name: "minst1", - paytoUris: [getTestHarnessPaytoForLabel("minst1")], - defaultWireTransferDelay: Duration.toTalerProtocolDuration( - Duration.fromSpec({ minutes: 1 }), - ), - }, { adminAccessToken: merchantAdminAccessToken }); + await merchant.addInstanceWithWireAccount( + { + id: "minst1", + name: "minst1", + paytoUris: [getTestHarnessPaytoForLabel("minst1")], + defaultWireTransferDelay: Duration.toTalerProtocolDuration( + Duration.fromSpec({ minutes: 1 }), + ), + }, + { adminAccessToken: merchantAdminAccessToken }, + ); const { walletClient, walletService } = await createWalletDaemonWithClient( t, @@ -613,6 +631,7 @@ export async function createSimpleTestkudosEnvironmentV3( name: "wallet", persistent: true, emitObservabilityEvents: !!opts.walletTestObservability, + config: opts.walletConfig, }, ); @@ -772,17 +791,21 @@ export async function createFaultInjectedMerchantTestkudosEnvironment( await merchant.start(); await merchant.pingUntilAvailable(); - const { accessToken: adminAccessToken } = await merchant.addInstanceWithWireAccount({ - id: "admin", - name: "Default Instance", - paytoUris: [getTestHarnessPaytoForLabel("merchant-default")], - }); + const { accessToken: adminAccessToken } = + await merchant.addInstanceWithWireAccount({ + id: "admin", + name: "Default Instance", + paytoUris: [getTestHarnessPaytoForLabel("merchant-default")], + }); - await merchant.addInstanceWithWireAccount({ - id: "minst1", - name: "minst1", - paytoUris: [getTestHarnessPaytoForLabel("minst1")], - }, { adminAccessToken }); + await merchant.addInstanceWithWireAccount( + { + id: "minst1", + name: "minst1", + paytoUris: [getTestHarnessPaytoForLabel("minst1")], + }, + { adminAccessToken }, + ); console.log("setup done!"); @@ -953,7 +976,10 @@ export async function makeTestPaymentV2( ); let orderStatus = succeedOrThrow( - await merchantClient.getOrderDetails(merchantAdminAccessToken, orderResp.order_id), + await merchantClient.getOrderDetails( + merchantAdminAccessToken, + orderResp.order_id, + ), ); t.assertTrue(orderStatus.order_status === "unpaid"); @@ -980,7 +1006,10 @@ export async function makeTestPaymentV2( // Check if payment was successful. orderStatus = succeedOrThrow( - await merchantClient.getOrderDetails(merchantAdminAccessToken, orderResp.order_id), + await merchantClient.getOrderDetails( + merchantAdminAccessToken, + orderResp.order_id, + ), ); t.assertDeepEqual(orderStatus.order_status, "paid"); @@ -1247,23 +1276,27 @@ export async function createKycTestkudosEnvironment( await merchant.start(); - const { accessToken: adminAccessToken } = await merchant.addInstanceWithWireAccount({ - id: "admin", - name: "Default Instance", - paytoUris: [getTestHarnessPaytoForLabel("merchant-default")], - defaultWireTransferDelay: Duration.toTalerProtocolDuration( - Duration.fromSpec({ minutes: 1 }), - ), - }); + const { accessToken: adminAccessToken } = + await merchant.addInstanceWithWireAccount({ + id: "admin", + name: "Default Instance", + paytoUris: [getTestHarnessPaytoForLabel("merchant-default")], + defaultWireTransferDelay: Duration.toTalerProtocolDuration( + Duration.fromSpec({ minutes: 1 }), + ), + }); - await merchant.addInstanceWithWireAccount({ - id: "minst1", - name: "minst1", - paytoUris: [getTestHarnessPaytoForLabel("minst1")], - defaultWireTransferDelay: Duration.toTalerProtocolDuration( - Duration.fromSpec({ minutes: 1 }), - ), - }, { adminAccessToken }); + await merchant.addInstanceWithWireAccount( + { + id: "minst1", + name: "minst1", + paytoUris: [getTestHarnessPaytoForLabel("minst1")], + defaultWireTransferDelay: Duration.toTalerProtocolDuration( + Duration.fromSpec({ minutes: 1 }), + ), + }, + { adminAccessToken }, + ); const exchangeBankAccount: HarnessExchangeBankAccount = { wireGatewayAuth: { @@ -1333,7 +1366,7 @@ export async function registerHarnessBankTestUser( const username = "user-" + encodeCrock(getRandomBytes(10)).toLowerCase(); const password = "pw-" + encodeCrock(getRandomBytes(10)).toLowerCase(); const createRes = succeedOrThrow( - await bankClient.createAccount(/**/undefined, { + await bankClient.createAccount(/**/ undefined, { name: username, username, password, diff --git a/packages/taler-harness/src/integrationtests/test-wallet-tokens.ts b/packages/taler-harness/src/integrationtests/test-wallet-tokens.ts @@ -18,12 +18,6 @@ * Imports. */ import { - createSimpleTestkudosEnvironmentV3, - withdrawViaBankV3, -} from "harness/environments.js"; -import { GlobalTestState } from "../harness/harness.js"; -import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; -import { AbsoluteTime, ChoiceSelectionDetailType, Duration, @@ -37,11 +31,33 @@ import { TokenAvailabilityHint, TokenFamilyKind, } from "@gnu-taler/taler-util"; +import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; +import { + createSimpleTestkudosEnvironmentV3, + withdrawViaBankV3, +} from "harness/environments.js"; +import { defaultCoinConfig } from "../harness/denomStructures.js"; +import { GlobalTestState } from "../harness/harness.js"; import { logger } from "./test-tops-challenger-twice.js"; export async function runWalletTokensTest(t: GlobalTestState) { - let { bankClient, exchange, merchant, walletClient, merchantAdminAccessToken } = - await createSimpleTestkudosEnvironmentV3(t); + let { + bankClient, + exchange, + merchant, + walletClient, + merchantAdminAccessToken, + } = await createSimpleTestkudosEnvironmentV3( + t, + defaultCoinConfig.map((x) => x("TESTKUDOS")), + { + walletConfig: { + features: { + enableV1Contracts: true, + }, + }, + }, + ); const merchantApi = new TalerMerchantInstanceHttpClient( merchant.makeInstanceBaseUrl(), @@ -67,14 +83,14 @@ export async function runWalletTokensTest(t: GlobalTestState) { valid_before: AbsoluteTime.toProtocolTimestamp( AbsoluteTime.addDuration( AbsoluteTime.now(), - Duration.fromSpec({years: 1}), + Duration.fromSpec({ years: 1 }), ), ), duration: Duration.toTalerProtocolDuration( - Duration.fromSpec({days: 90}), + Duration.fromSpec({ days: 90 }), ), validity_granularity: Duration.toTalerProtocolDuration( - Duration.fromSpec({days: 1}), + Duration.fromSpec({ days: 1 }), ), }), ); @@ -90,14 +106,14 @@ export async function runWalletTokensTest(t: GlobalTestState) { valid_before: AbsoluteTime.toProtocolTimestamp( AbsoluteTime.addDuration( AbsoluteTime.now(), - Duration.fromSpec({years: 1}), + Duration.fromSpec({ years: 1 }), ), ), duration: Duration.toTalerProtocolDuration( - Duration.fromSpec({days: 90}), + Duration.fromSpec({ days: 90 }), ), validity_granularity: Duration.toTalerProtocolDuration( - Duration.fromSpec({days: 1}), + Duration.fromSpec({ days: 1 }), ), }), ); @@ -109,24 +125,28 @@ export async function runWalletTokensTest(t: GlobalTestState) { pay_deadline: AbsoluteTime.toProtocolTimestamp( AbsoluteTime.addDuration( AbsoluteTime.now(), - Duration.fromSpec({days: 1}), + Duration.fromSpec({ days: 1 }), ), ), choices: [ { amount: "TESTKUDOS:2", inputs: [], - outputs: [{ - type: OrderOutputType.Token, - token_family_slug: "test_discount", - }], + outputs: [ + { + type: OrderOutputType.Token, + token_family_slug: "test_discount", + }, + ], }, { amount: "TESTKUDOS:1", - inputs: [{ - type: OrderInputType.Token, - token_family_slug: "test_discount", - }], + inputs: [ + { + type: OrderInputType.Token, + token_family_slug: "test_discount", + }, + ], outputs: [], }, ], @@ -139,34 +159,40 @@ export async function runWalletTokensTest(t: GlobalTestState) { pay_deadline: AbsoluteTime.toProtocolTimestamp( AbsoluteTime.addDuration( AbsoluteTime.now(), - Duration.fromSpec({days: 1}), + Duration.fromSpec({ days: 1 }), ), ), choices: [ { amount: "TESTKUDOS:10", inputs: [], - outputs: [{ - type: OrderOutputType.Token, - token_family_slug: "test_subscription", - count: 1, - }], + outputs: [ + { + type: OrderOutputType.Token, + token_family_slug: "test_subscription", + count: 1, + }, + ], }, { amount: "TESTKUDOS:0", - inputs: [{ - type: OrderInputType.Token, - token_family_slug: "test_subscription", - count: 1, - }], - outputs: [{ - type: OrderOutputType.Token, - token_family_slug: "test_subscription", - count: 1, - }], + inputs: [ + { + type: OrderInputType.Token, + token_family_slug: "test_subscription", + count: 1, + }, + ], + outputs: [ + { + type: OrderOutputType.Token, + token_family_slug: "test_subscription", + count: 1, + }, + ], }, ], - } + }; { logger.info("Payment with discount token output..."); @@ -179,7 +205,10 @@ export async function runWalletTokensTest(t: GlobalTestState) { ); let orderStatus = succeedOrThrow( - await merchantApi.getOrderDetails(merchantAdminAccessToken, orderResp.order_id), + await merchantApi.getOrderDetails( + merchantAdminAccessToken, + orderResp.order_id, + ), ); t.assertTrue(orderStatus.order_status === "unpaid"); @@ -221,7 +250,10 @@ export async function runWalletTokensTest(t: GlobalTestState) { ); let orderStatus = succeedOrThrow( - await merchantApi.getOrderDetails(merchantAdminAccessToken, orderResp.order_id), + await merchantApi.getOrderDetails( + merchantAdminAccessToken, + orderResp.order_id, + ), ); t.assertTrue(orderStatus.order_status === "unpaid"); @@ -233,7 +265,7 @@ export async function runWalletTokensTest(t: GlobalTestState) { WalletApiOperation.PreparePayForUri, { talerPayUri, - } + }, ); t.assertTrue( @@ -241,16 +273,20 @@ export async function runWalletTokensTest(t: GlobalTestState) { ); { - const choicesRes = await walletClient.call(WalletApiOperation.GetChoicesForPayment, { - transactionId: preparePayResult.transactionId, - }); + const choicesRes = await walletClient.call( + WalletApiOperation.GetChoicesForPayment, + { + transactionId: preparePayResult.transactionId, + }, + ); t.assertTrue(choicesRes.defaultChoiceIndex === 1); t.assertTrue(choicesRes.automaticExecution === false); t.assertTrue(choicesRes.automaticExecutableIndex === undefined); - t.assertTrue(choicesRes.choices[choiceIndex].status === - ChoiceSelectionDetailType.PaymentPossible, + t.assertTrue( + choicesRes.choices[choiceIndex].status === + ChoiceSelectionDetailType.PaymentPossible, ); const tokenDetails = choicesRes.choices[choiceIndex].tokenDetails; @@ -286,7 +322,10 @@ export async function runWalletTokensTest(t: GlobalTestState) { ); let orderStatus = succeedOrThrow( - await merchantApi.getOrderDetails(merchantAdminAccessToken, orderResp.order_id), + await merchantApi.getOrderDetails( + merchantAdminAccessToken, + orderResp.order_id, + ), ); t.assertTrue(orderStatus.order_status === "unpaid"); @@ -306,16 +345,20 @@ export async function runWalletTokensTest(t: GlobalTestState) { ); { - const choicesRes = await walletClient.call(WalletApiOperation.GetChoicesForPayment, { - transactionId: preparePayResult.transactionId, - }); + const choicesRes = await walletClient.call( + WalletApiOperation.GetChoicesForPayment, + { + transactionId: preparePayResult.transactionId, + }, + ); t.assertTrue(choicesRes.defaultChoiceIndex === 0); t.assertTrue(choicesRes.automaticExecution === false); t.assertTrue(choicesRes.automaticExecutableIndex === undefined); - t.assertTrue(choicesRes.choices[choiceIndex].status === - ChoiceSelectionDetailType.InsufficientBalance, + t.assertTrue( + choicesRes.choices[choiceIndex].status === + ChoiceSelectionDetailType.InsufficientBalance, ); const tokenDetails = choicesRes.choices[choiceIndex].tokenDetails; @@ -325,8 +368,10 @@ export async function runWalletTokensTest(t: GlobalTestState) { for (const tf in tokenDetails?.perTokenFamily ?? t.fail()) { t.assertTrue(tokenDetails?.perTokenFamily[tf].available === 0); t.assertTrue(tokenDetails?.perTokenFamily[tf].requested === 1); - t.assertTrue(tokenDetails?.perTokenFamily[tf].causeHint === - TokenAvailabilityHint.WalletTokensAvailableInsufficient); + t.assertTrue( + tokenDetails?.perTokenFamily[tf].causeHint === + TokenAvailabilityHint.WalletTokensAvailableInsufficient, + ); break; } } @@ -347,7 +392,9 @@ export async function runWalletTokensTest(t: GlobalTestState) { } { - logger.info("Payment with subscription token input and output, insufficient balance..."); + logger.info( + "Payment with subscription token input and output, insufficient balance...", + ); const choiceIndex = 1; const orderResp = succeedOrThrow( @@ -357,7 +404,10 @@ export async function runWalletTokensTest(t: GlobalTestState) { ); let orderStatus = succeedOrThrow( - await merchantApi.getOrderDetails(merchantAdminAccessToken, orderResp.order_id), + await merchantApi.getOrderDetails( + merchantAdminAccessToken, + orderResp.order_id, + ), ); t.assertTrue(orderStatus.order_status === "unpaid"); @@ -369,7 +419,7 @@ export async function runWalletTokensTest(t: GlobalTestState) { WalletApiOperation.PreparePayForUri, { talerPayUri, - } + }, ); t.assertTrue( @@ -377,16 +427,20 @@ export async function runWalletTokensTest(t: GlobalTestState) { ); { - const choicesRes = await walletClient.call(WalletApiOperation.GetChoicesForPayment, { - transactionId: preparePayResult.transactionId, - }); + const choicesRes = await walletClient.call( + WalletApiOperation.GetChoicesForPayment, + { + transactionId: preparePayResult.transactionId, + }, + ); t.assertTrue(choicesRes.defaultChoiceIndex === 0); t.assertTrue(choicesRes.automaticExecution === false); t.assertTrue(choicesRes.automaticExecutableIndex === choiceIndex); - t.assertTrue(choicesRes.choices[choiceIndex].status === - ChoiceSelectionDetailType.InsufficientBalance, + t.assertTrue( + choicesRes.choices[choiceIndex].status === + ChoiceSelectionDetailType.InsufficientBalance, ); const tokenDetails = choicesRes.choices[choiceIndex].tokenDetails; @@ -396,8 +450,10 @@ export async function runWalletTokensTest(t: GlobalTestState) { for (const tf in tokenDetails?.perTokenFamily ?? t.fail()) { t.assertTrue(tokenDetails?.perTokenFamily[tf].available === 0); t.assertTrue(tokenDetails?.perTokenFamily[tf].requested === 1); - t.assertTrue(tokenDetails?.perTokenFamily[tf].causeHint === - TokenAvailabilityHint.WalletTokensAvailableInsufficient); + t.assertTrue( + tokenDetails?.perTokenFamily[tf].causeHint === + TokenAvailabilityHint.WalletTokensAvailableInsufficient, + ); break; } } @@ -414,7 +470,10 @@ export async function runWalletTokensTest(t: GlobalTestState) { ); let orderStatus = succeedOrThrow( - await merchantApi.getOrderDetails(merchantAdminAccessToken, orderResp.order_id), + await merchantApi.getOrderDetails( + merchantAdminAccessToken, + orderResp.order_id, + ), ); t.assertTrue(orderStatus.order_status === "unpaid"); @@ -456,7 +515,10 @@ export async function runWalletTokensTest(t: GlobalTestState) { ); let orderStatus = succeedOrThrow( - await merchantApi.getOrderDetails(merchantAdminAccessToken, orderResp.order_id), + await merchantApi.getOrderDetails( + merchantAdminAccessToken, + orderResp.order_id, + ), ); t.assertTrue(orderStatus.order_status === "unpaid"); @@ -468,7 +530,7 @@ export async function runWalletTokensTest(t: GlobalTestState) { WalletApiOperation.PreparePayForUri, { talerPayUri, - } + }, ); t.assertTrue( @@ -476,16 +538,20 @@ export async function runWalletTokensTest(t: GlobalTestState) { ); { - const choicesRes = await walletClient.call(WalletApiOperation.GetChoicesForPayment, { - transactionId: preparePayResult.transactionId, - }); + const choicesRes = await walletClient.call( + WalletApiOperation.GetChoicesForPayment, + { + transactionId: preparePayResult.transactionId, + }, + ); t.assertTrue(choicesRes.defaultChoiceIndex === choiceIndex); t.assertTrue(choicesRes.automaticExecution === true); t.assertTrue(choicesRes.automaticExecutableIndex === choiceIndex); - t.assertTrue(choicesRes.choices[choiceIndex].status === - ChoiceSelectionDetailType.PaymentPossible, + t.assertTrue( + choicesRes.choices[choiceIndex].status === + ChoiceSelectionDetailType.PaymentPossible, ); const tokenDetails = choicesRes.choices[choiceIndex].tokenDetails; diff --git a/packages/taler-util/src/types-taler-wallet.ts b/packages/taler-util/src/types-taler-wallet.ts @@ -2338,16 +2338,6 @@ export const codecForForgetBankAccount = (): Codec<ForgetBankAccountRequest> => .property("bankAccountId", codecForString()) .build("ForgetBankAccountsRequest"); -export interface GetContractTermsDetailsRequest { - transactionId: string; -} - -export const codecForGetContractTermsDetails = - (): Codec<GetContractTermsDetailsRequest> => - buildCodecForObject<GetContractTermsDetailsRequest>() - .property("transactionId", codecForString()) - .build("GetContractTermsDetails"); - export interface PreparePayRequest { talerPayUri: string; } diff --git a/packages/taler-wallet-cli/src/index.ts b/packages/taler-wallet-cli/src/index.ts @@ -24,6 +24,7 @@ import { Amounts, AmountString, assertUnreachable, + ChoiceSelectionDetailType, codecForList, codecForString, ContractTermsUtil, @@ -113,6 +114,7 @@ async function doPay( processExit(1); return; } + let choiceIndex: number | undefined; if (result.status === PreparePayResultType.AlreadyConfirmed) { if (result.paid) { console.log("already paid!"); @@ -121,42 +123,74 @@ async function doPay( } processExit(0); return; - } - if (result.status === "payment-possible") { + } else if (result.status === PreparePayResultType.ChoiceSelection) { + console.log(`choices:`); + const choices = await wallet.call(WalletApiOperation.GetChoicesForPayment, { + transactionId: result.transactionId, + }); + console.log(j2s(choices)); + choiceIndex = await askChoice(choices.choices.length); + const myChoice = choices.choices[choiceIndex]; + if (myChoice.status !== ChoiceSelectionDetailType.PaymentPossible) { + console.log("insufficient balance for choice"); + processExit(1); + } console.log("paying ..."); + console.log("contract", result.contractTerms); + console.log("raw amount:", myChoice.amountRaw); + console.log("effective amount:", myChoice.amountEffective); + } else if (result.status === "payment-possible") { + console.log("paying ..."); + console.log("contract", result.contractTerms); + console.log("raw amount:", result.amountRaw); + console.log("effective amount:", result.amountEffective); } else { throw Error("not reached"); } - console.log("contract", result.contractTerms); - console.log("raw amount:", result.amountRaw); - console.log("effective amount:", result.amountEffective); - let pay; + let pay: boolean; if (options.alwaysYes) { pay = true; } else { - while (true) { - const yesNoResp = (await clk.prompt("Pay? [Y/n]")).toLowerCase(); - if (yesNoResp === "" || yesNoResp === "y" || yesNoResp === "yes") { - pay = true; - break; - } else if (yesNoResp === "n" || yesNoResp === "no") { - pay = false; - break; - } else { - console.log("please answer y/n"); - } - } + pay = await askYesNo(); } if (pay) { await wallet.call(WalletApiOperation.ConfirmPay, { transactionId: result.transactionId, + choiceIndex: choiceIndex, }); } else { console.log("not paying"); } } +async function askChoice(n: number): Promise<number> { + while (true) { + const choice = Number.parseInt( + await clk.prompt(`Select choice (0-${n - 1}):`), + ); + if (choice >= 0 && choice < n) { + return choice; + } else { + console.log("Please enter a valid choice."); + } + } +} + +async function askYesNo(): Promise<boolean> { + while (true) { + const yesNoResp = (await clk.prompt("Pay? [Y/n]")).toLowerCase(); + if (yesNoResp === "" || yesNoResp === "y" || yesNoResp === "yes") { + return true; + } else if (yesNoResp === "n" || yesNoResp === "no") { + return false; + break; + } else { + console.log("please answer y/n"); + } + } +} + function applyVerbose(verbose: boolean): void { // TODO } @@ -175,6 +209,9 @@ export const walletCli = clk .maybeOption("walletDbFile", ["--wallet-db"], clk.STRING, { help: "Location of the wallet database file", }) + .maybeOption("features", ["--features"], clk.STRING, { + help: "Comma-separated list of feature flags to enable.", + }) .maybeOption("walletConnection", ["--wallet-connection"], clk.STRING, { help: "Connect to an RPC wallet", }) @@ -278,6 +315,8 @@ async function createLocalWallet( applyVerbose(walletCliArgs.wallet.verbose); const res = { wallet: wh.wallet }; + const features = (walletCliArgs.wallet.features ?? "").split(","); + if (args.noInit) { return res; } @@ -293,6 +332,11 @@ async function createLocalWallet( emitObservabilityEvents: observabilityEventFile != null, skipDefaults: walletCliArgs.wallet.skipDefaults, }, + features: { + enableV1Contracts: features.includes("enableV1Contracts") + ? true + : undefined, + }, }, } satisfies InitRequest, ); @@ -568,12 +612,14 @@ transactionsCli .requiredArgument("transactionId", clk.STRING, { help: "Identifier of the transaction to delete", }) + .flag("includeContractTerms", ["--include-contract-terms"]) .action(async (args) => { await withWallet(args, { lazyTaskLoop: true }, async (wallet) => { const tx = await wallet.client.call( WalletApiOperation.GetTransactionById, { transactionId: args.lookup.transactionId, + includeContractTerms: args.lookup.includeContractTerms ?? false, }, ); console.log(j2s(tx)); diff --git a/packages/taler-wallet-core/src/pay-merchant.ts b/packages/taler-wallet-core/src/pay-merchant.ts @@ -340,6 +340,17 @@ export class PayMerchantTransactionContext implements TransactionContext { ]; } + let contractTerms: MerchantContractTerms | undefined; + if (req?.includeContractTerms) { + if (!this.wex.ws.config.features.enableV1Contracts) { + contractTerms = ContractTermsUtil.downgradeContractTerms( + download.contractTerms, + ); + } else { + contractTerms = download.contractTerms; + } + } + return { type: TransactionType.Payment, txState, @@ -362,9 +373,7 @@ export class PayMerchantTransactionContext implements TransactionContext { }), abortReason: purchaseRec.abortReason, info, - contractTerms: req?.includeContractTerms - ? download.contractTermsRaw - : undefined, + contractTerms, refundQueryActive: purchaseRec.purchaseStatus === PurchaseStatus.PendingQueryingRefund, ...(payRetryRec?.lastError ? { error: payRetryRec.lastError } : {}), @@ -1885,9 +1894,10 @@ async function checkPaymentByProposalId( } } } - const contractDl = await expectProposalDownload(wex, proposal); - const contractTerms = contractDl.contractTerms; - + let { contractTerms, contractTermsHash } = await expectProposalDownload( + wex, + proposal, + ); proposalId = proposal.proposalId; const ctx = new PayMerchantTransactionContext(wex, proposalId); @@ -1916,13 +1926,27 @@ async function checkPaymentByProposalId( purchase.purchaseStatus === PurchaseStatus.DialogShared ) { if (contractTerms.version === MerchantContractVersion.V1) { - return { - status: PreparePayResultType.ChoiceSelection, - transactionId, - contractTerms: contractTerms, - contractTermsHash: contractDl.contractTermsHash, - talerUri, - }; + if (!wex.ws.config.features.enableV1Contracts) { + let v0Contract = + ContractTermsUtil.downgradeContractTerms(contractTerms); + logger.warn( + `contract uses v1 features, trying to downgrade, as enableV1Contracts is false`, + ); + if (!v0Contract) { + throw Error( + "payment not possible (uses v1 contract features and downgrade failed)", + ); + } + contractTerms = v0Contract; + } else { + return { + status: PreparePayResultType.ChoiceSelection, + transactionId, + contractTerms: contractTerms, + contractTermsHash: contractTermsHash, + talerUri, + }; + } } const instructedAmount = Amounts.parseOrThrow(contractTerms.amount); @@ -1995,7 +2019,7 @@ async function checkPaymentByProposalId( amountEffective: Amounts.stringify(totalCost), amountRaw: Amounts.stringify(instructedAmount), scopes, - contractTermsHash: contractDl.contractTermsHash, + contractTermsHash, talerUri, }; } @@ -2109,24 +2133,6 @@ function isPurchasePaid(purchase: PurchaseRecord): boolean { ); } -export async function getContractTermsDetails( - wex: WalletExecutionContext, - proposalId: string, -): Promise<DownloadedContractData> { - const proposal = await wex.db.runReadOnlyTx( - { storeNames: ["purchases"] }, - async (tx) => { - return tx.purchases.get(proposalId); - }, - ); - - if (!proposal) { - throw Error(`proposal with id ${proposalId} not found`); - } - - return await expectProposalDownload(wex, proposal); -} - /** * Check if a payment for the given taler://pay/ URI is possible. * diff --git a/packages/taler-wallet-core/src/wallet-api-types.ts b/packages/taler-wallet-core/src/wallet-api-types.ts @@ -82,7 +82,6 @@ import { GetBankingChoicesForPaytoResponse, GetChoicesForPaymentRequest, GetChoicesForPaymentResult, - GetContractTermsDetailsRequest, GetCurrencySpecificationRequest, GetCurrencySpecificationResponse, GetDepositWireTypesForCurrencyRequest, @@ -170,7 +169,6 @@ import { UserAttentionsResponse, ValidateIbanRequest, ValidateIbanResponse, - DownloadedContractData, WalletCoreVersion, WithdrawTestBalanceRequest, WithdrawUriInfoResponse, @@ -327,18 +325,6 @@ export enum WalletApiOperation { * Use {@link WalletApiOperation.PrepareBankIntegratedWithdrawal} instead. */ GetWithdrawalDetailsForUri = "getWithdrawalDetailsForUri", - - /** - * @deprecated (2024-12-04) - * Use checkDeposit instead - */ - PrepareDeposit = "prepareDeposit", - - /** - * @deprecated (2024-12-04) - * Use getTransactionById with includeContractTerms: true instead - */ - GetContractTermsDetails = "getContractTermsDetails", } // group: Initialization @@ -643,12 +629,6 @@ export type PreparePayForTemplateOp = { response: PreparePayResult; }; -export type GetContractTermsDetailsOp = { - op: WalletApiOperation.GetContractTermsDetails; - request: GetContractTermsDetailsRequest; - response: DownloadedContractData; -}; - /** * Confirm a payment that was previously prepared with * {@link PreparePayForUriOp} @@ -919,15 +899,6 @@ export type CheckDepositOp = { response: CheckDepositResponse; }; -/** - * @deprecated use CheckDepositOp instead - */ -export type PrepareDepositOp = { - op: WalletApiOperation.PrepareDeposit; - request: CheckDepositRequest; - response: CheckDepositResponse; -}; - // group: Backups /** @@ -1426,7 +1397,6 @@ export type WalletOperations = { [WalletApiOperation.SharePayment]: SharePaymentOp; [WalletApiOperation.CheckPayForTemplate]: CheckPayForTemplateOp; [WalletApiOperation.PreparePayForTemplate]: PreparePayForTemplateOp; - [WalletApiOperation.GetContractTermsDetails]: GetContractTermsDetailsOp; [WalletApiOperation.WithdrawTestkudos]: WithdrawTestkudosOp; [WalletApiOperation.GetChoicesForPayment]: GetChoicesForPaymentOp; [WalletApiOperation.ConfirmPay]: ConfirmPayOp; @@ -1470,7 +1440,6 @@ export type WalletOperations = { [WalletApiOperation.GetExchangeTos]: GetExchangeTosOp; [WalletApiOperation.GetExchangeDetailedInfo]: GetExchangeDetailedInfoOp; [WalletApiOperation.GetExchangeEntryByUrl]: GetExchangeEntryByUrlOp; - [WalletApiOperation.PrepareDeposit]: PrepareDepositOp; [WalletApiOperation.CheckDeposit]: CheckDepositOp; [WalletApiOperation.GenerateDepositGroupTxId]: GenerateDepositGroupTxIdOp; [WalletApiOperation.CreateDepositGroup]: CreateDepositGroupOp; diff --git a/packages/taler-wallet-core/src/wallet.ts b/packages/taler-wallet-core/src/wallet.ts @@ -72,7 +72,6 @@ import { GetBankingChoicesForPaytoResponse, GetChoicesForPaymentRequest, GetChoicesForPaymentResult, - GetContractTermsDetailsRequest, GetCurrencySpecificationRequest, GetCurrencySpecificationResponse, GetDepositWireTypesForCurrencyRequest, @@ -174,7 +173,6 @@ import { codecForGetBankAccountByIdRequest, codecForGetBankingChoicesForPaytoRequest, codecForGetChoicesForPaymentRequest, - codecForGetContractTermsDetails, codecForGetCurrencyInfoRequest, codecForGetDepositWireTypesForCurrencyRequest, codecForGetDepositWireTypesRequest, @@ -326,7 +324,6 @@ import { checkPayForTemplate, confirmPay, getChoicesForPayment, - getContractTermsDetails, preparePayForTemplate, preparePayForUri, sharePayment, @@ -1155,17 +1152,6 @@ async function handleGetExchangeTos( ); } -async function handleGetContractTermsDetails( - wex: WalletExecutionContext, - req: GetContractTermsDetailsRequest, -): Promise<DownloadedContractData> { - const parsedTx = parseTransactionIdentifier(req.transactionId); - if (parsedTx?.tag !== TransactionType.Payment) { - throw Error("transactionId is not a payment transaction"); - } - return getContractTermsDetails(wex, parsedTx.proposalId); -} - async function handleGetQrCodesForPayto( wex: WalletExecutionContext, req: GetQrCodesForPaytoRequest, @@ -2015,10 +2001,6 @@ const handlers: { [T in WalletApiOperation]: HandlerWithValidator<T> } = { codec: codecForGetExchangeTosRequest(), handler: handleGetExchangeTos, }, - [WalletApiOperation.GetContractTermsDetails]: { - codec: codecForGetContractTermsDetails(), - handler: handleGetContractTermsDetails, - }, [WalletApiOperation.RetryPendingNow]: { codec: codecForEmptyObject(), handler: handleRetryPendingNow, @@ -2157,10 +2139,6 @@ const handlers: { [T in WalletApiOperation]: HandlerWithValidator<T> } = { codec: codecForEmptyObject(), handler: getBackupInfo, }, - [WalletApiOperation.PrepareDeposit]: { - codec: codecForCheckDepositRequest(), - handler: checkDepositGroup, - }, [WalletApiOperation.CheckDeposit]: { codec: codecForCheckDepositRequest(), handler: checkDepositGroup, @@ -2574,7 +2552,7 @@ function applyRunConfigDefaults(wcp?: PartialWalletRunConfig): WalletRunConfig { }, features: { allowHttp: true, - enableV1Contracts: false, + enableV1Contracts: wcp?.features?.enableV1Contracts ?? false, }, testing: { devModeActive: wcp?.testing?.devModeActive ?? false,