From 15feebecfeeda4758a96d1da99a98d9494c4bd2b Mon Sep 17 00:00:00 2001 From: Florian Dold Date: Sat, 22 Apr 2023 14:17:49 +0200 Subject: wallet-core: towards DD37 for deposits --- packages/taler-harness/src/harness/harness.ts | 48 ++++++++- packages/taler-harness/src/index.ts | 70 ++++++++++++- .../src/integrationtests/test-deposit.ts | 22 +++- .../src/integrationtests/test-wallet-dbless.ts | 27 +++-- packages/taler-util/src/notifications.ts | 4 +- packages/taler-util/src/transaction-test-data.ts | 13 +-- packages/taler-util/src/transactions-types.ts | 29 +++--- packages/taler-wallet-core/src/db.ts | 10 +- packages/taler-wallet-core/src/dbless.ts | 24 +++-- .../taler-wallet-core/src/operations/deposits.ts | 114 +++++++++++++++------ .../src/operations/transactions.ts | 56 +++++----- packages/taler-wallet-core/src/util/retries.ts | 6 ++ 12 files changed, 313 insertions(+), 110 deletions(-) (limited to 'packages') diff --git a/packages/taler-harness/src/harness/harness.ts b/packages/taler-harness/src/harness/harness.ts index 0a898414d..8b74c9e9f 100644 --- a/packages/taler-harness/src/harness/harness.ts +++ b/packages/taler-harness/src/harness/harness.ts @@ -970,6 +970,19 @@ export class ExchangeService implements ExchangeServiceInterface { ); } + async runAggregatorOnceWithTimetravel(opts: { + timetravelMicroseconds: number; + }) { + let timetravelArgArr = []; + timetravelArgArr.push(`--timetravel=${opts.timetravelMicroseconds}`); + await runCommand( + this.globalState, + `exchange-${this.name}-aggregator-once`, + "taler-exchange-aggregator", + [...timetravelArgArr, "-c", this.configFilename, "-t"], + ); + } + async runAggregatorOnce() { try { await runCommand( @@ -1147,6 +1160,9 @@ export class ExchangeService implements ExchangeServiceInterface { exchangeHttpProc: ProcessWrapper | undefined; exchangeWirewatchProc: ProcessWrapper | undefined; + exchangeTransferProc: ProcessWrapper | undefined; + exchangeAggregatorProc: ProcessWrapper | undefined; + helperCryptoRsaProc: ProcessWrapper | undefined; helperCryptoEddsaProc: ProcessWrapper | undefined; helperCryptoCsProc: ProcessWrapper | undefined; @@ -1200,6 +1216,18 @@ export class ExchangeService implements ExchangeServiceInterface { await wirewatch.wait(); this.exchangeWirewatchProc = undefined; } + const aggregatorProc = this.exchangeAggregatorProc; + if (aggregatorProc) { + aggregatorProc.proc.kill("SIGTERM"); + await aggregatorProc.wait(); + this.exchangeAggregatorProc = undefined; + } + const transferProc = this.exchangeTransferProc; + if (transferProc) { + transferProc.proc.kill("SIGTERM"); + await transferProc.wait(); + this.exchangeTransferProc = undefined; + } const httpd = this.exchangeHttpProc; if (httpd) { httpd.proc.kill("SIGTERM"); @@ -1369,6 +1397,22 @@ export class ExchangeService implements ExchangeServiceInterface { ); } + private internalCreateAggregatorProc() { + this.exchangeAggregatorProc = this.globalState.spawnService( + "taler-exchange-aggregator", + ["-c", this.configFilename, ...this.timetravelArgArr], + `exchange-aggregator-${this.name}`, + ); + } + + private internalCreateTransferProc() { + this.exchangeTransferProc = this.globalState.spawnService( + "taler-exchange-transfer", + ["-c", this.configFilename, ...this.timetravelArgArr], + `exchange-transfer-${this.name}`, + ); + } + async start(): Promise { if (this.isRunning()) { throw Error("exchange is already running"); @@ -1398,6 +1442,8 @@ export class ExchangeService implements ExchangeServiceInterface { ); this.internalCreateWirewatchProc(); + this.internalCreateTransferProc(); + this.internalCreateAggregatorProc(); this.exchangeHttpProc = this.globalState.spawnService( "taler-exchange-httpd", @@ -2062,7 +2108,7 @@ export class WalletService { [ "--wallet-db", dbPath, - "-LDEBUG", // FIXME: Make this configurable? + "-LTRACE", // FIXME: Make this configurable? "--no-throttle", // FIXME: Optionally do throttling for some tests? "advanced", "serve", diff --git a/packages/taler-harness/src/index.ts b/packages/taler-harness/src/index.ts index 59fa80411..287e1f5be 100644 --- a/packages/taler-harness/src/index.ts +++ b/packages/taler-harness/src/index.ts @@ -47,7 +47,14 @@ import { lintExchangeDeployment } from "./lint.js"; import { runEnvFull } from "./env-full.js"; import { clk } from "@gnu-taler/taler-util/clk"; import { createPlatformHttpLib } from "@gnu-taler/taler-util/http"; -import { BankAccessApiClient } from "@gnu-taler/taler-wallet-core"; +import { + BankAccessApiClient, + checkReserve, + CryptoDispatcher, + downloadExchangeInfo, + SynchronousCryptoWorkerFactoryPlain, + topupReserveWithDemobank, +} from "@gnu-taler/taler-wallet-core"; const logger = new Logger("taler-harness:index.ts"); @@ -162,7 +169,6 @@ advancedCli await runTestWithState(testState, runEnv1, "env1", true); }); - const sandcastleCli = testingCli.subcommand("sandcastleArgs", "sandcastle", { help: "Subcommands for handling GNU Taler sandcastle deployments.", }); @@ -260,6 +266,66 @@ deploymentCli // FIXME: Now delete reserves that are not filled yet }); +deploymentCli + .subcommand("testTalerdotnetDemo", "test-demo-talerdotnet") + .action(async (args) => { + const http = createPlatformHttpLib(); + const cryptiDisp = new CryptoDispatcher( + new SynchronousCryptoWorkerFactoryPlain(), + ); + const cryptoApi = cryptiDisp.cryptoApi; + const reserveKeyPair = await cryptoApi.createEddsaKeypair({}); + const exchangeBaseUrl = "https://exchange.demo.taler.net/"; + const exchangeInfo = await downloadExchangeInfo(exchangeBaseUrl, http); + await topupReserveWithDemobank({ + amount: "KUDOS:10", + bankAccessApiBaseUrl: + "https://bank.demo.taler.net/demobanks/default/access-api/", + bankBaseUrl: "", + exchangeInfo, + http, + reservePub: reserveKeyPair.pub, + }); + let reserveUrl = new URL(`reserves/${reserveKeyPair.pub}`, exchangeBaseUrl); + reserveUrl.searchParams.set("timeout_ms", "30000"); + console.log("requesting", reserveUrl.href); + const longpollReq = http.fetch(reserveUrl.href, { + method: "GET", + }); + const reserveStatusResp = await longpollReq; + console.log("reserve status", reserveStatusResp.status); + }); + +deploymentCli + .subcommand("testLocalhostDemo", "test-demo-localhost") + .action(async (args) => { + // Run checks against the "env-full" demo deployment on localhost + const http = createPlatformHttpLib(); + const cryptiDisp = new CryptoDispatcher( + new SynchronousCryptoWorkerFactoryPlain(), + ); + const cryptoApi = cryptiDisp.cryptoApi; + const reserveKeyPair = await cryptoApi.createEddsaKeypair({}); + const exchangeBaseUrl = "http://localhost:8081/"; + const exchangeInfo = await downloadExchangeInfo(exchangeBaseUrl, http); + await topupReserveWithDemobank({ + amount: "TESTKUDOS:10", + bankAccessApiBaseUrl: "http://localhost:8082/taler-bank-access/", + bankBaseUrl: "", + exchangeInfo, + http, + reservePub: reserveKeyPair.pub, + }); + let reserveUrl = new URL(`reserves/${reserveKeyPair.pub}`, exchangeBaseUrl); + reserveUrl.searchParams.set("timeout_ms", "30000"); + console.log("requesting", reserveUrl.href); + const longpollReq = http.fetch(reserveUrl.href, { + method: "GET", + }); + const reserveStatusResp = await longpollReq; + console.log("reserve status", reserveStatusResp.status); + }); + deploymentCli .subcommand("tipStatus", "tip-status") .requiredOption("merchantBaseUrl", ["--merchant-url"], clk.STRING) diff --git a/packages/taler-harness/src/integrationtests/test-deposit.ts b/packages/taler-harness/src/integrationtests/test-deposit.ts index 6aa086107..8ea3fc12e 100644 --- a/packages/taler-harness/src/integrationtests/test-deposit.ts +++ b/packages/taler-harness/src/integrationtests/test-deposit.ts @@ -17,7 +17,11 @@ /** * Imports. */ -import { NotificationType, TransactionState } from "@gnu-taler/taler-util"; +import { + NotificationType, + TransactionMajorState, + TransactionMinorState, +} from "@gnu-taler/taler-util"; import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; import { GlobalTestState, getPayto } from "../harness/harness.js"; import { @@ -52,11 +56,19 @@ export async function runDepositTest(t: GlobalTestState) { const depositTxId = dgIdResp.transactionId; + const depositTrack = walletClient.waitForNotificationCond( + (n) => + n.type == NotificationType.TransactionStateTransition && + n.transactionId == depositTxId && + n.newTxState.major == TransactionMajorState.Pending && + n.newTxState.minor == TransactionMinorState.Track, + ); + const depositDone = walletClient.waitForNotificationCond( (n) => n.type == NotificationType.TransactionStateTransition && n.transactionId == depositTxId && - n.newTxState == TransactionState.Done, + n.newTxState.major == TransactionMajorState.Done, ); const depositGroupResult = await walletClient.client.call( @@ -70,6 +82,12 @@ export async function runDepositTest(t: GlobalTestState) { t.assertDeepEqual(depositGroupResult.transactionId, depositTxId); + await depositTrack; + + await exchange.runAggregatorOnceWithTimetravel({ + timetravelMicroseconds: 1000 * 1000 * 60 * 60 * 3, + }); + await depositDone; const transactions = await walletClient.client.call( diff --git a/packages/taler-harness/src/integrationtests/test-wallet-dbless.ts b/packages/taler-harness/src/integrationtests/test-wallet-dbless.ts index a1de7617b..35c60a89d 100644 --- a/packages/taler-harness/src/integrationtests/test-wallet-dbless.ts +++ b/packages/taler-harness/src/integrationtests/test-wallet-dbless.ts @@ -59,16 +59,29 @@ export async function runWalletDblessTest(t: GlobalTestState) { const reserveKeyPair = await cryptoApi.createEddsaKeypair({}); - await topupReserveWithDemobank( + 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, - reserveKeyPair.pub, - bank.baseUrl, - bank.bankAccessApiBaseUrl, + reservePub: reserveKeyPair.pub, + bankAccessApiBaseUrl: bank.bankAccessApiBaseUrl, + bankBaseUrl: bank.baseUrl, exchangeInfo, - "TESTKUDOS:10", - ); + }); + + console.log("waiting for longpoll request"); + const resp = await longpollReq; + console.log(`got response, status ${resp.status}`); - await exchange.runWirewatchOnce(); + console.log(exchangeInfo); await checkReserve(http, exchange.baseUrl, reserveKeyPair.pub); diff --git a/packages/taler-util/src/notifications.ts b/packages/taler-util/src/notifications.ts index ff1017cd1..f0683b31b 100644 --- a/packages/taler-util/src/notifications.ts +++ b/packages/taler-util/src/notifications.ts @@ -22,7 +22,7 @@ /** * Imports. */ -import { TransactionState, TransactionSubstate } from "./transactions-types.js"; +import { TransactionState } from "./transactions-types.js"; import { TalerErrorDetail } from "./wallet-types.js"; export enum NotificationType { @@ -75,9 +75,7 @@ export interface TransactionStateTransitionNotification { type: NotificationType.TransactionStateTransition; transactionId: string; oldTxState: TransactionState; - oldTxSubstate: TransactionSubstate; newTxState: TransactionState; - newTxSubstate: TransactionSubstate; } export interface ProposalAcceptedNotification { diff --git a/packages/taler-util/src/transaction-test-data.ts b/packages/taler-util/src/transaction-test-data.ts index d0c609147..dc0903f53 100644 --- a/packages/taler-util/src/transaction-test-data.ts +++ b/packages/taler-util/src/transaction-test-data.ts @@ -16,10 +16,9 @@ import { TransactionType, - TransactionState, - TransactionSubstate, PaymentStatus, ExtendedStatus, + TransactionMajorState, } from "./transactions-types.js"; import { RefreshReason } from "./wallet-types.js"; @@ -29,8 +28,9 @@ import { RefreshReason } from "./wallet-types.js"; export const sampleWalletCoreTransactions = [ { type: TransactionType.Payment, - txState: TransactionState.Done, - txSubstate: TransactionSubstate.None, + txState: { + major: TransactionMajorState.Done, + }, amountRaw: "KUDOS:10", amountEffective: "KUDOS:10", totalRefundRaw: "KUDOS:0", @@ -75,8 +75,9 @@ export const sampleWalletCoreTransactions = [ }, { type: TransactionType.Refresh, - txState: TransactionState.Pending, - txSubstate: TransactionSubstate.None, + txState: { + major: TransactionMajorState.Pending, + }, refreshReason: RefreshReason.PayMerchant, amountEffective: "KUDOS:0", amountRaw: "KUDOS:0", diff --git a/packages/taler-util/src/transactions-types.ts b/packages/taler-util/src/transactions-types.ts index 474372c4f..29ddb22a3 100644 --- a/packages/taler-util/src/transactions-types.ts +++ b/packages/taler-util/src/transactions-types.ts @@ -59,11 +59,6 @@ export enum ExtendedStatus { KycRequired = "kyc-required", } -export interface TransactionStateInfo { - txState: TransactionState; - txSubstate: TransactionSubstate; -} - export interface TransactionsRequest { /** * return only transactions in the given currency @@ -81,7 +76,12 @@ export interface TransactionsRequest { includeRefreshes?: boolean; } -export enum TransactionState { +export interface TransactionState { + major: TransactionMajorState; + minor?: TransactionMinorState; +} + +export enum TransactionMajorState { // No state, only used when reporting transitions into the initial state None = "none", Pending = "pending", @@ -96,15 +96,13 @@ export enum TransactionState { Unknown = "unknown", } -export enum TransactionSubstate { +export enum TransactionMinorState { // Placeholder until D37 is fully implemented Unknown = "unknown", - // No substate - None = "none", - DepositPendingInitial = "initial", - DepositKycRequired = "kyc-required", - DepositPendingTrack = "track", - DepositAbortingRefresh = "refresh", + Deposit = "deposit", + KycRequired = "kyc-required", + Track = "track", + Refresh = "refresh", } export interface TransactionsResponse { @@ -126,10 +124,11 @@ export interface TransactionCommon { // main timestamp of the transaction timestamp: TalerProtocolTimestamp; + /** + * Transaction state, as per DD37. + */ txState: TransactionState; - txSubstate: TransactionSubstate; - /** * @deprecated in favor of statusMajor and statusMinor */ diff --git a/packages/taler-wallet-core/src/db.ts b/packages/taler-wallet-core/src/db.ts index f5342b4cd..0bfe11aaa 100644 --- a/packages/taler-wallet-core/src/db.ts +++ b/packages/taler-wallet-core/src/db.ts @@ -865,8 +865,10 @@ export enum DepositGroupOperationStatus { AbortingWithRefresh = 11 /* ACTIVE_START + 1 */, } -// FIXME: Improve name! This enum is very specific to deposits. -export enum TransactionStatus { +/** + * Status of a single element of a deposit group. + */ +export enum DepositElementStatus { Unknown = 10, Accepted = 20, KycRequired = 30, @@ -1686,7 +1688,7 @@ export interface DepositGroupRecord { operationStatus: OperationStatus; - transactionPerCoin: TransactionStatus[]; + transactionPerCoin: DepositElementStatus[]; trackingState?: { [signature: string]: { @@ -2605,7 +2607,7 @@ export const walletDbFixups: FixupDescription[] = [ return; } dg.transactionPerCoin = dg.depositedPerCoin.map( - (c) => TransactionStatus.Unknown, + (c) => DepositElementStatus.Unknown, ); await tx.depositGroups.put(dg); }); diff --git a/packages/taler-wallet-core/src/dbless.ts b/packages/taler-wallet-core/src/dbless.ts index 30c4247a8..3fb56924d 100644 --- a/packages/taler-wallet-core/src/dbless.ts +++ b/packages/taler-wallet-core/src/dbless.ts @@ -109,14 +109,26 @@ export async function checkReserve( } } +export interface TopupReserveWithDemobankArgs { + http: HttpRequestLibrary; + reservePub: string; + bankBaseUrl: string; + bankAccessApiBaseUrl: string; + exchangeInfo: ExchangeInfo; + amount: AmountString; +} + export async function topupReserveWithDemobank( - http: HttpRequestLibrary, - reservePub: string, - bankBaseUrl: string, - bankAccessApiBaseUrl: string, - exchangeInfo: ExchangeInfo, - amount: AmountString, + args: TopupReserveWithDemobankArgs, ) { + const { + bankBaseUrl, + http, + bankAccessApiBaseUrl, + amount, + exchangeInfo, + reservePub, + } = args; const bankHandle: BankServiceHandle = { baseUrl: bankBaseUrl, bankAccessApiBaseUrl: bankAccessApiBaseUrl, diff --git a/packages/taler-wallet-core/src/operations/deposits.ts b/packages/taler-wallet-core/src/operations/deposits.ts index f5ea41e01..6e56b0897 100644 --- a/packages/taler-wallet-core/src/operations/deposits.ts +++ b/packages/taler-wallet-core/src/operations/deposits.ts @@ -40,6 +40,7 @@ import { j2s, Logger, MerchantContractTerms, + NotificationType, parsePaytoUri, PayCoinSelection, PrepareDepositRequest, @@ -49,9 +50,9 @@ import { TalerErrorCode, TalerProtocolTimestamp, TrackTransaction, + TransactionMajorState, + TransactionMinorState, TransactionState, - TransactionStateInfo, - TransactionSubstate, TransactionType, URL, WireFee, @@ -60,13 +61,16 @@ import { DenominationRecord, DepositGroupRecord, OperationStatus, - TransactionStatus, + DepositElementStatus, } from "../db.js"; import { TalerError } from "@gnu-taler/taler-util"; import { getTotalRefreshCost, KycPendingInfo, KycUserType } from "../index.js"; import { InternalWalletState } from "../internal-wallet-state.js"; import { readSuccessResponseJsonOrThrow } from "@gnu-taler/taler-util/http"; -import { OperationAttemptResult } from "../util/retries.js"; +import { + OperationAttemptResult, + OperationAttemptResultType, +} from "../util/retries.js"; import { spendCoins } from "./common.js"; import { getExchangeDetails } from "./exchanges.js"; import { @@ -89,15 +93,13 @@ const logger = new Logger("deposits.ts"); * Get the (DD37-style) transaction status based on the * database record of a deposit group. */ -export async function computeDepositTransactionStatus( - ws: InternalWalletState, +export function computeDepositTransactionStatus( dg: DepositGroupRecord, -): Promise { +): TransactionState { switch (dg.operationStatus) { case OperationStatus.Finished: { return { - txState: TransactionState.Done, - txSubstate: TransactionSubstate.None, + major: TransactionMajorState.Done, }; } case OperationStatus.Pending: { @@ -110,10 +112,10 @@ export async function computeDepositTransactionStatus( numDeposited++; } switch (dg.transactionPerCoin[i]) { - case TransactionStatus.KycRequired: + case DepositElementStatus.KycRequired: numKycRequired++; break; - case TransactionStatus.Wired: + case DepositElementStatus.Wired: numWired++; break; } @@ -121,21 +123,21 @@ export async function computeDepositTransactionStatus( if (numKycRequired > 0) { return { - txState: TransactionState.Pending, - txSubstate: TransactionSubstate.DepositKycRequired, + major: TransactionMajorState.Pending, + minor: TransactionMinorState.KycRequired, }; } if (numDeposited == numTotal) { return { - txState: TransactionState.Pending, - txSubstate: TransactionSubstate.DepositPendingTrack, + major: TransactionMajorState.Pending, + minor: TransactionMinorState.Track, }; } return { - txState: TransactionState.Pending, - txSubstate: TransactionSubstate.DepositPendingInitial, + major: TransactionMajorState.Pending, + minor: TransactionMinorState.Deposit, }; } default: @@ -221,6 +223,13 @@ export async function processDepositGroup( return OperationAttemptResult.finishedEmpty(); } + const transactionId = constructTransactionIdentifier({ + tag: TransactionType.Deposit, + depositGroupId, + }); + + const txStateOld = computeDepositTransactionStatus(depositGroup); + const contractData = extractContractData( depositGroup.contractTermsRaw, depositGroup.contractTermsHash, @@ -239,7 +248,7 @@ export async function processDepositGroup( for (let i = 0; i < depositPermissions.length; i++) { const perm = depositPermissions[i]; - let updatedDeposit: boolean | undefined = undefined; + let updatedDeposit: boolean = false; if (!depositGroup.depositedPerCoin[i]) { const requestBody: ExchangeDepositRequest = { @@ -270,7 +279,7 @@ export async function processDepositGroup( updatedDeposit = true; } - let updatedTxStatus: TransactionStatus | undefined = undefined; + let updatedTxStatus: DepositElementStatus | undefined = undefined; type ValueOf = T[keyof T]; let newWiredTransaction: @@ -280,12 +289,12 @@ export async function processDepositGroup( } | undefined; - if (depositGroup.transactionPerCoin[i] !== TransactionStatus.Wired) { - const track = await trackDepositPermission(ws, depositGroup, perm); + if (depositGroup.transactionPerCoin[i] !== DepositElementStatus.Wired) { + const track = await trackDeposit(ws, depositGroup, perm); if (track.type === "accepted") { if (!track.kyc_ok && track.requirement_row !== undefined) { - updatedTxStatus = TransactionStatus.KycRequired; + updatedTxStatus = DepositElementStatus.KycRequired; const { requirement_row: requirementRow } = track; const paytoHash = encodeCrock( hashTruncate32(stringToBytes(depositGroup.wire.payto_uri + "\0")), @@ -297,10 +306,10 @@ export async function processDepositGroup( "individual", ); } else { - updatedTxStatus = TransactionStatus.Accepted; + updatedTxStatus = DepositElementStatus.Accepted; } } else if (track.type === "wired") { - updatedTxStatus = TransactionStatus.Wired; + updatedTxStatus = DepositElementStatus.Wired; const payto = parsePaytoUri(depositGroup.wire.payto_uri); if (!payto) { @@ -327,11 +336,11 @@ export async function processDepositGroup( id: track.exchange_sig, }; } else { - updatedTxStatus = TransactionStatus.Unknown; + updatedTxStatus = DepositElementStatus.Unknown; } } - if (updatedTxStatus !== undefined || updatedDeposit !== undefined) { + if (updatedTxStatus !== undefined || updatedDeposit) { await ws.db .mktx((x) => [x.depositGroups]) .runReadWrite(async (tx) => { @@ -358,18 +367,18 @@ export async function processDepositGroup( } } - await ws.db + const txStatusNew = await ws.db .mktx((x) => [x.depositGroups]) .runReadWrite(async (tx) => { const dg = await tx.depositGroups.get(depositGroupId); if (!dg) { - return; + return undefined; } let allDepositedAndWired = true; for (let i = 0; i < depositGroup.depositedPerCoin.length; i++) { if ( !depositGroup.depositedPerCoin[i] || - depositGroup.transactionPerCoin[i] !== TransactionStatus.Wired + depositGroup.transactionPerCoin[i] !== DepositElementStatus.Wired ) { allDepositedAndWired = false; break; @@ -380,8 +389,36 @@ export async function processDepositGroup( dg.operationStatus = OperationStatus.Finished; await tx.depositGroups.put(dg); } + return computeDepositTransactionStatus(dg); + }); + + if (!txStatusNew) { + // Doesn't exist anymore! + return OperationAttemptResult.finishedEmpty(); + } + + // Notify if state transitioned + if ( + txStateOld.major !== txStatusNew.major || + txStateOld.minor !== txStatusNew.minor + ) { + ws.notify({ + type: NotificationType.TransactionStateTransition, + transactionId, + oldTxState: txStateOld, + newTxState: txStatusNew, }); - return OperationAttemptResult.finishedEmpty(); + } + + // FIXME: consider other cases like aborting, suspend, ... + if ( + txStatusNew.major === TransactionMajorState.Pending || + txStatusNew.major === TransactionMajorState.Aborting + ) { + return OperationAttemptResult.pendingEmpty(); + } else { + return OperationAttemptResult.finishedEmpty(); + } } async function getExchangeWireFee( @@ -428,7 +465,7 @@ async function getExchangeWireFee( return fee; } -async function trackDepositPermission( +async function trackDeposit( ws: InternalWalletState, depositGroup: DepositGroupRecord, dp: CoinDepositPermission, @@ -448,6 +485,7 @@ async function trackDepositPermission( }); url.searchParams.set("merchant_sig", sigResp.sig); const httpResp = await ws.http.fetch(url.href, { method: "GET" }); + logger.trace(`deposits response status: ${httpResp.status}`); switch (httpResp.status) { case HttpStatusCode.Accepted: { const accepted = await readSuccessResponseJsonOrThrow( @@ -710,7 +748,7 @@ export async function createDepositGroup( timestampCreated: AbsoluteTime.toTimestamp(now), timestampFinished: undefined, transactionPerCoin: payCoinSel.coinSel.coinPubs.map( - () => TransactionStatus.Unknown, + () => DepositElementStatus.Unknown, ), payCoinSelection: payCoinSel.coinSel, payCoinSelectionUid: encodeCrock(getRandomBytes(32)), @@ -733,7 +771,7 @@ export async function createDepositGroup( depositGroupId, }); - await ws.db + const newTxState = await ws.db .mktx((x) => [ x.depositGroups, x.coins, @@ -752,8 +790,18 @@ export async function createDepositGroup( refreshReason: RefreshReason.PayDeposit, }); await tx.depositGroups.put(depositGroup); + return computeDepositTransactionStatus(depositGroup); }); + ws.notify({ + type: NotificationType.TransactionStateTransition, + transactionId, + oldTxState: { + major: TransactionMajorState.None, + }, + newTxState, + }); + return { depositGroupId, transactionId, diff --git a/packages/taler-wallet-core/src/operations/transactions.ts b/packages/taler-wallet-core/src/operations/transactions.ts index 6a71b5c1e..884844ba6 100644 --- a/packages/taler-wallet-core/src/operations/transactions.ts +++ b/packages/taler-wallet-core/src/operations/transactions.ts @@ -35,10 +35,9 @@ import { Transaction, TransactionByIdRequest, TransactionIdStr, + TransactionMajorState, TransactionsRequest, TransactionsResponse, - TransactionState, - TransactionSubstate, TransactionType, WithdrawalType, } from "@gnu-taler/taler-util"; @@ -58,7 +57,7 @@ import { WalletContractData, PeerPushPaymentInitiationStatus, PeerPullPaymentIncomingStatus, - TransactionStatus, + DepositElementStatus, WithdrawalGroupStatus, RefreshGroupRecord, RefreshOperationStatus, @@ -79,7 +78,10 @@ import { runOperationWithErrorReporting, TombstoneTag, } from "./common.js"; -import { processDepositGroup } from "./deposits.js"; +import { + computeDepositTransactionStatus, + processDepositGroup, +} from "./deposits.js"; import { getExchangeDetails } from "./exchanges.js"; import { abortPay, @@ -425,6 +427,11 @@ export async function getTransactionById( } } +// FIXME: Just a marker helper for unknown states until DD37 is fully implemented. +const mkTxStateUnknown = () => ({ + major: TransactionMajorState.Unknown, +}); + function buildTransactionForPushPaymentDebit( pi: PeerPushPaymentInitiationRecord, contractTerms: PeerContractTerms, @@ -432,8 +439,7 @@ function buildTransactionForPushPaymentDebit( ): Transaction { return { type: TransactionType.PeerPushDebit, - txState: TransactionState.Unknown, - txSubstate: TransactionSubstate.Unknown, + txState: mkTxStateUnknown(), amountEffective: pi.totalCost, amountRaw: pi.amount, exchangeBaseUrl: pi.exchangeBaseUrl, @@ -466,8 +472,7 @@ function buildTransactionForPullPaymentDebit( ): Transaction { return { type: TransactionType.PeerPullDebit, - txState: TransactionState.Unknown, - txSubstate: TransactionSubstate.Unknown, + txState: mkTxStateUnknown(), amountEffective: pi.coinSel?.totalCost ? pi.coinSel?.totalCost : Amounts.stringify(pi.contractTerms.amount), @@ -517,8 +522,7 @@ function buildTransactionForPeerPullCredit( }); return { type: TransactionType.PeerPullCredit, - txState: TransactionState.Unknown, - txSubstate: TransactionSubstate.Unknown, + txState: mkTxStateUnknown(), amountEffective: Amounts.stringify(wsr.denomsSel.totalCoinValue), amountRaw: Amounts.stringify(wsr.instructedAmount), exchangeBaseUrl: wsr.exchangeBaseUrl, @@ -553,8 +557,7 @@ function buildTransactionForPeerPullCredit( return { type: TransactionType.PeerPullCredit, - txState: TransactionState.Unknown, - txSubstate: TransactionSubstate.Unknown, + txState: mkTxStateUnknown(), amountEffective: Amounts.stringify(pullCredit.estimatedAmountEffective), amountRaw: Amounts.stringify(peerContractTerms.amount), exchangeBaseUrl: pullCredit.exchangeBaseUrl, @@ -593,8 +596,7 @@ function buildTransactionForPeerPushCredit( return { type: TransactionType.PeerPushCredit, - txState: TransactionState.Unknown, - txSubstate: TransactionSubstate.Unknown, + txState: mkTxStateUnknown(), amountEffective: Amounts.stringify(wsr.denomsSel.totalCoinValue), amountRaw: Amounts.stringify(wsr.instructedAmount), exchangeBaseUrl: wsr.exchangeBaseUrl, @@ -618,8 +620,7 @@ function buildTransactionForPeerPushCredit( return { type: TransactionType.PeerPushCredit, - txState: TransactionState.Unknown, - txSubstate: TransactionSubstate.Unknown, + txState: mkTxStateUnknown(), // FIXME: This is wrong, needs to consider fees! amountEffective: Amounts.stringify(peerContractTerms.amount), amountRaw: Amounts.stringify(peerContractTerms.amount), @@ -649,8 +650,7 @@ function buildTransactionForBankIntegratedWithdraw( return { type: TransactionType.Withdrawal, - txState: TransactionState.Unknown, - txSubstate: TransactionSubstate.Unknown, + txState: mkTxStateUnknown(), amountEffective: Amounts.stringify(wsr.denomsSel.totalCoinValue), amountRaw: Amounts.stringify(wsr.instructedAmount), withdrawalDetails: { @@ -696,8 +696,7 @@ function buildTransactionForManualWithdraw( return { type: TransactionType.Withdrawal, - txState: TransactionState.Unknown, - txSubstate: TransactionSubstate.Unknown, + txState: mkTxStateUnknown(), amountEffective: Amounts.stringify( withdrawalGroup.denomsSel.totalCoinValue, ), @@ -748,8 +747,7 @@ function buildTransactionForRefresh( ).amount; return { type: TransactionType.Refresh, - txState: TransactionState.Unknown, - txSubstate: TransactionSubstate.Unknown, + txState: mkTxStateUnknown(), refreshReason: refreshGroupRecord.reason, amountEffective: Amounts.stringify( Amounts.zeroOfCurrency(refreshGroupRecord.currency), @@ -791,8 +789,7 @@ function buildTransactionForDeposit( return { type: TransactionType.Deposit, - txState: TransactionState.Unknown, - txSubstate: TransactionSubstate.Unknown, + txState: computeDepositTransactionStatus(dg), amountRaw: Amounts.stringify(dg.effectiveDepositAmount), amountEffective: Amounts.stringify(dg.totalPayCost), extendedStatus: dg.timestampFinished @@ -810,7 +807,7 @@ function buildTransactionForDeposit( wireTransferProgress: (100 * dg.transactionPerCoin.reduce( - (prev, cur) => prev + (cur === TransactionStatus.Wired ? 1 : 0), + (prev, cur) => prev + (cur === DepositElementStatus.Wired ? 1 : 0), 0, )) / dg.transactionPerCoin.length, @@ -829,8 +826,7 @@ function buildTransactionForTip( return { type: TransactionType.Tip, - txState: TransactionState.Unknown, - txSubstate: TransactionSubstate.Unknown, + txState: mkTxStateUnknown(), amountEffective: Amounts.stringify(tipRecord.tipAmountEffective), amountRaw: Amounts.stringify(tipRecord.tipAmountRaw), extendedStatus: tipRecord.pickedUpTimestamp @@ -926,8 +922,7 @@ async function buildTransactionForRefund( return { type: TransactionType.Refund, - txState: TransactionState.Unknown, - txSubstate: TransactionSubstate.Unknown, + txState: mkTxStateUnknown(), info, refundedTransactionId: makeTransactionId( TransactionType.Payment, @@ -1030,8 +1025,7 @@ async function buildTransactionForPurchase( return { type: TransactionType.Payment, - txState: TransactionState.Unknown, - txSubstate: TransactionSubstate.Unknown, + txState: mkTxStateUnknown(), amountRaw: Amounts.stringify(contractData.amount), amountEffective: Amounts.stringify(purchaseRecord.payInfo.totalPayCost), totalRefundRaw: Amounts.stringify(totalRefund.raw), diff --git a/packages/taler-wallet-core/src/util/retries.ts b/packages/taler-wallet-core/src/util/retries.ts index 5b6645924..a021087be 100644 --- a/packages/taler-wallet-core/src/util/retries.ts +++ b/packages/taler-wallet-core/src/util/retries.ts @@ -70,6 +70,12 @@ export namespace OperationAttemptResult { result: undefined, }; } + export function pendingEmpty(): OperationAttemptResult { + return { + type: OperationAttemptResultType.Pending, + result: undefined, + }; + } } export interface OperationAttemptFinishedResult { -- cgit v1.2.3