aboutsummaryrefslogtreecommitdiff
path: root/packages/taler-util
diff options
context:
space:
mode:
Diffstat (limited to 'packages/taler-util')
-rw-r--r--packages/taler-util/package.json2
-rw-r--r--packages/taler-util/src/http-client/bank-integration.ts4
-rw-r--r--packages/taler-util/src/http-client/challenger.ts16
-rw-r--r--packages/taler-util/src/http-client/types.ts61
-rw-r--r--packages/taler-util/src/http-impl.node.ts2
-rw-r--r--packages/taler-util/src/http-impl.qtart.ts15
-rw-r--r--packages/taler-util/src/notifications.ts16
-rw-r--r--packages/taler-util/src/operation.ts51
-rw-r--r--packages/taler-util/src/qr.ts7
-rw-r--r--packages/taler-util/src/taler-types.ts3
-rw-r--r--packages/taler-util/src/taleruri.test.ts12
-rw-r--r--packages/taler-util/src/taleruri.ts10
-rw-r--r--packages/taler-util/src/transactions-types.ts5
-rw-r--r--packages/taler-util/src/wallet-types.ts24
14 files changed, 191 insertions, 37 deletions
diff --git a/packages/taler-util/package.json b/packages/taler-util/package.json
index 11213cdfa..c165489b3 100644
--- a/packages/taler-util/package.json
+++ b/packages/taler-util/package.json
@@ -1,6 +1,6 @@
{
"name": "@gnu-taler/taler-util",
- "version": "0.12.1",
+ "version": "0.12.2",
"description": "Generic helper functionality for GNU Taler",
"type": "module",
"types": "./lib/index.node.d.ts",
diff --git a/packages/taler-util/src/http-client/bank-integration.ts b/packages/taler-util/src/http-client/bank-integration.ts
index 23740328b..1e0f7e79c 100644
--- a/packages/taler-util/src/http-client/bank-integration.ts
+++ b/packages/taler-util/src/http-client/bank-integration.ts
@@ -18,6 +18,7 @@ import { HttpRequestLibrary, readTalerErrorResponse } from "../http-common.js";
import { HttpStatusCode } from "../http-status-codes.js";
import { createPlatformHttpLib } from "../http.js";
import { LibtoolVersion } from "../libtool-version.js";
+import { Logger } from "../logging.js";
import {
FailCasesByMethod,
ResultByMethod,
@@ -46,6 +47,8 @@ export type TalerBankIntegrationErrorsByMethod<
prop extends keyof TalerBankIntegrationHttpClient,
> = FailCasesByMethod<TalerBankIntegrationHttpClient, prop>;
+const logger = new Logger("bank-integration.ts");
+
/**
* The API is used by the wallets.
*/
@@ -81,6 +84,7 @@ export class TalerBankIntegrationHttpClient {
case HttpStatusCode.Ok:
return opSuccessFromHttp(resp, codecForIntegrationBankConfig());
default:
+ logger.warn(`config request failed, status ${resp.status}`)
return opUnknownFailure(resp, await readTalerErrorResponse(resp));
}
}
diff --git a/packages/taler-util/src/http-client/challenger.ts b/packages/taler-util/src/http-client/challenger.ts
index e7b128b02..d2f0dd201 100644
--- a/packages/taler-util/src/http-client/challenger.ts
+++ b/packages/taler-util/src/http-client/challenger.ts
@@ -8,7 +8,7 @@ import {
opKnownAlternativeFailure,
opKnownHttpFailure,
opSuccessFromHttp,
- opUnknownFailure
+ opUnknownFailure,
} from "../operation.js";
import {
AccessToken,
@@ -19,7 +19,7 @@ import {
codecForChallengeStatus,
codecForChallengerAuthResponse,
codecForChallengerInfoResponse,
- codecForChallengerTermsOfServiceResponse
+ codecForChallengerTermsOfServiceResponse,
} from "./types.js";
import {
CacheEvictor,
@@ -131,6 +131,8 @@ export class ChallengerHttpClient {
return opKnownHttpFailure(resp.status, resp);
case HttpStatusCode.NotAcceptable:
return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.TooManyRequests:
+ return opKnownHttpFailure(resp.status, resp);
case HttpStatusCode.InternalServerError:
return opKnownHttpFailure(resp.status, resp);
default:
@@ -144,7 +146,7 @@ export class ChallengerHttpClient {
* https://docs.taler.net/core/api-challenger.html#post--challenge-$NONCE
*
*/
- async challenge(nonce: string, body: Record<"email", string>) {
+ async challenge(nonce: string, body: Record<string, string>) {
const url = new URL(`challenge/${nonce}`, this.baseUrl);
const resp = await this.httpLib.fetch(url.href, {
@@ -164,8 +166,6 @@ export class ChallengerHttpClient {
}
case HttpStatusCode.BadRequest:
return opKnownHttpFailure(resp.status, resp);
- case HttpStatusCode.Forbidden:
- return opKnownHttpFailure(resp.status, resp);
case HttpStatusCode.NotFound:
return opKnownHttpFailure(resp.status, resp);
case HttpStatusCode.NotAcceptable:
@@ -205,7 +205,11 @@ export class ChallengerHttpClient {
case HttpStatusCode.BadRequest:
return opKnownHttpFailure(resp.status, resp);
case HttpStatusCode.Forbidden:
- return opKnownHttpFailure(resp.status, resp);
+ return opKnownAlternativeFailure(
+ resp,
+ HttpStatusCode.Forbidden,
+ codecForChallengeInvalidPinResponse(),
+ );
case HttpStatusCode.NotFound:
return opKnownHttpFailure(resp.status, resp);
case HttpStatusCode.NotAcceptable:
diff --git a/packages/taler-util/src/http-client/types.ts b/packages/taler-util/src/http-client/types.ts
index 6e758773c..3816b1598 100644
--- a/packages/taler-util/src/http-client/types.ts
+++ b/packages/taler-util/src/http-client/types.ts
@@ -1567,6 +1567,14 @@ export const codecForChallengerTermsOfServiceResponse =
.property("name", codecForConstString("challenger"))
.property("version", codecForString())
.property("implementation", codecOptional(codecForString()))
+ .property("restrictions", codecOptional(codecForMap(codecForAny())))
+ .property(
+ "address_type",
+ codecForEither(
+ codecForConstString("phone"),
+ codecForConstString("email"),
+ ),
+ )
.build("ChallengerApi.ChallengerTermsOfServiceResponse");
export const codecForChallengeSetupResponse =
@@ -1578,10 +1586,13 @@ export const codecForChallengeSetupResponse =
export const codecForChallengeStatus =
(): Codec<ChallengerApi.ChallengeStatus> =>
buildCodecForObject<ChallengerApi.ChallengeStatus>()
- .property("restrictions", codecOptional(codecForMap(codecForAny())))
.property("fix_address", codecForBoolean())
+ .property("solved", codecForBoolean())
.property("last_address", codecOptional(codecForMap(codecForAny())))
.property("changes_left", codecForNumber())
+ .property("retransmission_time", codecForTimestamp)
+ .property("pin_transmissions_left", codecForNumber())
+ .property("auth_attempts_left", codecForNumber())
.build("ChallengerApi.ChallengeStatus");
export const codecForChallengeResponse =
@@ -1596,10 +1607,10 @@ export const codecForChallengeCreateResponse =
(): Codec<ChallengerApi.ChallengeCreateResponse> =>
buildCodecForObject<ChallengerApi.ChallengeCreateResponse>()
.property("attempts_left", codecForNumber())
- .property("address", codecForAny())
.property("type", codecForConstString("created"))
+ .property("address", codecForAny())
.property("transmitted", codecForBoolean())
- .property("next_tx_time", codecForString())
+ .property("retransmission_time", codecForTimestamp)
.build("ChallengerApi.ChallengeCreateResponse");
export const codecForChallengeRedirect =
@@ -5385,6 +5396,19 @@ export namespace ChallengerApi {
// URN of the implementation (needed to interpret 'revision' in version).
// @since v0, may become mandatory in the future.
implementation?: string;
+
+ // Object; map of keys (names of the fields of the address
+ // to be entered by the user) to objects with a "regex" (string)
+ // containing an extended Posix regular expression for allowed
+ // address field values, and a "hint"/"hint_i18n" giving a
+ // human-readable explanation to display if the value entered
+ // by the user does not match the regex. Keys that are not mapped
+ // to such an object have no restriction on the value provided by
+ // the user. See "ADDRESS_RESTRICTIONS" in the challenger configuration.
+ restrictions: Record<string, Restriction> | undefined;
+
+ // @since v2.
+ address_type: "email" | "phone";
}
export interface ChallengeSetupResponse {
@@ -5399,16 +5423,6 @@ export namespace ChallengerApi {
}
export interface ChallengeStatus {
- // Object; map of keys (names of the fields of the address
- // to be entered by the user) to objects with a "regex" (string)
- // containing an extended Posix regular expression for allowed
- // address field values, and a "hint"/"hint_i18n" giving a
- // human-readable explanation to display if the value entered
- // by the user does not match the regex. Keys that are not mapped
- // to such an object have no restriction on the value provided by
- // the user. See "ADDRESS_RESTRICTIONS" in the challenger configuration.
- restrictions: Record<string, Restriction> | undefined;
-
// indicates if the given address cannot be changed anymore, the
// form should be read-only if set to true.
fix_address: boolean;
@@ -5420,6 +5434,25 @@ export namespace ChallengerApi {
// number of times the address can still be changed, may or may not be
// shown to the user
changes_left: Integer;
+
+ // is the challenge already solved?
+ solved: boolean;
+
+ // when we would re-transmit the challenge the next
+ // time (at the earliest) if requested by the user
+ // only present if challenge already created
+ // @since v2
+ retransmission_time: Timestamp;
+
+ // how many times might the PIN still be retransmitted
+ // only present if challenge already created
+ // @since v2
+ pin_transmissions_left: Integer;
+
+ // how many times might the user still try entering the PIN code
+ // only present if challenge already created
+ // @since v2
+ auth_attempts_left: Integer;
}
export type ChallengeResponse = ChallengeRedirect | ChallengeCreateResponse;
@@ -5447,7 +5480,7 @@ export namespace ChallengerApi {
// timestamp explaining when we would re-transmit the challenge the next
// time (at the earliest) if requested by the user
- next_tx_time: string;
+ retransmission_time: TalerProtocolTimestamp;
}
export type ChallengeSolveResponse = ChallengeRedirect | InvalidPinResponse;
diff --git a/packages/taler-util/src/http-impl.node.ts b/packages/taler-util/src/http-impl.node.ts
index d27fd878d..fc5fe5e98 100644
--- a/packages/taler-util/src/http-impl.node.ts
+++ b/packages/taler-util/src/http-impl.node.ts
@@ -237,7 +237,7 @@ export class HttpLibImpl implements HttpRequestLibrary {
};
doCleanup();
if (SHOW_CURL_HTTP_REQUEST) {
- console.log(`TALER_API_DEBUG: ${textDecoder.decode(data)}`)
+ console.log(`TALER_API_DEBUG: ${res.statusCode} ${textDecoder.decode(data)}`)
}
resolve(resp);
});
diff --git a/packages/taler-util/src/http-impl.qtart.ts b/packages/taler-util/src/http-impl.qtart.ts
index f60c82fc3..6ccd35b83 100644
--- a/packages/taler-util/src/http-impl.qtart.ts
+++ b/packages/taler-util/src/http-impl.qtart.ts
@@ -102,8 +102,8 @@ export class HttpLibImpl implements HttpRequestLibrary {
if (opt?.headers) {
Object.entries(opt?.headers).forEach(([key, value]) => {
if (value === undefined) return;
- requestHeadersMap[key] = value
- })
+ requestHeadersMap[key] = value;
+ });
}
let headersList: string[] = [];
for (let headerName of Object.keys(requestHeadersMap)) {
@@ -115,13 +115,12 @@ export class HttpLibImpl implements HttpRequestLibrary {
const cancelPromCap = openPromise<QjsHttpResp>();
+ logger.trace(`calling qtart fetchHttp`);
+
// Just like WHATWG fetch(), the qjs http client doesn't
// really support cancellation, so cancellation here just
// means that the result is ignored!
- const {
- promise: fetchProm,
- cancelFn
- } = qjsOs.fetchHttp(url, {
+ const { promise: fetchProm, cancelFn } = qjsOs.fetchHttp(url, {
method,
data,
headers: headersList,
@@ -138,6 +137,7 @@ export class HttpLibImpl implements HttpRequestLibrary {
if (opt?.cancellationToken) {
cancelCancelledHandler = opt.cancellationToken.onCancelled(() => {
+ logger.trace(`cancelling quickjs request`);
cancelFn();
cancelPromCap.reject(new RequestCancelledError());
});
@@ -147,6 +147,7 @@ export class HttpLibImpl implements HttpRequestLibrary {
try {
res = await Promise.race([fetchProm, cancelPromCap.promise]);
} catch (e) {
+ logger.trace(`got exception while waiting for qtart http response`);
if (e instanceof RequestCancelledError) {
throw TalerError.fromDetail(
TalerErrorCode.WALLET_UNEXPECTED_REQUEST_ERROR,
@@ -172,6 +173,8 @@ export class HttpLibImpl implements HttpRequestLibrary {
throw e;
}
+ logger.trace(`got qtart http response, status ${res.status}`);
+
if (timeoutHandle != null) {
clearTimeout(timeoutHandle);
}
diff --git a/packages/taler-util/src/notifications.ts b/packages/taler-util/src/notifications.ts
index a8a8c3299..49952295a 100644
--- a/packages/taler-util/src/notifications.ts
+++ b/packages/taler-util/src/notifications.ts
@@ -24,7 +24,11 @@
*/
import { AbsoluteTime } from "./time.js";
import { TransactionState } from "./transactions-types.js";
-import { ExchangeEntryState, TalerErrorDetail } from "./wallet-types.js";
+import {
+ ExchangeEntryState,
+ TalerErrorDetail,
+ TransactionIdStr,
+} from "./wallet-types.js";
export enum NotificationType {
BalanceChange = "balance-change",
@@ -134,6 +138,12 @@ export enum ObservabilityEventType {
CryptoFinishSuccess = "crypto-finish-success",
CryptoFinishError = "crypto-finish-error",
Message = "message",
+ /**
+ * Declare that an observability event is relevant to a particular transaction.
+ * If emitted from a request/task, all past/future events for that request/task
+ * should be shown for the transaction as well.
+ */
+ DeclareConcernsTransaction = "declare-concerns-transaction",
}
export type ObservabilityEvent =
@@ -217,6 +227,10 @@ export type ObservabilityEvent =
| {
type: ObservabilityEventType.Message;
contents: string;
+ }
+ | {
+ type: ObservabilityEventType.DeclareConcernsTransaction;
+ transactionId: TransactionIdStr;
};
export interface BackupOperationErrorNotification {
diff --git a/packages/taler-util/src/operation.ts b/packages/taler-util/src/operation.ts
index e2ab9d4e4..2d17238dc 100644
--- a/packages/taler-util/src/operation.ts
+++ b/packages/taler-util/src/operation.ts
@@ -146,7 +146,10 @@ export function opKnownTalerFailure<T extends TalerErrorCode>(
return { type: "fail", case: s, detail };
}
-export function opUnknownFailure(resp: HttpResponse, error: TalerErrorDetail): never {
+export function opUnknownFailure(
+ resp: HttpResponse,
+ error: TalerErrorDetail,
+): never {
throw TalerError.fromDetail(
TalerErrorCode.WALLET_UNEXPECTED_REQUEST_ERROR,
{
@@ -179,15 +182,51 @@ export function narrowOpSuccessOrThrow<Body, ErrorEnum>(
}
}
+export async function succeedOrThrow<R, E>(
+ promise: Promise<OperationResult<R, E>>,
+): Promise<R> {
+ const resp = await promise;
+ if (isOperationOk(resp)) {
+ return resp.body;
+ }
+
+ if (isOperationFail(resp)) {
+ throw TalerError.fromUncheckedDetail({ ...resp, case: resp.case } as any);
+ }
+ throw TalerError.fromException(resp);
+}
+
+export async function failOrThrow<E>(
+ s: E,
+ promise: Promise<OperationResult<unknown, E>>,
+): Promise<TalerErrorDetail | undefined> {
+ const resp = await promise;
+ if (isOperationOk(resp)) {
+ throw TalerError.fromException(
+ new Error(`request succeed but failure "${s}" was expected`),
+ );
+ }
+ if (isOperationFail(resp) && resp.case === s) {
+ return resp.detail;
+ }
+ throw TalerError.fromException(
+ new Error(
+ `request failed with "${JSON.stringify(
+ resp,
+ )}" but case "${s}" was expected`,
+ ),
+ );
+}
+
export type ResultByMethod<
TT extends object,
p extends keyof TT,
> = TT[p] extends (...args: any[]) => infer Ret
? Ret extends Promise<infer Result>
- ? Result extends OperationResult<any, any>
- ? Result
- : never
- : never //api always use Promises
+ ? Result extends OperationResult<any, any>
+ ? Result
+ : never
+ : never //api always use Promises
: never; //error cases just for functions
export type FailCasesByMethod<TT extends object, p extends keyof TT> = Exclude<
@@ -195,4 +234,4 @@ export type FailCasesByMethod<TT extends object, p extends keyof TT> = Exclude<
OperationOk<any>
>;
-export type RedirectResult = { redirectURL: URL }
+export type RedirectResult = { redirectURL: URL };
diff --git a/packages/taler-util/src/qr.ts b/packages/taler-util/src/qr.ts
index 372291250..4d90ccf14 100644
--- a/packages/taler-util/src/qr.ts
+++ b/packages/taler-util/src/qr.ts
@@ -34,6 +34,9 @@ function encodePaytoAsSwissQrBill(paytoUri: string): EncodeResult {
return { type: "skip" };
}
const amountStr = parsedPayto.params["amount"];
+ if (amountStr === undefined) {
+ return { type: "skip" };
+ }
const iban = parsedPayto.targetPath;
const countryCode = iban.slice(0, 2);
const lines = [
@@ -105,7 +108,9 @@ function encodePaytoAsEpcQr(paytoUri: string): EncodeResult {
"", // optional BIC
parsedPayto.params["receiver-name"], // Beneficiary name
parsedPayto.targetPath, // Beneficiary IBAN
- `${Amounts.currencyOf(amountStr)}${Amounts.stringifyValue(amountStr, 2)}`, // Amount
+ amountStr !== undefined
+ ? `${Amounts.currencyOf(amountStr)}${Amounts.stringifyValue(amountStr, 2)}`
+ : "", // Amount (optional)
"", // AT-44 Purpose
parsedPayto.params["message"], // AT-05 Unstructured remittance information
];
diff --git a/packages/taler-util/src/taler-types.ts b/packages/taler-util/src/taler-types.ts
index 66f98ea9a..ac42ca278 100644
--- a/packages/taler-util/src/taler-types.ts
+++ b/packages/taler-util/src/taler-types.ts
@@ -723,6 +723,8 @@ export class ExchangeKeysJson {
currency: string;
+ currency_specification?: CurrencySpecification;
+
/**
* The exchange's master public key.
*/
@@ -1504,6 +1506,7 @@ export const codecForExchangeKeysJson = (): Codec<ExchangeKeysJson> =>
buildCodecForObject<ExchangeKeysJson>()
.property("base_url", codecForString())
.property("currency", codecForString())
+ .property("currency_specification", codecOptional(codecForCurrencySpecificiation()))
.property("master_public_key", codecForString())
.property("auditors", codecForList(codecForAuditor()))
.property("list_issue_date", codecForTimestamp)
diff --git a/packages/taler-util/src/taleruri.test.ts b/packages/taler-util/src/taleruri.test.ts
index b92366fb3..d80470dab 100644
--- a/packages/taler-util/src/taleruri.test.ts
+++ b/packages/taler-util/src/taleruri.test.ts
@@ -54,6 +54,18 @@ test("taler withdraw uri parsing", (t) => {
t.is(r1.bankIntegrationApiBaseUrl, "https://bank.example.com/");
});
+test("taler withdraw uri parsing with external confirmation", (t) => {
+ const url1 = "taler://withdraw/bank.example.com/12345?external-confirmation=1";
+ const r1 = parseWithdrawUri(url1);
+ if (!r1) {
+ t.fail();
+ return;
+ }
+ t.is(r1.externalConfirmation, true);
+ t.is(r1.withdrawalOperationId, "12345");
+ t.is(r1.bankIntegrationApiBaseUrl, "https://bank.example.com/");
+});
+
test("taler withdraw uri parsing (http)", (t) => {
const url1 = "taler+http://withdraw/bank.example.com/12345";
const r1 = parseWithdrawUri(url1);
diff --git a/packages/taler-util/src/taleruri.ts b/packages/taler-util/src/taleruri.ts
index 54b7525e3..d3186d2f5 100644
--- a/packages/taler-util/src/taleruri.ts
+++ b/packages/taler-util/src/taleruri.ts
@@ -29,6 +29,7 @@ import { opFixedSuccess, opKnownTalerFailure } from "./operation.js";
import { TalerErrorCode } from "./taler-error-codes.js";
import { AmountString } from "./taler-types.js";
import { URL, URLSearchParams } from "./url.js";
+
/**
* A parsed taler URI.
*/
@@ -89,6 +90,7 @@ export interface WithdrawUriResult {
type: TalerUriAction.Withdraw;
bankIntegrationApiBaseUrl: string;
withdrawalOperationId: string;
+ externalConfirmation?: boolean;
}
export interface RefundUriResult {
@@ -140,7 +142,12 @@ export function parseWithdrawUriWithError(s: string) {
if (pi.type === "fail") {
return pi;
}
- const parts = pi.body.rest.split("/");
+
+ const c = pi.body.rest.split("?", 2);
+ const path = c[0];
+ const q = new URLSearchParams(c[1] ?? "");
+
+ const parts = path.split("/");
if (parts.length < 2) {
return opKnownTalerFailure(TalerErrorCode.WALLET_TALER_URI_MALFORMED, {
@@ -166,6 +173,7 @@ export function parseWithdrawUriWithError(s: string) {
`${pi.body.innerProto}://${p}/`,
),
withdrawalOperationId: withdrawId,
+ externalConfirmation: q.get("external-confirmation") == "1",
};
return opFixedSuccess(result);
}
diff --git a/packages/taler-util/src/transactions-types.ts b/packages/taler-util/src/transactions-types.ts
index a6ac5aec6..b4e2738ee 100644
--- a/packages/taler-util/src/transactions-types.ts
+++ b/packages/taler-util/src/transactions-types.ts
@@ -299,6 +299,11 @@ interface WithdrawalDetailsForTalerBankIntegrationApi {
*/
reserveIsReady: boolean;
+ /**
+ * Is the bank transfer for the withdrawal externally confirmed?
+ */
+ externalConfirmation?: boolean;
+
exchangeCreditAccountDetails?: WithdrawalExchangeAccountDetails[];
}
diff --git a/packages/taler-util/src/wallet-types.ts b/packages/taler-util/src/wallet-types.ts
index 2c92d9295..ec401f3f6 100644
--- a/packages/taler-util/src/wallet-types.ts
+++ b/packages/taler-util/src/wallet-types.ts
@@ -558,11 +558,13 @@ export enum ScopeType {
}
export type ScopeInfoGlobal = { type: ScopeType.Global; currency: string };
+
export type ScopeInfoExchange = {
type: ScopeType.Exchange;
currency: string;
url: string;
};
+
export type ScopeInfoAuditor = {
type: ScopeType.Auditor;
currency: string;
@@ -571,6 +573,22 @@ export type ScopeInfoAuditor = {
export type ScopeInfo = ScopeInfoGlobal | ScopeInfoExchange | ScopeInfoAuditor;
+/**
+ * Encode scope info as a string.
+ *
+ * Format must be stable as it's used in the database.
+ */
+export function stringifyScopeInfo(si: ScopeInfo): string {
+ switch (si.type) {
+ case ScopeType.Global:
+ return `taler-si:global/${si.currency}}`;
+ case ScopeType.Auditor:
+ return `taler-si:auditor/${si.currency}/${encodeURIComponent(si.url)}`;
+ case ScopeType.Exchange:
+ return `taler-si:exchange/${si.currency}/${encodeURIComponent(si.url)}`;
+ }
+}
+
export interface BalancesResponse {
balances: WalletBalance[];
}
@@ -3439,3 +3457,9 @@ export const codecForGetQrCodesForPaytoRequest = () =>
export interface GetQrCodesForPaytoResponse {
codes: QrCodeSpec[];
}
+
+export type EmptyObject = Record<string, never>;
+
+export const codecForEmptyObject = (): Codec<EmptyObject> =>
+ buildCodecForObject<EmptyObject>()
+ .build("EmptyObject"); \ No newline at end of file