diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/dbTypes.ts | 2 | ||||
| -rw-r--r-- | src/headless/merchant.ts | 3 | ||||
| -rw-r--r-- | src/headless/taler-wallet-cli.ts | 2 | ||||
| -rw-r--r-- | src/wallet-impl/errors.ts | 5 | ||||
| -rw-r--r-- | src/wallet-impl/pay.ts | 143 | ||||
| -rw-r--r-- | src/wallet-impl/pending.ts | 7 | ||||
| -rw-r--r-- | src/wallet-impl/withdraw.ts | 1 | ||||
| -rw-r--r-- | src/wallet.ts | 5 | ||||
| -rw-r--r-- | src/walletTypes.ts | 14 | 
9 files changed, 125 insertions, 57 deletions
| diff --git a/src/dbTypes.ts b/src/dbTypes.ts index 16edbf31a..096c3f04e 100644 --- a/src/dbTypes.ts +++ b/src/dbTypes.ts @@ -980,7 +980,7 @@ export enum PurchaseStatus {    QueryRefund = "query-refund",    ProcessRefund = "process-refund",    Abort = "abort", -  Done = "done", +  Dormant = "dormant",  }  /** diff --git a/src/headless/merchant.ts b/src/headless/merchant.ts index 1b9630732..5ce50cb53 100644 --- a/src/headless/merchant.ts +++ b/src/headless/merchant.ts @@ -89,12 +89,15 @@ export class MerchantBackendConnection {      summary: string,      fulfillmentUrl: string,    ): Promise<{ orderId: string }> { +    const t = Math.floor(new Date().getTime() / 1000) + 15 * 60;      const reqUrl = new URL("order", this.merchantBaseUrl).href;      const orderReq = {        order: {          amount,          summary,          fulfillment_url: fulfillmentUrl, +        refund_deadline: `/Date(${t})/`, +        wire_transfer_deadline: `/Date(${t})/`,        },      };      const resp = await axios({ diff --git a/src/headless/taler-wallet-cli.ts b/src/headless/taler-wallet-cli.ts index 931cac087..71bccadef 100644 --- a/src/headless/taler-wallet-cli.ts +++ b/src/headless/taler-wallet-cli.ts @@ -137,7 +137,7 @@ async function withWallet<T>(        console.error("Operation failed: " + e.message);        console.log("Hint: check pending operations for details.");      } else { -      console.error("caught exception:", e); +      console.error("caught unhandled exception (bug?):", e);      }      process.exit(1);    } finally { diff --git a/src/wallet-impl/errors.ts b/src/wallet-impl/errors.ts index 5df99b7d3..803497e66 100644 --- a/src/wallet-impl/errors.ts +++ b/src/wallet-impl/errors.ts @@ -52,8 +52,9 @@ export async function guardOperationException<T>(    onOpError: (e: OperationError) => Promise<void>,  ): Promise<T> {    try { -    return op(); +    return await op();    } catch (e) { +    console.log("guard: caught exception");      if (e instanceof OperationFailedAndReportedError) {        throw e;      } @@ -62,6 +63,7 @@ export async function guardOperationException<T>(        throw new OperationFailedAndReportedError(e.message);      }      if (e instanceof Error) { +      console.log("guard: caught Error");        await onOpError({          type: "exception",          message: e.message, @@ -69,6 +71,7 @@ export async function guardOperationException<T>(        });        throw new OperationFailedAndReportedError(e.message);      } +    console.log("guard: caught something else");      await onOpError({        type: "exception",        message: "non-error exception thrown", diff --git a/src/wallet-impl/pay.ts b/src/wallet-impl/pay.ts index 9b2da9c7d..f07b0328c 100644 --- a/src/wallet-impl/pay.ts +++ b/src/wallet-impl/pay.ts @@ -365,6 +365,8 @@ async function recordConfirmPay(        const p = await tx.get(Stores.proposals, proposal.proposalId);        if (p) {          p.proposalStatus = ProposalStatus.ACCEPTED; +        p.lastError = undefined; +        p.retryInfo = initRetryInfo(false);          await tx.put(Stores.proposals, p);        }        await tx.put(Stores.purchases, t); @@ -467,6 +469,7 @@ async function incrementPurchaseRetry(    proposalId: string,    err: OperationError | undefined,  ): Promise<void> { +  console.log("incrementing purchase retry with error", err);    await runWithWriteTransaction(ws.db, [Stores.purchases], async tx => {      const pr = await tx.get(Stores.purchases, proposalId);      if (!pr) { @@ -650,6 +653,8 @@ export async function submitPay(      throw Error("merchant payment signature invalid");    }    purchase.finished = true; +  purchase.status = PurchaseStatus.Dormant; +  purchase.lastError = undefined;    purchase.retryInfo = initRetryInfo(false);    const modifiedCoins: CoinRecord[] = [];    for (const pc of purchase.payReq.coins) { @@ -992,6 +997,7 @@ async function submitRefundsToExchange(    }    const pendingKeys = Object.keys(purchase.refundsPending);    if (pendingKeys.length === 0) { +    console.log("no pending refunds");      return;    }    for (const pk of pendingKeys) { @@ -1010,50 +1016,52 @@ async function submitRefundsToExchange(      const exchangeUrl = purchase.payReq.coins[0].exchange_url;      const reqUrl = new URL("refund", exchangeUrl);      const resp = await ws.http.postJson(reqUrl.href, req); +    console.log("sent refund permission");      if (resp.status !== 200) {        console.error("refund failed", resp);        continue;      } -    // Transactionally mark successful refunds as done -    const transformPurchase = ( -      t: PurchaseRecord | undefined, -    ): PurchaseRecord | undefined => { -      if (!t) { -        console.warn("purchase not found, not updating refund"); -        return; -      } -      if (t.refundsPending[pk]) { -        t.refundsDone[pk] = t.refundsPending[pk]; -        delete t.refundsPending[pk]; -      } -      return t; -    }; -    const transformCoin = ( -      c: CoinRecord | undefined, -    ): CoinRecord | undefined => { -      if (!c) { -        console.warn("coin not found, can't apply refund"); -        return; -      } -      const refundAmount = Amounts.parseOrThrow(perm.refund_amount); -      const refundFee = Amounts.parseOrThrow(perm.refund_fee); -      c.status = CoinStatus.Dirty; -      c.currentAmount = Amounts.add(c.currentAmount, refundAmount).amount; -      c.currentAmount = Amounts.sub(c.currentAmount, refundFee).amount; - -      return c; -    }; +    let allRefundsProcessed = false;      await runWithWriteTransaction(        ws.db,        [Stores.purchases, Stores.coins],        async tx => { -        await tx.mutate(Stores.purchases, proposalId, transformPurchase); -        await tx.mutate(Stores.coins, perm.coin_pub, transformCoin); +        const p = await tx.get(Stores.purchases, proposalId); +        if (!p) { +          return; +        } +        if (p.refundsPending[pk]) { +          p.refundsDone[pk] = p.refundsPending[pk]; +          delete p.refundsPending[pk]; +        } +        if (Object.keys(p.refundsPending).length === 0) { +          p.retryInfo = initRetryInfo(); +          p.lastError = undefined; +          p.status = PurchaseStatus.Dormant; +          allRefundsProcessed = true; +        } +        await tx.put(Stores.purchases, p); +        const c = await tx.get(Stores.coins, perm.coin_pub); +        if (!c) { +          console.warn("coin not found, can't apply refund"); +          return; +        } +        const refundAmount = Amounts.parseOrThrow(perm.refund_amount); +        const refundFee = Amounts.parseOrThrow(perm.refund_fee); +        c.status = CoinStatus.Dirty; +        c.currentAmount = Amounts.add(c.currentAmount, refundAmount).amount; +        c.currentAmount = Amounts.sub(c.currentAmount, refundFee).amount; +        await tx.put(Stores.coins, c);        },      ); -    refresh(ws, perm.coin_pub); +    if (allRefundsProcessed) { +      ws.notify({ +        type: NotificationType.RefundFinished, +      }) +    } +    await refresh(ws, perm.coin_pub);    }    ws.notify({ @@ -1062,7 +1070,6 @@ async function submitRefundsToExchange(    });  } -  async function acceptRefundResponse(    ws: InternalWalletState,    proposalId: string, @@ -1086,6 +1093,8 @@ async function acceptRefundResponse(      t.lastRefundTimestamp = getTimestampNow();      t.status = PurchaseStatus.ProcessRefund; +    t.lastError = undefined; +    t.retryInfo = initRetryInfo();      for (const perm of refundPermissions) {        if ( @@ -1102,14 +1111,21 @@ async function acceptRefundResponse(    await submitRefundsToExchange(ws, proposalId);  } - -async function queryRefund(ws: InternalWalletState, proposalId: string): Promise<void> { +async function queryRefund( +  ws: InternalWalletState, +  proposalId: string, +): Promise<void> {    const purchase = await oneShotGet(ws.db, Stores.purchases, proposalId);    if (purchase?.status !== PurchaseStatus.QueryRefund) {      return;    } -  const refundUrl = new URL("refund", purchase.contractTerms.merchant_base_url).href +  const refundUrlObj = new URL( +    "refund", +    purchase.contractTerms.merchant_base_url, +  ); +  refundUrlObj.searchParams.set("order_id", purchase.contractTerms.order_id); +  const refundUrl = refundUrlObj.href;    let resp;    try {      resp = await ws.http.get(refundUrl); @@ -1122,22 +1138,45 @@ async function queryRefund(ws: InternalWalletState, proposalId: string): Promise    await acceptRefundResponse(ws, proposalId, refundResponse);  } -async function startRefundQuery(ws: InternalWalletState, proposalId: string): Promise<void> { -  const success = await runWithWriteTransaction(ws.db, [Stores.purchases], async (tx) => { -    const p = await tx.get(Stores.purchases, proposalId); -    if (p?.status !== PurchaseStatus.Done) { -      return false; -    } -    p.status = PurchaseStatus.QueryRefund; -    return true; -  }); +async function startRefundQuery( +  ws: InternalWalletState, +  proposalId: string, +): Promise<void> { +  const success = await runWithWriteTransaction( +    ws.db, +    [Stores.purchases], +    async tx => { +      const p = await tx.get(Stores.purchases, proposalId); +      if (!p) { +        console.log("no purchase found for refund URL"); +        return false; +      } +      if (p.status === PurchaseStatus.QueryRefund) { +        return true; +      } +      if (p.status === PurchaseStatus.ProcessRefund) { +        return true; +      } +      if (p.status !== PurchaseStatus.Dormant) { +        console.log( +          `can't apply refund, as payment isn't done (status ${p.status})`, +        ); +        return false; +      } +      p.lastError = undefined; +      p.status = PurchaseStatus.QueryRefund; +      p.retryInfo = initRetryInfo(); +      await tx.put(Stores.purchases, p); +      return true; +    }, +  );    if (!success) {      return;    } -  await queryRefund(ws, proposalId); -} +  await processPurchase(ws, proposalId); +}  /**   * Accept a refund, return the contract hash for the contract @@ -1149,6 +1188,8 @@ export async function applyRefund(  ): Promise<string> {    const parseResult = parseRefundUri(talerRefundUri); +  console.log("applying refund"); +    if (!parseResult) {      throw Error("invalid refund URI");    } @@ -1163,6 +1204,7 @@ export async function applyRefund(      throw Error("no purchase for the taler://refund/ URI was found");    } +  console.log("processing purchase for refund");    await startRefundQuery(ws, purchase.proposalId);    return purchase.contractTermsHash; @@ -1180,7 +1222,7 @@ export async function processPurchase(    );  } -export async function processPurchaseImpl( +async function processPurchaseImpl(    ws: InternalWalletState,    proposalId: string,  ): Promise<void> { @@ -1188,8 +1230,9 @@ export async function processPurchaseImpl(    if (!purchase) {      return;    } +  logger.trace(`processing purchase ${proposalId}`);    switch (purchase.status) { -    case PurchaseStatus.Done: +    case PurchaseStatus.Dormant:        return;      case PurchaseStatus.Abort:        // FIXME @@ -1200,7 +1243,9 @@ export async function processPurchaseImpl(        await queryRefund(ws, proposalId);        break;      case PurchaseStatus.ProcessRefund: +      console.log("submitting refunds to exchange (toplvl)");        await submitRefundsToExchange(ws, proposalId); +      console.log("after submitting refunds to exchange (toplvl)");        break;      default:        throw assertUnreachable(purchase.status); diff --git a/src/wallet-impl/pending.ts b/src/wallet-impl/pending.ts index bd10538af..c86ed6959 100644 --- a/src/wallet-impl/pending.ts +++ b/src/wallet-impl/pending.ts @@ -32,7 +32,9 @@ import {    ReserveRecordStatus,    CoinStatus,    ProposalStatus, +  PurchaseStatus,  } from "../dbTypes"; +import { assertUnreachable } from "../util/assertUnreachable";  function updateRetryDelay(    oldDelay: Duration, @@ -353,7 +355,7 @@ async function gatherPurchasePending(    onlyDue: boolean = false,  ): Promise<void> {    await tx.iter(Stores.purchases).forEach((pr) => { -    if (pr.finished) { +    if (pr.status === PurchaseStatus.Dormant) {        return;      }      resp.nextRetryDelay = updateRetryDelay( @@ -369,6 +371,9 @@ async function gatherPurchasePending(        givesLifeness: true,        isReplay: false,        proposalId: pr.proposalId, +      status: pr.status, +      retryInfo: pr.retryInfo, +      lastError: pr.lastError,      });    }); diff --git a/src/wallet-impl/withdraw.ts b/src/wallet-impl/withdraw.ts index 7b7d0f640..3122a463c 100644 --- a/src/wallet-impl/withdraw.ts +++ b/src/wallet-impl/withdraw.ts @@ -282,6 +282,7 @@ async function processPlanchet(        }        if (numDone === ws.denoms.length) {          ws.finishTimestamp = getTimestampNow(); +        ws.lastError = undefined;          ws.retryInfo = initRetryInfo(false);          withdrawSessionFinished = true;        } diff --git a/src/wallet.ts b/src/wallet.ts index 86b3085f4..489bb2af8 100644 --- a/src/wallet.ts +++ b/src/wallet.ts @@ -49,7 +49,7 @@ import {    processDownloadProposal,    applyRefund,    getFullRefundFees, -  processPurchaseImpl, +  processPurchase,  } from "./wallet-impl/pay";  import { @@ -180,6 +180,7 @@ export class Wallet {      pending: PendingOperationInfo,      forceNow: boolean = false,    ): Promise<void> { +    console.log("running pending", pending);      switch (pending.type) {        case "bug":          // Nothing to do, will just be displayed to the user @@ -209,7 +210,7 @@ export class Wallet {          await processTip(this.ws, pending.tipId);          break;        case "pay": -        await processPurchaseImpl(this.ws, pending.proposalId); +        await processPurchase(this.ws, pending.proposalId);          break;        default:          assertUnreachable(pending); diff --git a/src/walletTypes.ts b/src/walletTypes.ts index d78fc8126..2413234eb 100644 --- a/src/walletTypes.ts +++ b/src/walletTypes.ts @@ -37,6 +37,7 @@ import {    ExchangeWireInfo,    WithdrawalSource,    RetryInfo, +  PurchaseStatus,  } from "./dbTypes";  import { CoinPaySig, ContractTerms, PayReq } from "./talerTypes"; @@ -520,6 +521,7 @@ export const enum NotificationType {    ReserveDepleted = "reserve-depleted",    WithdrawSessionFinished = "withdraw-session-finished",    WaitingForRetry = "waiting-for-retry", +  RefundFinished = "refund-finished",  }  export interface ProposalAcceptedNotification { @@ -585,6 +587,10 @@ export interface WaitingForRetryNotification {    numGivingLiveness: number;  } +export interface RefundFinishedNotification { +  type: NotificationType.RefundFinished; +} +  export type WalletNotification =    | ProposalAcceptedNotification    | ProposalDownloadedNotification @@ -599,7 +605,8 @@ export type WalletNotification =    | ReserveConfirmedNotification    | WithdrawSessionFinishedNotification    | ReserveDepletedNotification -  | WaitingForRetryNotification; +  | WaitingForRetryNotification +  | RefundFinishedNotification;  export interface OperationError {    type: string; @@ -612,7 +619,7 @@ export interface PendingExchangeUpdateOperation {    stage: string;    reason: string;    exchangeBaseUrl: string; -  lastError?: OperationError; +  lastError: OperationError | undefined;  }  export interface PendingBugOperation { @@ -674,6 +681,9 @@ export interface PendingPayOperation {    type: "pay";    proposalId: string;    isReplay: boolean; +  status: PurchaseStatus; +  retryInfo: RetryInfo, +  lastError: OperationError | undefined;  }  export interface PendingOperationInfoCommon { | 
