diff options
Diffstat (limited to 'packages')
| -rw-r--r-- | packages/taler-wallet-core/src/db.ts | 39 | ||||
| -rw-r--r-- | packages/taler-wallet-core/src/operations/backup/export.ts | 10 | ||||
| -rw-r--r-- | packages/taler-wallet-core/src/operations/pay-merchant.ts | 179 | ||||
| -rw-r--r-- | packages/taler-wallet-core/src/operations/pay-peer.ts | 348 | ||||
| -rw-r--r-- | packages/taler-wallet-core/src/operations/tip.ts | 108 | ||||
| -rw-r--r-- | packages/taler-wallet-core/src/operations/transactions.ts | 127 | ||||
| -rw-r--r-- | packages/taler-wallet-core/src/operations/withdraw.ts | 2 | 
7 files changed, 755 insertions, 58 deletions
| diff --git a/packages/taler-wallet-core/src/db.ts b/packages/taler-wallet-core/src/db.ts index 0e35fe27c..3edaf8af5 100644 --- a/packages/taler-wallet-core/src/db.ts +++ b/packages/taler-wallet-core/src/db.ts @@ -863,11 +863,22 @@ export interface TipRecord {     * The url to be redirected after the tip is accepted.     */    next_url: string | undefined; +    /**     * Timestamp for when the wallet finished picking up the tip     * from the merchant.     */    pickedUpTimestamp: TalerPreciseTimestamp | undefined; + +  status: TipRecordStatus; +} + +export enum TipRecordStatus { +  PendingPickup = 10, + +  SuspendidPickup = 21, + +  Done = 50,  }  export enum RefreshCoinStatus { @@ -1078,12 +1089,12 @@ export enum PurchaseStatus {    /**     * Not downloaded yet.     */ -  DownloadingProposal = 10, +  PendingDownloadingProposal = 10,    /**     * The user has accepted the proposal.     */ -  Paying = 11, +  PendingPaying = 11,    /**     * Currently in the process of aborting with a refund. @@ -1093,17 +1104,17 @@ export enum PurchaseStatus {    /**     * Paying a second time, likely with different session ID     */ -  PayingReplay = 13, +  PendingPayingReplay = 13,    /**     * Query for refunds (until query succeeds).     */ -  QueryingRefund = 14, +  PendingQueryingRefund = 14,    /**     * Query for refund (until auto-refund deadline is reached).     */ -  QueryingAutoRefund = 15, +  PendingQueryingAutoRefund = 15,    PendingAcceptRefund = 16, @@ -1902,7 +1913,7 @@ export interface PeerPullPaymentInitiationRecord {  export enum PeerPushPaymentIncomingStatus {    PendingMerge = 10 /* ACTIVE_START */, -  MergeKycRequired = 11 /* ACTIVE_START + 1 */, +  PendingMergeKycRequired = 11 /* ACTIVE_START + 1 */,    /**     * Merge was successful and withdrawal group has been created, now     * everything is in the hand of the withdrawal group. @@ -2829,6 +2840,22 @@ export const walletDbFixups: FixupDescription[] = [        });      },    }, +  { +    name: "TipRecordRecord_status_add", +    async fn(tx): Promise<void> { +      await tx.tips.iter().forEachAsync(async (r) => { +        // Remove legacy transactions that don't have the totalCost field yet. +        if (r.status == null) { +          if (r.pickedUpTimestamp) { +            r.status = TipRecordStatus.Done; +          } else { +            r.status = TipRecordStatus.PendingPickup; +          } +          await tx.tips.put(r); +        } +      }); +    }, +  },  ];  const logger = new Logger("db.ts"); diff --git a/packages/taler-wallet-core/src/operations/backup/export.ts b/packages/taler-wallet-core/src/operations/backup/export.ts index ff5f1e177..0aca45551 100644 --- a/packages/taler-wallet-core/src/operations/backup/export.ts +++ b/packages/taler-wallet-core/src/operations/backup/export.ts @@ -412,14 +412,14 @@ export async function exportBackup(          let propStatus: BackupProposalStatus;          switch (purch.purchaseStatus) {            case PurchaseStatus.Done: -          case PurchaseStatus.QueryingAutoRefund: -          case PurchaseStatus.QueryingRefund: +          case PurchaseStatus.PendingQueryingAutoRefund: +          case PurchaseStatus.PendingQueryingRefund:              propStatus = BackupProposalStatus.Paid;              break; -          case PurchaseStatus.PayingReplay: -          case PurchaseStatus.DownloadingProposal: +          case PurchaseStatus.PendingPayingReplay: +          case PurchaseStatus.PendingDownloadingProposal:            case PurchaseStatus.Proposed: -          case PurchaseStatus.Paying: +          case PurchaseStatus.PendingPaying:              propStatus = BackupProposalStatus.Proposed;              break;            case PurchaseStatus.FailedClaim: diff --git a/packages/taler-wallet-core/src/operations/pay-merchant.ts b/packages/taler-wallet-core/src/operations/pay-merchant.ts index 4ea41c695..30c75f695 100644 --- a/packages/taler-wallet-core/src/operations/pay-merchant.ts +++ b/packages/taler-wallet-core/src/operations/pay-merchant.ts @@ -131,6 +131,7 @@ import {  import {    constructTransactionIdentifier,    notifyTransition, +  stopLongpolling,  } from "./transactions.js";  /** @@ -339,7 +340,7 @@ async function processDownloadProposal(      };    } -  if (proposal.purchaseStatus != PurchaseStatus.DownloadingProposal) { +  if (proposal.purchaseStatus != PurchaseStatus.PendingDownloadingProposal) {      return {        type: OperationAttemptResultType.Finished,        result: undefined, @@ -516,7 +517,7 @@ async function processDownloadProposal(        if (!p) {          return;        } -      if (p.purchaseStatus !== PurchaseStatus.DownloadingProposal) { +      if (p.purchaseStatus !== PurchaseStatus.PendingDownloadingProposal) {          return;        }        const oldTxState = computePayMerchantTransactionState(p); @@ -626,7 +627,7 @@ async function createPurchase(      merchantBaseUrl,      orderId,      proposalId: proposalId, -    purchaseStatus: PurchaseStatus.DownloadingProposal, +    purchaseStatus: PurchaseStatus.PendingDownloadingProposal,      repurchaseProposalId: undefined,      downloadSessionId: sessionId,      autoRefundDeadline: undefined, @@ -699,7 +700,7 @@ async function storeFirstPaySuccess(          return;        }        const oldTxState = computePayMerchantTransactionState(purchase); -      if (purchase.purchaseStatus === PurchaseStatus.Paying) { +      if (purchase.purchaseStatus === PurchaseStatus.PendingPaying) {          purchase.purchaseStatus = PurchaseStatus.Done;        }        purchase.timestampFirstSuccessfulPay = now; @@ -721,7 +722,7 @@ async function storeFirstPaySuccess(        if (protoAr) {          const ar = Duration.fromTalerProtocolDuration(protoAr);          logger.info("auto_refund present"); -        purchase.purchaseStatus = PurchaseStatus.QueryingAutoRefund; +        purchase.purchaseStatus = PurchaseStatus.PendingQueryingAutoRefund;          purchase.autoRefundDeadline = AbsoluteTime.toProtocolTimestamp(            AbsoluteTime.addDuration(AbsoluteTime.now(), ar),          ); @@ -760,8 +761,8 @@ async function storePayReplaySuccess(        }        const oldTxState = computePayMerchantTransactionState(purchase);        if ( -        purchase.purchaseStatus === PurchaseStatus.Paying || -        purchase.purchaseStatus === PurchaseStatus.PayingReplay +        purchase.purchaseStatus === PurchaseStatus.PendingPaying || +        purchase.purchaseStatus === PurchaseStatus.PendingPayingReplay        ) {          purchase.purchaseStatus = PurchaseStatus.Done;        } @@ -1058,7 +1059,7 @@ export async function checkPaymentByProposalId(          }          const oldTxState = computePayMerchantTransactionState(p);          p.lastSessionId = sessionId; -        p.purchaseStatus = PurchaseStatus.PayingReplay; +        p.purchaseStatus = PurchaseStatus.PendingPayingReplay;          await tx.purchases.put(p);          const newTxState = computePayMerchantTransactionState(p);          return { oldTxState, newTxState }; @@ -1098,8 +1099,8 @@ export async function checkPaymentByProposalId(    } else {      const paid =        purchase.purchaseStatus === PurchaseStatus.Done || -      purchase.purchaseStatus === PurchaseStatus.QueryingRefund || -      purchase.purchaseStatus === PurchaseStatus.QueryingAutoRefund; +      purchase.purchaseStatus === PurchaseStatus.PendingQueryingRefund || +      purchase.purchaseStatus === PurchaseStatus.PendingQueryingAutoRefund;      const download = await expectProposalDownload(ws, purchase);      return {        status: PreparePayResultType.AlreadyConfirmed, @@ -1348,7 +1349,7 @@ export async function confirmPay(          logger.trace(`changing session ID to ${sessionIdOverride}`);          purchase.lastSessionId = sessionIdOverride;          if (purchase.purchaseStatus === PurchaseStatus.Done) { -          purchase.purchaseStatus = PurchaseStatus.PayingReplay; +          purchase.purchaseStatus = PurchaseStatus.PendingPayingReplay;          }          await tx.purchases.put(purchase);        } @@ -1424,7 +1425,7 @@ export async function confirmPay(            };            p.lastSessionId = sessionId;            p.timestampAccept = TalerPreciseTimestamp.now(); -          p.purchaseStatus = PurchaseStatus.Paying; +          p.purchaseStatus = PurchaseStatus.PendingPaying;            await tx.purchases.put(p);            await spendCoins(ws, tx, {              //`txn:proposal:${p.proposalId}` @@ -1440,7 +1441,7 @@ export async function confirmPay(            });            break;          case PurchaseStatus.Done: -        case PurchaseStatus.Paying: +        case PurchaseStatus.PendingPaying:          default:            break;        } @@ -1481,14 +1482,14 @@ export async function processPurchase(    }    switch (purchase.purchaseStatus) { -    case PurchaseStatus.DownloadingProposal: +    case PurchaseStatus.PendingDownloadingProposal:        return processDownloadProposal(ws, proposalId); -    case PurchaseStatus.Paying: -    case PurchaseStatus.PayingReplay: +    case PurchaseStatus.PendingPaying: +    case PurchaseStatus.PendingPayingReplay:        return processPurchasePay(ws, proposalId); -    case PurchaseStatus.QueryingRefund: +    case PurchaseStatus.PendingQueryingRefund:        return processPurchaseQueryRefund(ws, purchase); -    case PurchaseStatus.QueryingAutoRefund: +    case PurchaseStatus.PendingQueryingAutoRefund:        return processPurchaseAutoRefund(ws, purchase);      case PurchaseStatus.AbortingWithRefund:        return processPurchaseAbortingRefund(ws, purchase); @@ -1540,8 +1541,8 @@ export async function processPurchasePay(      };    }    switch (purchase.purchaseStatus) { -    case PurchaseStatus.Paying: -    case PurchaseStatus.PayingReplay: +    case PurchaseStatus.PendingPaying: +    case PurchaseStatus.PendingPayingReplay:        break;      default:        return OperationAttemptResult.finishedEmpty(); @@ -1757,11 +1758,11 @@ export async function abortPayMerchant(          logger.warn(`tried to abort successful payment`);          return;        } -      if (oldStatus === PurchaseStatus.Paying) { +      if (oldStatus === PurchaseStatus.PendingPaying) {          purchase.purchaseStatus = PurchaseStatus.AbortingWithRefund;        }        await tx.purchases.put(purchase); -      if (oldStatus === PurchaseStatus.Paying) { +      if (oldStatus === PurchaseStatus.PendingPaying) {          if (purchase.payInfo) {            const coinSel = purchase.payInfo.payCoinSelection;            const currency = Amounts.currencyOf(purchase.payInfo.totalPayCost); @@ -1789,32 +1790,146 @@ export async function abortPayMerchant(    ws.workAvailable.trigger();  } + +const transitionSuspend: { [x in PurchaseStatus]?: { +  next: PurchaseStatus | undefined, +} } = { +  [PurchaseStatus.PendingDownloadingProposal]: { +    next: PurchaseStatus.SuspendedDownloadingProposal, +  }, +  [PurchaseStatus.AbortingWithRefund]: { +    next: PurchaseStatus.SuspendedAbortingWithRefund, +  }, +  [PurchaseStatus.PendingPaying]: { +    next: PurchaseStatus.SuspendedPaying, +  }, +  [PurchaseStatus.PendingPayingReplay]: { +    next: PurchaseStatus.SuspendedPayingReplay, +  }, +  [PurchaseStatus.PendingQueryingAutoRefund]: { +    next: PurchaseStatus.SuspendedQueryingAutoRefund, +  } +} + +const transitionResume: { [x in PurchaseStatus]?: { +  next: PurchaseStatus | undefined, +} } = { +  [PurchaseStatus.SuspendedDownloadingProposal]: { +    next: PurchaseStatus.PendingDownloadingProposal, +  }, +  [PurchaseStatus.SuspendedAbortingWithRefund]: { +    next: PurchaseStatus.AbortingWithRefund, +  }, +  [PurchaseStatus.SuspendedPaying]: { +    next: PurchaseStatus.PendingPaying, +  }, +  [PurchaseStatus.SuspendedPayingReplay]: { +    next: PurchaseStatus.PendingPayingReplay, +  }, +  [PurchaseStatus.SuspendedQueryingAutoRefund]: { +    next: PurchaseStatus.PendingQueryingAutoRefund, +  } +} + + +export async function suspendPayMerchant( +  ws: InternalWalletState, +  proposalId: string, +): Promise<void> { +  const transactionId = constructTransactionIdentifier({ +    tag: TransactionType.Payment, +    proposalId, +  }); +  const opId = constructTaskIdentifier({ +    tag: PendingTaskType.Purchase, +    proposalId, +  }); +  stopLongpolling(ws, opId); +  const transitionInfo = await ws.db +    .mktx((x) => [ +      x.purchases, +    ]) +    .runReadWrite(async (tx) => { +      const purchase = await tx.purchases.get(proposalId); +      if (!purchase) { +        throw Error("purchase not found"); +      } +      const oldTxState = computePayMerchantTransactionState(purchase); +      let newStatus = transitionSuspend[purchase.purchaseStatus]; +      if (!newStatus) { +        return undefined; +      } +      await tx.purchases.put(purchase); +      const newTxState = computePayMerchantTransactionState(purchase); +      return { oldTxState, newTxState }; +    }); +  notifyTransition(ws, transactionId, transitionInfo); +  ws.workAvailable.trigger(); +} + + +export async function resumePayMerchant( +  ws: InternalWalletState, +  proposalId: string, +): Promise<void> { +  const transactionId = constructTransactionIdentifier({ +    tag: TransactionType.Payment, +    proposalId, +  }); +  const opId = constructTaskIdentifier({ +    tag: PendingTaskType.Purchase, +    proposalId, +  }); +  stopLongpolling(ws, opId); +  const transitionInfo = await ws.db +    .mktx((x) => [ +      x.purchases, +    ]) +    .runReadWrite(async (tx) => { +      const purchase = await tx.purchases.get(proposalId); +      if (!purchase) { +        throw Error("purchase not found"); +      } +      const oldTxState = computePayMerchantTransactionState(purchase); +      let newStatus = transitionResume[purchase.purchaseStatus]; +      if (!newStatus) { +        return undefined; +      } +      await tx.purchases.put(purchase); +      const newTxState = computePayMerchantTransactionState(purchase); +      return { oldTxState, newTxState }; +    }); +  ws.workAvailable.trigger(); +  notifyTransition(ws, transactionId, transitionInfo); +  ws.workAvailable.trigger(); +} +  export function computePayMerchantTransactionState(    purchaseRecord: PurchaseRecord,  ): TransactionState {    switch (purchaseRecord.purchaseStatus) {      // Pending States -    case PurchaseStatus.DownloadingProposal: +    case PurchaseStatus.PendingDownloadingProposal:        return {          major: TransactionMajorState.Pending,          minor: TransactionMinorState.ClaimProposal,        }; -    case PurchaseStatus.Paying: +    case PurchaseStatus.PendingPaying:        return {          major: TransactionMajorState.Pending,          minor: TransactionMinorState.SubmitPayment,        }; -    case PurchaseStatus.PayingReplay: +    case PurchaseStatus.PendingPayingReplay:        return {          major: TransactionMajorState.Pending,          minor: TransactionMinorState.RebindSession,        }; -    case PurchaseStatus.QueryingAutoRefund: +    case PurchaseStatus.PendingQueryingAutoRefund:        return {          major: TransactionMajorState.Pending,          minor: TransactionMinorState.AutoRefund,        }; -    case PurchaseStatus.QueryingRefund: +    case PurchaseStatus.PendingQueryingRefund:        return {          major: TransactionMajorState.Pending,          minor: TransactionMinorState.CheckRefund, @@ -1937,7 +2052,7 @@ async function processPurchaseAutoRefund(              logger.warn("purchase does not exist anymore");              return;            } -          if (p.purchaseStatus !== PurchaseStatus.QueryingRefund) { +          if (p.purchaseStatus !== PurchaseStatus.PendingQueryingRefund) {              return;            }            const oldTxState = computePayMerchantTransactionState(p); @@ -1982,7 +2097,7 @@ async function processPurchaseAutoRefund(              logger.warn("purchase does not exist anymore");              return;            } -          if (p.purchaseStatus !== PurchaseStatus.QueryingAutoRefund) { +          if (p.purchaseStatus !== PurchaseStatus.PendingQueryingAutoRefund) {              return;            }            const oldTxState = computePayMerchantTransactionState(p); @@ -2118,7 +2233,7 @@ async function processPurchaseQueryRefund(            logger.warn("purchase does not exist anymore");            return undefined;          } -        if (p.purchaseStatus !== PurchaseStatus.QueryingRefund) { +        if (p.purchaseStatus !== PurchaseStatus.PendingQueryingRefund) {            return undefined;          }          const oldTxState = computePayMerchantTransactionState(p); @@ -2143,7 +2258,7 @@ async function processPurchaseQueryRefund(            logger.warn("purchase does not exist anymore");            return;          } -        if (p.purchaseStatus !== PurchaseStatus.QueryingRefund) { +        if (p.purchaseStatus !== PurchaseStatus.PendingQueryingRefund) {            return;          }          const oldTxState = computePayMerchantTransactionState(p); @@ -2242,7 +2357,7 @@ export async function startQueryRefund(          return;        }        const oldTxState = computePayMerchantTransactionState(p); -      p.purchaseStatus = PurchaseStatus.QueryingRefund; +      p.purchaseStatus = PurchaseStatus.PendingQueryingRefund;        const newTxState = computePayMerchantTransactionState(p);        await tx.purchases.put(p);        return { oldTxState, newTxState }; diff --git a/packages/taler-wallet-core/src/operations/pay-peer.ts b/packages/taler-wallet-core/src/operations/pay-peer.ts index fb1260e3c..95878543b 100644 --- a/packages/taler-wallet-core/src/operations/pay-peer.ts +++ b/packages/taler-wallet-core/src/operations/pay-peer.ts @@ -1008,7 +1008,7 @@ export async function processPeerPushCredit(    const amount = Amounts.parseOrThrow(contractTerms.amount);    if ( -    peerInc.status === PeerPushPaymentIncomingStatus.MergeKycRequired && +    peerInc.status === PeerPushPaymentIncomingStatus.PendingMergeKycRequired &&      peerInc.kycInfo    ) {      const txId = constructTransactionIdentifier({ @@ -1080,7 +1080,7 @@ export async function processPeerPushCredit(            paytoHash: kycPending.h_payto,            requirementRow: kycPending.requirement_row,          }; -        peerInc.status = PeerPushPaymentIncomingStatus.MergeKycRequired; +        peerInc.status = PeerPushPaymentIncomingStatus.PendingMergeKycRequired;          await tx.peerPushPaymentIncoming.put(peerInc);        });      return { @@ -1122,7 +1122,7 @@ export async function processPeerPushCredit(        }        if (          peerInc.status === PeerPushPaymentIncomingStatus.PendingMerge || -        peerInc.status === PeerPushPaymentIncomingStatus.MergeKycRequired +        peerInc.status === PeerPushPaymentIncomingStatus.PendingMergeKycRequired        ) {          peerInc.status = PeerPushPaymentIncomingStatus.Done;        } @@ -2186,6 +2186,345 @@ export async function suspendPeerPushDebitTransaction(    notifyTransition(ws, transactionId, transitionInfo);  } +export async function suspendPeerPullDebitTransaction( +  ws: InternalWalletState, +  peerPullPaymentIncomingId: string, +) { +  const taskId = constructTaskIdentifier({ +    tag: PendingTaskType.PeerPullDebit, +    peerPullPaymentIncomingId, +  }); +  const transactionId = constructTransactionIdentifier({ +    tag: TransactionType.PeerPullDebit, +    peerPullPaymentIncomingId, +  }); +  stopLongpolling(ws, taskId); +  const transitionInfo = await ws.db +    .mktx((x) => [x.peerPullPaymentIncoming]) +    .runReadWrite(async (tx) => { +      const pullDebitRec = await tx.peerPullPaymentIncoming.get( +        peerPullPaymentIncomingId, +      ); +      if (!pullDebitRec) { +        logger.warn(`peer pull debit ${peerPullPaymentIncomingId} not found`); +        return; +      } +      let newStatus: PeerPullDebitRecordStatus | undefined = undefined; +      switch (pullDebitRec.status) { +        case PeerPullDebitRecordStatus.DialogProposed: +          break; +        case PeerPullDebitRecordStatus.DonePaid: +          break; +        case PeerPullDebitRecordStatus.PendingDeposit: +          newStatus = PeerPullDebitRecordStatus.SuspendedDeposit; +          break; +        case PeerPullDebitRecordStatus.SuspendedDeposit: +          break; +        default: +          assertUnreachable(pullDebitRec.status); +      } +      if (newStatus != null) { +        const oldTxState = computePeerPullDebitTransactionState(pullDebitRec); +        pullDebitRec.status = newStatus; +        const newTxState = computePeerPullDebitTransactionState(pullDebitRec); +        await tx.peerPullPaymentIncoming.put(pullDebitRec); +        return { +          oldTxState, +          newTxState, +        }; +      } +      return undefined; +    }); +  notifyTransition(ws, transactionId, transitionInfo); +} + +export async function resumePeerPullDebitTransaction( +  ws: InternalWalletState, +  peerPullPaymentIncomingId: string, +) { +  const taskId = constructTaskIdentifier({ +    tag: PendingTaskType.PeerPullDebit, +    peerPullPaymentIncomingId, +  }); +  const transactionId = constructTransactionIdentifier({ +    tag: TransactionType.PeerPullDebit, +    peerPullPaymentIncomingId, +  }); +  stopLongpolling(ws, taskId); +  const transitionInfo = await ws.db +    .mktx((x) => [x.peerPullPaymentIncoming]) +    .runReadWrite(async (tx) => { +      const pullDebitRec = await tx.peerPullPaymentIncoming.get( +        peerPullPaymentIncomingId, +      ); +      if (!pullDebitRec) { +        logger.warn(`peer pull debit ${peerPullPaymentIncomingId} not found`); +        return; +      } +      let newStatus: PeerPullDebitRecordStatus | undefined = undefined; +      switch (pullDebitRec.status) { +        case PeerPullDebitRecordStatus.DialogProposed: +        case PeerPullDebitRecordStatus.DonePaid: +        case PeerPullDebitRecordStatus.PendingDeposit: +          break; +        case PeerPullDebitRecordStatus.SuspendedDeposit: +          newStatus = PeerPullDebitRecordStatus.PendingDeposit; +          break; +        default: +          assertUnreachable(pullDebitRec.status); +      } +      if (newStatus != null) { +        const oldTxState = computePeerPullDebitTransactionState(pullDebitRec); +        pullDebitRec.status = newStatus; +        const newTxState = computePeerPullDebitTransactionState(pullDebitRec); +        await tx.peerPullPaymentIncoming.put(pullDebitRec); +        return { +          oldTxState, +          newTxState, +        }; +      } +      return undefined; +    }); +  ws.workAvailable.trigger(); +  notifyTransition(ws, transactionId, transitionInfo); +} + +export async function suspendPeerPushCreditTransaction( +  ws: InternalWalletState, +  peerPushPaymentIncomingId: string, +) { +  const taskId = constructTaskIdentifier({ +    tag: PendingTaskType.PeerPushCredit, +    peerPushPaymentIncomingId, +  }); +  const transactionId = constructTransactionIdentifier({ +    tag: TransactionType.PeerPushCredit, +    peerPushPaymentIncomingId, +  }); +  stopLongpolling(ws, taskId); +  const transitionInfo = await ws.db +    .mktx((x) => [x.peerPushPaymentIncoming]) +    .runReadWrite(async (tx) => { +      const pushCreditRec = await tx.peerPushPaymentIncoming.get( +        peerPushPaymentIncomingId, +      ); +      if (!pushCreditRec) { +        logger.warn(`peer push credit ${peerPushPaymentIncomingId} not found`); +        return; +      } +      let newStatus: PeerPushPaymentIncomingStatus | undefined = undefined; +      switch (pushCreditRec.status) { +        case PeerPushPaymentIncomingStatus.DialogProposed: +        case PeerPushPaymentIncomingStatus.Done: +        case PeerPushPaymentIncomingStatus.SuspendedMerge: +        case PeerPushPaymentIncomingStatus.SuspendedMergeKycRequired: +        case PeerPushPaymentIncomingStatus.SuspendedWithdrawing: +          break; +        case PeerPushPaymentIncomingStatus.PendingMergeKycRequired: +          newStatus = PeerPushPaymentIncomingStatus.SuspendedMergeKycRequired; +          break; +        case PeerPushPaymentIncomingStatus.PendingMerge: +          newStatus = PeerPushPaymentIncomingStatus.SuspendedMerge; +          break; +        case PeerPushPaymentIncomingStatus.PendingWithdrawing: +          // FIXME: Suspend internal withdrawal transaction! +          newStatus = PeerPushPaymentIncomingStatus.SuspendedWithdrawing; +          break; +        default: +          assertUnreachable(pushCreditRec.status); +      } +      if (newStatus != null) { +        const oldTxState = computePeerPushCreditTransactionState(pushCreditRec); +        pushCreditRec.status = newStatus; +        const newTxState = computePeerPushCreditTransactionState(pushCreditRec); +        await tx.peerPushPaymentIncoming.put(pushCreditRec); +        return { +          oldTxState, +          newTxState, +        }; +      } +      return undefined; +    }); +  notifyTransition(ws, transactionId, transitionInfo); +} + +export async function resumePeerPushCreditTransaction( +  ws: InternalWalletState, +  peerPushPaymentIncomingId: string, +) { +  const taskId = constructTaskIdentifier({ +    tag: PendingTaskType.PeerPushCredit, +    peerPushPaymentIncomingId, +  }); +  const transactionId = constructTransactionIdentifier({ +    tag: TransactionType.PeerPushCredit, +    peerPushPaymentIncomingId, +  }); +  stopLongpolling(ws, taskId); +  const transitionInfo = await ws.db +    .mktx((x) => [x.peerPushPaymentIncoming]) +    .runReadWrite(async (tx) => { +      const pushCreditRec = await tx.peerPushPaymentIncoming.get( +        peerPushPaymentIncomingId, +      ); +      if (!pushCreditRec) { +        logger.warn(`peer push credit ${peerPushPaymentIncomingId} not found`); +        return; +      } +      let newStatus: PeerPushPaymentIncomingStatus | undefined = undefined; +      switch (pushCreditRec.status) { +        case PeerPushPaymentIncomingStatus.DialogProposed: +        case PeerPushPaymentIncomingStatus.Done: +        case PeerPushPaymentIncomingStatus.PendingMergeKycRequired: +        case PeerPushPaymentIncomingStatus.PendingMerge: +        case PeerPushPaymentIncomingStatus.PendingWithdrawing: +        case PeerPushPaymentIncomingStatus.SuspendedMerge: +          newStatus = PeerPushPaymentIncomingStatus.PendingMerge; +          break; +        case PeerPushPaymentIncomingStatus.SuspendedMergeKycRequired: +          newStatus = PeerPushPaymentIncomingStatus.PendingMergeKycRequired; +          break; +        case PeerPushPaymentIncomingStatus.SuspendedWithdrawing: +          // FIXME: resume underlying "internal-withdrawal" transaction. +          newStatus = PeerPushPaymentIncomingStatus.PendingWithdrawing; +          break; +        default: +          assertUnreachable(pushCreditRec.status); +      } +      if (newStatus != null) { +        const oldTxState = computePeerPushCreditTransactionState(pushCreditRec); +        pushCreditRec.status = newStatus; +        const newTxState = computePeerPushCreditTransactionState(pushCreditRec); +        await tx.peerPushPaymentIncoming.put(pushCreditRec); +        return { +          oldTxState, +          newTxState, +        }; +      } +      return undefined; +    }); +  ws.workAvailable.trigger(); +  notifyTransition(ws, transactionId, transitionInfo); +} + +export async function suspendPeerPullCreditTransaction( +  ws: InternalWalletState, +  pursePub: string, +) { +  const taskId = constructTaskIdentifier({ +    tag: PendingTaskType.PeerPullCredit, +    pursePub, +  }); +  const transactionId = constructTransactionIdentifier({ +    tag: TransactionType.PeerPullCredit, +    pursePub, +  }); +  stopLongpolling(ws, taskId); +  const transitionInfo = await ws.db +    .mktx((x) => [x.peerPullPaymentInitiations]) +    .runReadWrite(async (tx) => { +      const pullCreditRec = await tx.peerPullPaymentInitiations.get(pursePub); +      if (!pullCreditRec) { +        logger.warn(`peer pull credit ${pursePub} not found`); +        return; +      } +      let newStatus: PeerPullPaymentInitiationStatus | undefined = undefined; +      switch (pullCreditRec.status) { +        case PeerPullPaymentInitiationStatus.PendingCreatePurse: +          newStatus = PeerPullPaymentInitiationStatus.SuspendedCreatePurse; +          break; +        case PeerPullPaymentInitiationStatus.PendingMergeKycRequired: +          newStatus = PeerPullPaymentInitiationStatus.SuspendedMergeKycRequired; +          break; +        case PeerPullPaymentInitiationStatus.PendingWithdrawing: +          newStatus = PeerPullPaymentInitiationStatus.SuspendedWithdrawing; +          break; +        case PeerPullPaymentInitiationStatus.PendingReady: +          newStatus = PeerPullPaymentInitiationStatus.SuspendedReady; +          break; +        case PeerPullPaymentInitiationStatus.DonePurseDeposited: +        case PeerPullPaymentInitiationStatus.SuspendedCreatePurse: +        case PeerPullPaymentInitiationStatus.SuspendedMergeKycRequired: +        case PeerPullPaymentInitiationStatus.SuspendedReady: +        case PeerPullPaymentInitiationStatus.SuspendedWithdrawing: +          break; +        default: +          assertUnreachable(pullCreditRec.status); +      } +      if (newStatus != null) { +        const oldTxState = computePeerPullCreditTransactionState(pullCreditRec); +        pullCreditRec.status = newStatus; +        const newTxState = computePeerPullCreditTransactionState(pullCreditRec); +        await tx.peerPullPaymentInitiations.put(pullCreditRec); +        return { +          oldTxState, +          newTxState, +        }; +      } +      return undefined; +    }); +  notifyTransition(ws, transactionId, transitionInfo); +} + +export async function resumePeerPullCreditTransaction( +  ws: InternalWalletState, +  pursePub: string, +) { +  const taskId = constructTaskIdentifier({ +    tag: PendingTaskType.PeerPullCredit, +    pursePub, +  }); +  const transactionId = constructTransactionIdentifier({ +    tag: TransactionType.PeerPullCredit, +    pursePub, +  }); +  stopLongpolling(ws, taskId); +  const transitionInfo = await ws.db +    .mktx((x) => [x.peerPullPaymentInitiations]) +    .runReadWrite(async (tx) => { +      const pullCreditRec = await tx.peerPullPaymentInitiations.get(pursePub); +      if (!pullCreditRec) { +        logger.warn(`peer pull credit ${pursePub} not found`); +        return; +      } +      let newStatus: PeerPullPaymentInitiationStatus | undefined = undefined; +      switch (pullCreditRec.status) { +        case PeerPullPaymentInitiationStatus.PendingCreatePurse: +        case PeerPullPaymentInitiationStatus.PendingMergeKycRequired: +        case PeerPullPaymentInitiationStatus.PendingWithdrawing: +        case PeerPullPaymentInitiationStatus.PendingReady: +        case PeerPullPaymentInitiationStatus.DonePurseDeposited: +        case PeerPullPaymentInitiationStatus.SuspendedCreatePurse: +          newStatus = PeerPullPaymentInitiationStatus.PendingCreatePurse; +          break; +        case PeerPullPaymentInitiationStatus.SuspendedMergeKycRequired: +          newStatus = PeerPullPaymentInitiationStatus.PendingMergeKycRequired; +          break; +        case PeerPullPaymentInitiationStatus.SuspendedReady: +          newStatus = PeerPullPaymentInitiationStatus.PendingReady; +          break; +        case PeerPullPaymentInitiationStatus.SuspendedWithdrawing: +          newStatus = PeerPullPaymentInitiationStatus.PendingWithdrawing; +          break; +        default: +          assertUnreachable(pullCreditRec.status); +      } +      if (newStatus != null) { +        const oldTxState = computePeerPullCreditTransactionState(pullCreditRec); +        pullCreditRec.status = newStatus; +        const newTxState = computePeerPullCreditTransactionState(pullCreditRec); +        await tx.peerPullPaymentInitiations.put(pullCreditRec); +        return { +          oldTxState, +          newTxState, +        }; +      } +      return undefined; +    }); +  ws.workAvailable.trigger(); +  notifyTransition(ws, transactionId, transitionInfo); +} +  export async function resumePeerPushDebitTransaction(    ws: InternalWalletState,    pursePub: string, @@ -2244,6 +2583,7 @@ export async function resumePeerPushDebitTransaction(        }        return undefined;      }); +  ws.workAvailable.trigger();    notifyTransition(ws, transactionId, transitionInfo);  } @@ -2265,7 +2605,7 @@ export function computePeerPushCreditTransactionState(        return {          major: TransactionMajorState.Done,        }; -    case PeerPushPaymentIncomingStatus.MergeKycRequired: +    case PeerPushPaymentIncomingStatus.PendingMergeKycRequired:        return {          major: TransactionMajorState.Pending,          minor: TransactionMinorState.KycRequired, diff --git a/packages/taler-wallet-core/src/operations/tip.ts b/packages/taler-wallet-core/src/operations/tip.ts index 524970faa..70b595c2f 100644 --- a/packages/taler-wallet-core/src/operations/tip.ts +++ b/packages/taler-wallet-core/src/operations/tip.ts @@ -48,6 +48,7 @@ import {    CoinSourceType,    DenominationRecord,    TipRecord, +  TipRecordStatus,  } from "../db.js";  import { makeErrorDetail } from "@gnu-taler/taler-util";  import { InternalWalletState } from "../internal-wallet-state.js"; @@ -57,6 +58,7 @@ import {  } from "@gnu-taler/taler-util/http";  import { checkDbInvariant, checkLogicInvariant } from "../util/invariants.js";  import { +  constructTaskIdentifier,    OperationAttemptResult,    OperationAttemptResultType,  } from "../util/retries.js"; @@ -68,7 +70,13 @@ import {    updateWithdrawalDenoms,  } from "./withdraw.js";  import { selectWithdrawalDenominations } from "../util/coinSelection.js"; -import { constructTransactionIdentifier } from "./transactions.js"; +import { +  constructTransactionIdentifier, +  notifyTransition, +  stopLongpolling, +} from "./transactions.js"; +import { PendingTaskType } from "../pending-types.js"; +import { assertUnreachable } from "../util/assertUnreachable.js";  const logger = new Logger("operations/tip.ts"); @@ -156,6 +164,7 @@ export async function prepareTip(      const newTipRecord: TipRecord = {        walletTipId: walletTipId,        acceptedTimestamp: undefined, +      status: TipRecordStatus.PendingPickup,        tipAmountRaw: Amounts.stringify(amount),        tipExpiration: tipPickupStatus.expiration,        exchangeBaseUrl: tipPickupStatus.exchange_url, @@ -180,7 +189,7 @@ export async function prepareTip(    const transactionId = constructTransactionIdentifier({      tag: TransactionType.Tip,      walletTipId: tipRecord.walletTipId, -  }) +  });    const tipStatus: PrepareTipResult = {      accepted: !!tipRecord && !!tipRecord.acceptedTimestamp, @@ -410,3 +419,98 @@ export async function acceptTip(      next_url: found?.next_url,    };  } + +export async function suspendTipTransaction( +  ws: InternalWalletState, +  walletTipId: string, +): Promise<void> { +  const taskId = constructTaskIdentifier({ +    tag: PendingTaskType.TipPickup, +    walletTipId, +  }); +  const transactionId = constructTransactionIdentifier({ +    tag: TransactionType.Tip, +    walletTipId, +  }); +  stopLongpolling(ws, taskId); +  const transitionInfo = await ws.db +    .mktx((x) => [x.tips]) +    .runReadWrite(async (tx) => { +      const tipRec = await tx.tips.get(walletTipId); +      if (!tipRec) { +        logger.warn(`transaction tip ${walletTipId} not found`); +        return; +      } +      let newStatus: TipRecordStatus | undefined = undefined; +      switch (tipRec.status) { +        case TipRecordStatus.Done: +        case TipRecordStatus.SuspendidPickup: +          break; +        case TipRecordStatus.PendingPickup: +          newStatus = TipRecordStatus.SuspendidPickup; +          break; +        default: +          assertUnreachable(tipRec.status); +      } +      if (newStatus != null) { +        const oldTxState = computeTipTransactionStatus(tipRec); +        tipRec.status = newStatus; +        const newTxState = computeTipTransactionStatus(tipRec); +        await tx.tips.put(tipRec); +        return { +          oldTxState, +          newTxState, +        }; +      } +      return undefined; +    }); +  ws.workAvailable.trigger(); +  notifyTransition(ws, transactionId, transitionInfo); +} + +export async function resumeTipTransaction( +  ws: InternalWalletState, +  walletTipId: string, +): Promise<void> { +  const taskId = constructTaskIdentifier({ +    tag: PendingTaskType.TipPickup, +    walletTipId, +  }); +  const transactionId = constructTransactionIdentifier({ +    tag: TransactionType.Tip, +    walletTipId, +  }); +  stopLongpolling(ws, taskId); +  const transitionInfo = await ws.db +    .mktx((x) => [x.tips]) +    .runReadWrite(async (tx) => { +      const tipRec = await tx.tips.get(walletTipId); +      if (!tipRec) { +        logger.warn(`transaction tip ${walletTipId} not found`); +        return; +      } +      let newStatus: TipRecordStatus | undefined = undefined; +      switch (tipRec.status) { +        case TipRecordStatus.Done: +        case TipRecordStatus.SuspendidPickup: +          newStatus = TipRecordStatus.PendingPickup; +          break; +        case TipRecordStatus.PendingPickup: +          break; +        default: +          assertUnreachable(tipRec.status); +      } +      if (newStatus != null) { +        const oldTxState = computeTipTransactionStatus(tipRec); +        tipRec.status = newStatus; +        const newTxState = computeTipTransactionStatus(tipRec); +        await tx.tips.put(tipRec); +        return { +          oldTxState, +          newTxState, +        }; +      } +      return undefined; +    }); +  notifyTransition(ws, transactionId, transitionInfo); +} diff --git a/packages/taler-wallet-core/src/operations/transactions.ts b/packages/taler-wallet-core/src/operations/transactions.ts index 3645edd93..f1cfaed45 100644 --- a/packages/taler-wallet-core/src/operations/transactions.ts +++ b/packages/taler-wallet-core/src/operations/transactions.ts @@ -86,20 +86,40 @@ import {    computeRefundTransactionState,    expectProposalDownload,    extractContractData, +  resumePayMerchant, +  suspendPayMerchant,  } from "./pay-merchant.js";  import {    computePeerPullCreditTransactionState,    computePeerPullDebitTransactionState,    computePeerPushCreditTransactionState,    computePeerPushDebitTransactionState, +  resumePeerPullCreditTransaction, +  resumePeerPullDebitTransaction, +  resumePeerPushCreditTransaction, +  resumePeerPushDebitTransaction, +  suspendPeerPullCreditTransaction, +  suspendPeerPullDebitTransaction, +  suspendPeerPushCreditTransaction, +  suspendPeerPushDebitTransaction,  } from "./pay-peer.js"; -import { computeRefreshTransactionState } from "./refresh.js"; -import { computeTipTransactionStatus } from "./tip.js"; +import { +  computeRefreshTransactionState, +  resumeRefreshGroup, +  suspendRefreshGroup, +} from "./refresh.js"; +import { +  computeTipTransactionStatus, +  resumeTipTransaction, +  suspendTipTransaction, +} from "./tip.js";  import {    abortWithdrawalTransaction,    augmentPaytoUrisForWithdrawal,    cancelAbortingWithdrawalTransaction,    computeWithdrawalTransactionStatus, +  resumeWithdrawalTransaction, +  suspendWithdrawalTransaction,  } from "./withdraw.js";  const logger = new Logger("taler-wallet-core:transactions.ts"); @@ -159,6 +179,7 @@ export async function getTransactionById(    }    switch (parsedTx.tag) { +    case TransactionType.InternalWithdrawal:      case TransactionType.Withdrawal: {        const withdrawalGroupId = parsedTx.withdrawalGroupId;        return await ws.db @@ -844,7 +865,7 @@ async function buildTransactionForPurchase(      proposalId: purchaseRecord.proposalId,      info,      refundQueryActive: -      purchaseRecord.purchaseStatus === PurchaseStatus.QueryingRefund, +      purchaseRecord.purchaseStatus === PurchaseStatus.PendingQueryingRefund,      ...(ort?.lastError ? { error: ort.lastError } : {}),    };  } @@ -1197,7 +1218,8 @@ export type ParsedTransactionIdentifier =    | { tag: TransactionType.Refresh; refreshGroupId: string }    | { tag: TransactionType.Refund; refundGroupId: string }    | { tag: TransactionType.Tip; walletTipId: string } -  | { tag: TransactionType.Withdrawal; withdrawalGroupId: string }; +  | { tag: TransactionType.Withdrawal; withdrawalGroupId: string } +  | { tag: TransactionType.InternalWithdrawal; withdrawalGroupId: string };  export function constructTransactionIdentifier(    pTxId: ParsedTransactionIdentifier, @@ -1223,6 +1245,8 @@ export function constructTransactionIdentifier(        return `txn:${pTxId.tag}:${pTxId.walletTipId}` as TransactionIdStr;      case TransactionType.Withdrawal:        return `txn:${pTxId.tag}:${pTxId.withdrawalGroupId}` as TransactionIdStr; +    case TransactionType.InternalWithdrawal: +      return `txn:${pTxId.tag}:${pTxId.withdrawalGroupId}` as TransactionIdStr;      default:        assertUnreachable(pTxId);    } @@ -1242,6 +1266,10 @@ export function parseTransactionIdentifier(    const [prefix, type, ...rest] = txnParts; +  if (prefix != "txn") { +    throw Error("invalid transaction identifier"); +  } +    switch (type) {      case TransactionType.Deposit:        return { tag: TransactionType.Deposit, depositGroupId: rest[0] }; @@ -1329,6 +1357,7 @@ export async function retryTransaction(        stopLongpolling(ws, taskId);        break;      } +    case TransactionType.InternalWithdrawal:      case TransactionType.Withdrawal: {        // FIXME: Abort current long-poller!        const taskId = constructTaskIdentifier({ @@ -1366,8 +1395,38 @@ export async function retryTransaction(        stopLongpolling(ws, taskId);        break;      } -    default: +    case TransactionType.PeerPullDebit: { +      const taskId = constructTaskIdentifier({ +        tag: PendingTaskType.PeerPullDebit, +        peerPullPaymentIncomingId: parsedTx.peerPullPaymentIncomingId, +      }); +      await resetOperationTimeout(ws, taskId); +      stopLongpolling(ws, taskId); +      break; +    } +    case TransactionType.PeerPushCredit: { +      const taskId = constructTaskIdentifier({ +        tag: PendingTaskType.PeerPushCredit, +        peerPushPaymentIncomingId: parsedTx.peerPushPaymentIncomingId, +      }); +      await resetOperationTimeout(ws, taskId); +      stopLongpolling(ws, taskId);        break; +    } +    case TransactionType.PeerPushDebit: { +      const taskId = constructTaskIdentifier({ +        tag: PendingTaskType.PeerPushDebit, +        pursePub: parsedTx.pursePub, +      }); +      await resetOperationTimeout(ws, taskId); +      stopLongpolling(ws, taskId); +      break; +    } +    case TransactionType.Refund: +      // Nothing to do for a refund transaction. +      break; +    default: +      assertUnreachable(parsedTx);    }  } @@ -1389,8 +1448,35 @@ export async function suspendTransaction(      case TransactionType.Deposit:        await suspendDepositGroup(ws, tx.depositGroupId);        return; +    case TransactionType.Refresh: +      await suspendRefreshGroup(ws, tx.refreshGroupId); +      return; +    case TransactionType.InternalWithdrawal: +    case TransactionType.Withdrawal: +      await suspendWithdrawalTransaction(ws, tx.withdrawalGroupId); +      return; +    case TransactionType.Payment: +      await suspendPayMerchant(ws, tx.proposalId); +      return; +    case TransactionType.PeerPullCredit: +      await suspendPeerPullCreditTransaction(ws, tx.pursePub); +      break; +    case TransactionType.PeerPushDebit: +      await suspendPeerPushDebitTransaction(ws, tx.pursePub); +      break; +    case TransactionType.PeerPullDebit: +      await suspendPeerPullDebitTransaction(ws, tx.peerPullPaymentIncomingId); +      break; +    case TransactionType.PeerPushCredit: +      await suspendPeerPushCreditTransaction(ws, tx.peerPushPaymentIncomingId); +      break; +    case TransactionType.Refund: +      throw Error("refund transactions can't be suspended or resumed"); +    case TransactionType.Tip: +      await suspendTipTransaction(ws, tx.walletTipId); +      break;      default: -      logger.warn(`unable to suspend transaction of type '${tx.tag}'`); +      assertUnreachable(tx);    }  } @@ -1429,8 +1515,33 @@ export async function resumeTransaction(      case TransactionType.Deposit:        await resumeDepositGroup(ws, tx.depositGroupId);        return; -    default: -      logger.warn(`unable to resume transaction of type '${tx.tag}'`); +    case TransactionType.Refresh: +      await resumeRefreshGroup(ws, tx.refreshGroupId); +      return; +    case TransactionType.InternalWithdrawal: +    case TransactionType.Withdrawal: +      await resumeWithdrawalTransaction(ws, tx.withdrawalGroupId); +      return; +    case TransactionType.Payment: +      await resumePayMerchant(ws, tx.proposalId); +      return; +    case TransactionType.PeerPullCredit: +      await resumePeerPullCreditTransaction(ws, tx.pursePub); +      break; +    case TransactionType.PeerPushDebit: +      await resumePeerPushDebitTransaction(ws, tx.pursePub); +      break; +    case TransactionType.PeerPullDebit: +      await resumePeerPullDebitTransaction(ws, tx.peerPullPaymentIncomingId); +      break; +    case TransactionType.PeerPushCredit: +      await resumePeerPushCreditTransaction(ws, tx.peerPushPaymentIncomingId); +      break; +    case TransactionType.Refund: +      throw Error("refund transactions can't be suspended or resumed"); +    case TransactionType.Tip: +      await resumeTipTransaction(ws, tx.walletTipId); +      break;    }  } diff --git a/packages/taler-wallet-core/src/operations/withdraw.ts b/packages/taler-wallet-core/src/operations/withdraw.ts index ae170fa2c..7636395bd 100644 --- a/packages/taler-wallet-core/src/operations/withdraw.ts +++ b/packages/taler-wallet-core/src/operations/withdraw.ts @@ -259,7 +259,7 @@ export async function resumeWithdrawalTransaction(        }        return undefined;      }); - +  ws.workAvailable.trigger();    const transactionId = constructTransactionIdentifier({      tag: TransactionType.Withdrawal,      withdrawalGroupId, | 
