From 04ab9f37801f6a42b85581cc79667239d3fc79e5 Mon Sep 17 00:00:00 2001 From: Florian Dold Date: Sat, 11 Feb 2023 14:24:29 +0100 Subject: wallet-core,harness: implement pay templating --- packages/taler-harness/src/harness/harness.ts | 31 +- .../taler-harness/src/harness/merchantApiTypes.ts | 336 ------------------- .../src/integrationtests/test-payment-template.ts | 95 ++++++ .../src/integrationtests/testrunner.ts | 2 + packages/taler-harness/tsconfig.json | 2 +- packages/taler-util/src/index.ts | 1 + packages/taler-util/src/merchant-api-types.ts | 368 +++++++++++++++++++++ packages/taler-util/src/taler-types.ts | 15 +- packages/taler-util/src/taleruri.test.ts | 36 ++ packages/taler-util/src/taleruri.ts | 65 +++- packages/taler-util/src/wallet-types.ts | 11 + packages/taler-wallet-core/src/wallet-api-types.ts | 14 +- packages/taler-wallet-core/src/wallet.ts | 56 +++- 13 files changed, 670 insertions(+), 362 deletions(-) delete mode 100644 packages/taler-harness/src/harness/merchantApiTypes.ts create mode 100644 packages/taler-harness/src/integrationtests/test-payment-template.ts create mode 100644 packages/taler-util/src/merchant-api-types.ts diff --git a/packages/taler-harness/src/harness/harness.ts b/packages/taler-harness/src/harness/harness.ts index 4e5d8238c..3659ea538 100644 --- a/packages/taler-harness/src/harness/harness.ts +++ b/packages/taler-harness/src/harness/harness.ts @@ -38,6 +38,7 @@ import { hash, j2s, Logger, + MerchantTemplateAddDetails, parsePaytoUri, stringToBytes, TalerProtocolDuration, @@ -66,15 +67,15 @@ import { CoinConfig } from "./denomStructures.js"; import { LibeufinNexusApi, LibeufinSandboxApi } from "./libeufin-apis.js"; import { codecForMerchantOrderPrivateStatusResponse, - codecForPostOrderResponse, + codecForMerchantPostOrderResponse, MerchantInstancesResponse, MerchantOrderPrivateStatusResponse, - PostOrderRequest, - PostOrderResponse, + MerchantPostOrderRequest, + MerchantPostOrderResponse, TipCreateConfirmation, TipCreateRequest, TippingReserveStatus, -} from "./merchantApiTypes.js"; +} from "@gnu-taler/taler-util"; import { createRemoteWallet, getClientFromRemoteWallet, @@ -1473,15 +1474,31 @@ export namespace MerchantPrivateApi { export async function createOrder( merchantService: MerchantServiceInterface, instanceName: string, - req: PostOrderRequest, + req: MerchantPostOrderRequest, withAuthorization: WithAuthorization = {}, - ): Promise { + ): Promise { const baseUrl = merchantService.makeInstanceBaseUrl(instanceName); let url = new URL("private/orders", baseUrl); const resp = await axios.post(url.href, req, { headers: withAuthorization as Record, }); - return codecForPostOrderResponse().decode(resp.data); + return codecForMerchantPostOrderResponse().decode(resp.data); + } + + export async function createTemplate( + merchantService: MerchantServiceInterface, + instanceName: string, + req: MerchantTemplateAddDetails, + withAuthorization: WithAuthorization = {}, + ) { + const baseUrl = merchantService.makeInstanceBaseUrl(instanceName); + let url = new URL("private/templates", baseUrl); + const resp = await axios.post(url.href, req, { + headers: withAuthorization as Record, + }); + if (resp.status !== 204) { + throw Error("failed to create template"); + } } export async function queryPrivateOrderStatus( diff --git a/packages/taler-harness/src/harness/merchantApiTypes.ts b/packages/taler-harness/src/harness/merchantApiTypes.ts deleted file mode 100644 index 1985e9150..000000000 --- a/packages/taler-harness/src/harness/merchantApiTypes.ts +++ /dev/null @@ -1,336 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2020 Taler Systems S.A. - - GNU Taler is free software; you can redistribute it and/or modify it under the - terms of the GNU General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - GNU Taler; see the file COPYING. If not, see - */ - -/** - * Test harness for various GNU Taler components. - * Also provides a fault-injection proxy. - * - * @author Florian Dold - */ - -/** - * Imports. - */ -import { - MerchantContractTerms, - Duration, - Codec, - buildCodecForObject, - codecForString, - codecOptional, - codecForConstString, - codecForBoolean, - codecForNumber, - codecForMerchantContractTerms, - codecForAny, - buildCodecForUnion, - AmountString, - AbsoluteTime, - CoinPublicKeyString, - EddsaPublicKeyString, - codecForAmountString, - TalerProtocolDuration, - codecForTimestamp, - TalerProtocolTimestamp, -} from "@gnu-taler/taler-util"; - -export interface PostOrderRequest { - // The order must at least contain the minimal - // order detail, but can override all - order: Partial; - - // if set, the backend will then set the refund deadline to the current - // time plus the specified delay. - refund_delay?: TalerProtocolDuration; - - // specifies the payment target preferred by the client. Can be used - // to select among the various (active) wire methods supported by the instance. - payment_target?: string; - - // FIXME: some fields are missing - - // Should a token for claiming the order be generated? - // False can make sense if the ORDER_ID is sufficiently - // high entropy to prevent adversarial claims (like it is - // if the backend auto-generates one). Default is 'true'. - create_token?: boolean; -} - -export type ClaimToken = string; - -export interface PostOrderResponse { - order_id: string; - token?: ClaimToken; -} - -export const codecForPostOrderResponse = (): Codec => - buildCodecForObject() - .property("order_id", codecForString()) - .property("token", codecOptional(codecForString())) - .build("PostOrderResponse"); - -export const codecForRefundDetails = (): Codec => - buildCodecForObject() - .property("reason", codecForString()) - .property("pending", codecForBoolean()) - .property("amount", codecForString()) - .property("timestamp", codecForTimestamp) - .build("PostOrderResponse"); - -export const codecForCheckPaymentPaidResponse = - (): Codec => - buildCodecForObject() - .property("order_status_url", codecForString()) - .property("order_status", codecForConstString("paid")) - .property("refunded", codecForBoolean()) - .property("wired", codecForBoolean()) - .property("deposit_total", codecForAmountString()) - .property("exchange_ec", codecForNumber()) - .property("exchange_hc", codecForNumber()) - .property("refund_amount", codecForAmountString()) - .property("contract_terms", codecForMerchantContractTerms()) - // FIXME: specify - .property("wire_details", codecForAny()) - .property("wire_reports", codecForAny()) - .property("refund_details", codecForAny()) - .build("CheckPaymentPaidResponse"); - -export const codecForCheckPaymentUnpaidResponse = - (): Codec => - buildCodecForObject() - .property("order_status", codecForConstString("unpaid")) - .property("taler_pay_uri", codecForString()) - .property("order_status_url", codecForString()) - .property("already_paid_order_id", codecOptional(codecForString())) - .build("CheckPaymentPaidResponse"); - -export const codecForCheckPaymentClaimedResponse = - (): Codec => - buildCodecForObject() - .property("order_status", codecForConstString("claimed")) - .property("contract_terms", codecForMerchantContractTerms()) - .build("CheckPaymentClaimedResponse"); - -export const codecForMerchantOrderPrivateStatusResponse = - (): Codec => - buildCodecForUnion() - .discriminateOn("order_status") - .alternative("paid", codecForCheckPaymentPaidResponse()) - .alternative("unpaid", codecForCheckPaymentUnpaidResponse()) - .alternative("claimed", codecForCheckPaymentClaimedResponse()) - .build("MerchantOrderPrivateStatusResponse"); - -export type MerchantOrderPrivateStatusResponse = - | CheckPaymentPaidResponse - | CheckPaymentUnpaidResponse - | CheckPaymentClaimedResponse; - -export interface CheckPaymentClaimedResponse { - // Wallet claimed the order, but didn't pay yet. - order_status: "claimed"; - - contract_terms: MerchantContractTerms; -} - -export interface CheckPaymentPaidResponse { - // did the customer pay for this contract - order_status: "paid"; - - // Was the payment refunded (even partially) - refunded: boolean; - - // Did the exchange wire us the funds - wired: boolean; - - // Total amount the exchange deposited into our bank account - // for this contract, excluding fees. - deposit_total: AmountString; - - // Numeric error code indicating errors the exchange - // encountered tracking the wire transfer for this purchase (before - // we even got to specific coin issues). - // 0 if there were no issues. - exchange_ec: number; - - // HTTP status code returned by the exchange when we asked for - // information to track the wire transfer for this purchase. - // 0 if there were no issues. - exchange_hc: number; - - // Total amount that was refunded, 0 if refunded is false. - refund_amount: AmountString; - - // Contract terms - contract_terms: MerchantContractTerms; - - // Ihe wire transfer status from the exchange for this order if available, otherwise empty array - wire_details: TransactionWireTransfer[]; - - // Reports about trouble obtaining wire transfer details, empty array if no trouble were encountered. - wire_reports: TransactionWireReport[]; - - // The refund details for this order. One entry per - // refunded coin; empty array if there are no refunds. - refund_details: RefundDetails[]; - - order_status_url: string; -} - -export interface CheckPaymentUnpaidResponse { - order_status: "unpaid"; - - // URI that the wallet must process to complete the payment. - taler_pay_uri: string; - - order_status_url: string; - - // Alternative order ID which was paid for already in the same session. - // Only given if the same product was purchased before in the same session. - already_paid_order_id?: string; - - // We do we NOT return the contract terms here because they may not - // exist in case the wallet did not yet claim them. -} - -export interface RefundDetails { - // Reason given for the refund - reason: string; - - // when was the refund approved - timestamp: TalerProtocolTimestamp; - - // has not been taken yet - pending: boolean; - - // Total amount that was refunded (minus a refund fee). - amount: AmountString; -} - -export interface TransactionWireTransfer { - // Responsible exchange - exchange_url: string; - - // 32-byte wire transfer identifier - wtid: string; - - // execution time of the wire transfer - execution_time: AbsoluteTime; - - // Total amount that has been wire transferred - // to the merchant - amount: AmountString; - - // Was this transfer confirmed by the merchant via the - // POST /transfers API, or is it merely claimed by the exchange? - confirmed: boolean; -} - -export interface TransactionWireReport { - // Numerical error code - code: number; - - // Human-readable error description - hint: string; - - // Numerical error code from the exchange. - exchange_ec: number; - - // HTTP status code received from the exchange. - exchange_hc: number; - - // Public key of the coin for which we got the exchange error. - coin_pub: CoinPublicKeyString; -} - -export interface TippingReserveStatus { - // Array of all known reserves (possibly empty!) - reserves: ReserveStatusEntry[]; -} - -export interface ReserveStatusEntry { - // Public key of the reserve - reserve_pub: string; - - // Timestamp when it was established - creation_time: AbsoluteTime; - - // Timestamp when it expires - expiration_time: AbsoluteTime; - - // Initial amount as per reserve creation call - merchant_initial_amount: AmountString; - - // Initial amount as per exchange, 0 if exchange did - // not confirm reserve creation yet. - exchange_initial_amount: AmountString; - - // Amount picked up so far. - pickup_amount: AmountString; - - // Amount approved for tips that exceeds the pickup_amount. - committed_amount: AmountString; - - // Is this reserve active (false if it was deleted but not purged) - active: boolean; -} - -export interface TipCreateConfirmation { - // Unique tip identifier for the tip that was created. - tip_id: string; - - // taler://tip URI for the tip - taler_tip_uri: string; - - // URL that will directly trigger processing - // the tip when the browser is redirected to it - tip_status_url: string; - - // when does the tip expire - tip_expiration: AbsoluteTime; -} - -export interface TipCreateRequest { - // Amount that the customer should be tipped - amount: AmountString; - - // Justification for giving the tip - justification: string; - - // URL that the user should be directed to after tipping, - // will be included in the tip_token. - next_url: string; -} - -export interface MerchantInstancesResponse { - // List of instances that are present in the backend (see Instance) - instances: MerchantInstanceDetail[]; -} - -export interface MerchantInstanceDetail { - // Merchant name corresponding to this instance. - name: string; - - // Merchant instance this response is about ($INSTANCE) - id: string; - - // Public key of the merchant/instance, in Crockford Base32 encoding. - merchant_pub: EddsaPublicKeyString; - - // List of the payment targets supported by this instance. Clients can - // specify the desired payment target in /order requests. Note that - // front-ends do not have to support wallets selecting payment targets. - payment_targets: string[]; -} diff --git a/packages/taler-harness/src/integrationtests/test-payment-template.ts b/packages/taler-harness/src/integrationtests/test-payment-template.ts new file mode 100644 index 000000000..41e43e28a --- /dev/null +++ b/packages/taler-harness/src/integrationtests/test-payment-template.ts @@ -0,0 +1,95 @@ +/* + This file is part of GNU Taler + (C) 2020 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see + */ + +/** + * Imports. + */ +import { ConfirmPayResultType, Duration, PreparePayResultType, TalerProtocolTimestamp } from "@gnu-taler/taler-util"; +import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; +import { GlobalTestState, MerchantPrivateApi } from "../harness/harness.js"; +import { + createSimpleTestkudosEnvironment, + withdrawViaBank, + makeTestPayment, +} from "../harness/helpers.js"; + +/** + * Test for taler://payment-template/ URIs + */ +export async function runPaymentTemplateTest(t: GlobalTestState) { + // Set up test environment + + const { wallet, bank, exchange, merchant } = + await createSimpleTestkudosEnvironment(t); + + await MerchantPrivateApi.createTemplate(merchant, "default", { + template_id: "template1", + template_description: "my test template", + template_contract: { + minimum_age: 0, + pay_duration: Duration.toTalerProtocolDuration( + Duration.fromSpec({ + minutes: 2, + }), + ), + summary: "hello, I'm a summary", + }, + }); + + // Withdraw digital cash into the wallet. + + await withdrawViaBank(t, { wallet, bank, exchange, amount: "TESTKUDOS:20" }); + + // Request a template payment + + const preparePayResult = await wallet.client.call( + WalletApiOperation.PreparePayForTemplate, + { + talerPayTemplateUri: `taler+http://pay-template/localhost:${merchant.port}/template1?amount=TESTKUDOS:1`, + templateParams: {}, + }, + ); + + console.log(preparePayResult); + + t.assertTrue( + preparePayResult.status === PreparePayResultType.PaymentPossible, + ); + + // Pay for it + + const r2 = await wallet.client.call(WalletApiOperation.ConfirmPay, { + proposalId: preparePayResult.proposalId, + }); + + t.assertTrue(r2.type === ConfirmPayResultType.Done); + + // Check if payment was successful. + + const orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus( + merchant, + { + orderId: preparePayResult.contractTerms.order_id, + instance: "default", + }, + ); + + t.assertTrue(orderStatus.order_status === "paid"); + + await wallet.runUntilDone(); +} + +runPaymentTemplateTest.suites = ["wallet"]; diff --git a/packages/taler-harness/src/integrationtests/testrunner.ts b/packages/taler-harness/src/integrationtests/testrunner.ts index 025f2e514..a20300e02 100644 --- a/packages/taler-harness/src/integrationtests/testrunner.ts +++ b/packages/taler-harness/src/integrationtests/testrunner.ts @@ -100,6 +100,7 @@ import { runKycTest } from "./test-kyc.js"; import { runPaymentAbortTest } from "./test-payment-abort.js"; import { runWithdrawalFeesTest } from "./test-withdrawal-fees.js"; import { runWalletBalanceTest } from "./test-wallet-balance.js"; +import { runPaymentTemplateTest } from "./test-payment-template.js"; /** * Test runner. @@ -163,6 +164,7 @@ const allTests: TestMainFunction[] = [ runPaymentIdempotencyTest, runPaymentMultipleTest, runPaymentTest, + runPaymentTemplateTest, runPaymentAbortTest, runPaymentTransientTest, runPaymentZeroTest, diff --git a/packages/taler-harness/tsconfig.json b/packages/taler-harness/tsconfig.json index 447d3f946..d022b16e8 100644 --- a/packages/taler-harness/tsconfig.json +++ b/packages/taler-harness/tsconfig.json @@ -21,7 +21,7 @@ "baseUrl": "./src", "typeRoots": ["./node_modules/@types"] }, - "include": ["src/**/*"], + "include": ["src/**/*", "../taler-util/src/merchant-api-types.ts"], "references": [ { "path": "../taler-wallet-core/" diff --git a/packages/taler-util/src/index.ts b/packages/taler-util/src/index.ts index 2f674d097..661b0332f 100644 --- a/packages/taler-util/src/index.ts +++ b/packages/taler-util/src/index.ts @@ -35,3 +35,4 @@ export { RequestThrottler } from "./RequestThrottler.js"; export * from "./CancellationToken.js"; export * from "./contract-terms.js"; export * from "./base64.js"; +export * from "./merchant-api-types.js"; diff --git a/packages/taler-util/src/merchant-api-types.ts b/packages/taler-util/src/merchant-api-types.ts new file mode 100644 index 000000000..61002191a --- /dev/null +++ b/packages/taler-util/src/merchant-api-types.ts @@ -0,0 +1,368 @@ +/* + This file is part of GNU Taler + (C) 2020 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see + */ + +/** + * Test harness for various GNU Taler components. + * Also provides a fault-injection proxy. + * + * @author Florian Dold + */ + +/** + * Imports. + */ +import { + MerchantContractTerms, + Codec, + buildCodecForObject, + codecForString, + codecOptional, + codecForConstString, + codecForBoolean, + codecForNumber, + codecForMerchantContractTerms, + codecForAny, + buildCodecForUnion, + AmountString, + AbsoluteTime, + CoinPublicKeyString, + EddsaPublicKeyString, + codecForAmountString, + TalerProtocolDuration, + codecForTimestamp, + TalerProtocolTimestamp, +} from "@gnu-taler/taler-util"; + +export interface MerchantPostOrderRequest { + // The order must at least contain the minimal + // order detail, but can override all + order: Partial; + + // if set, the backend will then set the refund deadline to the current + // time plus the specified delay. + refund_delay?: TalerProtocolDuration; + + // specifies the payment target preferred by the client. Can be used + // to select among the various (active) wire methods supported by the instance. + payment_target?: string; + + // FIXME: some fields are missing + + // Should a token for claiming the order be generated? + // False can make sense if the ORDER_ID is sufficiently + // high entropy to prevent adversarial claims (like it is + // if the backend auto-generates one). Default is 'true'. + create_token?: boolean; +} + +export type ClaimToken = string; + +export interface MerchantPostOrderResponse { + order_id: string; + token?: ClaimToken; +} + +export const codecForMerchantPostOrderResponse = (): Codec => + buildCodecForObject() + .property("order_id", codecForString()) + .property("token", codecOptional(codecForString())) + .build("PostOrderResponse"); + +export const codecForMerchantRefundDetails = (): Codec => + buildCodecForObject() + .property("reason", codecForString()) + .property("pending", codecForBoolean()) + .property("amount", codecForString()) + .property("timestamp", codecForTimestamp) + .build("PostOrderResponse"); + +export const codecForMerchantCheckPaymentPaidResponse = + (): Codec => + buildCodecForObject() + .property("order_status_url", codecForString()) + .property("order_status", codecForConstString("paid")) + .property("refunded", codecForBoolean()) + .property("wired", codecForBoolean()) + .property("deposit_total", codecForAmountString()) + .property("exchange_ec", codecForNumber()) + .property("exchange_hc", codecForNumber()) + .property("refund_amount", codecForAmountString()) + .property("contract_terms", codecForMerchantContractTerms()) + // FIXME: specify + .property("wire_details", codecForAny()) + .property("wire_reports", codecForAny()) + .property("refund_details", codecForAny()) + .build("CheckPaymentPaidResponse"); + +export const codecForCheckPaymentUnpaidResponse = + (): Codec => + buildCodecForObject() + .property("order_status", codecForConstString("unpaid")) + .property("taler_pay_uri", codecForString()) + .property("order_status_url", codecForString()) + .property("already_paid_order_id", codecOptional(codecForString())) + .build("CheckPaymentPaidResponse"); + +export const codecForCheckPaymentClaimedResponse = + (): Codec => + buildCodecForObject() + .property("order_status", codecForConstString("claimed")) + .property("contract_terms", codecForMerchantContractTerms()) + .build("CheckPaymentClaimedResponse"); + +export const codecForMerchantOrderPrivateStatusResponse = + (): Codec => + buildCodecForUnion() + .discriminateOn("order_status") + .alternative("paid", codecForMerchantCheckPaymentPaidResponse()) + .alternative("unpaid", codecForCheckPaymentUnpaidResponse()) + .alternative("claimed", codecForCheckPaymentClaimedResponse()) + .build("MerchantOrderPrivateStatusResponse"); + +export type MerchantOrderPrivateStatusResponse = + | MerchantCheckPaymentPaidResponse + | CheckPaymentUnpaidResponse + | CheckPaymentClaimedResponse; + +export interface CheckPaymentClaimedResponse { + // Wallet claimed the order, but didn't pay yet. + order_status: "claimed"; + + contract_terms: MerchantContractTerms; +} + +export interface MerchantCheckPaymentPaidResponse { + // did the customer pay for this contract + order_status: "paid"; + + // Was the payment refunded (even partially) + refunded: boolean; + + // Did the exchange wire us the funds + wired: boolean; + + // Total amount the exchange deposited into our bank account + // for this contract, excluding fees. + deposit_total: AmountString; + + // Numeric error code indicating errors the exchange + // encountered tracking the wire transfer for this purchase (before + // we even got to specific coin issues). + // 0 if there were no issues. + exchange_ec: number; + + // HTTP status code returned by the exchange when we asked for + // information to track the wire transfer for this purchase. + // 0 if there were no issues. + exchange_hc: number; + + // Total amount that was refunded, 0 if refunded is false. + refund_amount: AmountString; + + // Contract terms + contract_terms: MerchantContractTerms; + + // Ihe wire transfer status from the exchange for this order if available, otherwise empty array + wire_details: TransactionWireTransfer[]; + + // Reports about trouble obtaining wire transfer details, empty array if no trouble were encountered. + wire_reports: TransactionWireReport[]; + + // The refund details for this order. One entry per + // refunded coin; empty array if there are no refunds. + refund_details: RefundDetails[]; + + order_status_url: string; +} + +export interface CheckPaymentUnpaidResponse { + order_status: "unpaid"; + + // URI that the wallet must process to complete the payment. + taler_pay_uri: string; + + order_status_url: string; + + // Alternative order ID which was paid for already in the same session. + // Only given if the same product was purchased before in the same session. + already_paid_order_id?: string; + + // We do we NOT return the contract terms here because they may not + // exist in case the wallet did not yet claim them. +} + +export interface RefundDetails { + // Reason given for the refund + reason: string; + + // when was the refund approved + timestamp: TalerProtocolTimestamp; + + // has not been taken yet + pending: boolean; + + // Total amount that was refunded (minus a refund fee). + amount: AmountString; +} + +export interface TransactionWireTransfer { + // Responsible exchange + exchange_url: string; + + // 32-byte wire transfer identifier + wtid: string; + + // execution time of the wire transfer + execution_time: AbsoluteTime; + + // Total amount that has been wire transferred + // to the merchant + amount: AmountString; + + // Was this transfer confirmed by the merchant via the + // POST /transfers API, or is it merely claimed by the exchange? + confirmed: boolean; +} + +export interface TransactionWireReport { + // Numerical error code + code: number; + + // Human-readable error description + hint: string; + + // Numerical error code from the exchange. + exchange_ec: number; + + // HTTP status code received from the exchange. + exchange_hc: number; + + // Public key of the coin for which we got the exchange error. + coin_pub: CoinPublicKeyString; +} + +export interface TippingReserveStatus { + // Array of all known reserves (possibly empty!) + reserves: ReserveStatusEntry[]; +} + +export interface ReserveStatusEntry { + // Public key of the reserve + reserve_pub: string; + + // Timestamp when it was established + creation_time: AbsoluteTime; + + // Timestamp when it expires + expiration_time: AbsoluteTime; + + // Initial amount as per reserve creation call + merchant_initial_amount: AmountString; + + // Initial amount as per exchange, 0 if exchange did + // not confirm reserve creation yet. + exchange_initial_amount: AmountString; + + // Amount picked up so far. + pickup_amount: AmountString; + + // Amount approved for tips that exceeds the pickup_amount. + committed_amount: AmountString; + + // Is this reserve active (false if it was deleted but not purged) + active: boolean; +} + +export interface TipCreateConfirmation { + // Unique tip identifier for the tip that was created. + tip_id: string; + + // taler://tip URI for the tip + taler_tip_uri: string; + + // URL that will directly trigger processing + // the tip when the browser is redirected to it + tip_status_url: string; + + // when does the tip expire + tip_expiration: AbsoluteTime; +} + +export interface TipCreateRequest { + // Amount that the customer should be tipped + amount: AmountString; + + // Justification for giving the tip + justification: string; + + // URL that the user should be directed to after tipping, + // will be included in the tip_token. + next_url: string; +} + +export interface MerchantInstancesResponse { + // List of instances that are present in the backend (see Instance) + instances: MerchantInstanceDetail[]; +} + +export interface MerchantInstanceDetail { + // Merchant name corresponding to this instance. + name: string; + + // Merchant instance this response is about ($INSTANCE) + id: string; + + // Public key of the merchant/instance, in Crockford Base32 encoding. + merchant_pub: EddsaPublicKeyString; + + // List of the payment targets supported by this instance. Clients can + // specify the desired payment target in /order requests. Note that + // front-ends do not have to support wallets selecting payment targets. + payment_targets: string[]; +} + +export interface MerchantTemplateContractDetails { + // Human-readable summary for the template. + summary?: string; + + // The price is imposed by the merchant and cannot be changed by the customer. + // This parameter is optional. + amount?: AmountString; + + // Minimum age buyer must have (in years). Default is 0. + minimum_age: number; + + // The time the customer need to pay before his order will be deleted. + // It is deleted if the customer did not pay and if the duration is over. + pay_duration: TalerProtocolDuration; +} + +export interface MerchantTemplateAddDetails { + + // Template ID to use. + template_id: string; + + // Human-readable description for the template. + template_description: string; + + // A base64-encoded image selected by the merchant. + // This parameter is optional. + // We are not sure about it. + image?: string; + + // Additional information in a separate template. + template_contract: MerchantTemplateContractDetails; +} \ No newline at end of file diff --git a/packages/taler-util/src/taler-types.ts b/packages/taler-util/src/taler-types.ts index bb15f0494..6e7df2c04 100644 --- a/packages/taler-util/src/taler-types.ts +++ b/packages/taler-util/src/taler-types.ts @@ -1481,10 +1481,11 @@ export const codecForWithdrawResponse = (): Codec => .property("ev_sig", codecForBlindedDenominationSignature()) .build("WithdrawResponse"); -export const codecForWithdrawBatchResponse = (): Codec => - buildCodecForObject() - .property("ev_sigs", codecForList(codecForWithdrawResponse())) - .build("WithdrawBatchResponse"); +export const codecForWithdrawBatchResponse = + (): Codec => + buildCodecForObject() + .property("ev_sigs", codecForList(codecForWithdrawResponse())) + .build("WithdrawBatchResponse"); export const codecForMerchantPayResponse = (): Codec => buildCodecForObject() @@ -1757,7 +1758,6 @@ export interface ExchangeBatchWithdrawRequest { planchets: ExchangeWithdrawRequest[]; } - export interface ExchangeRefreshRevealRequest { new_denoms_h: HashCodeString[]; coin_evs: CoinEnvelope[]; @@ -2113,3 +2113,8 @@ export const codecForWalletKycUuid = (): Codec => .property("requirement_row", codecForNumber()) .property("h_payto", codecForString()) .build("WalletKycUuid"); + +export interface MerchantUsingTemplateDetails { + summary?: string; + amount?: AmountString; +} diff --git a/packages/taler-util/src/taleruri.test.ts b/packages/taler-util/src/taleruri.test.ts index 3ee243fb3..a6c4d89fc 100644 --- a/packages/taler-util/src/taleruri.test.ts +++ b/packages/taler-util/src/taleruri.test.ts @@ -22,6 +22,8 @@ import { parseTipUri, parsePayPushUri, constructPayPushUri, + parsePayTemplateUri, + constructPayUri, } from "./taleruri.js"; test("taler pay url parsing: wrong scheme", (t) => { @@ -225,3 +227,37 @@ test("taler peer to peer push URI (construction)", (t) => { }); t.deepEqual(url, "taler://pay-push/foo.example.com/bla/123"); }); + +test("taler pay URI (construction)", (t) => { + const url1 = constructPayUri("http://localhost:123/", "foo", ""); + t.deepEqual(url1, "taler+http://pay/localhost:123/foo/"); + + const url2 = constructPayUri("http://localhost:123/", "foo", "bla"); + t.deepEqual(url2, "taler+http://pay/localhost:123/foo/bla"); +}); + +test("taler pay template URI (parsing)", (t) => { + const url1 = + "taler://pay-template/merchant.example.com/FEGHYJY48FEGU6WETYIOIDEDE2QW3OCZVY?amount=KUDOS:5"; + const r1 = parsePayTemplateUri(url1); + if (!r1) { + t.fail(); + return; + } + t.deepEqual(r1.merchantBaseUrl, "https://merchant.example.com/"); + t.deepEqual(r1.templateId, "FEGHYJY48FEGU6WETYIOIDEDE2QW3OCZVY"); + t.deepEqual(r1.templateParams.amount, "KUDOS:5"); +}); + +test("taler pay template URI (parsing, http with port)", (t) => { + const url1 = + "taler+http://pay-template/merchant.example.com:1234/FEGHYJY48FEGU6WETYIOIDEDE2QW3OCZVY?amount=KUDOS:5"; + const r1 = parsePayTemplateUri(url1); + if (!r1) { + t.fail(); + return; + } + t.deepEqual(r1.merchantBaseUrl, "http://merchant.example.com:1234/"); + t.deepEqual(r1.templateId, "FEGHYJY48FEGU6WETYIOIDEDE2QW3OCZVY"); + t.deepEqual(r1.templateParams.amount, "KUDOS:5"); +}); diff --git a/packages/taler-util/src/taleruri.ts b/packages/taler-util/src/taleruri.ts index 4e47acbce..2aa9cb030 100644 --- a/packages/taler-util/src/taleruri.ts +++ b/packages/taler-util/src/taleruri.ts @@ -16,7 +16,6 @@ import { BackupRecovery } from "./backup-types.js"; import { canonicalizeBaseUrl } from "./helpers.js"; -import { initNodePrng } from "./prng-node.js"; import { URLSearchParams, URL } from "./url.js"; export interface PayUriResult { @@ -27,6 +26,12 @@ export interface PayUriResult { noncePriv: string | undefined; } +export interface PayTemplateUriResult { + merchantBaseUrl: string; + templateId: string; + templateParams: Record; +} + export interface WithdrawUriResult { bankIntegrationApiBaseUrl: string; withdrawalOperationId: string; @@ -91,6 +96,7 @@ export function parseWithdrawUri(s: string): WithdrawUriResult | undefined { export enum TalerUriType { TalerPay = "taler-pay", + TalerTemplate = "taler-template", TalerWithdraw = "taler-withdraw", TalerTip = "taler-tip", TalerRefund = "taler-refund", @@ -103,6 +109,7 @@ export enum TalerUriType { const talerActionPayPull = "pay-pull"; const talerActionPayPush = "pay-push"; +const talerActionPayTemplate = "pay-template"; /** * Classify a taler:// URI. @@ -121,6 +128,12 @@ export function classifyTalerUri(s: string): TalerUriType { if (sl.startsWith("taler+http://pay/")) { return TalerUriType.TalerPay; } + if (sl.startsWith("taler://pay-template/")) { + return TalerUriType.TalerPay; + } + if (sl.startsWith("taler+http://pay-template/")) { + return TalerUriType.TalerPay; + } if (sl.startsWith("taler://tip/")) { return TalerUriType.TalerTip; } @@ -216,6 +229,38 @@ export function parsePayUri(s: string): PayUriResult | undefined { }; } +export function parsePayTemplateUri( + s: string, +): PayTemplateUriResult | undefined { + const pi = parseProtoInfo(s, talerActionPayTemplate); + if (!pi) { + return undefined; + } + const c = pi?.rest.split("?"); + const q = new URLSearchParams(c[1] ?? ""); + const parts = c[0].split("/"); + if (parts.length < 2) { + return undefined; + } + const host = parts[0].toLowerCase(); + const templateId = parts[parts.length - 1]; + const pathSegments = parts.slice(1, parts.length - 1); + const p = [host, ...pathSegments].join("/"); + const merchantBaseUrl = canonicalizeBaseUrl(`${pi.innerProto}://${p}/`); + + const params: Record = {}; + + q.forEach((v, k) => { + params[k] = v; + }); + + return { + merchantBaseUrl, + templateId, + templateParams: params, + }; +} + export function constructPayUri( merchantBaseUrl: string, orderId: string, @@ -227,9 +272,21 @@ export function constructPayUri( const url = new URL(base); const isHttp = base.startsWith("http://"); let result = isHttp ? `taler+http://pay/` : `taler://pay/`; - result += `${url.hostname}${url.pathname}${orderId}/${sessionId}?`; - if (claimToken) result += `c=${claimToken}`; - if (noncePriv) result += `n=${noncePriv}`; + result += url.hostname; + if (url.port != "") { + result += `:${url.port}`; + } + result += `${url.pathname}${orderId}/${sessionId}`; + let queryPart = ""; + if (claimToken) { + queryPart += `c=${claimToken}`; + } + if (noncePriv) { + queryPart += `n=${noncePriv}`; + } + if (queryPart) { + result += "?" + queryPart; + } return result; } diff --git a/packages/taler-util/src/wallet-types.ts b/packages/taler-util/src/wallet-types.ts index d57a221f3..0f29b964b 100644 --- a/packages/taler-util/src/wallet-types.ts +++ b/packages/taler-util/src/wallet-types.ts @@ -1418,6 +1418,17 @@ export const codecForPreparePayRequest = (): Codec => .property("talerPayUri", codecForString()) .build("PreparePay"); +export interface PreparePayTemplateRequest { + talerPayTemplateUri: string; + templateParams: Record; +} + +export const codecForPreparePayTemplateRequest = (): Codec => + buildCodecForObject() + .property("talerPayTemplateUri", codecForString()) + .property("templateParams", codecForAny()) + .build("PreparePayTemplate"); + export interface ConfirmPayRequest { proposalId: string; sessionId?: string; diff --git a/packages/taler-wallet-core/src/wallet-api-types.ts b/packages/taler-wallet-core/src/wallet-api-types.ts index 61d1417f9..da57253a0 100644 --- a/packages/taler-wallet-core/src/wallet-api-types.ts +++ b/packages/taler-wallet-core/src/wallet-api-types.ts @@ -78,6 +78,7 @@ import { PrepareDepositResponse, PreparePayRequest, PreparePayResult, + PreparePayTemplateRequest, PreparePeerPullPaymentRequest, PreparePeerPullPaymentResponse, PreparePeerPushPaymentRequest, @@ -126,6 +127,7 @@ export enum WalletApiOperation { WithdrawTestkudos = "withdrawTestkudos", WithdrawTestBalance = "withdrawTestBalance", PreparePayForUri = "preparePayForUri", + PreparePayForTemplate = "preparePayForTemplate", GetContractTermsDetails = "getContractTermsDetails", RunIntegrationTest = "runIntegrationTest", TestCrypto = "testCrypto", @@ -313,7 +315,7 @@ export type AcceptManualWithdrawalOp = { // group: Merchant Payments /** - * Prepare to make a payment + * Prepare to make a payment based on a taler://pay/ URI. */ export type PreparePayForUriOp = { op: WalletApiOperation.PreparePayForUri; @@ -321,6 +323,15 @@ export type PreparePayForUriOp = { response: PreparePayResult; }; +/** + * Prepare to make a payment based on a taler://pay-template/ URI. + */ +export type PreparePayForTemplateOp = { + op: WalletApiOperation.PreparePayForTemplate; + request: PreparePayTemplateRequest; + response: PreparePayResult; +}; + export type GetContractTermsDetailsOp = { op: WalletApiOperation.GetContractTermsDetails; request: GetContractTermsDetailsRequest; @@ -835,6 +846,7 @@ export type WalletOperations = { [WalletApiOperation.GetVersion]: GetVersionOp; [WalletApiOperation.WithdrawFakebank]: WithdrawFakebankOp; [WalletApiOperation.PreparePayForUri]: PreparePayForUriOp; + [WalletApiOperation.PreparePayForTemplate]: PreparePayForTemplateOp; [WalletApiOperation.GetContractTermsDetails]: GetContractTermsDetailsOp; [WalletApiOperation.WithdrawTestkudos]: WithdrawTestkudosOp; [WalletApiOperation.ConfirmPay]: ConfirmPayOp; diff --git a/packages/taler-wallet-core/src/wallet.ts b/packages/taler-wallet-core/src/wallet.ts index f1ed592bd..57ae85c1c 100644 --- a/packages/taler-wallet-core/src/wallet.ts +++ b/packages/taler-wallet-core/src/wallet.ts @@ -56,8 +56,10 @@ import { codecForInitiatePeerPushPaymentRequest, codecForIntegrationTestArgs, codecForListKnownBankAccounts, + codecForMerchantPostOrderResponse, codecForPrepareDepositRequest, codecForPreparePayRequest, + codecForPreparePayTemplateRequest, codecForPreparePeerPullPaymentRequest, codecForPreparePeerPushPaymentRequest, codecForPrepareRefundRequest, @@ -77,6 +79,7 @@ import { CoinDumpJson, CoinRefreshRequest, CoinStatus, + constructPayUri, CoreApiResponse, DenominationInfo, DenomOperationMap, @@ -88,7 +91,6 @@ import { ExchangesListResponse, ExchangeTosStatusDetails, FeeDescription, - GetBalanceDetailRequest, GetExchangeTosResult, InitResponse, j2s, @@ -96,7 +98,9 @@ import { KnownBankAccountsInfo, Logger, ManualWithdrawalDetails, + MerchantUsingTemplateDetails, NotificationType, + parsePayTemplateUri, parsePaytoUri, RefreshReason, TalerErrorCode, @@ -156,11 +160,7 @@ import { runBackupCycle, } from "./operations/backup/index.js"; import { setWalletDeviceId } from "./operations/backup/state.js"; -import { - getBalanceDetail, - getBalances, - getMerchantPaymentBalanceDetails, -} from "./operations/balance.js"; +import { getBalanceDetail, getBalances } from "./operations/balance.js"; import { getExchangeTosStatus, makeExchangeListItem, @@ -186,7 +186,6 @@ import { } from "./operations/exchanges.js"; import { getMerchantInfo } from "./operations/merchants.js"; import { - abortPay as abortPay, applyRefund, applyRefundFromPurchaseId, confirmPay, @@ -1171,11 +1170,50 @@ async function dispatchRequestInternal( await runPending(ws, true); return {}; } - // FIXME: Deprecate one of the aliases! case WalletApiOperation.PreparePayForUri: { const req = codecForPreparePayRequest().decode(payload); return await preparePayForUri(ws, req.talerPayUri); } + case WalletApiOperation.PreparePayForTemplate: { + const req = codecForPreparePayTemplateRequest().decode(payload); + const url = parsePayTemplateUri(req.talerPayTemplateUri); + const templateDetails: MerchantUsingTemplateDetails = {}; + if (!url) { + throw Error("invalid taler-template URI"); + } + if ( + url.templateParams.amount && + typeof url.templateParams.amount === "string" + ) { + templateDetails.amount = + req.templateParams.amount ?? url.templateParams.amount; + } + if ( + url.templateParams.summary && + typeof url.templateParams.summary === "string" + ) { + templateDetails.summary = + req.templateParams.summary ?? url.templateParams.summary; + } + const reqUrl = new URL( + `templates/${url.templateId}`, + url.merchantBaseUrl, + ); + const httpReq = await ws.http.postJson(reqUrl.href, templateDetails); + const resp = await readSuccessResponseJsonOrThrow( + httpReq, + codecForMerchantPostOrderResponse(), + ); + + const payUri = constructPayUri( + url.merchantBaseUrl, + resp.order_id, + "", + resp.token, + ); + + return await preparePayForUri(ws, payUri); + } case WalletApiOperation.ConfirmPay: { const req = codecForConfirmPayRequest().decode(payload); return await confirmPay(ws, req.proposalId, req.sessionId); @@ -1434,6 +1472,8 @@ async function dispatchRequestInternal( case WalletApiOperation.GetVersion: { return getVersion(ws); } + //default: + // assertUnreachable(operation); } throw TalerError.fromDetail( TalerErrorCode.WALLET_CORE_API_OPERATION_UNKNOWN, -- cgit v1.2.3