commit 6a6fed5646a3ac085e223a0e112f94be0147885d parent 0a34f2b7afe2c4162223e09a0061e57388d94e52 Author: Antoine A <> Date: Thu, 24 Apr 2025 19:28:16 +0200 wallet: P2P migrate to TalerExchangeHttpClient and improve logic Diffstat:
13 files changed, 458 insertions(+), 345 deletions(-)
diff --git a/packages/taler-harness/src/integrationtests/test-peer-pull.ts b/packages/taler-harness/src/integrationtests/test-peer-pull.ts @@ -149,7 +149,7 @@ export async function runPeerPullTest(t: GlobalTestState) { { talerUri: "taler+http://pay-pull/localhost:8081/MQP1DP1J94ZZWNQS7TRDF1KJZ7V8H74CZF41V90FKXBPN5GNRN6G" } )); // FIXME this should fail with a proper error code - t.assertTrue(unknown_purse.errorDetail.code === TalerErrorCode.WALLET_UNEXPECTED_REQUEST_ERROR); + t.assertTrue(unknown_purse.errorDetail.code === TalerErrorCode.WALLET_UNEXPECTED_EXCEPTION); } t.logStep("P2P pull confirm"); @@ -362,7 +362,7 @@ export async function runPeerPullTest(t: GlobalTestState) { { talerUri: tx.talerUri! } )); // FIXME this should fail with a proper error code - t.assertTrue(aborted_contract.errorDetail.code === TalerErrorCode.WALLET_UNEXPECTED_REQUEST_ERROR); + t.assertTrue(aborted_contract.errorDetail.code === TalerErrorCode.WALLET_UNEXPECTED_EXCEPTION); } t.logStep("P2P pull expire"); diff --git a/packages/taler-harness/src/integrationtests/test-peer-push.ts b/packages/taler-harness/src/integrationtests/test-peer-push.ts @@ -145,7 +145,7 @@ export async function runPeerPushTest(t: GlobalTestState) { { talerUri: "taler+http://pay-push/localhost:8081/MQP1DP1J94ZZWNQS7TRDF1KJZ7V8H74CZF41V90FKXBPN5GNRN6G" } )); // FIXME this should fail with a proper error code - t.assertTrue(unknown_purse.errorDetail.code === TalerErrorCode.WALLET_UNEXPECTED_REQUEST_ERROR); + t.assertTrue(unknown_purse.errorDetail.code === TalerErrorCode.WALLET_UNEXPECTED_EXCEPTION); } t.logStep("P2P push confirm"); diff --git a/packages/taler-util/src/http-client/exchange.ts b/packages/taler-util/src/http-client/exchange.ts @@ -52,6 +52,7 @@ import { AccountKycStatus, AmlDecisionRequest, BatchWithdrawResponse, + ExchangeGetContractResponse, ExchangeKycUploadFormRequest, ExchangeLegacyBatchWithdrawRequest, ExchangeMergeConflictResponse, @@ -63,6 +64,8 @@ import { ExchangeVersionResponse, KycRequirementInformationId, LegitimizationNeededResponse, + PurseConflict, + PurseConflictPartial, WalletKycRequest, codecForAccountKycStatus, codecForAmlDecisionsResponse, @@ -71,6 +74,7 @@ import { codecForAvailableMeasureSummary, codecForEventCounter, codecForExchangeConfig, + codecForExchangeGetContractResponse, codecForExchangeKeysResponse, codecForExchangeMergeConflictResponse, codecForExchangeMergeSuccessResponse, @@ -79,6 +83,8 @@ import { codecForKycProcessClientInformation, codecForKycProcessStartInformation, codecForLegitimizationNeededResponse, + codecForPurseConflict, + codecForPurseConflictPartial, } from "../types-taler-exchange.js"; import { CacheEvictor, addPaginationParams, nullEvictor } from "./utils.js"; @@ -95,7 +101,7 @@ import { } from "../index.js"; import { TalerErrorCode } from "../taler-error-codes.js"; import { AbsoluteTime } from "../time.js"; -import { codecForEmptyObject, TalerErrorDetail } from "../types-taler-wallet.js"; +import { codecForEmptyObject } from "../types-taler-wallet.js"; export type TalerExchangeResultByMethod< prop extends keyof TalerExchangeHttpClient, @@ -613,8 +619,36 @@ export class TalerExchangeHttpClient { * https://docs.taler.net/core/api-exchange.html#post--purses-$PURSE_PUB-create * */ - async createPurseFromDeposit(): Promise<never> { - throw Error("not yet implemented"); + async createPurseFromDeposit( + pursePub: string, + body: any // FIXME + ): Promise< + | OperationOk<void> + | OperationFail<HttpStatusCode.Forbidden> + | OperationFail<HttpStatusCode.NotFound> + | OperationAlternative<HttpStatusCode.Conflict, PurseConflict> + | OperationFail<HttpStatusCode.TooEarly> + > { + const resp = await this.fetch(`purses/${pursePub}/create`, { + method: "POST", + body + }); + switch (resp.status) { + case HttpStatusCode.Ok: + // FIXME: parse PurseCreateSuccessResponse + return opSuccessFromHttp(resp, codecForAny()); + case HttpStatusCode.Conflict: + return opKnownAlternativeFailure(resp, resp.status, codecForPurseConflict()) + case HttpStatusCode.Forbidden: + case HttpStatusCode.NotFound: + case HttpStatusCode.TooEarly: + return opKnownHttpFailure( + resp.status, + resp + ); + default: + return opUnknownHttpFailure(resp); + } } /** @@ -666,6 +700,7 @@ export class TalerExchangeHttpClient { HttpStatusCode.Conflict, ExchangeMergeConflictResponse > + | OperationFail<HttpStatusCode.Forbidden> | OperationFail<HttpStatusCode.NotFound> | OperationFail<HttpStatusCode.Gone> > { @@ -689,6 +724,7 @@ export class TalerExchangeHttpClient { codecForExchangeMergeConflictResponse(), ); case HttpStatusCode.Gone: + case HttpStatusCode.Forbidden: case HttpStatusCode.NotFound: return opKnownHttpFailure(resp.status, resp); default: @@ -705,6 +741,13 @@ export class TalerExchangeHttpClient { body: ExchangeReservePurseRequest ): Promise< | OperationOk<void> + | OperationFail<HttpStatusCode.PaymentRequired> + | OperationFail<HttpStatusCode.Forbidden> + | OperationFail<HttpStatusCode.NotFound> + | OperationAlternative< + HttpStatusCode.Conflict, + PurseConflictPartial + > | OperationAlternative< HttpStatusCode.UnavailableForLegalReasons, LegitimizationNeededResponse @@ -718,6 +761,16 @@ export class TalerExchangeHttpClient { case HttpStatusCode.Ok: // FIXME: parse PurseCreateSuccessResponse return opSuccessFromHttp(resp, codecForAny()); + case HttpStatusCode.PaymentRequired: + case HttpStatusCode.Forbidden: + case HttpStatusCode.NotFound: + return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.Conflict: + return opKnownAlternativeFailure( + resp, + resp.status, + codecForPurseConflictPartial(), + ); case HttpStatusCode.UnavailableForLegalReasons: return opKnownAlternativeFailure( resp, @@ -730,6 +783,30 @@ export class TalerExchangeHttpClient { } /** + * https://docs.taler.net/core/api-exchange.html#get--contracts-$CONTRACT_PUB + * + */ + async getContract( + pursePub: string + ): Promise< + OperationOk<ExchangeGetContractResponse> + | OperationFail<HttpStatusCode.NotFound> + > { + const resp = await this.fetch(`contracts/${pursePub}`); + switch (resp.status) { + case HttpStatusCode.Ok: + return opSuccessFromHttp(resp, codecForExchangeGetContractResponse()); + case HttpStatusCode.NotFound: + return opKnownHttpFailure( + resp.status, + resp + ); + default: + return opUnknownHttpFailure(resp); + } + } + + /** * https://docs.taler.net/core/api-exchange.html#post--purses-$PURSE_PUB-deposit * */ @@ -738,7 +815,7 @@ export class TalerExchangeHttpClient { body: ExchangePurseDeposits, ): Promise< OperationOk<void> - | OperationAlternative<HttpStatusCode.Conflict, TalerErrorDetail> + | OperationAlternative<HttpStatusCode.Conflict, PurseConflict> | OperationFail<HttpStatusCode.Forbidden> | OperationFail<HttpStatusCode.NotFound> | OperationFail<HttpStatusCode.Gone> @@ -752,8 +829,7 @@ export class TalerExchangeHttpClient { // FIXME: parse PurseDepositSuccessResponse return opSuccessFromHttp(resp, codecForAny()); case HttpStatusCode.Conflict: - // FIXME: parse PurseConflict - return opKnownAlternativeFailure(resp, resp.status, codecForAny()); + return opKnownAlternativeFailure(resp, resp.status, codecForPurseConflict()); case HttpStatusCode.Forbidden: case HttpStatusCode.NotFound: case HttpStatusCode.Gone: diff --git a/packages/taler-util/src/http-impl.node.ts b/packages/taler-util/src/http-impl.node.ts @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2019 Taler Systems S.A. + (C) 2019-2025 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 @@ -16,9 +16,6 @@ SPDX-License-Identifier: AGPL3.0-or-later */ -/** - * Imports. - */ import type { FollowOptions, RedirectableRequest } from "follow-redirects"; import followRedirects from "follow-redirects"; import type { ClientRequest, IncomingMessage } from "node:http"; @@ -90,9 +87,14 @@ export class HttpLibImpl implements HttpRequestLibrary { async fetch(url: string, opt?: HttpRequestOptions): Promise<HttpResponse> { const method = opt?.method?.toUpperCase() ?? "GET"; const rid = this.idCounter++; - const logHeader = `request ${rid}` - logger.trace(`${logHeader} ${method} ${url}`); + if (logger.shouldLogTrace()) { + if (opt?.body != null) { + logger.trace(`request ${rid} ${method} ${url}: ${opt.body}`); + } else { + logger.trace(`request ${rid} ${method} ${url}`); + } + } const parsedUrl = new URL(url); if (this.throttlingEnabled && this.throttle.applyThrottle(url)) { @@ -238,19 +240,19 @@ export class HttpLibImpl implements HttpRequestLibrary { const text = textDecoder.decode(data); const json = JSON.parse(text) if (logger.shouldLogTrace()) { - logger.trace(`${logHeader} JSON: ${j2s(json)}`); + logger.trace(`request ${rid} JSON: ${j2s(json)}`); } return json; }, async text() { const text = textDecoder.decode(data); if (logger.shouldLogTrace()) { - logger.trace(`${logHeader} TEXT: ${text}`); + logger.trace(`request ${rid} TEXT: ${text}`); } return text; }, }; - logger.trace(`${logHeader} status code ${resp.status}`); + logger.trace(`request ${rid} status code ${resp.status}`); doCleanup(); if (SHOW_CURL_HTTP_REQUEST) { console.log(`TALER_API_DEBUG: ${res.statusCode} ${textDecoder.decode(data)}`) diff --git a/packages/taler-util/src/http-status-codes.ts b/packages/taler-util/src/http-status-codes.ts @@ -289,6 +289,11 @@ export enum HttpStatusCode { FailedDependency = 424, /** + * Indicates that the server is unwilling to risk processing a request that might be replayed. + */ + TooEarly = 425, + + /** * The client should switch to a different protocol such as TLS/1.0, given in the Upgrade header field. */ UpgradeRequired = 426, diff --git a/packages/taler-util/src/types-taler-exchange.ts b/packages/taler-util/src/types-taler-exchange.ts @@ -27,6 +27,7 @@ import { codecOptional, } from "./codec.js"; import { + TalerErrorCode, TalerFormAttributes, buildCodecForUnion, codecForAmountString, @@ -36,6 +37,8 @@ import { codecForEither, codecForMap, codecForPaytoString, + codecForTalerErrorDetail, + codecForTalerMerchantConfigResponse, codecForURN, codecOptionalDefault, strcmp, @@ -3188,3 +3191,129 @@ export interface ExchangeWithdrawRequest { // the reserves's private key. reserve_sig: EddsaSignature; } + +export type PurseConflict = + | DepositDoubleSpendError + | PurseCreateConflict + | PurseDepositConflict + | PurseContractConflict; +export type PurseConflictPartial = + | PurseCreateConflict + | PurseDepositConflict + | PurseContractConflict; + +interface DepositDoubleSpendError { + code: TalerErrorCode.EXCHANGE_GENERIC_INSUFFICIENT_FUNDS; + + // A string explaining that the user tried to + // double-spend. + hint: string; + + // EdDSA public key of a coin being double-spent. + coin_pub: EddsaPublicKey; +} +interface PurseCreateConflict { + code: TalerErrorCode.EXCHANGE_PURSE_CREATE_CONFLICTING_META_DATA; + + // Total amount to be merged into the reserve. + // (excludes fees). + amount: Amount; + + // Minimum age required for all coins deposited into the purse. + min_age: Integer; + + // Indicative time by which the purse should expire + // if it has not been merged into an account. At this + // point, all of the deposits made should be + // auto-refunded. + purse_expiration: Timestamp; + + // EdDSA signature of the purse over + // TALER_PurseMergeSignaturePS of + // purpose TALER_SIGNATURE_WALLET_PURSE_MERGE + // confirming that the + // above details hold for this purse. + purse_sig: EddsaSignature; + + // SHA-512 hash of the contact of the purse. + h_contract_terms: HashCode; + + // EdDSA public key used to approve merges of this purse. + merge_pub: EddsaPublicKey; +} +interface PurseDepositConflict { + code: TalerErrorCode.EXCHANGE_PURSE_DEPOSIT_CONFLICTING_META_DATA; + + // Public key of the coin being deposited into the purse. + coin_pub: EddsaPublicKey; + + // Signature over TALER_PurseDepositSignaturePS + // of purpose TALER_SIGNATURE_WALLET_PURSE_DEPOSIT + // made by the customer with the + // coin's private key. + coin_sig: EddsaSignature; + + // Target exchange URL for the purse. Not present for the + // same exchange. + partner_url?: string; + + // Amount to be contributed to the purse by this coin. + amount: AmountString; +} +interface PurseContractConflict { + code: TalerErrorCode.EXCHANGE_PURSE_ECONTRACT_CONFLICTING_META_DATA; + + // Hash of the encrypted contract. + h_econtract: HashCode; + + // Signature over the contract. + econtract_sig: EddsaSignature; + + // Ephemeral public key for the DH operation to decrypt the contract. + contract_pub: EddsaPublicKey; +} +export const codecForPurseConflict = () => + buildCodecForUnion<PurseConflict>() + .discriminateOn("code") + .alternative(TalerErrorCode.EXCHANGE_GENERIC_INSUFFICIENT_FUNDS, codecForDepositDoubleSpendError()) + .alternative(TalerErrorCode.EXCHANGE_PURSE_CREATE_CONFLICTING_META_DATA, codecForPurseCreateConflict()) + .alternative(TalerErrorCode.EXCHANGE_PURSE_DEPOSIT_CONFLICTING_META_DATA, codecForPurseDepositConflict()) + .alternative(TalerErrorCode.EXCHANGE_PURSE_ECONTRACT_CONFLICTING_META_DATA, codecForPurseContractConflict()) + .build("PurseConflict"); +export const codecForPurseConflictPartial = () => + buildCodecForUnion<PurseConflictPartial>() + .discriminateOn("code") + .alternative(TalerErrorCode.EXCHANGE_PURSE_CREATE_CONFLICTING_META_DATA, codecForPurseCreateConflict()) + .alternative(TalerErrorCode.EXCHANGE_PURSE_DEPOSIT_CONFLICTING_META_DATA, codecForPurseDepositConflict()) + .alternative(TalerErrorCode.EXCHANGE_PURSE_ECONTRACT_CONFLICTING_META_DATA, codecForPurseContractConflict()) + .build("PurseConflictPartial"); +export const codecForDepositDoubleSpendError = () => + buildCodecForObject<DepositDoubleSpendError>() + .property("code", codecForNumber()) + .property("hint", codecForString()) + .property("coin_pub", codecForString()) + .build("DepositDoubleSpendError"); +export const codecForPurseCreateConflict = () => + buildCodecForObject<PurseCreateConflict>() + .property("code", codecForNumber()) + .property("amount", codecForAmountString()) + .property("min_age", codecForNumber()) + .property("purse_expiration", codecForTimestamp) + .property("purse_sig", codecForString()) + .property("h_contract_terms", codecForString()) + .property("merge_pub", codecForString()) + .build("PurseCreateConflict"); +export const codecForPurseDepositConflict = () => + buildCodecForObject<PurseDepositConflict>() + .property("code", codecForNumber()) + .property("coin_pub", codecForString()) + .property("partner_url", codecOptional(codecForString())) + .property("amount", codecForAmountString()) + .build("PurseDepositConflict"); +export const codecForPurseContractConflict = () => + buildCodecForObject<PurseContractConflict>() + .property("code", codecForNumber()) + .property("h_econtract", codecForString()) + .property("econtract_sig", codecForString()) + .property("contract_pub", codecForString()) + .build("PurseContractConflict"); +\ No newline at end of file diff --git a/packages/taler-wallet-core/src/pay-peer-common.ts b/packages/taler-wallet-core/src/pay-peer-common.ts @@ -19,6 +19,7 @@ import { AmountJson, Amounts, ExchangePurseStatus, + HttpStatusCode, NotificationType, SelectedProspectiveCoin, TalerProtocolTimestamp, @@ -26,6 +27,7 @@ import { TransactionMajorState, TransactionState, WalletNotification, + assertUnreachable, checkDbInvariant, } from "@gnu-taler/taler-util"; import { SpendCoinDetails } from "./crypto/cryptoImplementation.js"; @@ -40,7 +42,7 @@ import { WalletStoresV1, } from "./db.js"; import { getTotalRefreshCost } from "./refresh.js"; -import { WalletExecutionContext, getDenomInfo } from "./wallet.js"; +import { WalletExecutionContext, getDenomInfo, walletExchangeClient } from "./wallet.js"; import { BalanceEffect, notifyTransition, TransitionInfo } from "./transactions.js"; import { TransitionResultType } from "./common.js"; import { IDBValidKey } from "@gnu-taler/idb-bridge"; @@ -171,6 +173,39 @@ export async function getMergeReserveInfo( return mergeReserveRecord; } +export async function waitForKycCompletion( + wex: WalletExecutionContext, + exchangeUrl: string, + kycPaytoHash: string, +): Promise<boolean> { + // FIXME: What if this changes? Should be part of the p2p record + + const mergeReserveInfo = await getMergeReserveInfo(wex, { + exchangeBaseUrl: exchangeUrl, + }); + + const sigResp = await wex.cryptoApi.signWalletKycAuth({ + accountPriv: mergeReserveInfo.reservePriv, + accountPub: mergeReserveInfo.reservePub, + }); + + const exchangeClient = walletExchangeClient(exchangeUrl, wex); + const resp = await exchangeClient.checkKycStatus(sigResp.sig, kycPaytoHash, true) + switch (resp.case) { + case "ok": + case HttpStatusCode.Ok: + return true; + case HttpStatusCode.Conflict: + case HttpStatusCode.Accepted: + return false; + case HttpStatusCode.Forbidden: + case HttpStatusCode.NotFound: + throw Error(`unexpected kyc status response ${resp.case}`) + default: + assertUnreachable(resp); + } +} + /** Check if a purse is merged */ export function isPurseMerged(purse: ExchangePurseStatus): boolean { const mergeTimestamp = purse.merge_timestamp; diff --git a/packages/taler-wallet-core/src/pay-peer-pull-credit.ts b/packages/taler-wallet-core/src/pay-peer-pull-credit.ts @@ -58,7 +58,6 @@ import { TaskRunResult, TransactionContext, TransitionResultType, - cancelableLongPoll, constructTaskIdentifier, genericWaitForStateVal, requireExchangeTosAcceptedOrThrow, @@ -93,6 +92,7 @@ import { recordTransition, recordUpdateMeta, recordDelete, + waitForKycCompletion, } from "./pay-peer-common.js"; import { constructTransactionIdentifier, @@ -479,8 +479,8 @@ async function queryPurseForPeerPullCredit( await recordTransitionStatus(ctx, PeerPullPaymentCreditStatus.PendingReady, PeerPullPaymentCreditStatus.Expired); return TaskRunResult.finished(); case HttpStatusCode.NotFound: - // FIXME: Maybe check error code? 404 could also mean something else. - return TaskRunResult.longpollReturnedPending(); + await ctx.failTransaction(resp.detail) + return TaskRunResult.finished(); default: assertUnreachable(resp) } @@ -523,36 +523,13 @@ async function longpollKycStatus( exchangeUrl: string, kycPaytoHash: string, ): Promise<TaskRunResult> { - // FIXME: What if this changes? Should be part of the p2p record - const mergeReserveInfo = await getMergeReserveInfo(wex, { - exchangeBaseUrl: exchangeUrl, - }); - - const sigResp = await wex.cryptoApi.signWalletKycAuth({ - accountPriv: mergeReserveInfo.reservePriv, - accountPub: mergeReserveInfo.reservePub, - }); - - const ctx = new PeerPullCreditTransactionContext(wex, pursePub); - const url = new URL(`kyc-check/${kycPaytoHash}`, exchangeUrl); - logger.info(`kyc url ${url.href}`); - const kycStatusRes = await cancelableLongPoll(wex, url, { - headers: { - ["Account-Owner-Signature"]: sigResp.sig, - } - }); - if ( - kycStatusRes.status === HttpStatusCode.Ok || - // FIXME: NoContent is not expected https://docs.taler.net/core/api-exchange.html#post--purses-$PURSE_PUB-merge - // remove after the exchange is fixed or clarified - kycStatusRes.status === HttpStatusCode.NoContent - ) { + const done = await waitForKycCompletion(wex, exchangeUrl, kycPaytoHash); + if (done) { + const ctx = new PeerPullCreditTransactionContext(wex, pursePub); await recordTransitionStatus(ctx, PeerPullPaymentCreditStatus.PendingMergeKycRequired, PeerPullPaymentCreditStatus.PendingCreatePurse); return TaskRunResult.progress(); - } else if (kycStatusRes.status === HttpStatusCode.Accepted) { - return TaskRunResult.longpollReturnedPending(); } else { - throw Error(`unexpected response from kyc-check (${kycStatusRes.status})`); + return TaskRunResult.longpollReturnedPending(); } } @@ -573,8 +550,8 @@ async function processPeerPullCreditAbortingDeletePurse( await recordTransitionStatus(ctx, PeerPullPaymentCreditStatus.AbortingDeletePurse, PeerPullPaymentCreditStatus.Aborted); return TaskRunResult.finished(); case HttpStatusCode.Forbidden: - // FIXME appropriate error - throw Error(`base signature`); + await ctx.failTransaction(resp.detail) + return TaskRunResult.finished(); case HttpStatusCode.Conflict: // FIXME check if done ? throw Error(`cannot be deleted`); @@ -712,21 +689,29 @@ async function handlePeerPullCreditCreatePurse( logger.info(`reserve purse request: ${j2s(reservePurseReqBody)}`); - const httpResp = await exchangeClient.createPurseFromReserve( + const resp = await exchangeClient.createPurseFromReserve( mergeReserve.reservePub, reservePurseReqBody ) - switch (httpResp.case) { + switch (resp.case) { case "ok": - logger.info(`reserve merge response: ${j2s((await httpResp).body)}`); break; case HttpStatusCode.UnavailableForLegalReasons: { - logger.info(`kyc uuid response: ${j2s(httpResp.body)}`); - return processPeerPullCreditKycRequired(wex, pullIni, httpResp.body.h_payto); + logger.info(`kyc uuid response: ${j2s(resp.body)}`); + return processPeerPullCreditKycRequired(wex, pullIni, resp.body.h_payto); } + case HttpStatusCode.Forbidden: + case HttpStatusCode.NotFound: + await ctx.failTransaction(resp.detail) + return TaskRunResult.finished(); + case HttpStatusCode.Conflict: + await ctx.failTransaction({ code: resp.body.code }) + return TaskRunResult.finished(); + case HttpStatusCode.PaymentRequired: + throw Error(`unexpected reserve merge response ${resp.case}`) default: - assertUnreachable(httpResp); + assertUnreachable(resp); } await recordTransition(ctx, {}, async (rec, _) => { diff --git a/packages/taler-wallet-core/src/pay-peer-pull-debit.ts b/packages/taler-wallet-core/src/pay-peer-pull-debit.ts @@ -33,13 +33,13 @@ import { PeerContractTerms, PreparePeerPullDebitRequest, PreparePeerPullDebitResponse, + PurseConflict, RefreshReason, SelectedProspectiveCoin, TalerError, TalerErrorCode, TalerErrorDetail, TalerPreciseTimestamp, - TalerProtocolViolationError, Transaction, TransactionAction, TransactionIdStr, @@ -51,7 +51,6 @@ import { assertUnreachable, checkDbInvariant, checkLogicInvariant, - codecForExchangeGetContractResponse, codecForPeerContractTerms, decodeCrock, eddsaGetPublic, @@ -61,9 +60,6 @@ import { makeTalerErrorDetail, parsePayPullUri, } from "@gnu-taler/taler-util"; -import { - readSuccessResponseJsonOrThrow, -} from "@gnu-taler/taler-util/http"; import { PreviousPayCoins, selectPeerCoins } from "./coinSelection.js"; import { PendingTaskType, @@ -71,7 +67,6 @@ import { TaskRunResult, TransactionContext, TransitionResultType, - cancelableFetch, constructTaskIdentifier, spendCoins, } from "./common.js"; @@ -317,29 +312,22 @@ export class PeerPullDebitTransactionContext implements TransactionContext { async function handlePurseCreationConflict( ctx: PeerPullDebitTransactionContext, peerPullInc: PeerPullPaymentIncomingRecord, - detail: TalerErrorDetail, + conflict: PurseConflict, ): Promise<TaskRunResult> { - const ws = ctx.wex; - if (detail.code !== TalerErrorCode.EXCHANGE_GENERIC_INSUFFICIENT_FUNDS) { + if (conflict.code !== TalerErrorCode.EXCHANGE_GENERIC_INSUFFICIENT_FUNDS) { await ctx.failTransaction(); return TaskRunResult.finished(); } - // FIXME: Properly parse! - const brokenCoinPub = (detail as any).coin_pub; + const brokenCoinPub = conflict.coin_pub; logger.trace(`excluded broken coin pub=${brokenCoinPub}`); - - if (!brokenCoinPub) { - // FIXME: Details! - throw new TalerProtocolViolationError(); - } - const instructedAmount = Amounts.parseOrThrow(peerPullInc.amount); const sel = peerPullInc.coinSel; - if (!sel) { - throw Error("invalid state (coin selection expected)"); - } + checkDbInvariant( + !!sel, + `no coin selected for peer pull deposit ${peerPullInc.pursePub}`, + ); const repair: PreviousPayCoins = []; @@ -352,7 +340,7 @@ async function handlePurseCreationConflict( } } - const coinSelRes = await selectPeerCoins(ws, { + const coinSelRes = await selectPeerCoins(ctx.wex, { instructedAmount, repair, }); @@ -374,7 +362,7 @@ async function handlePurseCreationConflict( } const totalAmount = await getTotalPeerPaymentCost( - ws, + ctx.wex, coinSelRes.result.coins, ); @@ -394,7 +382,7 @@ async function handlePurseCreationConflict( return TransitionResultType.Stay } }); - return TaskRunResult.backoff(); + return TaskRunResult.progress(); } async function processPeerPullDebitDialogProposed( @@ -413,8 +401,8 @@ async function processPeerPullDebitDialogProposed( await recordTransitionStatus(ctx, PeerPullDebitRecordStatus.DialogProposed, PeerPullDebitRecordStatus.Aborted); return TaskRunResult.finished(); case HttpStatusCode.NotFound: - // FIXME: Maybe check error code? 404 could also mean something else. - return TaskRunResult.longpollReturnedPending(); + await ctx.failTransaction(resp.detail) + return TaskRunResult.finished(); default: assertUnreachable(resp) } @@ -549,16 +537,14 @@ async function processPeerPullDebitPendingDeposit( {}, ), ); - return TaskRunResult.backoff(); + return TaskRunResult.finished(); } case HttpStatusCode.Conflict: return handlePurseCreationConflict(ctx, peerPullInc, resp.body); case HttpStatusCode.Forbidden: - // FIXME: appropriated error code - throw Error("bad signature") case HttpStatusCode.NotFound: - // FIXME: appropriated error code - throw Error("unknown peer pull debit") + await ctx.failTransaction(resp.detail) + return TaskRunResult.finished(); default: assertUnreachable(resp) } @@ -802,25 +788,26 @@ export async function preparePeerPullDebit( const exchangeBaseUrl = uri.exchangeBaseUrl; const contractPriv = uri.contractPriv; const contractPub = encodeCrock(eddsaGetPublic(decodeCrock(contractPriv))); + const exchangeClient = walletExchangeClient(exchangeBaseUrl, wex) - const getContractUrl = new URL(`contracts/${contractPub}`, exchangeBaseUrl); - - const contractHttpResp = await cancelableFetch(wex, getContractUrl); - - const contractResp = await readSuccessResponseJsonOrThrow( - contractHttpResp, - codecForExchangeGetContractResponse(), - ); - - const pursePub = contractResp.purse_pub; + const contractResp = await exchangeClient.getContract(contractPub); + switch (contractResp.case) { + case "ok": + break; + case HttpStatusCode.NotFound: + // FIXME: appropriated error code + throw Error("unknown P2P contract") + default: + assertUnreachable(contractResp) + } + const pursePub = contractResp.body.purse_pub; const dec = await wex.cryptoApi.decryptContractForDeposit({ - ciphertext: contractResp.econtract, + ciphertext: contractResp.body.econtract, contractPriv: contractPriv, pursePub: pursePub, }); - const exchangeClient = walletExchangeClient(exchangeBaseUrl, wex) const resp = await exchangeClient.getPurseStatusAtMerge(pursePub); switch (resp.case) { case "ok": diff --git a/packages/taler-wallet-core/src/pay-peer-push-credit.ts b/packages/taler-wallet-core/src/pay-peer-push-credit.ts @@ -42,8 +42,6 @@ import { WalletNotification, assertUnreachable, checkDbInvariant, - codecForAccountKycStatus, - codecForExchangeGetContractResponse, codecForPeerContractTerms, decodeCrock, eddsaGetPublic, @@ -54,18 +52,12 @@ import { talerPaytoFromExchangeReserve, } from "@gnu-taler/taler-util"; import { - readResponseJsonOrThrow, - readSuccessResponseJsonOrThrow, -} from "@gnu-taler/taler-util/http"; -import { PendingTaskType, TaskIdStr, TaskIdentifiers, TaskRunResult, TransactionContext, TransitionResultType, - cancelableFetch, - cancelableLongPoll, constructTaskIdentifier, genericWaitForStateVal, requireExchangeTosAcceptedOrThrow, @@ -102,6 +94,7 @@ import { recordTransition, recordTransitionStatus, recordUpdateMeta, + waitForKycCompletion, } from "./pay-peer-common.js"; import { BalanceEffect, @@ -490,26 +483,27 @@ export async function preparePeerPushCredit( const contractPriv = uri.contractPriv; const contractPub = encodeCrock(eddsaGetPublic(decodeCrock(contractPriv))); + const exchangeClient = walletExchangeClient(exchangeBaseUrl, wex); - const getContractUrl = new URL(`contracts/${contractPub}`, exchangeBaseUrl); - - const contractHttpResp = await cancelableFetch(wex, getContractUrl); - - const contractResp = await readSuccessResponseJsonOrThrow( - contractHttpResp, - codecForExchangeGetContractResponse(), - ); - - const pursePub = contractResp.purse_pub; + const contractResp = await exchangeClient.getContract(contractPub); + switch (contractResp.case) { + case "ok": + break; + case HttpStatusCode.NotFound: + // FIXME: appropriated error code + throw Error("unknown P2P contract") + default: + assertUnreachable(contractResp) + } + const pursePub = contractResp.body.purse_pub; const dec = await wex.cryptoApi.decryptContractForMerge({ - ciphertext: contractResp.econtract, + ciphertext: contractResp.body.econtract, contractPriv: contractPriv, pursePub: pursePub, }); const contractTerms = codecForPeerContractTerms().decode(dec.contractTerms); - const exchangeClient = walletExchangeClient(exchangeBaseUrl, wex) const resp = await exchangeClient.getPurseStatusAtDeposit(pursePub); switch (resp.case) { case "ok": @@ -578,9 +572,16 @@ export async function preparePeerPushCredit( wex.taskScheduler.startShepherdTask(ctx.taskId); const currency = Amounts.currencyOf(wi.withdrawalAmountRaw); - const scopeInfo = await wex.db.runAllStoresReadOnlyTx( - {}, - async (tx) => await getExchangeScopeInfo(tx, exchangeBaseUrl, currency), + const scopeInfo = await wex.db.runReadOnlyTx( + { + storeNames: [ + "exchanges", + "exchangeDetails", + "globalCurrencyExchanges", + "globalCurrencyAuditors" + ] + }, + (tx) => getExchangeScopeInfo(tx, exchangeBaseUrl, currency), ); return { @@ -601,37 +602,13 @@ async function longpollKycStatus( exchangeUrl: string, kycPaytoHash: string, ): Promise<TaskRunResult> { - const ctx = new PeerPushCreditTransactionContext(wex, peerPushCreditId); - - // FIXME: What if this changes? Should be part of the p2p record - const mergeReserveInfo = await getMergeReserveInfo(wex, { - exchangeBaseUrl: exchangeUrl, - }); - - const sigResp = await wex.cryptoApi.signWalletKycAuth({ - accountPriv: mergeReserveInfo.reservePriv, - accountPub: mergeReserveInfo.reservePub, - }); - - const url = new URL(`kyc-check/${kycPaytoHash}`, exchangeUrl); - logger.info(`kyc url ${url.href}`); - const kycStatusRes = await cancelableLongPoll(wex, url, { - headers: { - ["Account-Owner-Signature"]: sigResp.sig, - }, - }); - - if ( - kycStatusRes.status === HttpStatusCode.Ok || - kycStatusRes.status === HttpStatusCode.NoContent - ) { + const done = await waitForKycCompletion(wex, exchangeUrl, kycPaytoHash); + if (done) { + const ctx = new PeerPushCreditTransactionContext(wex, peerPushCreditId); await recordTransitionStatus(ctx, PeerPushCreditStatus.PendingMergeKycRequired, PeerPushCreditStatus.PendingMerge); return TaskRunResult.progress(); - } else if (kycStatusRes.status === HttpStatusCode.Accepted) { - // Access token / URL stays the same, just long-poll again. - return TaskRunResult.longpollReturnedPending(); } else { - throw Error(`unexpected response from kyc-check (${kycStatusRes.status})`); + return TaskRunResult.longpollReturnedPending(); } } @@ -655,63 +632,50 @@ async function processPeerPushCreditKycRequired( accountPub: mergeReserveInfo.reservePub, }); - const url = new URL( - `kyc-check/${kycPending.h_payto}`, - peerInc.exchangeBaseUrl, - ); + const exchangeClient = walletExchangeClient(peerInc.exchangeBaseUrl, wex); + const resp = await exchangeClient.checkKycStatus(sigResp.sig, kycPending.h_payto) - logger.info(`kyc url ${url.href}`); - const kycStatusRes = await cancelableFetch(wex, url, { - headers: { - ["Account-Owner-Signature"]: sigResp.sig, - }, - }); - - logger.info(`kyc result status ${kycStatusRes.status}`); - - if ( - kycStatusRes.status === HttpStatusCode.Ok || - kycStatusRes.status === HttpStatusCode.NoContent - ) { - logger.warn("kyc requested, but already fulfilled"); - return TaskRunResult.finished(); - } else if (kycStatusRes.status === HttpStatusCode.Accepted) { - const statusResp = await readResponseJsonOrThrow( - kycStatusRes, - codecForAccountKycStatus(), - ); - logger.info(`kyc status: ${j2s(statusResp)}`); - const { transitionInfo, result } = await wex.db.runReadWriteTx( - { storeNames: ["peerPushCredit", "transactionsMeta"] }, - async (tx) => { - const peerInc = await tx.peerPushCredit.get(ctx.peerPushCreditId); - if (!peerInc) { + switch (resp.case) { + case "ok": + case HttpStatusCode.Ok: + logger.warn("kyc requested, but already fulfilled"); + return TaskRunResult.finished(); + case HttpStatusCode.Accepted: + const { transitionInfo, result } = await wex.db.runReadWriteTx( + { storeNames: ["peerPushCredit", "transactionsMeta"] }, + async (tx) => { + const peerInc = await tx.peerPushCredit.get(ctx.peerPushCreditId); + if (!peerInc) { + return { + transitionInfo: undefined, + result: TaskRunResult.finished(), + }; + } + const oldTxState = computePeerPushCreditTransactionState(peerInc); + peerInc.kycPaytoHash = kycPending.h_payto; + peerInc.kycAccessToken = resp.body.access_token; + peerInc.status = PeerPushCreditStatus.PendingMergeKycRequired; + const newTxState = computePeerPushCreditTransactionState(peerInc); + await tx.peerPushCredit.put(peerInc); + await ctx.updateTransactionMeta(tx); return { - transitionInfo: undefined, - result: TaskRunResult.finished(), + transitionInfo: { + oldTxState, + newTxState, + balanceEffect: BalanceEffect.Any, + }, + result: TaskRunResult.progress(), }; - } - const oldTxState = computePeerPushCreditTransactionState(peerInc); - peerInc.kycPaytoHash = kycPending.h_payto; - peerInc.kycAccessToken = statusResp.access_token; - peerInc.status = PeerPushCreditStatus.PendingMergeKycRequired; - const newTxState = computePeerPushCreditTransactionState(peerInc); - await tx.peerPushCredit.put(peerInc); - await ctx.updateTransactionMeta(tx); - return { - transitionInfo: { - oldTxState, - newTxState, - balanceEffect: BalanceEffect.Any, - }, - result: TaskRunResult.progress(), - }; - }, - ); - notifyTransition(wex, ctx.transactionId, transitionInfo); - return result; - } else { - throw Error(`unexpected response from kyc-check (${kycStatusRes.status})`); + }, + ); + notifyTransition(wex, ctx.transactionId, transitionInfo); + return result; + case HttpStatusCode.Conflict: + case HttpStatusCode.Forbidden: + case HttpStatusCode.NotFound: + throw Error(`unexpected kyc status response ${resp.case}`) + default: + assertUnreachable(resp); } } @@ -790,12 +754,11 @@ async function handlePendingMerge( case "ok": logger.trace(`merge response: ${j2s(mergeResp.body)}`); break; - case HttpStatusCode.UnavailableForLegalReasons: { + case HttpStatusCode.UnavailableForLegalReasons: const kycLegiNeededResp = mergeResp.body; logger.info(`kyc legitimization needed response: ${j2s(mergeResp.body)}`); return processPeerPushCreditKycRequired(wex, peerInc, kycLegiNeededResp); - } - case HttpStatusCode.Conflict: { + case HttpStatusCode.Conflict: // FIXME: Check signature. // FIXME: status completed by other await recordTransitionStatus(ctx, @@ -803,17 +766,14 @@ async function handlePendingMerge( PeerPushCreditStatus.Aborted, ); return TaskRunResult.finished(); - } - case HttpStatusCode.NotFound: { - await ctx.failTransaction(mergeResp.detail); - return TaskRunResult.finished(); - } - case HttpStatusCode.Gone: { + case HttpStatusCode.Gone: // FIXME: status expired - ctx.abortTransaction() + await ctx.abortTransaction() + return TaskRunResult.finished(); + case HttpStatusCode.Forbidden: + case HttpStatusCode.NotFound: await ctx.failTransaction(mergeResp.detail); return TaskRunResult.finished(); - } default: assertUnreachable(mergeResp); } @@ -970,8 +930,8 @@ async function processPeerPushDebitDialogProposed( await recordTransitionStatus(ctx, PeerPushCreditStatus.DialogProposed, PeerPushCreditStatus.Aborted); return TaskRunResult.finished(); case HttpStatusCode.NotFound: - // FIXME: Maybe check error code? 404 could also mean something else. - return TaskRunResult.longpollReturnedPending(); + await ctx.failTransaction(resp.detail); + return TaskRunResult.finished(); default: assertUnreachable(resp) } @@ -1030,7 +990,7 @@ export async function processPeerPushCredit( switch (peerInc.status) { case PeerPushCreditStatus.DialogProposed: return processPeerPushDebitDialogProposed(wex, peerInc); - case PeerPushCreditStatus.PendingMergeKycRequired: { + case PeerPushCreditStatus.PendingMergeKycRequired: if (!peerInc.kycPaytoHash) { throw Error("invalid state, kycPaytoHash required"); } @@ -1040,17 +1000,13 @@ export async function processPeerPushCredit( peerInc.exchangeBaseUrl, peerInc.kycPaytoHash, ); - } - case PeerPushCreditStatus.PendingMerge: { + case PeerPushCreditStatus.PendingMerge: return handlePendingMerge(wex, peerInc, contractTerms); - } - case PeerPushCreditStatus.PendingWithdrawing: { + case PeerPushCreditStatus.PendingWithdrawing: return handlePendingWithdrawing(wex, peerInc); - } case PeerPushCreditStatus.PendingBalanceKycInit: - case PeerPushCreditStatus.PendingBalanceKycRequired: { + case PeerPushCreditStatus.PendingBalanceKycRequired: return processPeerPushCreditBalanceKyc(ctx, peerInc); - } default: return TaskRunResult.finished(); } @@ -1305,6 +1261,3 @@ export function computePeerPushCreditTransactionActions( assertUnreachable(pushCreditRecord.status); } } -function checkPeerHardLimitExceeded(exchanges: any, amount: any) { - throw new Error("Function not implemented."); -} diff --git a/packages/taler-wallet-core/src/pay-peer-push-debit.ts b/packages/taler-wallet-core/src/pay-peer-push-debit.ts @@ -26,6 +26,7 @@ import { InitiatePeerPushDebitRequest, InitiatePeerPushDebitResponse, Logger, + PurseConflict, RefreshReason, ScopeInfo, ScopeType, @@ -35,7 +36,6 @@ import { TalerErrorDetail, TalerPreciseTimestamp, TalerProtocolTimestamp, - TalerProtocolViolationError, Transaction, TransactionAction, TransactionIdStr, @@ -52,10 +52,6 @@ import { stringifyPayPushUri, } from "@gnu-taler/taler-util"; import { - HttpResponse, - readTalerErrorResponse, -} from "@gnu-taler/taler-util/http"; -import { PreviousPayCoins, selectPeerCoins, selectPeerCoinsInTx, @@ -64,10 +60,8 @@ import { PendingTaskType, TaskIdStr, TaskRunResult, - TaskRunResultType, TransactionContext, TransitionResultType, - cancelableFetch, constructTaskIdentifier, runWithClientCancellation, spendCoins, @@ -435,25 +429,18 @@ async function internalCheckPeerPushDebit( async function handlePurseCreationConflict( wex: WalletExecutionContext, peerPushInitiation: PeerPushDebitRecord, - resp: HttpResponse, + conflict: PurseConflict, ): Promise<TaskRunResult> { const pursePub = peerPushInitiation.pursePub; - const errResp = await readTalerErrorResponse(resp); const ctx = new PeerPushDebitTransactionContext(wex, pursePub); - if (errResp.code !== TalerErrorCode.EXCHANGE_GENERIC_INSUFFICIENT_FUNDS) { + if (conflict.code !== TalerErrorCode.EXCHANGE_GENERIC_INSUFFICIENT_FUNDS) { await ctx.failTransaction(); return TaskRunResult.finished(); } - // FIXME: Properly parse! - const brokenCoinPub = (errResp as any).coin_pub; + const brokenCoinPub = conflict.coin_pub; logger.trace(`excluded broken coin pub=${brokenCoinPub}`); - if (!brokenCoinPub) { - // FIXME: Details! - throw new TalerProtocolViolationError(); - } - const instructedAmount = Amounts.parseOrThrow(peerPushInitiation.amount); const sel = peerPushInitiation.coinSel; @@ -515,7 +502,7 @@ async function processPeerPushDebitCreateReserve( wex: WalletExecutionContext, peerPushInitiation: PeerPushDebitRecord, ): Promise<TaskRunResult> { - const { pursePub, purseExpiration, contractTermsHash } = peerPushInitiation; + const { pursePub, purseExpiration, contractTermsHash, exchangeBaseUrl } = peerPushInitiation; const ctx = new PeerPushDebitTransactionContext(wex, pursePub); logger.trace(`processing ${ctx.transactionId} pending(create-reserve)`); @@ -633,6 +620,7 @@ async function processPeerPushDebitCreateReserve( ); const maxBatchSize = 100; + const exchangeClient = walletExchangeClient(exchangeBaseUrl, wex); for (let i = 0; i < coins.length; i += maxBatchSize) { const batchSize = Math.min(maxBatchSize, coins.length - i); @@ -649,11 +637,6 @@ async function processPeerPushDebitCreateReserve( logger.trace(`encrypt contract request: ${j2s(encryptContractRequest)}`); - const createPurseUrl = new URL( - `purses/${peerPushInitiation.pursePub}/create`, - peerPushInitiation.exchangeBaseUrl, - ); - const reqBody = { // Older wallets do not have amountPurse amount: purseAmount, @@ -666,79 +649,49 @@ async function processPeerPushDebitCreateReserve( econtract: econtractResp.econtract, }; - if (logger.shouldLogTrace()) { - logger.trace(`request body: ${j2s(reqBody)}`); - } - - const httpResp = await cancelableFetch(wex, createPurseUrl, { - method: "POST", - body: reqBody - }); - - switch (httpResp.status) { - case HttpStatusCode.Ok: + const resp = await exchangeClient.createPurseFromDeposit(peerPushInitiation.pursePub, reqBody); + switch (resp.case) { + case "ok": // Possibly on to the next batch. continue; - case HttpStatusCode.Forbidden: { - const errResp = await readTalerErrorResponse(httpResp); - logger.error(`${j2s(errResp)}`); - // FIXME: Store this error! - await ctx.failTransaction(); + case HttpStatusCode.Forbidden: + case HttpStatusCode.NotFound: + await ctx.failTransaction(resp.detail); return TaskRunResult.finished(); - } - case HttpStatusCode.Conflict: { - // Handle double-spending - return handlePurseCreationConflict(wex, peerPushInitiation, httpResp); - } - default: { - const errResp = await readTalerErrorResponse(httpResp); - return { - type: TaskRunResultType.Error, - errorDetail: errResp, - }; - } + case HttpStatusCode.Conflict: + return handlePurseCreationConflict(wex, peerPushInitiation, resp.body); + case HttpStatusCode.TooEarly: + return TaskRunResult.backoff(); + default: + assertUnreachable(resp) } } else { - const purseDepositUrl = new URL( - `purses/${pursePub}/deposit`, - peerPushInitiation.exchangeBaseUrl, - ); - const depositPayload: ExchangePurseDeposits = { deposits: depositSigsResp.deposits, }; - - const httpResp = await cancelableFetch(wex, purseDepositUrl, { - method: "POST", - body: depositPayload - }); - - switch (httpResp.status) { - case HttpStatusCode.Ok: + const resp = await exchangeClient.depositIntoPurse(peerPushInitiation.pursePub, depositPayload); + switch (resp.case) { + case "ok": // Possibly on to the next batch. - break; - case HttpStatusCode.Forbidden: { - // FIXME: Store this error! - await ctx.failTransaction(); - return TaskRunResult.finished(); - } - case HttpStatusCode.Conflict: { + continue; + case HttpStatusCode.Gone: + // FIXME we need PeerPushDebitStatus.ExpiredDeletePurse + await recordTransitionStatus(ctx, PeerPushDebitStatus.PendingCreatePurse, PeerPushDebitStatus.AbortingDeletePurse) + return TaskRunResult.progress() + case HttpStatusCode.Conflict: // Handle double-spending - return handlePurseCreationConflict(wex, peerPushInitiation, httpResp); - } - default: { - const errResp = await readTalerErrorResponse(httpResp); - return { - type: TaskRunResultType.Error, - errorDetail: errResp, - }; - } + return handlePurseCreationConflict(wex, peerPushInitiation, resp.body); + case HttpStatusCode.Forbidden: + case HttpStatusCode.NotFound: + await ctx.failTransaction(resp.detail); + return TaskRunResult.finished(); + default: + assertUnreachable(resp) } } } // All batches done! - const exchangeClient = walletExchangeClient(peerPushInitiation.exchangeBaseUrl, wex) const resp = await exchangeClient.getPurseStatusAtDeposit(pursePub); switch (resp.case) { case "ok": @@ -749,7 +702,8 @@ async function processPeerPushDebitCreateReserve( await recordTransitionStatus(ctx, PeerPushDebitStatus.PendingCreatePurse, PeerPushDebitStatus.AbortingDeletePurse) return TaskRunResult.progress() case HttpStatusCode.NotFound: - throw Error("peer push credit disappeared"); + await ctx.failTransaction(resp.detail); + return TaskRunResult.finished(); default: assertUnreachable(resp) } @@ -759,23 +713,24 @@ async function processPeerPushDebitAbortingDeletePurse( wex: WalletExecutionContext, peerPushInitiation: PeerPushDebitRecord, ): Promise<TaskRunResult> { - const { pursePub, pursePriv } = peerPushInitiation; + const { pursePub, pursePriv, exchangeBaseUrl } = peerPushInitiation; const ctx = new PeerPushDebitTransactionContext(wex, pursePub); - + const exchangeClient = walletExchangeClient(exchangeBaseUrl, wex) const sigResp = await wex.cryptoApi.signDeletePurse({ pursePriv, }); - const purseUrl = new URL( - `purses/${pursePub}`, - peerPushInitiation.exchangeBaseUrl, - ); - const resp = await cancelableFetch(wex, purseUrl, { - method: "DELETE", - headers: { - "taler-purse-signature": sigResp.sig, - } - }); - logger.info(`deleted purse with response status ${resp.status}`); + const resp = await exchangeClient.deletePurse(pursePub, sigResp.sig) + + switch (resp.case) { + case "ok": + case HttpStatusCode.NotFound: + break; + case HttpStatusCode.Conflict: + throw Error("purse deletion conflict") + case HttpStatusCode.Forbidden: + ctx.failTransaction(resp.detail); + return TaskRunResult.finished() + } await recordTransition(ctx, { diff --git a/packages/taler-wallet-core/src/refresh.ts b/packages/taler-wallet-core/src/refresh.ts @@ -933,7 +933,6 @@ async function refreshMelt( denoms_h: newDenomsFlat, value_with_fee: Amounts.stringify(derived.meltValueWithFee), }; - logger.info(`melt request body: ${j2s(meltReqBody)}`); const resp = await wex.ws.runSequentialized( [EXCHANGE_COINS_LOCK], async () => { diff --git a/packages/taler-wallet-core/src/shepherd.ts b/packages/taler-wallet-core/src/shepherd.ts @@ -175,7 +175,7 @@ export class TaskSchedulerImpl implements TaskScheduler { isRunning: boolean = false; - constructor(private ws: InternalWalletState) {} + constructor(private ws: InternalWalletState) { } private async loadTasksFromDb(): Promise<void> { const activeTasks = await getActiveTaskIds(this.ws); @@ -370,8 +370,7 @@ export class TaskSchedulerImpl implements TaskScheduler { if (this.ws.stopped) { logger.trace(`Shepherd for ${taskId} stopping as wallet is stopped`); return; - } - if (info.cts.token.isCancelled) { + } else if (info.cts.token.isCancelled) { logger.trace(`Shepherd for ${taskId} got cancelled`); return; } @@ -396,30 +395,17 @@ export class TaskSchedulerImpl implements TaskScheduler { try { res = await callOperationHandlerForTaskId(wex, taskId); } catch (e) { - if (info.cts.token.isCancelled) { - logger.trace(`task ${taskId} cancelled, ignoring task error`); - return; - } - if (this.ws.stopped) { - logger.trace("wallet stopped, ignoring task error"); - return; - } - const errorDetail = getErrorDetailFromException(e); - logger.trace( - `Shepherd error ${taskId} saving response ${j2s(errorDetail)}`, - ); res = { type: TaskRunResultType.Error, - errorDetail, + errorDetail: getErrorDetailFromException(e), }; } - if (info.cts.token.isCancelled) { - logger.trace(`task ${taskId} cancelled, not processing result`); - return; - } if (this.ws.stopped) { logger.trace("wallet stopped, not processing result"); return; + } else if (info.cts.token.isCancelled) { + logger.trace(`task ${taskId} cancelled, not processing result`); + return; } wex.oc.observe({ type: ObservabilityEventType.ShepherdTaskResult, @@ -641,7 +627,7 @@ function getWalletExecutionContextForTask( ); } else { oc = { - observe(evt) {}, + observe(evt) { }, }; wex = getNormalWalletExecutionContext(ws, cancellationToken, undefined, oc); }