diff options
Diffstat (limited to 'packages/taler-wallet-core/src/operations/deposits.ts')
-rw-r--r-- | packages/taler-wallet-core/src/operations/deposits.ts | 211 |
1 files changed, 200 insertions, 11 deletions
diff --git a/packages/taler-wallet-core/src/operations/deposits.ts b/packages/taler-wallet-core/src/operations/deposits.ts index 14d1f9e3f..e1d699775 100644 --- a/packages/taler-wallet-core/src/operations/deposits.ts +++ b/packages/taler-wallet-core/src/operations/deposits.ts @@ -27,12 +27,14 @@ import { codecForTackTransactionAccepted, codecForTackTransactionWired, CoinDepositPermission, + CoinRefreshRequest, CreateDepositGroupRequest, CreateDepositGroupResponse, DepositGroupFees, durationFromSpec, encodeCrock, ExchangeDepositRequest, + ExchangeRefundRequest, getRandomBytes, hashTruncate32, hashWire, @@ -65,11 +67,14 @@ import { } from "../db.js"; import { TalerError } from "@gnu-taler/taler-util"; import { + createRefreshGroup, DepositOperationStatus, + DepositTrackingInfo, getTotalRefreshCost, KycPendingInfo, KycUserType, PendingTaskType, + RefreshOperationStatus, } from "../index.js"; import { InternalWalletState } from "../internal-wallet-state.js"; import { readSuccessResponseJsonOrThrow } from "@gnu-taler/taler-util/http"; @@ -88,6 +93,7 @@ import { stopLongpolling, } from "./transactions.js"; import { constructTaskIdentifier } from "../util/retries.js"; +import { checkDbInvariant, checkLogicInvariant } from "../util/invariants.js"; /** * Logger. @@ -126,6 +132,9 @@ export function computeDepositTransactionStatus( } } + logger.info(`num total ${numTotal}`); + logger.info(`num deposited ${numDeposited}`); + if (numKycRequired > 0) { return { major: TransactionMajorState.Pending, @@ -352,6 +361,184 @@ async function checkDepositKycStatus( } /** + * Check whether the refresh associated with the + * aborting deposit group is done. + * + * If done, mark the deposit transaction as aborted. + * + * Otherwise continue waiting. + * + * FIXME: Wait for the refresh group notifications instead of periodically + * checking the refresh group status. + * FIXME: This is just one transaction, can't we do this in the initial + * transaction of processDepositGroup? + */ +async function waitForRefreshOnDepositGroup( + ws: InternalWalletState, + depositGroup: DepositGroupRecord, +): Promise<OperationAttemptResult> { + const abortRefreshGroupId = depositGroup.abortRefreshGroupId; + checkLogicInvariant(!!abortRefreshGroupId); + // FIXME: Emit notification on state transition! + const res = await ws.db + .mktx((x) => [x.refreshGroups, x.depositGroups]) + .runReadWrite(async (tx) => { + const refreshGroup = await tx.refreshGroups.get(abortRefreshGroupId); + let newOpState: DepositOperationStatus | undefined; + if (!refreshGroup) { + // Maybe it got manually deleted? Means that we should + // just go into aborted. + logger.warn("no aborting refresh group found for deposit group"); + newOpState = DepositOperationStatus.Aborted; + } else { + if (refreshGroup.operationStatus === RefreshOperationStatus.Finished) { + newOpState = DepositOperationStatus.Aborted; + } else if ( + refreshGroup.operationStatus === + RefreshOperationStatus.FinishedWithError + ) { + newOpState = DepositOperationStatus.Aborted; + } + } + if (newOpState) { + const newDg = await tx.depositGroups.get(depositGroup.depositGroupId); + if (!newDg) { + return; + } + const oldDepositTxStatus = computeDepositTransactionStatus(newDg); + newDg.operationStatus = newOpState; + const newDepositTxStatus = computeDepositTransactionStatus(newDg); + await tx.depositGroups.put(newDg); + return { oldDepositTxStatus, newDepositTxStatus }; + } + return undefined; + }); + + if (res) { + const transactionId = constructTransactionIdentifier({ + tag: TransactionType.Deposit, + depositGroupId: depositGroup.depositGroupId, + }); + ws.notify({ + type: NotificationType.TransactionStateTransition, + transactionId, + oldTxState: res.oldDepositTxStatus, + newTxState: res.newDepositTxStatus, + }); + return OperationAttemptResult.pendingEmpty(); + } else { + return OperationAttemptResult.pendingEmpty(); + } +} + +async function refundDepositGroup( + ws: InternalWalletState, + depositGroup: DepositGroupRecord, +): Promise<OperationAttemptResult> { + const newTxPerCoin = [...depositGroup.transactionPerCoin]; + for (let i = 0; i < depositGroup.transactionPerCoin.length; i++) { + const st = depositGroup.transactionPerCoin[i]; + switch (st) { + case DepositElementStatus.RefundFailed: + case DepositElementStatus.RefundSuccess: + break; + default: { + const coinPub = depositGroup.payCoinSelection.coinPubs[i]; + const coinExchange = await ws.db + .mktx((x) => [x.coins]) + .runReadOnly(async (tx) => { + const coinRecord = await tx.coins.get(coinPub); + checkDbInvariant(!!coinRecord); + return coinRecord.exchangeBaseUrl; + }); + const refundAmount = depositGroup.payCoinSelection.coinContributions[i]; + // We use a constant refund transaction ID, since there can + // only be one refund. + const rtid = 1; + const sig = await ws.cryptoApi.signRefund({ + coinPub, + contractTermsHash: depositGroup.contractTermsHash, + merchantPriv: depositGroup.merchantPriv, + merchantPub: depositGroup.merchantPub, + refundAmount: refundAmount, + rtransactionId: rtid, + }); + const refundReq: ExchangeRefundRequest = { + h_contract_terms: depositGroup.contractTermsHash, + merchant_pub: depositGroup.merchantPub, + merchant_sig: sig.sig, + refund_amount: refundAmount, + rtransaction_id: rtid, + }; + const refundUrl = new URL(`coins/${coinPub}/refund`, coinExchange); + const httpResp = await ws.http.fetch(refundUrl.href, { + method: "POST", + body: refundReq, + }); + let newStatus: DepositElementStatus; + if (httpResp.status === 200) { + // FIXME: validate response + newStatus = DepositElementStatus.RefundSuccess; + } else { + // FIXME: Store problem somewhere! + newStatus = DepositElementStatus.RefundFailed; + } + // FIXME: Handle case where refund request needs to be tried again + newTxPerCoin[i] = newStatus; + break; + } + } + } + let isDone = true; + for (let i = 0; i < newTxPerCoin.length; i++) { + if ( + newTxPerCoin[i] != DepositElementStatus.RefundFailed || + newTxPerCoin[i] != DepositElementStatus.RefundSuccess + ) { + isDone = false; + } + } + + const currency = Amounts.currencyOf(depositGroup.totalPayCost); + + await ws.db + .mktx((x) => [ + x.depositGroups, + x.refreshGroups, + x.coins, + x.denominations, + x.coinAvailability, + ]) + .runReadWrite(async (tx) => { + const newDg = await tx.depositGroups.get(depositGroup.depositGroupId); + if (!newDg) { + return; + } + newDg.transactionPerCoin = newTxPerCoin; + const refreshCoins: CoinRefreshRequest[] = []; + for (let i = 0; i < newTxPerCoin.length; i++) { + refreshCoins.push({ + amount: depositGroup.payCoinSelection.coinContributions[i], + coinPub: depositGroup.payCoinSelection.coinPubs[i], + }); + } + if (isDone) { + const rgid = await createRefreshGroup( + ws, + tx, + currency, + refreshCoins, + RefreshReason.AbortDeposit, + ); + newDg.abortRefreshGroupId = rgid.refreshGroupId; + } + await tx.depositGroups.put(newDg); + }); + + return OperationAttemptResult.pendingEmpty(); +} + +/** * Process a deposit group that is not in its final state yet. */ export async function processDepositGroup( @@ -401,7 +588,7 @@ export async function processDepositGroup( for (let i = 0; i < depositPermissions.length; i++) { const perm = depositPermissions[i]; - let updatedDeposit: boolean = false; + let didDeposit: boolean = false; if (!depositGroup.depositedPerCoin[i]) { const requestBody: ExchangeDepositRequest = { @@ -435,16 +622,15 @@ export async function processDepositGroup( httpResp, codecForDepositSuccess(), ); - updatedDeposit = true; + didDeposit = true; } let updatedTxStatus: DepositElementStatus | undefined = undefined; - type ValueOf<T> = T[keyof T]; let newWiredCoin: | { id: string; - value: ValueOf<NonNullable<DepositGroupRecord["trackingState"]>>; + value: DepositTrackingInfo; } | undefined; @@ -499,7 +685,7 @@ export async function processDepositGroup( } } - if (updatedTxStatus !== undefined || updatedDeposit) { + if (updatedTxStatus !== undefined || didDeposit) { await ws.db .mktx((x) => [x.depositGroups]) .runReadWrite(async (tx) => { @@ -507,8 +693,8 @@ export async function processDepositGroup( if (!dg) { return; } - if (updatedDeposit !== undefined) { - dg.depositedPerCoin[i] = updatedDeposit; + if (didDeposit) { + dg.depositedPerCoin[i] = didDeposit; } if (updatedTxStatus !== undefined) { dg.transactionPerCoin[i] = updatedTxStatus; @@ -526,7 +712,8 @@ export async function processDepositGroup( dg.trackingState = {}; } - dg.trackingState[newWiredCoin.id] = newWiredCoin.value; + dg.trackingState[newWiredCoin.id] = + newWiredCoin.value; } await tx.depositGroups.put(dg); }); @@ -588,10 +775,12 @@ export async function processDepositGroup( } if (depositGroup.operationStatus === DepositOperationStatus.Aborting) { - // FIXME: Implement! - return OperationAttemptResult.pendingEmpty(); + const abortRefreshGroupId = depositGroup.abortRefreshGroupId; + if (!abortRefreshGroupId) { + return refundDepositGroup(ws, depositGroup); + } + return waitForRefreshOnDepositGroup(ws, depositGroup); } - return OperationAttemptResult.finishedEmpty(); } |