From ebb1c58e7a2be01e42f35dfe6d890a08cf992c79 Mon Sep 17 00:00:00 2001 From: Florian Dold Date: Tue, 29 Aug 2023 13:44:50 +0200 Subject: [PATCH] wallet-core: remove usage of /wire --- .../test-exchange-timetravel.ts | 86 ++++++--- .../test-merchant-spec-public-orders.ts | 74 ++++---- packages/taler-harness/src/lint.ts | 6 +- packages/taler-util/src/taler-types.ts | 135 ++++++++++++-- .../taler-wallet-core/src/bank-api-client.ts | 2 +- packages/taler-wallet-core/src/db.ts | 1 + packages/taler-wallet-core/src/dbless.ts | 9 +- .../taler-wallet-core/src/dev-experiments.ts | 2 +- .../src/operations/backup/index.ts | 2 +- .../src/operations/exchanges.ts | 170 ++++++++---------- .../src/operations/merchants.ts | 2 +- .../src/operations/pay-peer-pull-debit.ts | 2 +- .../src/operations/pay-peer-push-credit.ts | 2 +- .../src/operations/recoup.ts | 2 +- .../src/operations/reward.ts | 2 +- .../src/operations/testing.ts | 2 +- .../src/operations/withdraw.ts | 6 +- 17 files changed, 317 insertions(+), 188 deletions(-) diff --git a/packages/taler-harness/src/integrationtests/test-exchange-timetravel.ts b/packages/taler-harness/src/integrationtests/test-exchange-timetravel.ts index dee00d1ff..d8f8767e6 100644 --- a/packages/taler-harness/src/integrationtests/test-exchange-timetravel.ts +++ b/packages/taler-harness/src/integrationtests/test-exchange-timetravel.ts @@ -19,10 +19,16 @@ */ import { AbsoluteTime, + Amounts, codecForExchangeKeysJson, DenominationPubKey, + DenomKeyType, Duration, durationFromSpec, + encodeCrock, + ExchangeKeysJson, + hashDenomPub, + Logger, } from "@gnu-taler/taler-util"; import { createPlatformHttpLib, @@ -40,6 +46,52 @@ import { } from "../harness/harness.js"; import { withdrawViaBank } from "../harness/helpers.js"; +const logger = new Logger("test-exchange-timetravel.ts"); + +interface DenomInfo { + denomPub: DenominationPubKey; + expireDeposit: string; +} + +function getDenomInfoFromKeys(ek: ExchangeKeysJson): DenomInfo[] { + const denomInfos: DenomInfo[] = []; + for (const denomGroup of ek.denominations) { + switch (denomGroup.cipher) { + case "RSA": + case "RSA+age_restricted": { + let ageMask = 0; + if (denomGroup.cipher === "RSA+age_restricted") { + ageMask = denomGroup.age_mask; + } + for (const denomIn of denomGroup.denoms) { + const denomPub: DenominationPubKey = { + age_mask: ageMask, + cipher: DenomKeyType.Rsa, + rsa_public_key: denomIn.rsa_pub, + }; + denomInfos.push({ + denomPub, + expireDeposit: AbsoluteTime.stringify( + AbsoluteTime.fromProtocolTimestamp(denomIn.stamp_expire_deposit), + ), + }); + } + break; + } + case "CS+age_restricted": + case "CS": + logger.warn("Clause-Schnorr denominations not supported"); + continue; + default: + logger.warn( + `denomination type ${(denomGroup as any).cipher} not supported`, + ); + continue; + } + } + return denomInfos; +} + async function applyTimeTravel( timetravelDuration: Duration, s: { @@ -144,7 +196,7 @@ export async function runExchangeTimetravelTest(t: GlobalTestState) { await withdrawViaBank(t, { wallet, bank, exchange, amount: "TESTKUDOS:15" }); - const keysResp1 = await http.get(exchange.baseUrl + "keys"); + const keysResp1 = await http.fetch(exchange.baseUrl + "keys"); const keys1 = await readSuccessResponseJsonOrThrow( keysResp1, codecForExchangeKeysJson(), @@ -163,7 +215,7 @@ export async function runExchangeTimetravelTest(t: GlobalTestState) { merchant, }); - const keysResp2 = await http.get(exchange.baseUrl + "keys"); + const keysResp2 = await http.fetch(exchange.baseUrl + "keys"); const keys2 = await readSuccessResponseJsonOrThrow( keysResp2, codecForExchangeKeysJson(), @@ -173,41 +225,31 @@ export async function runExchangeTimetravelTest(t: GlobalTestState) { JSON.stringify(keys2, undefined, 2), ); - const denomPubs1 = keys1.denoms.map((x) => { - return { - denomPub: x.denom_pub, - expireDeposit: AbsoluteTime.stringify( - AbsoluteTime.fromProtocolTimestamp(x.stamp_expire_deposit), - ), - }; - }); + const denomPubs1 = getDenomInfoFromKeys(keys1); + const denomPubs2 = getDenomInfoFromKeys(keys2); - const denomPubs2 = keys2.denoms.map((x) => { - return { - denomPub: x.denom_pub, - expireDeposit: AbsoluteTime.stringify( - AbsoluteTime.fromProtocolTimestamp(x.stamp_expire_deposit), - ), - }; - }); const dps2 = new Set(denomPubs2.map((x) => x.denomPub)); console.log("=== KEYS RESPONSE 1 ==="); console.log( "list issue date", - AbsoluteTime.stringify(AbsoluteTime.fromProtocolTimestamp(keys1.list_issue_date)), + AbsoluteTime.stringify( + AbsoluteTime.fromProtocolTimestamp(keys1.list_issue_date), + ), ); - console.log("num denoms", keys1.denoms.length); + console.log("num denoms", denomPubs1.length); console.log("denoms", JSON.stringify(denomPubs1, undefined, 2)); console.log("=== KEYS RESPONSE 2 ==="); console.log( "list issue date", - AbsoluteTime.stringify(AbsoluteTime.fromProtocolTimestamp(keys2.list_issue_date)), + AbsoluteTime.stringify( + AbsoluteTime.fromProtocolTimestamp(keys2.list_issue_date), + ), ); - console.log("num denoms", keys2.denoms.length); + console.log("num denoms", denomPubs2.length); console.log("denoms", JSON.stringify(denomPubs2, undefined, 2)); for (const da of denomPubs1) { diff --git a/packages/taler-harness/src/integrationtests/test-merchant-spec-public-orders.ts b/packages/taler-harness/src/integrationtests/test-merchant-spec-public-orders.ts index fca368dad..e959e813b 100644 --- a/packages/taler-harness/src/integrationtests/test-merchant-spec-public-orders.ts +++ b/packages/taler-harness/src/integrationtests/test-merchant-spec-public-orders.ts @@ -72,7 +72,7 @@ async function testWithClaimToken( let talerPayUri: string; { - const httpResp = await httpLib.get( + const httpResp = await httpLib.fetch( new URL(`orders/${orderId}`, merchantBaseUrl).href, ); const r = await httpResp.json(); @@ -83,7 +83,7 @@ async function testWithClaimToken( { const url = new URL(`orders/${orderId}`, merchantBaseUrl); url.searchParams.set("token", claimToken); - const httpResp = await httpLib.get(url.href); + const httpResp = await httpLib.fetch(url.href); const r = await httpResp.json(); t.assertDeepEqual(httpResp.status, 402); console.log(r); @@ -94,7 +94,7 @@ async function testWithClaimToken( { const url = new URL(`orders/${orderId}`, merchantBaseUrl); url.searchParams.set("token", claimToken); - const httpResp = await httpLib.get(url.href, { + const httpResp = await httpLib.fetch(url.href, { headers: { Accept: "text/html", }, @@ -120,7 +120,7 @@ async function testWithClaimToken( const url = new URL(`orders/${orderId}`, merchantBaseUrl); const hcWrong = encodeCrock(getRandomBytes(64)); url.searchParams.set("h_contract", hcWrong); - const httpResp = await httpLib.get(url.href); + const httpResp = await httpLib.fetch(url.href); const r = await httpResp.json(); console.log(r); t.assertDeepEqual(httpResp.status, 403); @@ -131,7 +131,7 @@ async function testWithClaimToken( const url = new URL(`orders/${orderId}`, merchantBaseUrl); const ctWrong = encodeCrock(getRandomBytes(16)); url.searchParams.set("token", ctWrong); - const httpResp = await httpLib.get(url.href); + const httpResp = await httpLib.fetch(url.href); const r = await httpResp.json(); console.log(r); t.assertDeepEqual(httpResp.status, 403); @@ -141,7 +141,7 @@ async function testWithClaimToken( { const url = new URL(`orders/${orderId}`, merchantBaseUrl); url.searchParams.set("token", claimToken); - const httpResp = await httpLib.get(url.href); + const httpResp = await httpLib.fetch(url.href); const r = await httpResp.json(); console.log(r); t.assertDeepEqual(httpResp.status, 402); @@ -151,7 +151,7 @@ async function testWithClaimToken( { const url = new URL(`orders/${orderId}`, merchantBaseUrl); url.searchParams.set("h_contract", contractTermsHash); - const httpResp = await httpLib.get(url.href); + const httpResp = await httpLib.fetch(url.href); const r = await httpResp.json(); console.log(r); t.assertDeepEqual(httpResp.status, 402); @@ -160,7 +160,7 @@ async function testWithClaimToken( // claimed, unpaid, access without credentials { const url = new URL(`orders/${orderId}`, merchantBaseUrl); - const httpResp = await httpLib.get(url.href); + const httpResp = await httpLib.fetch(url.href); const r = await httpResp.json(); console.log(r); t.assertDeepEqual(httpResp.status, 202); @@ -178,7 +178,7 @@ async function testWithClaimToken( // paid, access without credentials { const url = new URL(`orders/${orderId}`, merchantBaseUrl); - const httpResp = await httpLib.get(url.href); + const httpResp = await httpLib.fetch(url.href); const r = await httpResp.json(); console.log(r); t.assertDeepEqual(httpResp.status, 202); @@ -189,7 +189,7 @@ async function testWithClaimToken( const url = new URL(`orders/${orderId}`, merchantBaseUrl); const hcWrong = encodeCrock(getRandomBytes(64)); url.searchParams.set("h_contract", hcWrong); - const httpResp = await httpLib.get(url.href); + const httpResp = await httpLib.fetch(url.href); const r = await httpResp.json(); console.log(r); t.assertDeepEqual(httpResp.status, 403); @@ -200,7 +200,7 @@ async function testWithClaimToken( const url = new URL(`orders/${orderId}`, merchantBaseUrl); const ctWrong = encodeCrock(getRandomBytes(16)); url.searchParams.set("token", ctWrong); - const httpResp = await httpLib.get(url.href); + const httpResp = await httpLib.fetch(url.href); const r = await httpResp.json(); console.log(r); t.assertDeepEqual(httpResp.status, 403); @@ -210,7 +210,7 @@ async function testWithClaimToken( { const url = new URL(`orders/${orderId}`, merchantBaseUrl); url.searchParams.set("h_contract", contractTermsHash); - const httpResp = await httpLib.get(url.href); + const httpResp = await httpLib.fetch(url.href); const r = await httpResp.json(); console.log(r); t.assertDeepEqual(httpResp.status, 200); @@ -220,7 +220,7 @@ async function testWithClaimToken( { const url = new URL(`orders/${orderId}`, merchantBaseUrl); url.searchParams.set("token", claimToken); - const httpResp = await httpLib.get(url.href); + const httpResp = await httpLib.fetch(url.href); const r = await httpResp.json(); console.log(r); t.assertDeepEqual(httpResp.status, 200); @@ -232,7 +232,7 @@ async function testWithClaimToken( { const url = new URL(`orders/${orderId}`, merchantBaseUrl); url.searchParams.set("token", claimToken); - const httpResp = await httpLib.get(url.href, { + const httpResp = await httpLib.fetch(url.href, { headers: { Accept: "text/html" }, }); t.assertDeepEqual(httpResp.status, 200); @@ -269,7 +269,7 @@ async function testWithClaimToken( { const url = new URL(`orders/${apOrderId}`, merchantBaseUrl); url.searchParams.set("token", apToken); - const httpResp = await httpLib.get(url.href); + const httpResp = await httpLib.fetch(url.href); const r = await httpResp.json(); console.log(r); t.assertDeepEqual(httpResp.status, 402); @@ -280,7 +280,7 @@ async function testWithClaimToken( const url = new URL(`orders/${apOrderId}`, merchantBaseUrl); url.searchParams.set("token", apToken); url.searchParams.set("session_id", sessionId); - const httpResp = await httpLib.get(url.href); + const httpResp = await httpLib.fetch(url.href); const r = await httpResp.json(); console.log(r); t.assertDeepEqual(httpResp.status, 402); @@ -293,7 +293,7 @@ async function testWithClaimToken( const url = new URL(`orders/${apOrderId}`, merchantBaseUrl); url.searchParams.set("token", apToken); url.searchParams.set("session_id", sessionId); - const httpResp = await httpLib.get(url.href, { + const httpResp = await httpLib.fetch(url.href, { headers: { Accept: "text/html" }, }); t.assertDeepEqual(httpResp.status, 302); @@ -326,7 +326,7 @@ async function testWithoutClaimToken( let talerPayUri: string; { - const httpResp = await httpLib.get( + const httpResp = await httpLib.fetch( new URL(`orders/${orderId}`, merchantBaseUrl).href, ); const r = await httpResp.json(); @@ -336,7 +336,7 @@ async function testWithoutClaimToken( { const url = new URL(`orders/${orderId}`, merchantBaseUrl); - const httpResp = await httpLib.get(url.href); + const httpResp = await httpLib.fetch(url.href); const r = await httpResp.json(); t.assertDeepEqual(httpResp.status, 402); console.log(r); @@ -346,7 +346,7 @@ async function testWithoutClaimToken( { const url = new URL(`orders/${orderId}`, merchantBaseUrl); - const httpResp = await httpLib.get(url.href, { + const httpResp = await httpLib.fetch(url.href, { headers: { Accept: "text/html", }, @@ -374,7 +374,7 @@ async function testWithoutClaimToken( const url = new URL(`orders/${orderId}`, merchantBaseUrl); const hcWrong = encodeCrock(getRandomBytes(64)); url.searchParams.set("h_contract", hcWrong); - const httpResp = await httpLib.get(url.href); + const httpResp = await httpLib.fetch(url.href); const r = await httpResp.json(); console.log(r); t.assertDeepEqual(httpResp.status, 403); @@ -385,7 +385,7 @@ async function testWithoutClaimToken( const url = new URL(`orders/${orderId}`, merchantBaseUrl); const ctWrong = encodeCrock(getRandomBytes(16)); url.searchParams.set("token", ctWrong); - const httpResp = await httpLib.get(url.href); + const httpResp = await httpLib.fetch(url.href); const r = await httpResp.json(); console.log(r); t.assertDeepEqual(httpResp.status, 403); @@ -394,7 +394,7 @@ async function testWithoutClaimToken( // claimed, unpaid, no claim token { const url = new URL(`orders/${orderId}`, merchantBaseUrl); - const httpResp = await httpLib.get(url.href); + const httpResp = await httpLib.fetch(url.href); const r = await httpResp.json(); console.log(r); t.assertDeepEqual(httpResp.status, 402); @@ -404,7 +404,7 @@ async function testWithoutClaimToken( { const url = new URL(`orders/${orderId}`, merchantBaseUrl); url.searchParams.set("h_contract", contractTermsHash); - const httpResp = await httpLib.get(url.href); + const httpResp = await httpLib.fetch(url.href); const r = await httpResp.json(); console.log(r); t.assertDeepEqual(httpResp.status, 402); @@ -413,7 +413,7 @@ async function testWithoutClaimToken( // claimed, unpaid, access without credentials { const url = new URL(`orders/${orderId}`, merchantBaseUrl); - const httpResp = await httpLib.get(url.href); + const httpResp = await httpLib.fetch(url.href); const r = await httpResp.json(); console.log(r); // No credentials, but the order doesn't require a claim token. @@ -434,7 +434,7 @@ async function testWithoutClaimToken( // paid, access without credentials { const url = new URL(`orders/${orderId}`, merchantBaseUrl); - const httpResp = await httpLib.get(url.href); + const httpResp = await httpLib.fetch(url.href); const r = await httpResp.json(); console.log(r); t.assertDeepEqual(httpResp.status, 200); @@ -445,7 +445,7 @@ async function testWithoutClaimToken( const url = new URL(`orders/${orderId}`, merchantBaseUrl); const hcWrong = encodeCrock(getRandomBytes(64)); url.searchParams.set("h_contract", hcWrong); - const httpResp = await httpLib.get(url.href); + const httpResp = await httpLib.fetch(url.href); const r = await httpResp.json(); console.log(r); t.assertDeepEqual(httpResp.status, 403); @@ -456,7 +456,7 @@ async function testWithoutClaimToken( const url = new URL(`orders/${orderId}`, merchantBaseUrl); const ctWrong = encodeCrock(getRandomBytes(16)); url.searchParams.set("token", ctWrong); - const httpResp = await httpLib.get(url.href); + const httpResp = await httpLib.fetch(url.href); const r = await httpResp.json(); console.log(r); t.assertDeepEqual(httpResp.status, 403); @@ -466,7 +466,7 @@ async function testWithoutClaimToken( { const url = new URL(`orders/${orderId}`, merchantBaseUrl); url.searchParams.set("h_contract", contractTermsHash); - const httpResp = await httpLib.get(url.href); + const httpResp = await httpLib.fetch(url.href); const r = await httpResp.json(); console.log(r); t.assertDeepEqual(httpResp.status, 200); @@ -475,7 +475,7 @@ async function testWithoutClaimToken( // paid, JSON { const url = new URL(`orders/${orderId}`, merchantBaseUrl); - const httpResp = await httpLib.get(url.href); + const httpResp = await httpLib.fetch(url.href); const r = await httpResp.json(); console.log(r); t.assertDeepEqual(httpResp.status, 200); @@ -486,7 +486,7 @@ async function testWithoutClaimToken( // paid, HTML { const url = new URL(`orders/${orderId}`, merchantBaseUrl); - const httpResp = await httpLib.get(url.href, { + const httpResp = await httpLib.fetch(url.href, { headers: { Accept: "text/html" }, }); t.assertDeepEqual(httpResp.status, 200); @@ -523,7 +523,7 @@ async function testWithoutClaimToken( { const url = new URL(`orders/${apOrderId}`, merchantBaseUrl); url.searchParams.set("token", apToken); - const httpResp = await httpLib.get(url.href); + const httpResp = await httpLib.fetch(url.href); const r = await httpResp.json(); console.log(r); t.assertDeepEqual(httpResp.status, 402); @@ -534,7 +534,7 @@ async function testWithoutClaimToken( const url = new URL(`orders/${apOrderId}`, merchantBaseUrl); url.searchParams.set("token", apToken); url.searchParams.set("session_id", sessionId); - const httpResp = await httpLib.get(url.href); + const httpResp = await httpLib.fetch(url.href); const r = await httpResp.json(); console.log(r); t.assertDeepEqual(httpResp.status, 402); @@ -547,7 +547,7 @@ async function testWithoutClaimToken( const url = new URL(`orders/${apOrderId}`, merchantBaseUrl); url.searchParams.set("token", apToken); url.searchParams.set("session_id", sessionId); - const httpResp = await httpLib.get(url.href, { + const httpResp = await httpLib.fetch(url.href, { headers: { Accept: "text/html" }, }); t.assertDeepEqual(httpResp.status, 302); @@ -572,14 +572,14 @@ export async function runMerchantSpecPublicOrdersTest(t: GlobalTestState) { const merchantBaseUrl = merchant.makeInstanceBaseUrl(); { - const httpResp = await httpLib.get(new URL("config", merchantBaseUrl).href); + const httpResp = await httpLib.fetch(new URL("config", merchantBaseUrl).href); const r = await httpResp.json(); console.log(r); t.assertDeepEqual(r.currency, "TESTKUDOS"); } { - const httpResp = await httpLib.get( + const httpResp = await httpLib.fetch( new URL("orders/foo", merchantBaseUrl).href, ); const r = await httpResp.json(); @@ -589,7 +589,7 @@ export async function runMerchantSpecPublicOrdersTest(t: GlobalTestState) { } { - const httpResp = await httpLib.get( + const httpResp = await httpLib.fetch( new URL("orders/foo", merchantBaseUrl).href, { headers: { diff --git a/packages/taler-harness/src/lint.ts b/packages/taler-harness/src/lint.ts index f13049710..6d8e679db 100644 --- a/packages/taler-harness/src/lint.ts +++ b/packages/taler-harness/src/lint.ts @@ -407,7 +407,7 @@ export async function checkExchangeHttpd( { const mgmtUrl = new URL("management/keys", baseUrl); - const resp = await httpLib.get(mgmtUrl.href); + const resp = await httpLib.fetch(mgmtUrl.href); const futureKeys = await readSuccessResponseJsonOrThrow( resp, @@ -431,7 +431,7 @@ export async function checkExchangeHttpd( { const keysUrl = new URL("keys", baseUrl); - const resp = await Promise.race([httpLib.get(keysUrl.href), delayMs(2000)]); + const resp = await Promise.race([httpLib.fetch(keysUrl.href), delayMs(2000)]); if (!resp) { context.numErr++; @@ -467,7 +467,7 @@ export async function checkExchangeHttpd( { const keysUrl = new URL("wire", baseUrl); - const resp = await Promise.race([httpLib.get(keysUrl.href), delayMs(2000)]); + const resp = await Promise.race([httpLib.fetch(keysUrl.href), delayMs(2000)]); if (!resp) { context.numErr++; diff --git a/packages/taler-util/src/taler-types.ts b/packages/taler-util/src/taler-types.ts index 4d4a60d91..a78df7452 100644 --- a/packages/taler-util/src/taler-types.ts +++ b/packages/taler-util/src/taler-types.ts @@ -25,7 +25,7 @@ * Imports. */ -import { codecForAmountString } from "./amounts.js"; +import { Amounts, codecForAmountString } from "./amounts.js"; import { buildCodecForObject, buildCodecForUnion, @@ -719,16 +719,12 @@ export class ExchangeSignKeyJson { * Structure that the exchange gives us in /keys. */ export class ExchangeKeysJson { - /** * Canonical, public base URL of the exchange. */ base_url: string; - /** - * List of offered denominations. - */ - denoms: ExchangeDenomination[]; + currency: string; /** * The exchange's master public key. @@ -764,6 +760,111 @@ export class ExchangeKeysJson { reserve_closing_delay: TalerProtocolDuration; global_fees: GlobalFees[]; + + accounts: AccountInfo[]; + + wire_fees: { [methodName: string]: WireFeesJson[] }; + + denominations: DenomGroup[]; +} + +export type DenomGroup = + | DenomGroupRsa + | DenomGroupCs + | DenomGroupRsaAgeRestricted + | DenomGroupCsAgeRestricted; + +export 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; + + // XOR of all the SHA-512 hash values of the denominations' public keys + // in this group. Note that for hashing, the binary format of the + // public keys is used, and not their base32 encoding. + hash: HashCodeString; +} + +export interface DenomCommon { + // Signature of TALER_DenominationKeyValidityPS. + master_sig: EddsaSignatureString; + + // When does the denomination key become valid? + stamp_start: TalerProtocolTimestamp; + + // When is it no longer possible to deposit coins + // of this denomination? + stamp_expire_withdraw: TalerProtocolTimestamp; + + // 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: TalerProtocolTimestamp; + + stamp_expire_deposit: TalerProtocolTimestamp; + + // 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; +} + +export type RsaPublicKeySring = string; +export type AgeMask = number; + +/** + * 32-byte value representing a point on Curve25519. + */ +export type Cs25519Point = string; + +export interface DenomGroupRsa extends DenomGroupCommon { + cipher: "RSA"; + + denoms: ({ + rsa_pub: RsaPublicKeySring; + } & DenomCommon)[]; +} + +export interface DenomGroupRsaAgeRestricted extends DenomGroupCommon { + cipher: "RSA+age_restricted"; + age_mask: AgeMask; + + denoms: ({ + rsa_pub: RsaPublicKeySring; + } & DenomCommon)[]; +} + +export interface DenomGroupCs extends DenomGroupCommon { + cipher: "CS"; + age_mask: AgeMask; + + denoms: ({ + cs_pub: Cs25519Point; + } & DenomCommon)[]; +} + +export interface DenomGroupCsAgeRestricted extends DenomGroupCommon { + cipher: "CS+age_restricted"; + age_mask: AgeMask; + + denoms: ({ + cs_pub: Cs25519Point; + } & DenomCommon)[]; } export interface GlobalFees { @@ -847,10 +948,10 @@ export interface AccountInfo { debit_restrictions?: any; } -export interface ExchangeWireJson { - accounts: AccountInfo[]; - fees: { [methodName: string]: WireFeesJson[] }; -} +/** + * @deprecated + */ +export interface ExchangeWireJson {} /** * Proposal returned from the contract URL. @@ -1404,10 +1505,13 @@ export const codecForGlobalFees = (): Codec => .property("master_sig", codecForString()) .build("GlobalFees"); +// FIXME: Validate properly! +export const codecForNgDenominations: Codec = codecForAny(); + export const codecForExchangeKeysJson = (): Codec => buildCodecForObject() - .property("denoms", codecForList(codecForDenomination())) .property("base_url", codecForString()) + .property("currency", codecForString()) .property("master_public_key", codecForString()) .property("auditors", codecForList(codecForAuditor())) .property("list_issue_date", codecForTimestamp) @@ -1416,6 +1520,9 @@ export const codecForExchangeKeysJson = (): Codec => .property("version", codecForString()) .property("reserve_closing_delay", codecForDuration) .property("global_fees", codecForList(codecForGlobalFees())) + .property("accounts", codecForList(codecForAccountInfo())) + .property("wire_fees", codecForMap(codecForList(codecForWireFeesJson()))) + .property("denominations", codecForList(codecForNgDenominations)) .build("ExchangeKeysJson"); export const codecForWireFeesJson = (): Codec => @@ -1436,12 +1543,6 @@ export const codecForAccountInfo = (): Codec => .property("debit_restrictions", codecForAny()) .build("AccountInfo"); -export const codecForExchangeWireJson = (): Codec => - buildCodecForObject() - .property("accounts", codecForList(codecForAccountInfo())) - .property("fees", codecForMap(codecForList(codecForWireFeesJson()))) - .build("ExchangeWireJson"); - export const codecForProposal = (): Codec => buildCodecForObject() .property("contract_terms", codecForAny()) diff --git a/packages/taler-wallet-core/src/bank-api-client.ts b/packages/taler-wallet-core/src/bank-api-client.ts index 8e351cb48..3174667f1 100644 --- a/packages/taler-wallet-core/src/bank-api-client.ts +++ b/packages/taler-wallet-core/src/bank-api-client.ts @@ -224,7 +224,7 @@ export namespace BankAccessApi { `accounts/${bankUser.username}`, bank.bankAccessApiBaseUrl, ); - const resp = await bank.http.get(url.href, { + const resp = await bank.http.fetch(url.href, { headers: { Authorization: makeBasicAuthHeader( bankUser.username, diff --git a/packages/taler-wallet-core/src/db.ts b/packages/taler-wallet-core/src/db.ts index 1d0d3a6e5..c5f8b6448 100644 --- a/packages/taler-wallet-core/src/db.ts +++ b/packages/taler-wallet-core/src/db.ts @@ -352,6 +352,7 @@ export interface DenomFees { export interface DenominationRecord { currency: string; + // FIXME: Use binary encoding of amount instead? amountVal: number; amountFrac: number; diff --git a/packages/taler-wallet-core/src/dbless.ts b/packages/taler-wallet-core/src/dbless.ts index 4dfdff3f7..5532345ae 100644 --- a/packages/taler-wallet-core/src/dbless.ts +++ b/packages/taler-wallet-core/src/dbless.ts @@ -137,7 +137,7 @@ export async function topupReserveWithDemobank( throw Error("no suggested exchange"); } const plainPaytoUris = - exchangeInfo.wire.accounts.map((x) => x.payto_uri) ?? []; + exchangeInfo.keys.accounts.map((x) => x.payto_uri) ?? []; if (plainPaytoUris.length <= 0) { throw new Error(); } @@ -338,7 +338,10 @@ export async function refreshCoin(req: { logger.info("requesting melt done"); - const meltHttpResp = await http.postJson(meltReqUrl.href, meltReqBody); + const meltHttpResp = await http.fetch(meltReqUrl.href, { + method: "POST", + body: meltReqBody, + }); const meltResponse = await readSuccessResponseJsonOrThrow( meltHttpResp, @@ -386,7 +389,7 @@ export async function createFakebankReserve(args: { exchangeInfo: ExchangeInfo; }): Promise { const { http, fakebankBaseUrl, amount, reservePub } = args; - const paytoUri = args.exchangeInfo.wire.accounts[0].payto_uri; + const paytoUri = args.exchangeInfo.keys.accounts[0].payto_uri; const pt = parsePaytoUri(paytoUri); if (!pt) { throw Error("failed to parse payto URI"); diff --git a/packages/taler-wallet-core/src/dev-experiments.ts b/packages/taler-wallet-core/src/dev-experiments.ts index 113e9bede..176ed09d9 100644 --- a/packages/taler-wallet-core/src/dev-experiments.ts +++ b/packages/taler-wallet-core/src/dev-experiments.ts @@ -70,7 +70,7 @@ export class DevExperimentHttpLib implements HttpRequestLibrary { opt?: HttpRequestOptions | undefined, ): Promise { logger.trace(`devexperiment httplib ${url}`); - return this.underlyingLib.get(url, opt); + return this.underlyingLib.fetch(url, opt); } postJson( diff --git a/packages/taler-wallet-core/src/operations/backup/index.ts b/packages/taler-wallet-core/src/operations/backup/index.ts index 236ef1e0f..e35765165 100644 --- a/packages/taler-wallet-core/src/operations/backup/index.ts +++ b/packages/taler-wallet-core/src/operations/backup/index.ts @@ -661,7 +661,7 @@ export async function addBackupProvider( } }); const termsUrl = new URL("config", canonUrl); - const resp = await ws.http.get(termsUrl.href); + const resp = await ws.http.fetch(termsUrl.href); const terms = await readSuccessResponseJsonOrThrow( resp, codecForSyncTermsOfServiceResponse(), diff --git a/packages/taler-wallet-core/src/operations/exchanges.ts b/packages/taler-wallet-core/src/operations/exchanges.ts index 8bf70fa27..c6b46e360 100644 --- a/packages/taler-wallet-core/src/operations/exchanges.ts +++ b/packages/taler-wallet-core/src/operations/exchanges.ts @@ -19,12 +19,14 @@ */ import { AbsoluteTime, + AccountInfo, Amounts, CancellationToken, canonicalizeBaseUrl, codecForExchangeKeysJson, - codecForExchangeWireJson, + DenomGroup, DenominationPubKey, + DenomKeyType, Duration, durationFromSpec, encodeCrock, @@ -51,6 +53,7 @@ import { URL, WireFee, WireFeeMap, + WireFeesJson, WireInfo, } from "@gnu-taler/taler-util"; import { @@ -84,43 +87,6 @@ import { const logger = new Logger("exchanges.ts"); -function denominationRecordFromKeys( - exchangeBaseUrl: string, - exchangeMasterPub: string, - listIssueDate: TalerProtocolTimestamp, - denomIn: ExchangeDenomination, -): DenominationRecord { - let denomPub: DenominationPubKey; - denomPub = denomIn.denom_pub; - const denomPubHash = encodeCrock(hashDenomPub(denomPub)); - const value = Amounts.parseOrThrow(denomIn.value); - const d: DenominationRecord = { - denomPub, - denomPubHash, - exchangeBaseUrl, - exchangeMasterPub, - fees: { - feeDeposit: Amounts.stringify(denomIn.fee_deposit), - feeRefresh: Amounts.stringify(denomIn.fee_refresh), - feeRefund: Amounts.stringify(denomIn.fee_refund), - feeWithdraw: Amounts.stringify(denomIn.fee_withdraw), - }, - isOffered: true, - isRevoked: false, - masterSig: denomIn.master_sig, - stampExpireDeposit: denomIn.stamp_expire_deposit, - stampExpireLegal: denomIn.stamp_expire_legal, - stampExpireWithdraw: denomIn.stamp_expire_withdraw, - stampStart: denomIn.stamp_start, - verificationStatus: DenominationVerificationStatus.Unverified, - amountFrac: value.fraction, - amountVal: value.value, - currency: value.currency, - listIssueDate, - }; - return d; -} - export function getExchangeRequestTimeout(): Duration { return Duration.fromSpec({ seconds: 5, @@ -145,7 +111,7 @@ export async function downloadExchangeWithTermsOfService( Accept: contentType, }; - const resp = await http.get(reqUrl.href, { + const resp = await http.fetch(reqUrl.href, { headers, timeout, }); @@ -241,7 +207,7 @@ export async function acceptExchangeTermsOfService( async function validateWireInfo( ws: InternalWalletState, versionCurrent: number, - wireInfo: ExchangeWireJson, + wireInfo: ExchangeKeysDownloadResult, masterPublicKey: string, ): Promise { for (const a of wireInfo.accounts) { @@ -267,9 +233,9 @@ async function validateWireInfo( } logger.trace("account validation done"); const feesForType: WireFeeMap = {}; - for (const wireMethod of Object.keys(wireInfo.fees)) { + for (const wireMethod of Object.keys(wireInfo.wireFees)) { const feeList: WireFee[] = []; - for (const x of wireInfo.fees[wireMethod]) { + for (const x of wireInfo.wireFees[wireMethod]) { const startStamp = x.start_date; const endStamp = x.end_date; const fee: WireFee = { @@ -343,7 +309,6 @@ async function validateGlobalFees( } export interface ExchangeInfo { - wire: ExchangeWireJson; keys: ExchangeKeysDownloadResult; } @@ -351,11 +316,6 @@ export async function downloadExchangeInfo( exchangeBaseUrl: string, http: HttpRequestLibrary, ): Promise { - const wireInfo = await downloadExchangeWireInfo( - exchangeBaseUrl, - http, - Duration.getForever(), - ); const keysInfo = await downloadExchangeKeysInfo( exchangeBaseUrl, http, @@ -363,33 +323,9 @@ export async function downloadExchangeInfo( ); return { keys: keysInfo, - wire: wireInfo, }; } -/** - * Fetch wire information for an exchange. - * - * @param exchangeBaseUrl Exchange base URL, assumed to be already normalized. - */ -async function downloadExchangeWireInfo( - exchangeBaseUrl: string, - http: HttpRequestLibrary, - timeout: Duration, -): Promise { - const reqUrl = new URL("wire", exchangeBaseUrl); - - const resp = await http.get(reqUrl.href, { - timeout, - }); - const wireInfo = await readSuccessResponseJsonOrThrow( - resp, - codecForExchangeWireJson(), - ); - - return wireInfo; -} - export async function provideExchangeRecordInTx( ws: InternalWalletState, tx: GetReadWriteAccess<{ @@ -434,6 +370,8 @@ interface ExchangeKeysDownloadResult { recoup: Recoup[]; listIssueDate: TalerProtocolTimestamp; globalFees: GlobalFees[]; + accounts: AccountInfo[]; + wireFees: { [methodName: string]: WireFeesJson[] }; } /** @@ -446,7 +384,7 @@ async function downloadExchangeKeysInfo( ): Promise { const keysUrl = new URL("keys", baseUrl); - const resp = await http.get(keysUrl.href, { + const resp = await http.fetch(keysUrl.href, { timeout, }); const exchangeKeysJsonUnchecked = await readSuccessResponseJsonOrThrow( @@ -454,7 +392,7 @@ async function downloadExchangeKeysInfo( codecForExchangeKeysJson(), ); - if (exchangeKeysJsonUnchecked.denoms.length === 0) { + if (exchangeKeysJsonUnchecked.denominations.length === 0) { throw TalerError.fromDetail( TalerErrorCode.WALLET_EXCHANGE_DENOMINATIONS_INSUFFICIENT, { @@ -481,23 +419,72 @@ async function downloadExchangeKeysInfo( ); } - const currency = Amounts.parseOrThrow( - exchangeKeysJsonUnchecked.denoms[0].value, - ).currency.toUpperCase(); + const currency = exchangeKeysJsonUnchecked.currency; + + const currentDenominations: DenominationRecord[] = []; + + for (const denomGroup of exchangeKeysJsonUnchecked.denominations) { + switch (denomGroup.cipher) { + case "RSA": + case "RSA+age_restricted": { + let ageMask = 0; + if (denomGroup.cipher === "RSA+age_restricted") { + ageMask = denomGroup.age_mask; + } + for (const denomIn of denomGroup.denoms) { + const denomPub: DenominationPubKey = { + age_mask: ageMask, + cipher: DenomKeyType.Rsa, + rsa_public_key: denomIn.rsa_pub, + }; + const denomPubHash = encodeCrock(hashDenomPub(denomPub)); + const value = Amounts.parseOrThrow(denomGroup.value); + const rec: DenominationRecord = { + denomPub, + denomPubHash, + exchangeBaseUrl: baseUrl, + exchangeMasterPub: exchangeKeysJsonUnchecked.master_public_key, + isOffered: true, + isRevoked: false, + amountFrac: value.fraction, + amountVal: value.value, + currency: value.currency, + stampExpireDeposit: denomIn.stamp_expire_deposit, + stampExpireLegal: denomIn.stamp_expire_legal, + stampExpireWithdraw: denomIn.stamp_expire_withdraw, + stampStart: denomIn.stamp_start, + verificationStatus: DenominationVerificationStatus.Unverified, + masterSig: denomIn.master_sig, + listIssueDate: exchangeKeysJsonUnchecked.list_issue_date, + fees: { + feeDeposit: Amounts.stringify(denomGroup.fee_deposit), + feeRefresh: Amounts.stringify(denomGroup.fee_refresh), + feeRefund: Amounts.stringify(denomGroup.fee_refund), + feeWithdraw: Amounts.stringify(denomGroup.fee_withdraw), + }, + }; + currentDenominations.push(rec); + } + break; + } + case "CS+age_restricted": + case "CS": + logger.warn("Clause-Schnorr denominations not supported"); + continue; + default: + logger.warn( + `denomination type ${(denomGroup as any).cipher} not supported`, + ); + continue; + } + } return { masterPublicKey: exchangeKeysJsonUnchecked.master_public_key, currency, baseUrl: exchangeKeysJsonUnchecked.base_url, auditors: exchangeKeysJsonUnchecked.auditors, - currentDenominations: exchangeKeysJsonUnchecked.denoms.map((d) => - denominationRecordFromKeys( - baseUrl, - exchangeKeysJsonUnchecked.master_public_key, - exchangeKeysJsonUnchecked.list_issue_date, - d, - ), - ), + currentDenominations, protocolVersion: exchangeKeysJsonUnchecked.version, signingKeys: exchangeKeysJsonUnchecked.signkeys, reserveClosingDelay: exchangeKeysJsonUnchecked.reserve_closing_delay, @@ -509,6 +496,8 @@ async function downloadExchangeKeysInfo( recoup: exchangeKeysJsonUnchecked.recoup ?? [], listIssueDate: exchangeKeysJsonUnchecked.list_issue_date, globalFees: exchangeKeysJsonUnchecked.global_fees, + accounts: exchangeKeysJsonUnchecked.accounts, + wireFees: exchangeKeysJsonUnchecked.wire_fees, }; } @@ -654,14 +643,7 @@ export async function updateExchangeFromUrlHandler( } } - logger.trace("updating exchange /wire info"); - const wireInfoDownload = await downloadExchangeWireInfo( - exchangeBaseUrl, - ws.http, - timeout, - ); - - logger.trace("validating exchange /wire info"); + logger.trace("validating exchange wire info"); const version = LibtoolVersion.parseVersion(keysInfo.protocolVersion); if (!version) { @@ -672,7 +654,7 @@ export async function updateExchangeFromUrlHandler( const wireInfo = await validateWireInfo( ws, version.current, - wireInfoDownload, + keysInfo, keysInfo.masterPublicKey, ); diff --git a/packages/taler-wallet-core/src/operations/merchants.ts b/packages/taler-wallet-core/src/operations/merchants.ts index c47ec4a0a..a148953f0 100644 --- a/packages/taler-wallet-core/src/operations/merchants.ts +++ b/packages/taler-wallet-core/src/operations/merchants.ts @@ -41,7 +41,7 @@ export async function getMerchantInfo( } const configUrl = new URL("config", canonBaseUrl); - const resp = await ws.http.get(configUrl.href); + const resp = await ws.http.fetch(configUrl.href); const configResp = await readSuccessResponseJsonOrThrow( resp, diff --git a/packages/taler-wallet-core/src/operations/pay-peer-pull-debit.ts b/packages/taler-wallet-core/src/operations/pay-peer-pull-debit.ts index eca3bc91b..8ba84585c 100644 --- a/packages/taler-wallet-core/src/operations/pay-peer-pull-debit.ts +++ b/packages/taler-wallet-core/src/operations/pay-peer-pull-debit.ts @@ -530,7 +530,7 @@ export async function preparePeerPullDebit( const getContractUrl = new URL(`contracts/${contractPub}`, exchangeBaseUrl); - const contractHttpResp = await ws.http.get(getContractUrl.href); + const contractHttpResp = await ws.http.fetch(getContractUrl.href); const contractResp = await readSuccessResponseJsonOrThrow( contractHttpResp, diff --git a/packages/taler-wallet-core/src/operations/pay-peer-push-credit.ts b/packages/taler-wallet-core/src/operations/pay-peer-push-credit.ts index c552d63f0..47e9eaddd 100644 --- a/packages/taler-wallet-core/src/operations/pay-peer-push-credit.ts +++ b/packages/taler-wallet-core/src/operations/pay-peer-push-credit.ts @@ -165,7 +165,7 @@ export async function preparePeerPushCredit( const getPurseUrl = new URL(`purses/${pursePub}/deposit`, exchangeBaseUrl); - const purseHttpResp = await ws.http.get(getPurseUrl.href); + const purseHttpResp = await ws.http.fetch(getPurseUrl.href); const purseStatus = await readSuccessResponseJsonOrThrow( purseHttpResp, diff --git a/packages/taler-wallet-core/src/operations/recoup.ts b/packages/taler-wallet-core/src/operations/recoup.ts index abeca1119..6a18e5de6 100644 --- a/packages/taler-wallet-core/src/operations/recoup.ts +++ b/packages/taler-wallet-core/src/operations/recoup.ts @@ -358,7 +358,7 @@ export async function processRecoupGroup( ); logger.info(`querying reserve status for recoup via ${reserveUrl}`); - const resp = await ws.http.get(reserveUrl.href); + const resp = await ws.http.fetch(reserveUrl.href); const result = await readSuccessResponseJsonOrThrow( resp, diff --git a/packages/taler-wallet-core/src/operations/reward.ts b/packages/taler-wallet-core/src/operations/reward.ts index 47956f15f..69c888d7a 100644 --- a/packages/taler-wallet-core/src/operations/reward.ts +++ b/packages/taler-wallet-core/src/operations/reward.ts @@ -161,7 +161,7 @@ export async function prepareTip( res.merchantBaseUrl, ); logger.trace("checking tip status from", tipStatusUrl.href); - const merchantResp = await ws.http.get(tipStatusUrl.href); + const merchantResp = await ws.http.fetch(tipStatusUrl.href); const tipPickupStatus = await readSuccessResponseJsonOrThrow( merchantResp, codecForTipPickupGetResponse(), diff --git a/packages/taler-wallet-core/src/operations/testing.ts b/packages/taler-wallet-core/src/operations/testing.ts index 3090549d5..aff92622a 100644 --- a/packages/taler-wallet-core/src/operations/testing.ts +++ b/packages/taler-wallet-core/src/operations/testing.ts @@ -293,7 +293,7 @@ async function checkPayment( ): Promise { const reqUrl = new URL(`private/orders/${orderId}`, merchantBackend.baseUrl); reqUrl.searchParams.set("order_id", orderId); - const resp = await http.get(reqUrl.href, { + const resp = await http.fetch(reqUrl.href, { headers: getMerchantAuthHeader(merchantBackend), }); return readSuccessResponseJsonOrThrow(resp, codecForCheckPaymentResponse()); diff --git a/packages/taler-wallet-core/src/operations/withdraw.ts b/packages/taler-wallet-core/src/operations/withdraw.ts index 673129928..040d191e1 100644 --- a/packages/taler-wallet-core/src/operations/withdraw.ts +++ b/packages/taler-wallet-core/src/operations/withdraw.ts @@ -554,7 +554,7 @@ export async function getBankWithdrawalInfo( const configReqUrl = new URL("config", uriResult.bankIntegrationApiBaseUrl); - const configResp = await http.get(configReqUrl.href); + const configResp = await http.fetch(configReqUrl.href); const config = await readSuccessResponseJsonOrThrow( configResp, codecForTalerConfigResponse(), @@ -582,7 +582,7 @@ export async function getBankWithdrawalInfo( logger.info(`bank withdrawal status URL: ${reqUrl.href}}`); - const resp = await http.get(reqUrl.href); + const resp = await http.fetch(reqUrl.href); const status = await readSuccessResponseJsonOrThrow( resp, codecForWithdrawOperationStatusResponse(), @@ -2098,7 +2098,7 @@ async function processReserveBankStatus( const bankStatusUrl = getBankStatusUrl(bankInfo.talerWithdrawUri); - const statusResp = await ws.http.get(bankStatusUrl, { + const statusResp = await ws.http.fetch(bankStatusUrl, { timeout: getReserveRequestTimeout(withdrawalGroup), }); const status = await readSuccessResponseJsonOrThrow(