aboutsummaryrefslogtreecommitdiff
path: root/packages/taler-wallet-core/src/operations/pay-peer-pull-debit.ts
diff options
context:
space:
mode:
Diffstat (limited to 'packages/taler-wallet-core/src/operations/pay-peer-pull-debit.ts')
-rw-r--r--packages/taler-wallet-core/src/operations/pay-peer-pull-debit.ts253
1 files changed, 177 insertions, 76 deletions
diff --git a/packages/taler-wallet-core/src/operations/pay-peer-pull-debit.ts b/packages/taler-wallet-core/src/operations/pay-peer-pull-debit.ts
index 2be21c68d..280ad567f 100644
--- a/packages/taler-wallet-core/src/operations/pay-peer-pull-debit.ts
+++ b/packages/taler-wallet-core/src/operations/pay-peer-pull-debit.ts
@@ -29,6 +29,7 @@ import {
TalerError,
TalerErrorCode,
TalerPreciseTimestamp,
+ TalerProtocolViolationError,
TransactionAction,
TransactionMajorState,
TransactionMinorState,
@@ -44,7 +45,11 @@ import {
j2s,
parsePayPullUri,
} from "@gnu-taler/taler-util";
-import { readSuccessResponseJsonOrThrow } from "@gnu-taler/taler-util/http";
+import {
+ HttpResponse,
+ readSuccessResponseJsonOrThrow,
+ readTalerErrorResponse,
+} from "@gnu-taler/taler-util/http";
import {
InternalWalletState,
PeerPullDebitRecordStatus,
@@ -62,6 +67,7 @@ import {
} from "../util/retries.js";
import { runOperationWithErrorReporting, spendCoins } from "./common.js";
import {
+ PeerCoinRepair,
codecForExchangePurseStatus,
getTotalPeerPaymentCost,
queryCoinInfosForSelection,
@@ -76,6 +82,84 @@ import { checkLogicInvariant } from "../util/invariants.js";
const logger = new Logger("pay-peer-pull-debit.ts");
+async function handlePurseCreationConflict(
+ ws: InternalWalletState,
+ peerPullInc: PeerPullPaymentIncomingRecord,
+ resp: HttpResponse,
+): Promise<OperationAttemptResult> {
+ const pursePub = peerPullInc.pursePub;
+ const errResp = await readTalerErrorResponse(resp);
+ if (errResp.code !== TalerErrorCode.EXCHANGE_GENERIC_INSUFFICIENT_FUNDS) {
+ await failPeerPullDebitTransaction(ws, pursePub);
+ return OperationAttemptResult.finishedEmpty();
+ }
+
+ // FIXME: Properly parse!
+ const brokenCoinPub = (errResp as any).coin_pub;
+ logger.trace(`excluded broken coin pub=${brokenCoinPub}`);
+
+ if (!brokenCoinPub) {
+ // FIXME: Details!
+ throw new TalerProtocolViolationError();
+ }
+
+ const instructedAmount = Amounts.parseOrThrow(
+ peerPullInc.contractTerms.amount,
+ );
+
+ const sel = peerPullInc.coinSel;
+ if (!sel) {
+ throw Error("invalid state (coin selection expected)");
+ }
+
+ const repair: PeerCoinRepair = {
+ coinPubs: sel.coinPubs,
+ contribs: sel.contributions.map((x) => Amounts.parseOrThrow(x)),
+ exchangeBaseUrl: peerPullInc.exchangeBaseUrl,
+ };
+
+ const coinSelRes = await selectPeerCoins(ws, { instructedAmount, repair });
+
+ if (coinSelRes.type == "failure") {
+ // FIXME: Details!
+ throw Error(
+ "insufficient balance to re-select coins to repair double spending",
+ );
+ }
+
+ const totalAmount = await getTotalPeerPaymentCost(
+ ws,
+ coinSelRes.result.coins,
+ );
+
+ await ws.db
+ .mktx((x) => [x.peerPullPaymentIncoming])
+ .runReadWrite(async (tx) => {
+ const myPpi = await tx.peerPullPaymentIncoming.get(
+ peerPullInc.peerPullPaymentIncomingId,
+ );
+ if (!myPpi) {
+ return;
+ }
+ switch (myPpi.status) {
+ case PeerPullDebitRecordStatus.PendingDeposit:
+ case PeerPullDebitRecordStatus.SuspendedDeposit: {
+ const sel = coinSelRes.result;
+ myPpi.coinSel = {
+ coinPubs: sel.coins.map((x) => x.coinPub),
+ contributions: sel.coins.map((x) => x.contribution),
+ totalCost: Amounts.stringify(totalAmount),
+ };
+ break;
+ }
+ default:
+ return;
+ }
+ await tx.peerPullPaymentIncoming.put(myPpi);
+ });
+ return OperationAttemptResult.finishedEmpty();
+}
+
async function processPeerPullDebitPendingDeposit(
ws: InternalWalletState,
peerPullInc: PeerPullPaymentIncomingRecord,
@@ -118,81 +202,98 @@ async function processPeerPullDebitPendingDeposit(
method: "POST",
body: depositPayload,
});
- if (httpResp.status === HttpStatusCode.Gone) {
- const transitionInfo = await ws.db
- .mktx((x) => [
- x.peerPullPaymentIncoming,
- x.refreshGroups,
- x.denominations,
- x.coinAvailability,
- x.coins,
- ])
- .runReadWrite(async (tx) => {
- const pi = await tx.peerPullPaymentIncoming.get(
- peerPullPaymentIncomingId,
- );
- if (!pi) {
- throw Error("peer pull payment not found anymore");
- }
- if (pi.status !== PeerPullDebitRecordStatus.PendingDeposit) {
- return;
- }
- const oldTxState = computePeerPullDebitTransactionState(pi);
-
- const currency = Amounts.currencyOf(pi.totalCostEstimated);
- const coinPubs: CoinRefreshRequest[] = [];
-
- if (!pi.coinSel) {
- throw Error("invalid db state");
- }
-
- for (let i = 0; i < pi.coinSel.coinPubs.length; i++) {
- coinPubs.push({
- amount: pi.coinSel.contributions[i],
- coinPub: pi.coinSel.coinPubs[i],
- });
- }
-
- const refresh = await createRefreshGroup(
- ws,
- tx,
- currency,
- coinPubs,
- RefreshReason.AbortPeerPushDebit,
- );
-
- pi.status = PeerPullDebitRecordStatus.AbortingRefresh;
- pi.abortRefreshGroupId = refresh.refreshGroupId;
- const newTxState = computePeerPullDebitTransactionState(pi);
- await tx.peerPullPaymentIncoming.put(pi);
- return { oldTxState, newTxState };
- });
- notifyTransition(ws, transactionId, transitionInfo);
- } else {
- const resp = await readSuccessResponseJsonOrThrow(httpResp, codecForAny());
- logger.trace(`purse deposit response: ${j2s(resp)}`);
-
- const transitionInfo = await ws.db
- .mktx((x) => [x.peerPullPaymentIncoming])
- .runReadWrite(async (tx) => {
- const pi = await tx.peerPullPaymentIncoming.get(
- peerPullPaymentIncomingId,
- );
- if (!pi) {
- throw Error("peer pull payment not found anymore");
- }
- if (pi.status !== PeerPullDebitRecordStatus.PendingDeposit) {
- return;
- }
- const oldTxState = computePeerPullDebitTransactionState(pi);
- pi.status = PeerPullDebitRecordStatus.DonePaid;
- const newTxState = computePeerPullDebitTransactionState(pi);
- await tx.peerPullPaymentIncoming.put(pi);
- return { oldTxState, newTxState };
- });
- notifyTransition(ws, transactionId, transitionInfo);
+ switch (httpResp.status) {
+ case HttpStatusCode.Ok: {
+ const resp = await readSuccessResponseJsonOrThrow(
+ httpResp,
+ codecForAny(),
+ );
+ logger.trace(`purse deposit response: ${j2s(resp)}`);
+
+ const transitionInfo = await ws.db
+ .mktx((x) => [x.peerPullPaymentIncoming])
+ .runReadWrite(async (tx) => {
+ const pi = await tx.peerPullPaymentIncoming.get(
+ peerPullPaymentIncomingId,
+ );
+ if (!pi) {
+ throw Error("peer pull payment not found anymore");
+ }
+ if (pi.status !== PeerPullDebitRecordStatus.PendingDeposit) {
+ return;
+ }
+ const oldTxState = computePeerPullDebitTransactionState(pi);
+ pi.status = PeerPullDebitRecordStatus.DonePaid;
+ const newTxState = computePeerPullDebitTransactionState(pi);
+ await tx.peerPullPaymentIncoming.put(pi);
+ return { oldTxState, newTxState };
+ });
+ notifyTransition(ws, transactionId, transitionInfo);
+ break;
+ }
+ case HttpStatusCode.Gone: {
+ const transitionInfo = await ws.db
+ .mktx((x) => [
+ x.peerPullPaymentIncoming,
+ x.refreshGroups,
+ x.denominations,
+ x.coinAvailability,
+ x.coins,
+ ])
+ .runReadWrite(async (tx) => {
+ const pi = await tx.peerPullPaymentIncoming.get(
+ peerPullPaymentIncomingId,
+ );
+ if (!pi) {
+ throw Error("peer pull payment not found anymore");
+ }
+ if (pi.status !== PeerPullDebitRecordStatus.PendingDeposit) {
+ return;
+ }
+ const oldTxState = computePeerPullDebitTransactionState(pi);
+
+ const currency = Amounts.currencyOf(pi.totalCostEstimated);
+ const coinPubs: CoinRefreshRequest[] = [];
+
+ if (!pi.coinSel) {
+ throw Error("invalid db state");
+ }
+
+ for (let i = 0; i < pi.coinSel.coinPubs.length; i++) {
+ coinPubs.push({
+ amount: pi.coinSel.contributions[i],
+ coinPub: pi.coinSel.coinPubs[i],
+ });
+ }
+
+ const refresh = await createRefreshGroup(
+ ws,
+ tx,
+ currency,
+ coinPubs,
+ RefreshReason.AbortPeerPushDebit,
+ );
+
+ pi.status = PeerPullDebitRecordStatus.AbortingRefresh;
+ pi.abortRefreshGroupId = refresh.refreshGroupId;
+ const newTxState = computePeerPullDebitTransactionState(pi);
+ await tx.peerPullPaymentIncoming.put(pi);
+ return { oldTxState, newTxState };
+ });
+ notifyTransition(ws, transactionId, transitionInfo);
+ break;
+ }
+ case HttpStatusCode.Conflict: {
+ return handlePurseCreationConflict(ws, peerPullInc, httpResp);
+ }
+ default: {
+ const errResp = await readTalerErrorResponse(httpResp);
+ return {
+ type: OperationAttemptResultType.Error,
+ errorDetail: errResp,
+ };
+ }
}
-
return {
type: OperationAttemptResultType.Finished,
result: undefined,
@@ -434,7 +535,7 @@ export async function preparePeerPullDebit(
const getPurseUrl = new URL(`purses/${pursePub}/merge`, exchangeBaseUrl);
- const purseHttpResp = await ws.http.get(getPurseUrl.href);
+ const purseHttpResp = await ws.http.fetch(getPurseUrl.href);
const purseStatus = await readSuccessResponseJsonOrThrow(
purseHttpResp,