wallet-core,harness: implement pay templating

This commit is contained in:
Florian Dold 2023-02-11 14:24:29 +01:00
parent a9073a6797
commit 04ab9f3780
No known key found for this signature in database
GPG Key ID: D2E4F00F29D02A4B
12 changed files with 346 additions and 38 deletions

View File

@ -38,6 +38,7 @@ import {
hash, hash,
j2s, j2s,
Logger, Logger,
MerchantTemplateAddDetails,
parsePaytoUri, parsePaytoUri,
stringToBytes, stringToBytes,
TalerProtocolDuration, TalerProtocolDuration,
@ -66,15 +67,15 @@ import { CoinConfig } from "./denomStructures.js";
import { LibeufinNexusApi, LibeufinSandboxApi } from "./libeufin-apis.js"; import { LibeufinNexusApi, LibeufinSandboxApi } from "./libeufin-apis.js";
import { import {
codecForMerchantOrderPrivateStatusResponse, codecForMerchantOrderPrivateStatusResponse,
codecForPostOrderResponse, codecForMerchantPostOrderResponse,
MerchantInstancesResponse, MerchantInstancesResponse,
MerchantOrderPrivateStatusResponse, MerchantOrderPrivateStatusResponse,
PostOrderRequest, MerchantPostOrderRequest,
PostOrderResponse, MerchantPostOrderResponse,
TipCreateConfirmation, TipCreateConfirmation,
TipCreateRequest, TipCreateRequest,
TippingReserveStatus, TippingReserveStatus,
} from "./merchantApiTypes.js"; } from "@gnu-taler/taler-util";
import { import {
createRemoteWallet, createRemoteWallet,
getClientFromRemoteWallet, getClientFromRemoteWallet,
@ -1473,15 +1474,31 @@ export namespace MerchantPrivateApi {
export async function createOrder( export async function createOrder(
merchantService: MerchantServiceInterface, merchantService: MerchantServiceInterface,
instanceName: string, instanceName: string,
req: PostOrderRequest, req: MerchantPostOrderRequest,
withAuthorization: WithAuthorization = {}, withAuthorization: WithAuthorization = {},
): Promise<PostOrderResponse> { ): Promise<MerchantPostOrderResponse> {
const baseUrl = merchantService.makeInstanceBaseUrl(instanceName); const baseUrl = merchantService.makeInstanceBaseUrl(instanceName);
let url = new URL("private/orders", baseUrl); let url = new URL("private/orders", baseUrl);
const resp = await axios.post(url.href, req, { const resp = await axios.post(url.href, req, {
headers: withAuthorization as Record<string, string>, headers: withAuthorization as Record<string, string>,
}); });
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<string, string>,
});
if (resp.status !== 204) {
throw Error("failed to create template");
}
} }
export async function queryPrivateOrderStatus( export async function queryPrivateOrderStatus(

View File

@ -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 <http://www.gnu.org/licenses/>
*/
/**
* 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"];

View File

@ -100,6 +100,7 @@ import { runKycTest } from "./test-kyc.js";
import { runPaymentAbortTest } from "./test-payment-abort.js"; import { runPaymentAbortTest } from "./test-payment-abort.js";
import { runWithdrawalFeesTest } from "./test-withdrawal-fees.js"; import { runWithdrawalFeesTest } from "./test-withdrawal-fees.js";
import { runWalletBalanceTest } from "./test-wallet-balance.js"; import { runWalletBalanceTest } from "./test-wallet-balance.js";
import { runPaymentTemplateTest } from "./test-payment-template.js";
/** /**
* Test runner. * Test runner.
@ -163,6 +164,7 @@ const allTests: TestMainFunction[] = [
runPaymentIdempotencyTest, runPaymentIdempotencyTest,
runPaymentMultipleTest, runPaymentMultipleTest,
runPaymentTest, runPaymentTest,
runPaymentTemplateTest,
runPaymentAbortTest, runPaymentAbortTest,
runPaymentTransientTest, runPaymentTransientTest,
runPaymentZeroTest, runPaymentZeroTest,

View File

@ -21,7 +21,7 @@
"baseUrl": "./src", "baseUrl": "./src",
"typeRoots": ["./node_modules/@types"] "typeRoots": ["./node_modules/@types"]
}, },
"include": ["src/**/*"], "include": ["src/**/*", "../taler-util/src/merchant-api-types.ts"],
"references": [ "references": [
{ {
"path": "../taler-wallet-core/" "path": "../taler-wallet-core/"

View File

@ -35,3 +35,4 @@ export { RequestThrottler } from "./RequestThrottler.js";
export * from "./CancellationToken.js"; export * from "./CancellationToken.js";
export * from "./contract-terms.js"; export * from "./contract-terms.js";
export * from "./base64.js"; export * from "./base64.js";
export * from "./merchant-api-types.js";

View File

@ -26,7 +26,6 @@
*/ */
import { import {
MerchantContractTerms, MerchantContractTerms,
Duration,
Codec, Codec,
buildCodecForObject, buildCodecForObject,
codecForString, codecForString,
@ -47,7 +46,7 @@ import {
TalerProtocolTimestamp, TalerProtocolTimestamp,
} from "@gnu-taler/taler-util"; } from "@gnu-taler/taler-util";
export interface PostOrderRequest { export interface MerchantPostOrderRequest {
// The order must at least contain the minimal // The order must at least contain the minimal
// order detail, but can override all // order detail, but can override all
order: Partial<MerchantContractTerms>; order: Partial<MerchantContractTerms>;
@ -71,18 +70,18 @@ export interface PostOrderRequest {
export type ClaimToken = string; export type ClaimToken = string;
export interface PostOrderResponse { export interface MerchantPostOrderResponse {
order_id: string; order_id: string;
token?: ClaimToken; token?: ClaimToken;
} }
export const codecForPostOrderResponse = (): Codec<PostOrderResponse> => export const codecForMerchantPostOrderResponse = (): Codec<MerchantPostOrderResponse> =>
buildCodecForObject<PostOrderResponse>() buildCodecForObject<MerchantPostOrderResponse>()
.property("order_id", codecForString()) .property("order_id", codecForString())
.property("token", codecOptional(codecForString())) .property("token", codecOptional(codecForString()))
.build("PostOrderResponse"); .build("PostOrderResponse");
export const codecForRefundDetails = (): Codec<RefundDetails> => export const codecForMerchantRefundDetails = (): Codec<RefundDetails> =>
buildCodecForObject<RefundDetails>() buildCodecForObject<RefundDetails>()
.property("reason", codecForString()) .property("reason", codecForString())
.property("pending", codecForBoolean()) .property("pending", codecForBoolean())
@ -90,9 +89,9 @@ export const codecForRefundDetails = (): Codec<RefundDetails> =>
.property("timestamp", codecForTimestamp) .property("timestamp", codecForTimestamp)
.build("PostOrderResponse"); .build("PostOrderResponse");
export const codecForCheckPaymentPaidResponse = export const codecForMerchantCheckPaymentPaidResponse =
(): Codec<CheckPaymentPaidResponse> => (): Codec<MerchantCheckPaymentPaidResponse> =>
buildCodecForObject<CheckPaymentPaidResponse>() buildCodecForObject<MerchantCheckPaymentPaidResponse>()
.property("order_status_url", codecForString()) .property("order_status_url", codecForString())
.property("order_status", codecForConstString("paid")) .property("order_status", codecForConstString("paid"))
.property("refunded", codecForBoolean()) .property("refunded", codecForBoolean())
@ -128,13 +127,13 @@ export const codecForMerchantOrderPrivateStatusResponse =
(): Codec<MerchantOrderPrivateStatusResponse> => (): Codec<MerchantOrderPrivateStatusResponse> =>
buildCodecForUnion<MerchantOrderPrivateStatusResponse>() buildCodecForUnion<MerchantOrderPrivateStatusResponse>()
.discriminateOn("order_status") .discriminateOn("order_status")
.alternative("paid", codecForCheckPaymentPaidResponse()) .alternative("paid", codecForMerchantCheckPaymentPaidResponse())
.alternative("unpaid", codecForCheckPaymentUnpaidResponse()) .alternative("unpaid", codecForCheckPaymentUnpaidResponse())
.alternative("claimed", codecForCheckPaymentClaimedResponse()) .alternative("claimed", codecForCheckPaymentClaimedResponse())
.build("MerchantOrderPrivateStatusResponse"); .build("MerchantOrderPrivateStatusResponse");
export type MerchantOrderPrivateStatusResponse = export type MerchantOrderPrivateStatusResponse =
| CheckPaymentPaidResponse | MerchantCheckPaymentPaidResponse
| CheckPaymentUnpaidResponse | CheckPaymentUnpaidResponse
| CheckPaymentClaimedResponse; | CheckPaymentClaimedResponse;
@ -145,7 +144,7 @@ export interface CheckPaymentClaimedResponse {
contract_terms: MerchantContractTerms; contract_terms: MerchantContractTerms;
} }
export interface CheckPaymentPaidResponse { export interface MerchantCheckPaymentPaidResponse {
// did the customer pay for this contract // did the customer pay for this contract
order_status: "paid"; order_status: "paid";
@ -334,3 +333,36 @@ export interface MerchantInstanceDetail {
// front-ends do not have to support wallets selecting payment targets. // front-ends do not have to support wallets selecting payment targets.
payment_targets: string[]; 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;
}

View File

@ -1481,10 +1481,11 @@ export const codecForWithdrawResponse = (): Codec<ExchangeWithdrawResponse> =>
.property("ev_sig", codecForBlindedDenominationSignature()) .property("ev_sig", codecForBlindedDenominationSignature())
.build("WithdrawResponse"); .build("WithdrawResponse");
export const codecForWithdrawBatchResponse = (): Codec<ExchangeWithdrawBatchResponse> => export const codecForWithdrawBatchResponse =
buildCodecForObject<ExchangeWithdrawBatchResponse>() (): Codec<ExchangeWithdrawBatchResponse> =>
.property("ev_sigs", codecForList(codecForWithdrawResponse())) buildCodecForObject<ExchangeWithdrawBatchResponse>()
.build("WithdrawBatchResponse"); .property("ev_sigs", codecForList(codecForWithdrawResponse()))
.build("WithdrawBatchResponse");
export const codecForMerchantPayResponse = (): Codec<MerchantPayResponse> => export const codecForMerchantPayResponse = (): Codec<MerchantPayResponse> =>
buildCodecForObject<MerchantPayResponse>() buildCodecForObject<MerchantPayResponse>()
@ -1757,7 +1758,6 @@ export interface ExchangeBatchWithdrawRequest {
planchets: ExchangeWithdrawRequest[]; planchets: ExchangeWithdrawRequest[];
} }
export interface ExchangeRefreshRevealRequest { export interface ExchangeRefreshRevealRequest {
new_denoms_h: HashCodeString[]; new_denoms_h: HashCodeString[];
coin_evs: CoinEnvelope[]; coin_evs: CoinEnvelope[];
@ -2113,3 +2113,8 @@ export const codecForWalletKycUuid = (): Codec<WalletKycUuid> =>
.property("requirement_row", codecForNumber()) .property("requirement_row", codecForNumber())
.property("h_payto", codecForString()) .property("h_payto", codecForString())
.build("WalletKycUuid"); .build("WalletKycUuid");
export interface MerchantUsingTemplateDetails {
summary?: string;
amount?: AmountString;
}

View File

@ -22,6 +22,8 @@ import {
parseTipUri, parseTipUri,
parsePayPushUri, parsePayPushUri,
constructPayPushUri, constructPayPushUri,
parsePayTemplateUri,
constructPayUri,
} from "./taleruri.js"; } from "./taleruri.js";
test("taler pay url parsing: wrong scheme", (t) => { 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"); 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");
});

View File

@ -16,7 +16,6 @@
import { BackupRecovery } from "./backup-types.js"; import { BackupRecovery } from "./backup-types.js";
import { canonicalizeBaseUrl } from "./helpers.js"; import { canonicalizeBaseUrl } from "./helpers.js";
import { initNodePrng } from "./prng-node.js";
import { URLSearchParams, URL } from "./url.js"; import { URLSearchParams, URL } from "./url.js";
export interface PayUriResult { export interface PayUriResult {
@ -27,6 +26,12 @@ export interface PayUriResult {
noncePriv: string | undefined; noncePriv: string | undefined;
} }
export interface PayTemplateUriResult {
merchantBaseUrl: string;
templateId: string;
templateParams: Record<string, string>;
}
export interface WithdrawUriResult { export interface WithdrawUriResult {
bankIntegrationApiBaseUrl: string; bankIntegrationApiBaseUrl: string;
withdrawalOperationId: string; withdrawalOperationId: string;
@ -91,6 +96,7 @@ export function parseWithdrawUri(s: string): WithdrawUriResult | undefined {
export enum TalerUriType { export enum TalerUriType {
TalerPay = "taler-pay", TalerPay = "taler-pay",
TalerTemplate = "taler-template",
TalerWithdraw = "taler-withdraw", TalerWithdraw = "taler-withdraw",
TalerTip = "taler-tip", TalerTip = "taler-tip",
TalerRefund = "taler-refund", TalerRefund = "taler-refund",
@ -103,6 +109,7 @@ export enum TalerUriType {
const talerActionPayPull = "pay-pull"; const talerActionPayPull = "pay-pull";
const talerActionPayPush = "pay-push"; const talerActionPayPush = "pay-push";
const talerActionPayTemplate = "pay-template";
/** /**
* Classify a taler:// URI. * Classify a taler:// URI.
@ -121,6 +128,12 @@ export function classifyTalerUri(s: string): TalerUriType {
if (sl.startsWith("taler+http://pay/")) { if (sl.startsWith("taler+http://pay/")) {
return TalerUriType.TalerPay; 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/")) { if (sl.startsWith("taler://tip/")) {
return TalerUriType.TalerTip; 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<string, string> = {};
q.forEach((v, k) => {
params[k] = v;
});
return {
merchantBaseUrl,
templateId,
templateParams: params,
};
}
export function constructPayUri( export function constructPayUri(
merchantBaseUrl: string, merchantBaseUrl: string,
orderId: string, orderId: string,
@ -227,9 +272,21 @@ export function constructPayUri(
const url = new URL(base); const url = new URL(base);
const isHttp = base.startsWith("http://"); const isHttp = base.startsWith("http://");
let result = isHttp ? `taler+http://pay/` : `taler://pay/`; let result = isHttp ? `taler+http://pay/` : `taler://pay/`;
result += `${url.hostname}${url.pathname}${orderId}/${sessionId}?`; result += url.hostname;
if (claimToken) result += `c=${claimToken}`; if (url.port != "") {
if (noncePriv) result += `n=${noncePriv}`; 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; return result;
} }

View File

@ -1418,6 +1418,17 @@ export const codecForPreparePayRequest = (): Codec<PreparePayRequest> =>
.property("talerPayUri", codecForString()) .property("talerPayUri", codecForString())
.build("PreparePay"); .build("PreparePay");
export interface PreparePayTemplateRequest {
talerPayTemplateUri: string;
templateParams: Record<string, string>;
}
export const codecForPreparePayTemplateRequest = (): Codec<PreparePayTemplateRequest> =>
buildCodecForObject<PreparePayTemplateRequest>()
.property("talerPayTemplateUri", codecForString())
.property("templateParams", codecForAny())
.build("PreparePayTemplate");
export interface ConfirmPayRequest { export interface ConfirmPayRequest {
proposalId: string; proposalId: string;
sessionId?: string; sessionId?: string;

View File

@ -78,6 +78,7 @@ import {
PrepareDepositResponse, PrepareDepositResponse,
PreparePayRequest, PreparePayRequest,
PreparePayResult, PreparePayResult,
PreparePayTemplateRequest,
PreparePeerPullPaymentRequest, PreparePeerPullPaymentRequest,
PreparePeerPullPaymentResponse, PreparePeerPullPaymentResponse,
PreparePeerPushPaymentRequest, PreparePeerPushPaymentRequest,
@ -126,6 +127,7 @@ export enum WalletApiOperation {
WithdrawTestkudos = "withdrawTestkudos", WithdrawTestkudos = "withdrawTestkudos",
WithdrawTestBalance = "withdrawTestBalance", WithdrawTestBalance = "withdrawTestBalance",
PreparePayForUri = "preparePayForUri", PreparePayForUri = "preparePayForUri",
PreparePayForTemplate = "preparePayForTemplate",
GetContractTermsDetails = "getContractTermsDetails", GetContractTermsDetails = "getContractTermsDetails",
RunIntegrationTest = "runIntegrationTest", RunIntegrationTest = "runIntegrationTest",
TestCrypto = "testCrypto", TestCrypto = "testCrypto",
@ -313,7 +315,7 @@ export type AcceptManualWithdrawalOp = {
// group: Merchant Payments // group: Merchant Payments
/** /**
* Prepare to make a payment * Prepare to make a payment based on a taler://pay/ URI.
*/ */
export type PreparePayForUriOp = { export type PreparePayForUriOp = {
op: WalletApiOperation.PreparePayForUri; op: WalletApiOperation.PreparePayForUri;
@ -321,6 +323,15 @@ export type PreparePayForUriOp = {
response: PreparePayResult; 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 = { export type GetContractTermsDetailsOp = {
op: WalletApiOperation.GetContractTermsDetails; op: WalletApiOperation.GetContractTermsDetails;
request: GetContractTermsDetailsRequest; request: GetContractTermsDetailsRequest;
@ -835,6 +846,7 @@ export type WalletOperations = {
[WalletApiOperation.GetVersion]: GetVersionOp; [WalletApiOperation.GetVersion]: GetVersionOp;
[WalletApiOperation.WithdrawFakebank]: WithdrawFakebankOp; [WalletApiOperation.WithdrawFakebank]: WithdrawFakebankOp;
[WalletApiOperation.PreparePayForUri]: PreparePayForUriOp; [WalletApiOperation.PreparePayForUri]: PreparePayForUriOp;
[WalletApiOperation.PreparePayForTemplate]: PreparePayForTemplateOp;
[WalletApiOperation.GetContractTermsDetails]: GetContractTermsDetailsOp; [WalletApiOperation.GetContractTermsDetails]: GetContractTermsDetailsOp;
[WalletApiOperation.WithdrawTestkudos]: WithdrawTestkudosOp; [WalletApiOperation.WithdrawTestkudos]: WithdrawTestkudosOp;
[WalletApiOperation.ConfirmPay]: ConfirmPayOp; [WalletApiOperation.ConfirmPay]: ConfirmPayOp;

View File

@ -56,8 +56,10 @@ import {
codecForInitiatePeerPushPaymentRequest, codecForInitiatePeerPushPaymentRequest,
codecForIntegrationTestArgs, codecForIntegrationTestArgs,
codecForListKnownBankAccounts, codecForListKnownBankAccounts,
codecForMerchantPostOrderResponse,
codecForPrepareDepositRequest, codecForPrepareDepositRequest,
codecForPreparePayRequest, codecForPreparePayRequest,
codecForPreparePayTemplateRequest,
codecForPreparePeerPullPaymentRequest, codecForPreparePeerPullPaymentRequest,
codecForPreparePeerPushPaymentRequest, codecForPreparePeerPushPaymentRequest,
codecForPrepareRefundRequest, codecForPrepareRefundRequest,
@ -77,6 +79,7 @@ import {
CoinDumpJson, CoinDumpJson,
CoinRefreshRequest, CoinRefreshRequest,
CoinStatus, CoinStatus,
constructPayUri,
CoreApiResponse, CoreApiResponse,
DenominationInfo, DenominationInfo,
DenomOperationMap, DenomOperationMap,
@ -88,7 +91,6 @@ import {
ExchangesListResponse, ExchangesListResponse,
ExchangeTosStatusDetails, ExchangeTosStatusDetails,
FeeDescription, FeeDescription,
GetBalanceDetailRequest,
GetExchangeTosResult, GetExchangeTosResult,
InitResponse, InitResponse,
j2s, j2s,
@ -96,7 +98,9 @@ import {
KnownBankAccountsInfo, KnownBankAccountsInfo,
Logger, Logger,
ManualWithdrawalDetails, ManualWithdrawalDetails,
MerchantUsingTemplateDetails,
NotificationType, NotificationType,
parsePayTemplateUri,
parsePaytoUri, parsePaytoUri,
RefreshReason, RefreshReason,
TalerErrorCode, TalerErrorCode,
@ -156,11 +160,7 @@ import {
runBackupCycle, runBackupCycle,
} from "./operations/backup/index.js"; } from "./operations/backup/index.js";
import { setWalletDeviceId } from "./operations/backup/state.js"; import { setWalletDeviceId } from "./operations/backup/state.js";
import { import { getBalanceDetail, getBalances } from "./operations/balance.js";
getBalanceDetail,
getBalances,
getMerchantPaymentBalanceDetails,
} from "./operations/balance.js";
import { import {
getExchangeTosStatus, getExchangeTosStatus,
makeExchangeListItem, makeExchangeListItem,
@ -186,7 +186,6 @@ import {
} from "./operations/exchanges.js"; } from "./operations/exchanges.js";
import { getMerchantInfo } from "./operations/merchants.js"; import { getMerchantInfo } from "./operations/merchants.js";
import { import {
abortPay as abortPay,
applyRefund, applyRefund,
applyRefundFromPurchaseId, applyRefundFromPurchaseId,
confirmPay, confirmPay,
@ -1171,11 +1170,50 @@ async function dispatchRequestInternal<Op extends WalletApiOperation>(
await runPending(ws, true); await runPending(ws, true);
return {}; return {};
} }
// FIXME: Deprecate one of the aliases!
case WalletApiOperation.PreparePayForUri: { case WalletApiOperation.PreparePayForUri: {
const req = codecForPreparePayRequest().decode(payload); const req = codecForPreparePayRequest().decode(payload);
return await preparePayForUri(ws, req.talerPayUri); 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: { case WalletApiOperation.ConfirmPay: {
const req = codecForConfirmPayRequest().decode(payload); const req = codecForConfirmPayRequest().decode(payload);
return await confirmPay(ws, req.proposalId, req.sessionId); return await confirmPay(ws, req.proposalId, req.sessionId);
@ -1434,6 +1472,8 @@ async function dispatchRequestInternal<Op extends WalletApiOperation>(
case WalletApiOperation.GetVersion: { case WalletApiOperation.GetVersion: {
return getVersion(ws); return getVersion(ws);
} }
//default:
// assertUnreachable(operation);
} }
throw TalerError.fromDetail( throw TalerError.fromDetail(
TalerErrorCode.WALLET_CORE_API_OPERATION_UNKNOWN, TalerErrorCode.WALLET_CORE_API_OPERATION_UNKNOWN,