diff options
author | Sebastian <sebasjm@gmail.com> | 2024-02-15 16:59:56 -0300 |
---|---|---|
committer | Sebastian <sebasjm@gmail.com> | 2024-02-15 16:59:56 -0300 |
commit | fd45d189259cef0a3a51683bb12412cd8c6fb9eb (patch) | |
tree | 64091fbe1034a66a0caa6f4a1ef68ccbd3daab05 | |
parent | 8124d2400e7787020cca549ccc9d021eb5f75a09 (diff) | |
download | wallet-core-fd45d189259cef0a3a51683bb12412cd8c6fb9eb.tar.gz wallet-core-fd45d189259cef0a3a51683bb12412cd8c6fb9eb.tar.bz2 wallet-core-fd45d189259cef0a3a51683bb12412cd8c6fb9eb.zip |
add exchange key in the http lib and default toString for taler error
-rw-r--r-- | packages/taler-util/src/errors.ts | 17 | ||||
-rw-r--r-- | packages/taler-util/src/http-client/exchange.ts | 20 | ||||
-rw-r--r-- | packages/taler-util/src/http-client/types.ts | 424 | ||||
-rw-r--r-- | packages/taler-util/src/http-impl.node.ts | 7 | ||||
-rw-r--r-- | packages/taler-util/src/logging.ts | 8 | ||||
-rw-r--r-- | packages/taler-util/src/operation.ts | 26 | ||||
-rw-r--r-- | packages/taler-util/src/transactions-types.ts | 5 |
7 files changed, 488 insertions, 19 deletions
diff --git a/packages/taler-util/src/errors.ts b/packages/taler-util/src/errors.ts index 69990d41f..3370825f4 100644 --- a/packages/taler-util/src/errors.ts +++ b/packages/taler-util/src/errors.ts @@ -222,9 +222,11 @@ export type TalerHttpError = export class TalerError<T = any> extends Error { errorDetail: TalerErrorDetail & T; - private constructor(d: TalerErrorDetail & T) { + cause: Error | undefined; + private constructor(d: TalerErrorDetail & T, cause?: Error) { super(d.hint ?? `Error (code ${d.code})`); this.errorDetail = d; + this.cause = cause; Object.setPrototypeOf(this, TalerError.prototype); } @@ -232,21 +234,22 @@ export class TalerError<T = any> extends Error { code: C, detail: ErrBody<C>, hint?: string, + cause?: Error, ): TalerError { if (!hint) { hint = getDefaultHint(code); } const when = AbsoluteTime.now(); - return new TalerError<unknown>({ code, when, hint, ...detail }); + return new TalerError<unknown>({ code, when, hint, ...detail }, cause); } - static fromUncheckedDetail(d: TalerErrorDetail): TalerError { - return new TalerError<unknown>({ ...d }); + static fromUncheckedDetail(d: TalerErrorDetail, c?: Error): TalerError { + return new TalerError<unknown>({ ...d }, c); } static fromException(e: any): TalerError { const errDetail = getErrorDetailFromException(e); - return new TalerError(errDetail); + return new TalerError(errDetail, e); } hasErrorCode<C extends keyof DetailsMap>( @@ -254,6 +257,10 @@ export class TalerError<T = any> extends Error { ): this is TalerError<DetailsMap[C]> { return this.errorDetail.code === code; } + + toString(): string { + return `TalerError: ${JSON.stringify(this.errorDetail)}`; + } } /** diff --git a/packages/taler-util/src/http-client/exchange.ts b/packages/taler-util/src/http-client/exchange.ts index 726c28204..003410ddb 100644 --- a/packages/taler-util/src/http-client/exchange.ts +++ b/packages/taler-util/src/http-client/exchange.ts @@ -31,6 +31,7 @@ import { codecForAmlDecisionDetails, codecForAmlRecords, codecForExchangeConfig, + codecForExchangeKeys, } from "./types.js"; import { addPaginationParams } from "./utils.js"; @@ -59,7 +60,7 @@ export class TalerExchangeHttpClient { return compare?.compatible ?? false; } /** - * https://docs.taler.net/core/api-merchant.html#get--config + * https://docs.taler.net/core/api-exchange.html#get--config * */ async getConfig() { @@ -74,6 +75,23 @@ export class TalerExchangeHttpClient { return opUnknownFailure(resp, await resp.text()); } } + /** + * https://docs.taler.net/core/api-merchant.html#get--config + * + * PARTIALLY IMPLEMENTED!! + */ + async getKeys() { + const url = new URL(`keys`, this.baseUrl); + const resp = await this.httpLib.fetch(url.href, { + method: "GET", + }); + switch (resp.status) { + case HttpStatusCode.Ok: + return opSuccess(resp, codecForExchangeKeys()); + default: + return opUnknownFailure(resp, await resp.text()); + } + } // TERMS diff --git a/packages/taler-util/src/http-client/types.ts b/packages/taler-util/src/http-client/types.ts index 6c8bf4efd..05fce4a49 100644 --- a/packages/taler-util/src/http-client/types.ts +++ b/packages/taler-util/src/http-client/types.ts @@ -243,6 +243,7 @@ export interface CurrencySpecification { alt_unit_names: { [log10: string]: string }; } +//FIXME: implement this codec export const codecForAccessToken = codecForString as () => Codec<AccessToken>; export const codecForTokenSuccessResponse = (): Codec<TalerAuthentication.TokenSuccessResponse> => @@ -294,6 +295,9 @@ export const codecForCoreBankConfig = (): Codec<TalerCorebankApi.Config> => .property("wire_type", codecForString()) .build("TalerCorebankApi.Config"); +//FIXME: implement this codec +export const codecForURN = codecForString; + export const codecForMerchantConfig = (): Codec<TalerMerchantApi.VersionResponse> => buildCodecForObject<TalerMerchantApi.VersionResponse>() @@ -308,11 +312,20 @@ export const codecForExchangeConfig = buildCodecForObject<TalerExchangeApi.ExchangeVersionResponse>() .property("version", codecForString()) .property("name", codecForConstString("taler-exchange")) + .property("implementation", codecOptional(codecForURN())) .property("currency", codecForString()) .property("currency_specification", codecForCurrencySpecificiation()) .property("supported_kyc_requirements", codecForList(codecForString())) .build("TalerExchangeApi.ExchangeVersionResponse"); +export const codecForExchangeKeys = + (): Codec<TalerExchangeApi.ExchangeKeysResponse> => + buildCodecForObject<TalerExchangeApi.ExchangeKeysResponse>() + .property("version", codecForString()) + .property("base_url", codecForString()) + .property("currency", codecForString()) + .build("TalerExchangeApi.ExchangeKeysResponse"); + const codecForBalance = (): Codec<TalerCorebankApi.Balance> => buildCodecForObject<TalerCorebankApi.Balance>() .property("amount", codecForAmountString()) @@ -867,6 +880,8 @@ type Base32 = string; type DecimalNumber = string; type RsaSignature = string; +type Float = number; +type LibtoolVersion = string; // The type of a coin's blinded envelope depends on the cipher that is used // for signing with a denomination key. type CoinEnvelope = RSACoinEnvelope | CSCoinEnvelope; @@ -891,9 +906,13 @@ interface CSCoinEnvelope { // a 256-bit nonce, converted to Crockford Base32. type DenominationBlindingKeyP = string; +//FIXME: implement this codec const codecForURL = codecForString; +//FIXME: implement this codec const codecForLibtoolVersion = codecForString; +//FIXME: implement this codec const codecForCurrencyName = codecForString; +//FIXME: implement this codec const codecForDecimalNumber = codecForString; export type WithdrawalOperationStatus = @@ -1892,7 +1911,12 @@ export namespace TalerExchangeApi { // Name of the protocol. name: "taler-exchange"; - // Currency supported by this exchange. + // URN of the implementation (needed to interpret 'revision' in version). + // @since v18, may become mandatory in the future. + implementation?: string; + + // Currency supported by this exchange, given + // as a currency code ("USD" or "EUR"). currency: string; // How wallets should render this currency. @@ -1959,6 +1983,404 @@ export namespace TalerExchangeApi { // with purpose TALER_SIGNATURE_MASTER_WIRE_DETAILS. master_sig: EddsaSignature; } + + export interface ExchangeKeysResponse { + // libtool-style representation of the Exchange protocol version, see + // https://www.gnu.org/software/libtool/manual/html_node/Versioning.html#Versioning + // The format is "current:revision:age". + version: string; + + // The exchange's base URL. + base_url: string; + + // The exchange's currency or asset unit. + currency: string; + + /** + * FIXME: PARTIALLY IMPLEMENTED!! + */ + + // How wallets should render this currency. + // currency_specification: CurrencySpecification; + + // // Absolute cost offset for the STEFAN curve used + // // to (over) approximate fees payable by amount. + // stefan_abs: AmountString; + + // // Factor to multiply the logarithm of the amount + // // with to (over) approximate fees payable by amount. + // // Note that the total to be paid is first to be + // // divided by the smallest denomination to obtain + // // the value that the logarithm is to be taken of. + // stefan_log: AmountString; + + // // Linear cost factor for the STEFAN curve used + // // to (over) approximate fees payable by amount. + // // + // // Note that this is a scalar, as it is multiplied + // // with the actual amount. + // stefan_lin: Float; + + // // Type of the asset. "fiat", "crypto", "regional" + // // or "stock". Wallets should adjust their UI/UX + // // based on this value. + // asset_type: string; + + // // Array of wire accounts operated by the exchange for + // // incoming wire transfers. + // accounts: WireAccount[]; + + // // Object mapping names of wire methods (i.e. "iban" or "x-taler-bank") + // // to wire fees. + // wire_fees: { method: AggregateTransferFee[] }; + + // // List of exchanges that this exchange is partnering + // // with to enable wallet-to-wallet transfers. + // wads: ExchangePartner[]; + + // // Set to true if this exchange allows the use + // // of reserves for rewards. + // // @deprecated in protocol v18. + // rewards_allowed: false; + + // // EdDSA master public key of the exchange, used to sign entries + // // in denoms and signkeys. + // master_public_key: EddsaPublicKey; + + // // Relative duration until inactive reserves are closed; + // // not signed (!), can change without notice. + // reserve_closing_delay: RelativeTime; + + // // Threshold amounts beyond which wallet should + // // trigger the KYC process of the issuing + // // exchange. Optional option, if not given there is no limit. + // // Currency must match currency. + // wallet_balance_limit_without_kyc?: AmountString[]; + + // // Denominations offered by this exchange + // denominations: DenomGroup[]; + + // // Compact EdDSA signature (binary-only) over the + // // contatentation of all of the master_sigs (in reverse + // // chronological order by group) in the arrays under + // // "denominations". Signature of TALER_ExchangeKeySetPS + // exchange_sig: EddsaSignature; + + // // Public EdDSA key of the exchange that was used to generate the signature. + // // Should match one of the exchange's signing keys from signkeys. It is given + // // explicitly as the client might otherwise be confused by clock skew as to + // // which signing key was used for the exchange_sig. + // exchange_pub: EddsaPublicKey; + + // // Denominations for which the exchange currently offers/requests recoup. + // recoup: Recoup[]; + + // // Array of globally applicable fees by time range. + // global_fees: GlobalFees[]; + + // // The date when the denomination keys were last updated. + // list_issue_date: Timestamp; + + // // Auditors of the exchange. + // auditors: AuditorKeys[]; + + // // The exchange's signing keys. + // signkeys: SignKey[]; + + // // Optional field with a dictionary of (name, object) pairs defining the + // // supported and enabled extensions, such as age_restriction. + // extensions?: { name: ExtensionManifest }; + + // // Signature by the exchange master key of the SHA-256 hash of the + // // normalized JSON-object of field extensions, if it was set. + // // The signature has purpose TALER_SIGNATURE_MASTER_EXTENSIONS. + // extensions_sig?: EddsaSignature; + } + + interface ExtensionManifest { + // The criticality of the extension MUST be provided. It has the same + // semantics as "critical" has for extensions in X.509: + // - if "true", the client must "understand" the extension before + // proceeding, + // - if "false", clients can safely skip extensions they do not + // understand. + // (see https://datatracker.ietf.org/doc/html/rfc5280#section-4.2) + critical: boolean; + + // The version information MUST be provided in Taler's protocol version + // ranges notation, see + // https://docs.taler.net/core/api-common.html#protocol-version-ranges + version: LibtoolVersion; + + // Optional configuration object, defined by the feature itself + config?: object; + } + + interface SignKey { + // The actual exchange's EdDSA signing public key. + key: EddsaPublicKey; + + // Initial validity date for the signing key. + stamp_start: Timestamp; + + // Date when the exchange will stop using the signing key, allowed to overlap + // slightly with the next signing key's validity to allow for clock skew. + stamp_expire: Timestamp; + + // Date when all signatures made by the signing key expire and should + // henceforth no longer be considered valid in legal disputes. + stamp_end: Timestamp; + + // Signature over key and stamp_expire by the exchange master key. + // Signature of TALER_ExchangeSigningKeyValidityPS. + // Must have purpose TALER_SIGNATURE_MASTER_SIGNING_KEY_VALIDITY. + master_sig: EddsaSignature; + } + + interface AuditorKeys { + // The auditor's EdDSA signing public key. + auditor_pub: EddsaPublicKey; + + // The auditor's URL. + auditor_url: string; + + // The auditor's name (for humans). + auditor_name: string; + + // An array of denomination keys the auditor affirms with its signature. + // Note that the message only includes the hash of the public key, while the + // signature is actually over the expanded information including expiration + // times and fees. The exact format is described below. + denomination_keys: AuditorDenominationKey[]; + } + interface AuditorDenominationKey { + // Hash of the public RSA key used to sign coins of the respective + // denomination. Note that the auditor's signature covers more than just + // the hash, but this other information is already provided in denoms and + // thus not repeated here. + denom_pub_h: HashCode; + + // Signature of TALER_ExchangeKeyValidityPS. + auditor_sig: EddsaSignature; + } + + interface GlobalFees { + // What date (inclusive) does these fees go into effect? + start_date: Timestamp; + + // What date (exclusive) does this fees stop going into effect? + end_date: Timestamp; + + // Account history fee, charged when a user wants to + // obtain a reserve/account history. + history_fee: AmountString; + + // Annual fee charged for having an open account at the + // exchange. Charged to the account. If the account + // balance is insufficient to cover this fee, the account + // is automatically deleted/closed. (Note that the exchange + // will keep the account history around for longer for + // regulatory reasons.) + account_fee: AmountString; + + // Purse fee, charged only if a purse is abandoned + // and was not covered by the account limit. + purse_fee: AmountString; + + // How long will the exchange preserve the account history? + // After an account was deleted/closed, the exchange will + // retain the account history for legal reasons until this time. + history_expiration: RelativeTime; + + // Non-negative number of concurrent purses that any + // account holder is allowed to create without having + // to pay the purse_fee. + purse_account_limit: Integer; + + // How long does an exchange keep a purse around after a purse + // has expired (or been successfully merged)? A 'GET' request + // for a purse will succeed until the purse expiration time + // plus this value. + purse_timeout: RelativeTime; + + // Signature of TALER_GlobalFeesPS. + master_sig: EddsaSignature; + } + + interface Recoup { + // Hash of the public key of the denomination that is being revoked under + // emergency protocol (see /recoup). + h_denom_pub: HashCode; + + // We do not include any signature here, as the primary use-case for + // this emergency involves the exchange having lost its signing keys, + // so such a signature here would be pretty worthless. However, the + // exchange will not honor /recoup requests unless they are for + // denomination keys listed here. + } + + interface AggregateTransferFee { + // Per transfer wire transfer fee. + wire_fee: AmountString; + + // Per transfer closing fee. + closing_fee: AmountString; + + // What date (inclusive) does this fee go into effect? + // The different fees must cover the full time period in which + // any of the denomination keys are valid without overlap. + start_date: Timestamp; + + // What date (exclusive) does this fee stop going into effect? + // The different fees must cover the full time period in which + // any of the denomination keys are valid without overlap. + end_date: Timestamp; + + // Signature of TALER_MasterWireFeePS with + // purpose TALER_SIGNATURE_MASTER_WIRE_FEES. + sig: EddsaSignature; + } + + interface ExchangePartner { + // Base URL of the partner exchange. + partner_base_url: string; + + // Public master key of the partner exchange. + partner_master_pub: EddsaPublicKey; + + // Per exchange-to-exchange transfer (wad) fee. + wad_fee: AmountString; + + // Exchange-to-exchange wad (wire) transfer frequency. + wad_frequency: RelativeTime; + + // When did this partnership begin (under these conditions)? + start_date: Timestamp; + + // How long is this partnership expected to last? + end_date: Timestamp; + + // Signature using the exchange's offline key over + // TALER_WadPartnerSignaturePS + // with purpose TALER_SIGNATURE_MASTER_PARTNER_DETAILS. + master_sig: EddsaSignature; + } + + type DenomGroup = + | DenomGroupRsa + | DenomGroupCs + | DenomGroupRsaAgeRestricted + | DenomGroupCsAgeRestricted; + interface DenomGroupRsa extends DenomGroupCommon { + cipher: "RSA"; + + denoms: ({ + rsa_pub: RsaPublicKey; + } & DenomCommon)[]; + } + interface DenomGroupCs extends DenomGroupCommon { + cipher: "CS"; + + denoms: ({ + cs_pub: Cs25519Point; + } & DenomCommon)[]; + } + + // Binary representation of the age groups. + // The bits set in the mask mark the edges at the beginning of a next age + // group. F.e. for the age groups + // 0-7, 8-9, 10-11, 12-13, 14-15, 16-17, 18-21, 21-* + // the following bits are set: + // + // 31 24 16 8 0 + // | | | | | + // oooooooo oo1oo1o1 o1o1o1o1 ooooooo1 + // + // A value of 0 means that the exchange does not support the extension for + // age-restriction. + type AgeMask = Integer; + + interface DenomGroupRsaAgeRestricted extends DenomGroupCommon { + cipher: "RSA+age_restricted"; + age_mask: AgeMask; + + denoms: ({ + rsa_pub: RsaPublicKey; + } & DenomCommon)[]; + } + interface DenomGroupCsAgeRestricted extends DenomGroupCommon { + cipher: "CS+age_restricted"; + age_mask: AgeMask; + + denoms: ({ + cs_pub: Cs25519Point; + } & DenomCommon)[]; + } + // Common attributes for all denomination groups + interface DenomGroupCommon { + // How much are coins of this denomination worth? + value: AmountString; + + // Fee charged by the exchange for withdrawing a coin of this denomination. + fee_withdraw: AmountString; + + // Fee charged by the exchange for depositing a coin of this denomination. + fee_deposit: AmountString; + + // Fee charged by the exchange for refreshing a coin of this denomination. + fee_refresh: AmountString; + + // Fee charged by the exchange for refunding a coin of this denomination. + fee_refund: AmountString; + } + interface DenomCommon { + // Signature of TALER_DenominationKeyValidityPS. + master_sig: EddsaSignature; + + // When does the denomination key become valid? + stamp_start: Timestamp; + + // When is it no longer possible to withdraw coins + // of this denomination? + stamp_expire_withdraw: Timestamp; + + // When is it no longer possible to deposit coins + // of this denomination? + stamp_expire_deposit: Timestamp; + + // Timestamp indicating by when legal disputes relating to these coins must + // be settled, as the exchange will afterwards destroy its evidence relating to + // transactions involving this coin. + stamp_expire_legal: Timestamp; + + // Set to 'true' if the exchange somehow "lost" + // the private key. The denomination was not + // necessarily revoked, but still cannot be used + // to withdraw coins at this time (theoretically, + // the private key could be recovered in the + // future; coins signed with the private key + // remain valid). + lost?: boolean; + } + type DenominationKey = RsaDenominationKey | CSDenominationKey; + interface RsaDenominationKey { + cipher: "RSA"; + + // 32-bit age mask. + age_mask: Integer; + + // RSA public key + rsa_public_key: RsaPublicKey; + } + interface CSDenominationKey { + cipher: "CS"; + + // 32-bit age mask. + age_mask: Integer; + + // Public key of the denomination. + cs_public_key: Cs25519Point; + } } export namespace TalerMerchantApi { diff --git a/packages/taler-util/src/http-impl.node.ts b/packages/taler-util/src/http-impl.node.ts index 8ca2deecd..dec4e3f31 100644 --- a/packages/taler-util/src/http-impl.node.ts +++ b/packages/taler-util/src/http-impl.node.ts @@ -21,7 +21,12 @@ */ import * as net from "node:net"; import type { ClientRequest, IncomingMessage } from "node:http"; -import { FollowOptions, RedirectableRequest, http, https } from "follow-redirects"; +import { + FollowOptions, + RedirectableRequest, + http, + https, +} from "follow-redirects"; import { RequestOptions } from "node:http"; import { TalerError } from "./errors.js"; import { encodeBody, getDefaultHeaders, HttpLibArgs } from "./http-common.js"; diff --git a/packages/taler-util/src/logging.ts b/packages/taler-util/src/logging.ts index 663bc59c8..17bb184f7 100644 --- a/packages/taler-util/src/logging.ts +++ b/packages/taler-util/src/logging.ts @@ -37,7 +37,6 @@ const byTagLogLevel: Record<string, LogLevel> = {}; let nativeLogging: boolean = false; - // from https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error/toString Error.prototype.toString = function () { if ( @@ -51,14 +50,13 @@ Error.prototype.toString = function () { let msg = this.message; msg = msg === undefined ? "" : `${msg}`; - let cause = "" + let cause = ""; if ("cause" in this) { - cause = `\n Caused by: ${this.cause}` + cause = `\n Caused by: ${this.cause}`; } return `${name}: ${msg}${cause}`; }; - export function getGlobalLogLevel(): string { return globalLogLevel; } @@ -148,7 +146,7 @@ function writeNodeLog( * and uses the corresponding console.* method to log in the browser. */ export class Logger { - constructor(private tag: string) { } + constructor(private tag: string) {} shouldLogTrace(): boolean { const level = byTagLogLevel[this.tag] ?? globalLogLevel; diff --git a/packages/taler-util/src/operation.ts b/packages/taler-util/src/operation.ts index a554e1f31..02cf70196 100644 --- a/packages/taler-util/src/operation.ts +++ b/packages/taler-util/src/operation.ts @@ -30,10 +30,14 @@ import { TalerErrorDetail, } from "./index.js"; -export type OperationResult<Body, ErrorEnum> = +type OperationFailWithBodyOrNever<ErrorEnum, ErrorMap> = + ErrorEnum extends keyof ErrorMap ? OperationFailWithBody<ErrorMap> : never; + +export type OperationResult<Body, ErrorEnum, K = never> = | OperationOk<Body> - | OperationAlternative<ErrorEnum, Body> - | OperationFail<ErrorEnum>; + | OperationAlternative<ErrorEnum, any> + | OperationFail<ErrorEnum> + | OperationFailWithBodyOrNever<ErrorEnum, K>; export function isOperationOk<T, E>( c: OperationResult<T, E>, @@ -89,6 +93,15 @@ export interface OperationAlternative<T, B> { body: B; } +export interface OperationFailWithBody<B> { + type: "fail"; + + httpResp: HttpResponse; + + case: keyof B; + body: B[OperationFailWithBody<B>["case"]]; +} + export async function opSuccess<T>( resp: HttpResponse, codec: Codec<T>, @@ -109,6 +122,13 @@ export function opEmptySuccess(resp: HttpResponse): OperationOk<void> { return { type: "ok" as const, body: void 0, httpResp: resp }; } +export async function opKnownFailureWithBody<B>( + case_: keyof B, + body: B[typeof case_], +): Promise<OperationFailWithBody<B>> { + return { type: "fail", case: case_, body, httpResp: {} as any }; +} + export async function opKnownAlternativeFailure<T extends HttpStatusCode, B>( resp: HttpResponse, s: T, diff --git a/packages/taler-util/src/transactions-types.ts b/packages/taler-util/src/transactions-types.ts index 3460d2d87..4754603e6 100644 --- a/packages/taler-util/src/transactions-types.ts +++ b/packages/taler-util/src/transactions-types.ts @@ -59,9 +59,9 @@ import { export interface TransactionsRequest { /** * return only transactions in the given currency - * + * * it will be removed in next release - * + * * @deprecated use scopeInfo */ currency?: string; @@ -88,7 +88,6 @@ export interface TransactionsRequest { */ includeRefreshes?: boolean; - filterByState?: TransactionStateFilter; } |