From b13bd85215ad64e7a2764ac7e7fee5945ffa1c07 Mon Sep 17 00:00:00 2001 From: Florian Dold Date: Tue, 29 Aug 2023 09:02:16 +0200 Subject: taler-harness: remove axios usage, renovate some tests --- packages/taler-util/src/http-common.ts | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) (limited to 'packages/taler-util/src') diff --git a/packages/taler-util/src/http-common.ts b/packages/taler-util/src/http-common.ts index 4f6aaaf44..93cf9bba0 100644 --- a/packages/taler-util/src/http-common.ts +++ b/packages/taler-util/src/http-common.ts @@ -19,7 +19,12 @@ import { CancellationToken } from "./CancellationToken.js"; import { Codec } from "./codec.js"; import { j2s } from "./helpers.js"; -import { TalerError, makeErrorDetail } from "./index.js"; +import { + TalerError, + base64FromArrayBuffer, + makeErrorDetail, + stringToBytes, +} from "./index.js"; import { Logger } from "./logging.js"; import { TalerErrorCode } from "./taler-error-codes.js"; import { Duration, AbsoluteTime } from "./time.js"; @@ -306,6 +311,16 @@ export async function readSuccessResponseJsonOrThrow( throwUnexpectedRequestError(httpResponse, r.talerErrorResponse); } +export async function expectSuccessResponseOrThrow( + httpResponse: HttpResponse, +): Promise { + if (httpResponse.status >= 200 && httpResponse.status <= 299) { + return; + } + const errResp = await readTalerErrorResponse(httpResponse); + throwUnexpectedRequestError(httpResponse, errResp); +} + export async function readSuccessResponseTextOrErrorCode( httpResponse: HttpResponse, ): Promise> { @@ -452,3 +467,15 @@ export function getDefaultHeaders(method: string): Record { return headers; } + +/** + * Helper function to generate the "Authorization" HTTP header. + */ +export function makeBasicAuthHeader( + username: string, + password: string, +): string { + const auth = `${username}:${password}`; + const authEncoded: string = base64FromArrayBuffer(stringToBytes(auth)); + return `Basic ${authEncoded}`; +} -- cgit v1.2.3 From 55bdc161b58ddf4f24e32dff9acd8011a4364327 Mon Sep 17 00:00:00 2001 From: Florian Dold Date: Tue, 29 Aug 2023 09:45:45 +0200 Subject: taler-harness: add exchange-purse test --- .../src/integrationtests/test-exchange-purse.ts | 224 +++++++++++++++++++++ .../src/integrationtests/testrunner.ts | 9 +- packages/taler-util/src/payto.ts | 22 ++ packages/taler-util/src/taler-crypto.ts | 5 +- packages/taler-wallet-core/src/index.ts | 8 +- .../src/operations/pay-peer-common.ts | 22 -- .../src/operations/pay-peer-pull-credit.ts | 2 +- .../src/operations/pay-peer-push-credit.ts | 2 +- 8 files changed, 261 insertions(+), 33 deletions(-) create mode 100644 packages/taler-harness/src/integrationtests/test-exchange-purse.ts (limited to 'packages/taler-util/src') diff --git a/packages/taler-harness/src/integrationtests/test-exchange-purse.ts b/packages/taler-harness/src/integrationtests/test-exchange-purse.ts new file mode 100644 index 000000000..5a1d02692 --- /dev/null +++ b/packages/taler-harness/src/integrationtests/test-exchange-purse.ts @@ -0,0 +1,224 @@ +/* + This file is part of GNU Taler + (C) 2023 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 + */ + +/** + * Imports. + */ +import { + AbsoluteTime, + ContractTermsUtil, + decodeCrock, + Duration, + encodeCrock, + getRandomBytes, + hash, + j2s, + PeerContractTerms, + TalerError, + TalerPreciseTimestamp, +} from "@gnu-taler/taler-util"; +import { + checkReserve, + CryptoDispatcher, + downloadExchangeInfo, + EncryptContractRequest, + findDenomOrThrow, + SpendCoinDetails, + SynchronousCryptoWorkerFactoryPlain, + topupReserveWithDemobank, + Wallet, + withdrawCoin, +} from "@gnu-taler/taler-wallet-core"; +import { GlobalTestState, harnessHttpLib } from "../harness/harness.js"; +import { createSimpleTestkudosEnvironmentV2 } from "../harness/helpers.js"; + +/** + * Test the exchange's purse API. + */ +export async function runExchangePurseTest(t: GlobalTestState) { + // Set up test environment + + const { bank, exchange } = await createSimpleTestkudosEnvironmentV2(t); + + const http = harnessHttpLib; + const cryptoDisp = new CryptoDispatcher( + new SynchronousCryptoWorkerFactoryPlain(), + ); + const cryptoApi = cryptoDisp.cryptoApi; + + try { + // Withdraw digital cash into the wallet. + + const exchangeInfo = await downloadExchangeInfo(exchange.baseUrl, http); + + const reserveKeyPair = await cryptoApi.createEddsaKeypair({}); + + let reserveUrl = new URL( + `reserves/${reserveKeyPair.pub}`, + exchange.baseUrl, + ); + reserveUrl.searchParams.set("timeout_ms", "30000"); + const longpollReq = http.fetch(reserveUrl.href, { + method: "GET", + }); + + await topupReserveWithDemobank({ + amount: "TESTKUDOS:10", + http, + reservePub: reserveKeyPair.pub, + bankAccessApiBaseUrl: bank.bankAccessApiBaseUrl, + exchangeInfo, + }); + + console.log("waiting for longpoll request"); + const resp = await longpollReq; + console.log(`got response, status ${resp.status}`); + + console.log(exchangeInfo); + + await checkReserve(http, exchange.baseUrl, reserveKeyPair.pub); + + const d1 = findDenomOrThrow(exchangeInfo, "TESTKUDOS:8", { + denomselAllowLate: Wallet.defaultConfig.testing.denomselAllowLate, + }); + + const coin = await withdrawCoin({ + http, + cryptoApi, + reserveKeyPair: { + reservePriv: reserveKeyPair.priv, + reservePub: reserveKeyPair.pub, + }, + denom: d1, + exchangeBaseUrl: exchange.baseUrl, + }); + + const amount = "TESTKUDOS:5"; + const purseFee = "TESTKUDOS:0"; + + const mergeTimestamp = TalerPreciseTimestamp.now(); + + const contractTerms: PeerContractTerms = { + amount, + summary: "Hello", + purse_expiration: AbsoluteTime.toProtocolTimestamp( + AbsoluteTime.addDuration( + AbsoluteTime.now(), + Duration.fromSpec({ minutes: 1 }), + ), + ), + }; + + const mergeReservePair = await cryptoApi.createEddsaKeypair({}); + const pursePair = await cryptoApi.createEddsaKeypair({}); + const mergePair = await cryptoApi.createEddsaKeypair({}); + const contractPair = await cryptoApi.createEddsaKeypair({}); + const contractEncNonce = encodeCrock(getRandomBytes(24)); + + const pursePub = pursePair.pub; + + const hContractTerms = ContractTermsUtil.hashContractTerms(contractTerms); + + const purseSigResp = await cryptoApi.signPurseCreation({ + hContractTerms, + mergePub: mergePair.pub, + minAge: 0, + purseAmount: amount, + purseExpiration: contractTerms.purse_expiration, + pursePriv: pursePair.priv, + }); + + const coinSpend: SpendCoinDetails = { + ageCommitmentProof: undefined, + coinPriv: coin.coinPriv, + coinPub: coin.coinPub, + contribution: amount, + denomPubHash: coin.denomPubHash, + denomSig: coin.denomSig, + }; + + const depositSigsResp = await cryptoApi.signPurseDeposits({ + exchangeBaseUrl: exchange.baseUrl, + pursePub: pursePair.pub, + coins: [coinSpend], + }); + + const encryptContractRequest: EncryptContractRequest = { + contractTerms: contractTerms, + mergePriv: mergePair.priv, + pursePriv: pursePair.priv, + pursePub: pursePair.pub, + contractPriv: contractPair.priv, + contractPub: contractPair.pub, + nonce: contractEncNonce, + }; + + const econtractResp = await cryptoApi.encryptContractForMerge( + encryptContractRequest, + ); + + const econtractHash = encodeCrock( + hash(decodeCrock(econtractResp.econtract.econtract)), + ); + + const createPurseUrl = new URL( + `purses/${pursePair.pub}/create`, + exchange.baseUrl, + ); + + const reqBody = { + amount: amount, + merge_pub: mergePair.pub, + purse_sig: purseSigResp.sig, + h_contract_terms: hContractTerms, + purse_expiration: contractTerms.purse_expiration, + deposits: depositSigsResp.deposits, + min_age: 0, + econtract: econtractResp.econtract, + }; + + const httpResp = await http.fetch(createPurseUrl.href, { + method: "POST", + body: reqBody, + }); + + const respBody = await httpResp.json(); + + console.log("status", httpResp.status); + + console.log(j2s(respBody)); + + const mergeUrl = new URL(`purses/${pursePub}/merge`, exchange.baseUrl); + mergeUrl.searchParams.set("timeout_ms", "300"); + const statusResp = await http.fetch(mergeUrl.href, {}); + + const statusRespBody = await statusResp.json(); + + console.log(j2s(statusRespBody)); + + t.assertTrue(statusRespBody.merge_timestamp === undefined); + } catch (e) { + if (e instanceof TalerError) { + console.log(e); + console.log(j2s(e.errorDetail)); + } else { + console.log(e); + } + throw e; + } +} + +runExchangePurseTest.suites = ["wallet"]; diff --git a/packages/taler-harness/src/integrationtests/testrunner.ts b/packages/taler-harness/src/integrationtests/testrunner.ts index cbdca04b9..226fd6b09 100644 --- a/packages/taler-harness/src/integrationtests/testrunner.ts +++ b/packages/taler-harness/src/integrationtests/testrunner.ts @@ -14,7 +14,12 @@ GNU Taler; see the file COPYING. If not, see */ -import { CancellationToken, Logger, minimatch, setGlobalLogLevelFromString } from "@gnu-taler/taler-util"; +import { + CancellationToken, + Logger, + minimatch, + setGlobalLogLevelFromString, +} from "@gnu-taler/taler-util"; import * as child_process from "child_process"; import * as fs from "fs"; import * as os from "os"; @@ -105,6 +110,7 @@ import { runPeerRepairTest } from "./test-peer-repair.js"; import { runPaymentShareTest } from "./test-payment-share.js"; import { runSimplePaymentTest } from "./test-simple-payment.js"; import { runTermOfServiceFormatTest } from "./test-tos-format.js"; +import { runExchangePurseTest } from "./test-exchange-purse.js"; /** * Test runner. @@ -137,6 +143,7 @@ const allTests: TestMainFunction[] = [ runFeeRegressionTest, runForcedSelectionTest, runKycTest, + runExchangePurseTest, runExchangeDepositTest, runLibeufinAnastasisFacadeTest, runLibeufinApiBankaccountTest, diff --git a/packages/taler-util/src/payto.ts b/packages/taler-util/src/payto.ts index dd35b44be..2b0af4cc2 100644 --- a/packages/taler-util/src/payto.ts +++ b/packages/taler-util/src/payto.ts @@ -239,3 +239,25 @@ export function parsePaytoUri(s: string): PaytoUri | undefined { isKnown: false, }; } + +export function talerPaytoFromExchangeReserve( + exchangeBaseUrl: string, + reservePub: string, +): string { + const url = new URL(exchangeBaseUrl); + let proto: string; + if (url.protocol === "http:") { + proto = "taler-reserve-http"; + } else if (url.protocol === "https:") { + proto = "taler-reserve"; + } else { + throw Error(`unsupported exchange base URL protocol (${url.protocol})`); + } + + let path = url.pathname; + if (!path.endsWith("/")) { + path = path + "/"; + } + + return `payto://${proto}/${url.host}${url.pathname}${reservePub}`; +} diff --git a/packages/taler-util/src/taler-crypto.ts b/packages/taler-util/src/taler-crypto.ts index d52edc1e5..9425a9320 100644 --- a/packages/taler-util/src/taler-crypto.ts +++ b/packages/taler-util/src/taler-crypto.ts @@ -1004,7 +1004,7 @@ export enum TalerSignaturePurpose { SYNC_BACKUP_UPLOAD = 1450, } -export const enum WalletAccountMergeFlags { +export enum WalletAccountMergeFlags { /** * Not a legal mode! */ @@ -1281,7 +1281,8 @@ export namespace AgeRestriction { } const PublishedAgeRestrictionBaseKey: Edx25519PublicKey = decodeCrock( - "CH0VKFDZ2GWRWHQBBGEK9MWV5YDQVJ0RXEE0KYT3NMB69F0R96TG"); + "CH0VKFDZ2GWRWHQBBGEK9MWV5YDQVJ0RXEE0KYT3NMB69F0R96TG", + ); export async function restrictionCommitSeeded( ageMask: number, diff --git a/packages/taler-wallet-core/src/index.ts b/packages/taler-wallet-core/src/index.ts index 8dd06fe2b..d64f7d5e6 100644 --- a/packages/taler-wallet-core/src/index.ts +++ b/packages/taler-wallet-core/src/index.ts @@ -51,12 +51,8 @@ export * from "./operations/refresh.js"; export * from "./dbless.js"; -export { - nativeCryptoR, - nativeCrypto, - nullCrypto, - TalerCryptoInterface, -} from "./crypto/cryptoImplementation.js"; +export * from "./crypto/cryptoTypes.js"; +export * from "./crypto/cryptoImplementation.js"; export * from "./util/timer.js"; export * from "./util/denominations.js"; diff --git a/packages/taler-wallet-core/src/operations/pay-peer-common.ts b/packages/taler-wallet-core/src/operations/pay-peer-common.ts index 1bc2e8d49..4fdfecb4d 100644 --- a/packages/taler-wallet-core/src/operations/pay-peer-common.ts +++ b/packages/taler-wallet-core/src/operations/pay-peer-common.ts @@ -420,28 +420,6 @@ export const codecForExchangePurseStatus = (): Codec => .property("merge_timestamp", codecOptional(codecForTimestamp)) .build("ExchangePurseStatus"); -export function talerPaytoFromExchangeReserve( - exchangeBaseUrl: string, - reservePub: string, -): string { - const url = new URL(exchangeBaseUrl); - let proto: string; - if (url.protocol === "http:") { - proto = "taler-reserve-http"; - } else if (url.protocol === "https:") { - proto = "taler-reserve"; - } else { - throw Error(`unsupported exchange base URL protocol (${url.protocol})`); - } - - let path = url.pathname; - if (!path.endsWith("/")) { - path = path + "/"; - } - - return `payto://${proto}/${url.host}${url.pathname}${reservePub}`; -} - export async function getMergeReserveInfo( ws: InternalWalletState, req: { diff --git a/packages/taler-wallet-core/src/operations/pay-peer-pull-credit.ts b/packages/taler-wallet-core/src/operations/pay-peer-pull-credit.ts index 88b441cdd..954300264 100644 --- a/packages/taler-wallet-core/src/operations/pay-peer-pull-credit.ts +++ b/packages/taler-wallet-core/src/operations/pay-peer-pull-credit.ts @@ -45,6 +45,7 @@ import { j2s, makeErrorDetail, stringifyTalerUri, + talerPaytoFromExchangeReserve, } from "@gnu-taler/taler-util"; import { readSuccessResponseJsonOrErrorCode, @@ -74,7 +75,6 @@ import { import { codecForExchangePurseStatus, getMergeReserveInfo, - talerPaytoFromExchangeReserve, } from "./pay-peer-common.js"; import { constructTransactionIdentifier, 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 e76b934fa..c552d63f0 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 @@ -47,6 +47,7 @@ import { j2s, makeErrorDetail, parsePayPushUri, + talerPaytoFromExchangeReserve, } from "@gnu-taler/taler-util"; import { readSuccessResponseJsonOrThrow } from "@gnu-taler/taler-util/http"; import { @@ -71,7 +72,6 @@ import { updateExchangeFromUrl } from "./exchanges.js"; import { codecForExchangePurseStatus, getMergeReserveInfo, - talerPaytoFromExchangeReserve, } from "./pay-peer-common.js"; import { TransitionInfo, -- cgit v1.2.3 From ebb1c58e7a2be01e42f35dfe6d890a08cf992c79 Mon Sep 17 00:00:00 2001 From: Florian Dold Date: Tue, 29 Aug 2023 13:44:50 +0200 Subject: wallet-core: remove usage of /wire --- .../integrationtests/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 +++++++++++++--- packages/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 +- packages/taler-wallet-core/src/dev-experiments.ts | 2 +- .../src/operations/backup/index.ts | 2 +- .../taler-wallet-core/src/operations/exchanges.ts | 170 +++++++++------------ .../taler-wallet-core/src/operations/merchants.ts | 2 +- .../src/operations/pay-peer-pull-debit.ts | 2 +- .../src/operations/pay-peer-push-credit.ts | 2 +- .../taler-wallet-core/src/operations/recoup.ts | 2 +- .../taler-wallet-core/src/operations/reward.ts | 2 +- .../taler-wallet-core/src/operations/testing.ts | 2 +- .../taler-wallet-core/src/operations/withdraw.ts | 6 +- 17 files changed, 317 insertions(+), 188 deletions(-) (limited to 'packages/taler-util/src') 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( -- cgit v1.2.3