diff options
author | Florian Dold <florian@dold.me> | 2023-04-23 22:49:42 +0200 |
---|---|---|
committer | Florian Dold <florian@dold.me> | 2023-04-23 22:49:42 +0200 |
commit | eff3920bd5a2bff58d66ac72ba8bd2c1577f452f (patch) | |
tree | 1e7435da254b56bb9b8a543ce488aeb84f032ac9 /packages/taler-wallet-core/src/operations | |
parent | 321252040efdb0712a38b3488297a7a802c5cb59 (diff) | |
download | wallet-core-eff3920bd5a2bff58d66ac72ba8bd2c1577f452f.tar.xz |
wallet-core: further towards deposit DD37
Diffstat (limited to 'packages/taler-wallet-core/src/operations')
-rw-r--r-- | packages/taler-wallet-core/src/operations/deposits.ts | 513 | ||||
-rw-r--r-- | packages/taler-wallet-core/src/operations/transactions.ts | 26 |
2 files changed, 364 insertions, 175 deletions
diff --git a/packages/taler-wallet-core/src/operations/deposits.ts b/packages/taler-wallet-core/src/operations/deposits.ts index 6e56b0897..051cbc176 100644 --- a/packages/taler-wallet-core/src/operations/deposits.ts +++ b/packages/taler-wallet-core/src/operations/deposits.ts @@ -64,13 +64,16 @@ import { DepositElementStatus, } from "../db.js"; import { TalerError } from "@gnu-taler/taler-util"; -import { getTotalRefreshCost, KycPendingInfo, KycUserType } from "../index.js"; +import { + DepositOperationStatus, + getTotalRefreshCost, + KycPendingInfo, + KycUserType, + PendingTaskType, +} from "../index.js"; import { InternalWalletState } from "../internal-wallet-state.js"; import { readSuccessResponseJsonOrThrow } from "@gnu-taler/taler-util/http"; -import { - OperationAttemptResult, - OperationAttemptResultType, -} from "../util/retries.js"; +import { OperationAttemptResult } from "../util/retries.js"; import { spendCoins } from "./common.js"; import { getExchangeDetails } from "./exchanges.js"; import { @@ -82,7 +85,9 @@ import { selectPayCoinsNew } from "../util/coinSelection.js"; import { constructTransactionIdentifier, parseTransactionIdentifier, + stopLongpolling, } from "./transactions.js"; +import { constructTaskIdentifier } from "../util/retries.js"; /** * Logger. @@ -97,12 +102,12 @@ export function computeDepositTransactionStatus( dg: DepositGroupRecord, ): TransactionState { switch (dg.operationStatus) { - case OperationStatus.Finished: { + case DepositOperationStatus.Finished: { return { major: TransactionMajorState.Done, }; } - case OperationStatus.Pending: { + case DepositOperationStatus.Pending: { const numTotal = dg.payCoinSelection.coinPubs.length; let numDeposited = 0; let numKycRequired = 0; @@ -140,6 +145,10 @@ export function computeDepositTransactionStatus( minor: TransactionMinorState.Deposit, }; } + case DepositOperationStatus.Suspended: + return { + major: TransactionMajorState.Suspended, + }; default: throw Error("unexpected deposit group state"); } @@ -149,13 +158,156 @@ export async function suspendDepositGroup( ws: InternalWalletState, depositGroupId: string, ): Promise<void> { - throw Error("not implemented"); + const transactionId = constructTransactionIdentifier({ + tag: TransactionType.Deposit, + depositGroupId, + }); + const retryTag = constructTaskIdentifier({ + tag: PendingTaskType.Deposit, + depositGroupId, + }); + let res = await ws.db + .mktx((x) => [x.depositGroups]) + .runReadWrite(async (tx) => { + const dg = await tx.depositGroups.get(depositGroupId); + if (!dg) { + logger.warn( + `can't suspend deposit group, depositGroupId=${depositGroupId} not found`, + ); + return undefined; + } + const oldState = computeDepositTransactionStatus(dg); + switch (dg.operationStatus) { + case DepositOperationStatus.Finished: + return undefined; + case DepositOperationStatus.Pending: { + dg.operationStatus = DepositOperationStatus.Suspended; + await tx.depositGroups.put(dg); + return { + oldTxState: oldState, + newTxState: computeDepositTransactionStatus(dg), + }; + } + case DepositOperationStatus.Suspended: + return undefined; + } + return undefined; + }); + stopLongpolling(ws, retryTag); + if (res) { + ws.notify({ + type: NotificationType.TransactionStateTransition, + transactionId, + oldTxState: res.oldTxState, + newTxState: res.newTxState, + }); + } +} + +export async function resumeDepositGroup( + ws: InternalWalletState, + depositGroupId: string, +): Promise<void> { + const transactionId = constructTransactionIdentifier({ + tag: TransactionType.Deposit, + depositGroupId, + }); + let res = await ws.db + .mktx((x) => [x.depositGroups]) + .runReadWrite(async (tx) => { + const dg = await tx.depositGroups.get(depositGroupId); + if (!dg) { + logger.warn( + `can't resume deposit group, depositGroupId=${depositGroupId} not found`, + ); + return; + } + const oldState = computeDepositTransactionStatus(dg); + switch (dg.operationStatus) { + case DepositOperationStatus.Finished: + return; + case DepositOperationStatus.Pending: { + return; + } + case DepositOperationStatus.Suspended: + dg.operationStatus = DepositOperationStatus.Pending; + await tx.depositGroups.put(dg); + return { + oldTxState: oldState, + newTxState: computeDepositTransactionStatus(dg), + }; + } + return undefined; + }); + ws.latch.trigger(); + if (res) { + ws.notify({ + type: NotificationType.TransactionStateTransition, + transactionId, + oldTxState: res.oldTxState, + newTxState: res.newTxState, + }); + } } export async function abortDepositGroup( ws: InternalWalletState, depositGroupId: string, ): Promise<void> { + const transactionId = constructTransactionIdentifier({ + tag: TransactionType.Deposit, + depositGroupId, + }); + const retryTag = constructTaskIdentifier({ + tag: PendingTaskType.Deposit, + depositGroupId, + }); + let res = await ws.db + .mktx((x) => [x.depositGroups]) + .runReadWrite(async (tx) => { + const dg = await tx.depositGroups.get(depositGroupId); + if (!dg) { + logger.warn( + `can't suspend deposit group, depositGroupId=${depositGroupId} not found`, + ); + return undefined; + } + const oldState = computeDepositTransactionStatus(dg); + switch (dg.operationStatus) { + case DepositOperationStatus.Finished: + return undefined; + case DepositOperationStatus.Pending: { + dg.operationStatus = DepositOperationStatus.Aborting; + await tx.depositGroups.put(dg); + return { + oldTxState: oldState, + newTxState: computeDepositTransactionStatus(dg), + }; + } + case DepositOperationStatus.Suspended: + // FIXME: Can we abort a suspended transaction?! + return undefined; + } + return undefined; + }); + stopLongpolling(ws, retryTag); + // Need to process the operation again. + ws.latch.trigger(); + if (res) { + ws.notify({ + type: NotificationType.TransactionStateTransition, + transactionId, + oldTxState: res.oldTxState, + newTxState: res.newTxState, + }); + } +} + +export async function deleteDepositGroup( + ws: InternalWalletState, + depositGroupId: boolean, + opts: { forced?: boolean } = {}, +) { throw Error("not implemented"); } @@ -230,195 +382,210 @@ export async function processDepositGroup( const txStateOld = computeDepositTransactionStatus(depositGroup); - const contractData = extractContractData( - depositGroup.contractTermsRaw, - depositGroup.contractTermsHash, - "", - ); + if (depositGroup.operationStatus === DepositOperationStatus.Pending) { + const contractData = extractContractData( + depositGroup.contractTermsRaw, + depositGroup.contractTermsHash, + "", + ); - // Check for cancellation before expensive operations. - options.cancellationToken?.throwIfCancelled(); - // FIXME: Cache these! - const depositPermissions = await generateDepositPermissions( - ws, - depositGroup.payCoinSelection, - contractData, - ); + // Check for cancellation before expensive operations. + options.cancellationToken?.throwIfCancelled(); + // FIXME: Cache these! + const depositPermissions = await generateDepositPermissions( + ws, + depositGroup.payCoinSelection, + contractData, + ); - for (let i = 0; i < depositPermissions.length; i++) { - const perm = depositPermissions[i]; - - let updatedDeposit: boolean = false; - - if (!depositGroup.depositedPerCoin[i]) { - const requestBody: ExchangeDepositRequest = { - contribution: Amounts.stringify(perm.contribution), - merchant_payto_uri: depositGroup.wire.payto_uri, - wire_salt: depositGroup.wire.salt, - h_contract_terms: depositGroup.contractTermsHash, - ub_sig: perm.ub_sig, - timestamp: depositGroup.contractTermsRaw.timestamp, - wire_transfer_deadline: - depositGroup.contractTermsRaw.wire_transfer_deadline, - refund_deadline: depositGroup.contractTermsRaw.refund_deadline, - coin_sig: perm.coin_sig, - denom_pub_hash: perm.h_denom, - merchant_pub: depositGroup.merchantPub, - h_age_commitment: perm.h_age_commitment, - }; - // Check for cancellation before making network request. - options.cancellationToken?.throwIfCancelled(); - const url = new URL(`coins/${perm.coin_pub}/deposit`, perm.exchange_url); - logger.info(`depositing to ${url}`); - const httpResp = await ws.http.fetch(url.href, { - method: "POST", - body: requestBody, - cancellationToken: options.cancellationToken, - }); - await readSuccessResponseJsonOrThrow(httpResp, codecForDepositSuccess()); - updatedDeposit = true; - } + for (let i = 0; i < depositPermissions.length; i++) { + const perm = depositPermissions[i]; + + let updatedDeposit: boolean = false; + + if (!depositGroup.depositedPerCoin[i]) { + const requestBody: ExchangeDepositRequest = { + contribution: Amounts.stringify(perm.contribution), + merchant_payto_uri: depositGroup.wire.payto_uri, + wire_salt: depositGroup.wire.salt, + h_contract_terms: depositGroup.contractTermsHash, + ub_sig: perm.ub_sig, + timestamp: depositGroup.contractTermsRaw.timestamp, + wire_transfer_deadline: + depositGroup.contractTermsRaw.wire_transfer_deadline, + refund_deadline: depositGroup.contractTermsRaw.refund_deadline, + coin_sig: perm.coin_sig, + denom_pub_hash: perm.h_denom, + merchant_pub: depositGroup.merchantPub, + h_age_commitment: perm.h_age_commitment, + }; + // Check for cancellation before making network request. + options.cancellationToken?.throwIfCancelled(); + const url = new URL( + `coins/${perm.coin_pub}/deposit`, + perm.exchange_url, + ); + logger.info(`depositing to ${url}`); + const httpResp = await ws.http.fetch(url.href, { + method: "POST", + body: requestBody, + cancellationToken: options.cancellationToken, + }); + await readSuccessResponseJsonOrThrow( + httpResp, + codecForDepositSuccess(), + ); + updatedDeposit = true; + } - let updatedTxStatus: DepositElementStatus | undefined = undefined; - type ValueOf<T> = T[keyof T]; + let updatedTxStatus: DepositElementStatus | undefined = undefined; + type ValueOf<T> = T[keyof T]; - let newWiredTransaction: - | { - id: string; - value: ValueOf<NonNullable<DepositGroupRecord["trackingState"]>>; - } - | undefined; + let newWiredTransaction: + | { + id: string; + value: ValueOf<NonNullable<DepositGroupRecord["trackingState"]>>; + } + | undefined; - if (depositGroup.transactionPerCoin[i] !== DepositElementStatus.Wired) { - const track = await trackDeposit(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 = DepositElementStatus.KycRequired; - const { requirement_row: requirementRow } = track; - const paytoHash = encodeCrock( - hashTruncate32(stringToBytes(depositGroup.wire.payto_uri + "\0")), - ); - await checkDepositKycStatus( + if (track.type === "accepted") { + if (!track.kyc_ok && track.requirement_row !== undefined) { + updatedTxStatus = DepositElementStatus.KycRequired; + const { requirement_row: requirementRow } = track; + const paytoHash = encodeCrock( + hashTruncate32(stringToBytes(depositGroup.wire.payto_uri + "\0")), + ); + await checkDepositKycStatus( + ws, + perm.exchange_url, + { paytoHash, requirementRow }, + "individual", + ); + } else { + updatedTxStatus = DepositElementStatus.Accepted; + } + } else if (track.type === "wired") { + updatedTxStatus = DepositElementStatus.Wired; + + const payto = parsePaytoUri(depositGroup.wire.payto_uri); + if (!payto) { + throw Error(`unparsable payto: ${depositGroup.wire.payto_uri}`); + } + + const fee = await getExchangeWireFee( ws, + payto.targetType, perm.exchange_url, - { paytoHash, requirementRow }, - "individual", + track.execution_time, ); + const raw = Amounts.parseOrThrow(track.coin_contribution); + const wireFee = Amounts.parseOrThrow(fee.wireFee); + + newWiredTransaction = { + value: { + amountRaw: Amounts.stringify(raw), + wireFee: Amounts.stringify(wireFee), + exchangePub: track.exchange_pub, + timestampExecuted: track.execution_time, + wireTransferId: track.wtid, + }, + id: track.exchange_sig, + }; } else { - updatedTxStatus = DepositElementStatus.Accepted; + updatedTxStatus = DepositElementStatus.Unknown; } - } else if (track.type === "wired") { - updatedTxStatus = DepositElementStatus.Wired; + } - const payto = parsePaytoUri(depositGroup.wire.payto_uri); - if (!payto) { - throw Error(`unparsable payto: ${depositGroup.wire.payto_uri}`); - } + if (updatedTxStatus !== undefined || updatedDeposit) { + await ws.db + .mktx((x) => [x.depositGroups]) + .runReadWrite(async (tx) => { + const dg = await tx.depositGroups.get(depositGroupId); + if (!dg) { + return; + } + if (updatedDeposit !== undefined) { + dg.depositedPerCoin[i] = updatedDeposit; + } + if (updatedTxStatus !== undefined) { + dg.transactionPerCoin[i] = updatedTxStatus; + } + if (newWiredTransaction) { + if (!dg.trackingState) { + dg.trackingState = {}; + } - const fee = await getExchangeWireFee( - ws, - payto.targetType, - perm.exchange_url, - track.execution_time, - ); - const raw = Amounts.parseOrThrow(track.coin_contribution); - const wireFee = Amounts.parseOrThrow(fee.wireFee); - - newWiredTransaction = { - value: { - amountRaw: Amounts.stringify(raw), - wireFee: Amounts.stringify(wireFee), - exchangePub: track.exchange_pub, - timestampExecuted: track.execution_time, - wireTransferId: track.wtid, - }, - id: track.exchange_sig, - }; - } else { - updatedTxStatus = DepositElementStatus.Unknown; + dg.trackingState[newWiredTransaction.id] = + newWiredTransaction.value; + } + await tx.depositGroups.put(dg); + }); } } - if (updatedTxStatus !== undefined || updatedDeposit) { - await ws.db - .mktx((x) => [x.depositGroups]) - .runReadWrite(async (tx) => { - const dg = await tx.depositGroups.get(depositGroupId); - if (!dg) { - return; - } - if (updatedDeposit !== undefined) { - dg.depositedPerCoin[i] = updatedDeposit; - } - if (updatedTxStatus !== undefined) { - dg.transactionPerCoin[i] = updatedTxStatus; - } - if (newWiredTransaction) { - if (!dg.trackingState) { - dg.trackingState = {}; - } - - dg.trackingState[newWiredTransaction.id] = - newWiredTransaction.value; + const txStatusNew = await ws.db + .mktx((x) => [x.depositGroups]) + .runReadWrite(async (tx) => { + const dg = await tx.depositGroups.get(depositGroupId); + if (!dg) { + return undefined; + } + let allDepositedAndWired = true; + for (let i = 0; i < depositGroup.depositedPerCoin.length; i++) { + if ( + !depositGroup.depositedPerCoin[i] || + depositGroup.transactionPerCoin[i] !== DepositElementStatus.Wired + ) { + allDepositedAndWired = false; + break; } + } + if (allDepositedAndWired) { + dg.timestampFinished = TalerProtocolTimestamp.now(); + dg.operationStatus = DepositOperationStatus.Finished; await tx.depositGroups.put(dg); - }); - } - } - - const txStatusNew = await ws.db - .mktx((x) => [x.depositGroups]) - .runReadWrite(async (tx) => { - const dg = await tx.depositGroups.get(depositGroupId); - if (!dg) { - return undefined; - } - let allDepositedAndWired = true; - for (let i = 0; i < depositGroup.depositedPerCoin.length; i++) { - if ( - !depositGroup.depositedPerCoin[i] || - depositGroup.transactionPerCoin[i] !== DepositElementStatus.Wired - ) { - allDepositedAndWired = false; - break; } - } - if (allDepositedAndWired) { - dg.timestampFinished = TalerProtocolTimestamp.now(); - dg.operationStatus = OperationStatus.Finished; - await tx.depositGroups.put(dg); - } - return computeDepositTransactionStatus(dg); - }); + return computeDepositTransactionStatus(dg); + }); - if (!txStatusNew) { - // Doesn't exist anymore! - return OperationAttemptResult.finishedEmpty(); - } + 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, - }); + // Notify if state transitioned + if ( + txStateOld.major !== txStatusNew.major || + txStateOld.minor !== txStatusNew.minor + ) { + ws.notify({ + type: NotificationType.TransactionStateTransition, + transactionId, + oldTxState: txStateOld, + newTxState: txStatusNew, + }); + } + + // FIXME: consider other cases like aborting, suspend, ... + if ( + txStatusNew.major === TransactionMajorState.Pending || + txStatusNew.major === TransactionMajorState.Aborting + ) { + return OperationAttemptResult.pendingEmpty(); + } else { + return OperationAttemptResult.finishedEmpty(); + } } - // FIXME: consider other cases like aborting, suspend, ... - if ( - txStatusNew.major === TransactionMajorState.Pending || - txStatusNew.major === TransactionMajorState.Aborting - ) { + if (depositGroup.operationStatus === DepositOperationStatus.Aborting) { + // FIXME: Implement! return OperationAttemptResult.pendingEmpty(); - } else { - return OperationAttemptResult.finishedEmpty(); } + + return OperationAttemptResult.finishedEmpty(); } async function getExchangeWireFee( @@ -763,7 +930,7 @@ export async function createDepositGroup( payto_uri: req.depositPaytoUri, salt: wireSalt, }, - operationStatus: OperationStatus.Pending, + operationStatus: DepositOperationStatus.Pending, }; const transactionId = constructTransactionIdentifier({ diff --git a/packages/taler-wallet-core/src/operations/transactions.ts b/packages/taler-wallet-core/src/operations/transactions.ts index 884844ba6..1a511583a 100644 --- a/packages/taler-wallet-core/src/operations/transactions.ts +++ b/packages/taler-wallet-core/src/operations/transactions.ts @@ -81,6 +81,7 @@ import { import { computeDepositTransactionStatus, processDepositGroup, + suspendDepositGroup, } from "./deposits.js"; import { getExchangeDetails } from "./exchanges.js"; import { @@ -1615,7 +1616,19 @@ export async function retryTransaction( export async function suspendTransaction( ws: InternalWalletState, transactionId: string, -): Promise<void> {} +): Promise<void> { + const tx = parseTransactionIdentifier(transactionId); + if (!tx) { + throw Error("invalid transaction ID"); + } + switch (tx.tag) { + case TransactionType.Deposit: + await suspendDepositGroup(ws, tx.depositGroupId); + return; + default: + logger.warn(`unable to suspend transaction of type '${tx.tag}'`); + } +} /** * Resume a suspended transaction. @@ -1623,7 +1636,16 @@ export async function suspendTransaction( export async function resumeTransaction( ws: InternalWalletState, transactionId: string, -): Promise<void> {} +): Promise<void> { + const tx = parseTransactionIdentifier(transactionId); + if (!tx) { + throw Error("invalid transaction ID"); + } + switch (tx.tag) { + default: + logger.warn(`unable to resume transaction of type '${tx.tag}'`); + } +} /** * Permanently delete a transaction based on the transaction ID. |