aboutsummaryrefslogtreecommitdiff
path: root/packages/taler-util
diff options
context:
space:
mode:
authorFlorian Dold <florian@dold.me>2023-02-11 14:24:29 +0100
committerFlorian Dold <florian@dold.me>2023-02-11 14:24:29 +0100
commit04ab9f37801f6a42b85581cc79667239d3fc79e5 (patch)
tree7f5841f5a872a6374251137b75a17d00a258740e /packages/taler-util
parenta9073a67971e56dc58e8633d10c5e0c7c3920c8a (diff)
wallet-core,harness: implement pay templating
Diffstat (limited to 'packages/taler-util')
-rw-r--r--packages/taler-util/src/index.ts1
-rw-r--r--packages/taler-util/src/merchant-api-types.ts368
-rw-r--r--packages/taler-util/src/taler-types.ts15
-rw-r--r--packages/taler-util/src/taleruri.test.ts36
-rw-r--r--packages/taler-util/src/taleruri.ts65
-rw-r--r--packages/taler-util/src/wallet-types.ts11
6 files changed, 487 insertions, 9 deletions
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 <http://www.gnu.org/licenses/>
+ */
+
+/**
+ * Test harness for various GNU Taler components.
+ * Also provides a fault-injection proxy.
+ *
+ * @author Florian Dold <dold@taler.net>
+ */
+
+/**
+ * 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<MerchantContractTerms>;
+
+ // 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<MerchantPostOrderResponse> =>
+ buildCodecForObject<MerchantPostOrderResponse>()
+ .property("order_id", codecForString())
+ .property("token", codecOptional(codecForString()))
+ .build("PostOrderResponse");
+
+export const codecForMerchantRefundDetails = (): Codec<RefundDetails> =>
+ buildCodecForObject<RefundDetails>()
+ .property("reason", codecForString())
+ .property("pending", codecForBoolean())
+ .property("amount", codecForString())
+ .property("timestamp", codecForTimestamp)
+ .build("PostOrderResponse");
+
+export const codecForMerchantCheckPaymentPaidResponse =
+ (): Codec<MerchantCheckPaymentPaidResponse> =>
+ buildCodecForObject<MerchantCheckPaymentPaidResponse>()
+ .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<CheckPaymentUnpaidResponse> =>
+ buildCodecForObject<CheckPaymentUnpaidResponse>()
+ .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<CheckPaymentClaimedResponse> =>
+ buildCodecForObject<CheckPaymentClaimedResponse>()
+ .property("order_status", codecForConstString("claimed"))
+ .property("contract_terms", codecForMerchantContractTerms())
+ .build("CheckPaymentClaimedResponse");
+
+export const codecForMerchantOrderPrivateStatusResponse =
+ (): Codec<MerchantOrderPrivateStatusResponse> =>
+ buildCodecForUnion<MerchantOrderPrivateStatusResponse>()
+ .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<ExchangeWithdrawResponse> =>
.property("ev_sig", codecForBlindedDenominationSignature())
.build("WithdrawResponse");
-export const codecForWithdrawBatchResponse = (): Codec<ExchangeWithdrawBatchResponse> =>
- buildCodecForObject<ExchangeWithdrawBatchResponse>()
- .property("ev_sigs", codecForList(codecForWithdrawResponse()))
- .build("WithdrawBatchResponse");
+export const codecForWithdrawBatchResponse =
+ (): Codec<ExchangeWithdrawBatchResponse> =>
+ buildCodecForObject<ExchangeWithdrawBatchResponse>()
+ .property("ev_sigs", codecForList(codecForWithdrawResponse()))
+ .build("WithdrawBatchResponse");
export const codecForMerchantPayResponse = (): Codec<MerchantPayResponse> =>
buildCodecForObject<MerchantPayResponse>()
@@ -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<WalletKycUuid> =>
.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<string, string>;
+}
+
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<string, string> = {};
+
+ 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<PreparePayRequest> =>
.property("talerPayUri", codecForString())
.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 {
proposalId: string;
sessionId?: string;