gnunet-svn
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

[taler-wallet-core] 02/03: get operation plan impl, no test


From: gnunet
Subject: [taler-wallet-core] 02/03: get operation plan impl, no test
Date: Tue, 13 Jun 2023 21:46:40 +0200

This is an automated email from the git hooks/post-receive script.

sebasjm pushed a commit to branch master
in repository wallet-core.

commit 8b74bda065e1f2e01b322662dce3a002aa0fb20b
Author: Sebastian <sebasjm@gmail.com>
AuthorDate: Tue Jun 13 16:46:16 2023 -0300

    get operation plan impl, no test
---
 .../src/operations/pay-peer-common.ts              |  12 +-
 .../taler-wallet-core/src/util/coinSelection.ts    | 512 ++++++++++++++++++++-
 packages/taler-wallet-core/src/wallet-api-types.ts |  10 +
 packages/taler-wallet-core/src/wallet.ts           |  14 +-
 4 files changed, 534 insertions(+), 14 deletions(-)

diff --git a/packages/taler-wallet-core/src/operations/pay-peer-common.ts 
b/packages/taler-wallet-core/src/operations/pay-peer-common.ts
index 717b25f49..72e48cb03 100644
--- a/packages/taler-wallet-core/src/operations/pay-peer-common.ts
+++ b/packages/taler-wallet-core/src/operations/pay-peer-common.ts
@@ -52,7 +52,11 @@ import { InternalWalletState } from 
"../internal-wallet-state.js";
 import { checkDbInvariant } from "../util/invariants.js";
 import { getPeerPaymentBalanceDetailsInTx } from "./balance.js";
 import { getTotalRefreshCost } from "./refresh.js";
-import { OperationAttemptLongpollResult, OperationAttemptResult, 
OperationAttemptResultType } from "../util/retries.js";
+import {
+  OperationAttemptLongpollResult,
+  OperationAttemptResult,
+  OperationAttemptResultType,
+} from "../util/retries.js";
 
 const logger = new Logger("operations/peer-to-peer.ts");
 
@@ -113,6 +117,12 @@ export type SelectPeerCoinsResult =
       insufficientBalanceDetails: PayPeerInsufficientBalanceDetails;
     };
 
+/**
+ * Get information about the coin selected for signatures
+ * @param ws
+ * @param csel
+ * @returns
+ */
 export async function queryCoinInfosForSelection(
   ws: InternalWalletState,
   csel: PeerPushPaymentCoinSelection,
diff --git a/packages/taler-wallet-core/src/util/coinSelection.ts 
b/packages/taler-wallet-core/src/util/coinSelection.ts
index f4066bf51..8fd09ea2b 100644
--- a/packages/taler-wallet-core/src/util/coinSelection.ts
+++ b/packages/taler-wallet-core/src/util/coinSelection.ts
@@ -30,29 +30,29 @@ import {
   AgeRestriction,
   AmountJson,
   Amounts,
+  AmountString,
   CoinStatus,
   DenominationInfo,
   DenominationPubKey,
   DenomSelectionState,
   ForcedCoinSel,
   ForcedDenomSel,
+  GetPlanForOperationRequest,
+  GetPlanForOperationResponse,
   j2s,
   Logger,
   parsePaytoUri,
   PayCoinSelection,
   PayMerchantInsufficientBalanceDetails,
   strcmp,
+  TransactionType,
 } from "@gnu-taler/taler-util";
 import {
   AllowedAuditorInfo,
   AllowedExchangeInfo,
   DenominationRecord,
 } from "../db.js";
-import {
-  getExchangeDetails,
-  isWithdrawableDenom,
-  WalletConfig,
-} from "../index.js";
+import { getExchangeDetails, isWithdrawableDenom } from "../index.js";
 import { InternalWalletState } from "../internal-wallet-state.js";
 import { getMerchantPaymentBalanceDetails } from "../operations/balance.js";
 import { checkDbInvariant, checkLogicInvariant } from "./invariants.js";
@@ -150,7 +150,7 @@ export interface CoinSelectionTally {
 /**
  * Account for the fees of spending a coin.
  */
-export function tallyFees(
+function tallyFees(
   tally: Readonly<CoinSelectionTally>,
   wireFeesPerExchange: Record<string, AmountJson>,
   wireFeeAmortization: number,
@@ -542,7 +542,7 @@ export type AvailableDenom = DenominationInfo & {
   numAvailable: number;
 };
 
-export async function selectCandidates(
+async function selectCandidates(
   ws: InternalWalletState,
   req: SelectPayCoinRequestNg,
 ): Promise<[AvailableDenom[], Record<string, AmountJson>]> {
@@ -789,3 +789,501 @@ export function selectForcedWithdrawalDenominations(
     totalWithdrawCost: Amounts.stringify(totalWithdrawCost),
   };
 }
+
+/**
+ * simulate a coin selection and return the amount
+ * that will effectively change the wallet balance and
+ * the raw amount of the operation
+ *
+ * @param ws
+ * @param br
+ * @returns
+ */
+export async function getPlanForOperation(
+  ws: InternalWalletState,
+  req: GetPlanForOperationRequest,
+): Promise<GetPlanForOperationResponse> {
+  const amount = Amounts.parseOrThrow(req.instructedAmount);
+
+  switch (req.type) {
+    case TransactionType.Withdrawal: {
+      const availableCoins = await getAvailableCoins(
+        ws,
+        "credit",
+        amount.currency,
+        false,
+        false,
+        undefined,
+        undefined,
+        undefined,
+      );
+      const usableCoins = selectCoinForOperation(
+        "credit",
+        amount,
+        req.mode === "effective" ? "net" : "gross",
+        availableCoins.denoms,
+      );
+
+      return getAmountsWithFee(
+        "credit",
+        usableCoins.totalValue,
+        usableCoins.totalContribution,
+        usableCoins,
+      );
+    }
+    case TransactionType.Deposit: {
+      const payto = parsePaytoUri(req.account)!;
+      const availableCoins = await getAvailableCoins(
+        ws,
+        "debit",
+        amount.currency,
+        true,
+        false,
+        undefined,
+        [payto.targetType],
+        undefined,
+      );
+      //FIXME: just doing for 1 exchange now
+      //assuming that the wallet has one exchange and all the coins available
+      //are from that exchange
+
+      const wireFee = Object.entries(availableCoins.wfPerExchange)[0][1][
+        payto.targetType
+      ];
+
+      let usableCoins;
+
+      if (req.mode === "effective") {
+        usableCoins = selectCoinForOperation(
+          "debit",
+          amount,
+          "gross",
+          availableCoins.denoms,
+        );
+
+        usableCoins.totalContribution = Amounts.stringify(
+          Amounts.sub(usableCoins.totalContribution, wireFee).amount,
+        );
+      } else {
+        const adjustedAmount = Amounts.add(amount, wireFee).amount;
+
+        usableCoins = selectCoinForOperation(
+          "debit",
+          adjustedAmount,
+          // amount,
+          "net",
+          availableCoins.denoms,
+        );
+
+        usableCoins.totalContribution = Amounts.stringify(
+          Amounts.sub(usableCoins.totalContribution, wireFee).amount,
+        );
+      }
+
+      return getAmountsWithFee(
+        "debit",
+        usableCoins.totalValue,
+        usableCoins.totalContribution,
+        usableCoins,
+      );
+    }
+    default: {
+      throw Error("operation not supported");
+    }
+  }
+}
+
+function getAmountsWithFee(
+  op: "debit" | "credit",
+  value: AmountString,
+  contribution: AmountString,
+  details: any,
+): GetPlanForOperationResponse {
+  return {
+    rawAmount: op === "credit" ? value : contribution,
+    effectiveAmount: op === "credit" ? contribution : value,
+    details,
+  };
+}
+
+/**
+ *
+ * @param op defined which fee are we taking into consideration: deposits or 
withdraw
+ * @param limit the total amount limit of the operation
+ * @param mode if the total amount is includes the fees or just the 
contribution
+ * @param denoms list of available denomination for the operation
+ * @returns
+ */
+function selectCoinForOperation(
+  op: "debit" | "credit",
+  limit: AmountJson,
+  mode: "net" | "gross",
+  denoms: AvailableDenom[],
+): SelectedCoins {
+  const result: SelectedCoins = {
+    totalValue: Amounts.stringify(Amounts.zeroOfCurrency(limit.currency)),
+    totalWithdrawalFee: Amounts.stringify(
+      Amounts.zeroOfCurrency(limit.currency),
+    ),
+    totalDepositFee: Amounts.stringify(Amounts.zeroOfCurrency(limit.currency)),
+    totalContribution: Amounts.stringify(
+      Amounts.zeroOfCurrency(limit.currency),
+    ),
+    coins: [],
+  };
+  if (!denoms.length) return result;
+  /**
+   * We can make this faster. We should prevent sorting and
+   * keep the information ready for multiple calls since this
+   * function is expected to work on embedded devices and
+   * create a response on key press
+   */
+
+  //rank coins
+  denoms.sort(
+    op === "credit"
+      ? denomsByDescendingWithdrawContribution
+      : denomsByDescendingDepositContribution,
+  );
+
+  //take coins in order until amount
+  let selectedCoinsAreEnough = false;
+  let denomIdx = 0;
+  iterateDenoms: while (denomIdx < denoms.length) {
+    const cur = denoms[denomIdx];
+    // for (const cur of denoms) {
+    let total = op === "credit" ? Number.MAX_SAFE_INTEGER : cur.numAvailable;
+    const opFee = op === "credit" ? cur.feeWithdraw : cur.feeDeposit;
+    const contribution = Amounts.sub(cur.value, opFee).amount;
+
+    if (Amounts.isZero(contribution)) {
+      // 0 contribution denoms should be the last
+      break iterateDenoms;
+    }
+    iterateCoins: while (total > 0) {
+      const nextValue = Amounts.add(result.totalValue, cur.value).amount;
+
+      const nextContribution = Amounts.add(
+        result.totalContribution,
+        contribution,
+      ).amount;
+
+      const progress = mode === "gross" ? nextValue : nextContribution;
+
+      if (Amounts.cmp(progress, limit) === 1) {
+        //the current coin is more than we need, try next denom
+        break iterateCoins;
+      }
+
+      result.totalValue = Amounts.stringify(nextValue);
+      result.totalContribution = Amounts.stringify(nextContribution);
+
+      result.totalDepositFee = Amounts.stringify(
+        Amounts.add(result.totalDepositFee, cur.feeDeposit).amount,
+      );
+
+      result.totalWithdrawalFee = Amounts.stringify(
+        Amounts.add(result.totalWithdrawalFee, cur.feeWithdraw).amount,
+      );
+
+      result.coins.push(cur.denomPubHash);
+
+      if (Amounts.cmp(progress, limit) === 0) {
+        selectedCoinsAreEnough = true;
+        // we have just enough coins, complete
+        break iterateDenoms;
+      }
+
+      //go next coin
+      total--;
+    }
+    //go next denom
+    denomIdx++;
+  }
+
+  if (selectedCoinsAreEnough) {
+    // we made it
+    return result;
+  }
+  if (op === "credit") {
+    //doing withdraw there is no way to cover the gap
+    return result;
+  }
+  //tried all the coins but there is a gap
+  //doing deposit we can try refreshing coins
+
+  const total = mode === "gross" ? result.totalValue : 
result.totalContribution;
+  const gap = Amounts.sub(limit, total).amount;
+
+  //about recursive calls
+  //the only way to get here is by doing a deposit (that will do a refresh)
+  //and now we are calculating fee for credit (which does not need to 
calculate refresh)
+
+  let refreshIdx = 0;
+  let choice: RefreshChoice | undefined = undefined;
+  refreshIteration: while (refreshIdx < denoms.length) {
+    const d = denoms[refreshIdx];
+    const denomContribution =
+      mode === "gross"
+        ? Amounts.sub(d.value, d.feeRefresh).amount
+        : Amounts.sub(d.value, d.feeDeposit, d.feeRefresh).amount;
+
+    const changeAfterDeposit = Amounts.sub(denomContribution, gap).amount;
+    if (Amounts.isZero(changeAfterDeposit)) {
+      //the rest of the coins are very small
+      break refreshIteration;
+    }
+
+    const changeCost = selectCoinForOperation(
+      "credit",
+      changeAfterDeposit,
+      mode,
+      denoms,
+    );
+    const totalFee = Amounts.add(
+      d.feeDeposit,
+      d.feeRefresh,
+      changeCost.totalWithdrawalFee,
+    ).amount;
+
+    if (!choice || Amounts.cmp(totalFee, choice.totalFee) === -1) {
+      //found cheaper change
+      choice = {
+        gap: Amounts.stringify(gap),
+        totalFee: Amounts.stringify(totalFee),
+        selected: d.denomPubHash,
+        totalValue: d.value,
+        totalRefreshFee: Amounts.stringify(d.feeRefresh),
+        totalDepositFee: d.feeDeposit,
+        totalChangeValue: Amounts.stringify(changeCost.totalValue),
+        totalChangeContribution: Amounts.stringify(
+          changeCost.totalContribution,
+        ),
+        totalChangeWithdrawalFee: Amounts.stringify(
+          changeCost.totalWithdrawalFee,
+        ),
+        change: changeCost.coins,
+      };
+    }
+    refreshIdx++;
+  }
+  if (choice) {
+    if (mode === "gross") {
+      result.totalValue = Amounts.stringify(
+        Amounts.add(result.totalValue, gap).amount,
+      );
+      result.totalContribution = Amounts.stringify(
+        Amounts.add(result.totalContribution, gap).amount,
+      );
+      result.totalContribution = Amounts.stringify(
+        Amounts.sub(result.totalContribution, choice.totalFee).amount,
+      );
+    } else {
+      result.totalContribution = Amounts.stringify(
+        Amounts.add(result.totalContribution, gap).amount,
+      );
+      result.totalValue = Amounts.stringify(
+        Amounts.add(result.totalValue, gap, choice.totalFee).amount,
+      );
+    }
+  }
+
+  // console.log("gap", Amounts.stringify(limit), Amounts.stringify(gap), 
choice);
+  result.refresh = choice;
+  return result;
+}
+
+function denomsByDescendingDepositContribution(
+  d1: AvailableDenom,
+  d2: AvailableDenom,
+) {
+  const contrib1 = Amounts.sub(d1.value, d1.feeDeposit).amount;
+  const contrib2 = Amounts.sub(d2.value, d2.feeDeposit).amount;
+  return (
+    Amounts.cmp(contrib2, contrib1) || strcmp(d1.denomPubHash, d2.denomPubHash)
+  );
+}
+function denomsByDescendingWithdrawContribution(
+  d1: AvailableDenom,
+  d2: AvailableDenom,
+) {
+  const contrib1 = Amounts.sub(d1.value, d1.feeWithdraw).amount;
+  const contrib2 = Amounts.sub(d2.value, d2.feeWithdraw).amount;
+  return (
+    Amounts.cmp(contrib2, contrib1) || strcmp(d1.denomPubHash, d2.denomPubHash)
+  );
+}
+
+interface RefreshChoice {
+  gap: AmountString;
+  totalFee: AmountString;
+  selected: string;
+
+  totalValue: AmountString;
+  totalDepositFee: AmountString;
+  totalRefreshFee: AmountString;
+  totalChangeValue: AmountString;
+  totalChangeContribution: AmountString;
+  totalChangeWithdrawalFee: AmountString;
+  change: string[];
+}
+
+interface SelectedCoins {
+  totalValue: AmountString;
+  totalContribution: AmountString;
+  totalWithdrawalFee: AmountString;
+  totalDepositFee: AmountString;
+  coins: string[];
+  refresh?: RefreshChoice;
+}
+
+/**
+ * Get all the denoms that can be used for a operation that is limited
+ * by the following restrictions.
+ * This function is costly (by the database access) but with high chances
+ * of being cached
+ */
+async function getAvailableCoins(
+  ws: InternalWalletState,
+  op: "credit" | "debit",
+  currency: string,
+  shouldCalculateWireFee: boolean,
+  shouldCalculatePurseFee: boolean,
+  exchangeFilter: string[] | undefined,
+  wireMethodFilter: string[] | undefined,
+  ageRestrictedFilter: number | undefined,
+) {
+  return await ws.db
+    .mktx((x) => [
+      x.exchanges,
+      x.exchangeDetails,
+      x.denominations,
+      x.coinAvailability,
+    ])
+    .runReadOnly(async (tx) => {
+      const denoms: AvailableDenom[] = [];
+      const wfPerExchange: Record<string, Record<string, AmountJson>> = {};
+      const pfPerExchange: Record<string, AmountJson> = {};
+
+      const databaseExchanges = await tx.exchanges.iter().toArray();
+      const exchanges =
+        exchangeFilter === undefined
+          ? databaseExchanges.map((e) => e.baseUrl)
+          : exchangeFilter;
+
+      for (const exchangeBaseUrl of exchanges) {
+        const exchangeDetails = await getExchangeDetails(tx, exchangeBaseUrl);
+        // 1.- exchange has same currency
+        if (exchangeDetails?.currency !== currency) {
+          continue;
+        }
+
+        const wireMethodFee: Record<string, AmountJson> = {};
+        // 2.- exchange supports wire method
+        if (shouldCalculateWireFee) {
+          for (const acc of exchangeDetails.wireInfo.accounts) {
+            const pp = parsePaytoUri(acc.payto_uri);
+            checkLogicInvariant(!!pp);
+            // also check that wire method is supported now
+            if (wireMethodFilter !== undefined) {
+              if (wireMethodFilter.indexOf(pp.targetType) === -1) {
+                continue;
+              }
+            }
+            const wireFeeStr = exchangeDetails.wireInfo.feesForType[
+              pp.targetType
+            ]?.find((x) => {
+              return AbsoluteTime.isBetween(
+                AbsoluteTime.now(),
+                AbsoluteTime.fromProtocolTimestamp(x.startStamp),
+                AbsoluteTime.fromProtocolTimestamp(x.endStamp),
+              );
+            })?.wireFee;
+
+            if (wireFeeStr) {
+              wireMethodFee[pp.targetType] = Amounts.parseOrThrow(wireFeeStr);
+            }
+            break;
+          }
+          if (Object.keys(wireMethodFee).length === 0) {
+            throw Error(
+              `exchange ${exchangeBaseUrl} doesn't have wire fee defined for 
this period`,
+            );
+          }
+        }
+        wfPerExchange[exchangeBaseUrl] = wireMethodFee;
+
+        // 3.- exchange supports wire method
+        if (shouldCalculatePurseFee) {
+          const purseFeeFound = exchangeDetails.globalFees.find((x) => {
+            return AbsoluteTime.isBetween(
+              AbsoluteTime.now(),
+              AbsoluteTime.fromProtocolTimestamp(x.startDate),
+              AbsoluteTime.fromProtocolTimestamp(x.endDate),
+            );
+          })?.purseFee;
+          if (!purseFeeFound) {
+            throw Error(
+              `exchange ${exchangeBaseUrl} doesn't have purse fee defined for 
this period`,
+            );
+          }
+          const purseFee = Amounts.parseOrThrow(purseFeeFound);
+          pfPerExchange[exchangeBaseUrl] = purseFee;
+        }
+
+        //4.- filter coins restricted by age
+        if (op === "credit") {
+          const ds = await tx.denominations.indexes.byExchangeBaseUrl.getAll(
+            exchangeBaseUrl,
+          );
+          for (const denom of ds) {
+            denoms.push({
+              ...DenominationRecord.toDenomInfo(denom),
+              numAvailable: Number.MAX_SAFE_INTEGER,
+              maxAge: AgeRestriction.AGE_UNRESTRICTED,
+            });
+          }
+        } else {
+          const ageLower = !ageRestrictedFilter ? 0 : ageRestrictedFilter;
+          const ageUpper = AgeRestriction.AGE_UNRESTRICTED;
+
+          const myExchangeCoins =
+            await tx.coinAvailability.indexes.byExchangeAgeAvailability.getAll(
+              GlobalIDB.KeyRange.bound(
+                [exchangeDetails.exchangeBaseUrl, ageLower, 1],
+                [
+                  exchangeDetails.exchangeBaseUrl,
+                  ageUpper,
+                  Number.MAX_SAFE_INTEGER,
+                ],
+              ),
+            );
+          //5.- save denoms with how many coins are available
+          // FIXME: Check that the individual denomination is audited!
+          // FIXME: Should we exclude denominations that are
+          // not spendable anymore?
+          for (const coinAvail of myExchangeCoins) {
+            const denom = await tx.denominations.get([
+              coinAvail.exchangeBaseUrl,
+              coinAvail.denomPubHash,
+            ]);
+            checkDbInvariant(!!denom);
+            if (denom.isRevoked || !denom.isOffered) {
+              continue;
+            }
+            denoms.push({
+              ...DenominationRecord.toDenomInfo(denom),
+              numAvailable: coinAvail.freshCoinCount ?? 0,
+              maxAge: coinAvail.maxAge,
+            });
+          }
+        }
+      }
+
+      return {
+        denoms,
+        wfPerExchange,
+        pfPerExchange,
+      };
+    });
+}
diff --git a/packages/taler-wallet-core/src/wallet-api-types.ts 
b/packages/taler-wallet-core/src/wallet-api-types.ts
index 21a228b64..3b0d11039 100644
--- a/packages/taler-wallet-core/src/wallet-api-types.ts
+++ b/packages/taler-wallet-core/src/wallet-api-types.ts
@@ -58,6 +58,8 @@ import {
   GetContractTermsDetailsRequest,
   GetExchangeTosRequest,
   GetExchangeTosResult,
+  GetPlanForOperationRequest,
+  GetPlanForOperationResponse,
   GetWithdrawalDetailsForAmountRequest,
   GetWithdrawalDetailsForUriRequest,
   InitRequest,
@@ -143,6 +145,7 @@ export enum WalletApiOperation {
   AcceptManualWithdrawal = "acceptManualWithdrawal",
   GetBalances = "getBalances",
   GetBalanceDetail = "getBalanceDetail",
+  GetPlanForOperation = "getPlanForOperation",
   GetUserAttentionRequests = "getUserAttentionRequests",
   GetUserAttentionUnreadCount = "getUserAttentionUnreadCount",
   MarkAttentionRequestAsRead = "markAttentionRequestAsRead",
@@ -275,6 +278,12 @@ export type GetBalancesDetailOp = {
   response: MerchantPaymentBalanceDetails;
 };
 
+export type GetPlanForOperationOp = {
+  op: WalletApiOperation.GetPlanForOperation;
+  request: GetPlanForOperationRequest;
+  response: GetPlanForOperationResponse;
+};
+
 // group: Managing Transactions
 
 /**
@@ -940,6 +949,7 @@ export type WalletOperations = {
   [WalletApiOperation.SuspendTransaction]: SuspendTransactionOp;
   [WalletApiOperation.ResumeTransaction]: ResumeTransactionOp;
   [WalletApiOperation.GetBalances]: GetBalancesOp;
+  [WalletApiOperation.GetPlanForOperation]: GetPlanForOperationOp;
   [WalletApiOperation.GetBalanceDetail]: GetBalancesDetailOp;
   [WalletApiOperation.GetTransactions]: GetTransactionsOp;
   [WalletApiOperation.TestingGetSampleTransactions]: 
TestingGetSampleTransactionsOp;
diff --git a/packages/taler-wallet-core/src/wallet.ts 
b/packages/taler-wallet-core/src/wallet.ts
index 5277916de..a04464630 100644
--- a/packages/taler-wallet-core/src/wallet.ts
+++ b/packages/taler-wallet-core/src/wallet.ts
@@ -75,6 +75,7 @@ import {
   codecForGetBalanceDetailRequest,
   codecForGetContractTermsDetails,
   codecForGetExchangeTosRequest,
+  codecForGetPlanForOperationRequest,
   codecForGetWithdrawalDetailsForAmountRequest,
   codecForGetWithdrawalDetailsForUri,
   codecForImportDbRequest,
@@ -218,9 +219,7 @@ import {
   processPeerPushDebit,
 } from "./operations/pay-peer-push-debit.js";
 import { getPendingOperations } from "./operations/pending.js";
-import {
-  createRecoupGroup, processRecoupGroup,
-} from "./operations/recoup.js";
+import { createRecoupGroup, processRecoupGroup } from "./operations/recoup.js";
 import {
   autoRefresh,
   createRefreshGroup,
@@ -283,6 +282,7 @@ import {
   WalletCoreApiClient,
   WalletCoreResponseType,
 } from "./wallet-api-types.js";
+import { getPlanForOperation } from "./util/coinSelection.js";
 
 const logger = new Logger("wallet.ts");
 
@@ -331,9 +331,7 @@ async function callOperationHandler(
 /**
  * Process pending operations.
  */
-export async function runPending(
-  ws: InternalWalletState,
-): Promise<void> {
+export async function runPending(ws: InternalWalletState): Promise<void> {
   const pendingOpsResponse = await getPendingOperations(ws);
   for (const p of pendingOpsResponse.pendingOperations) {
     if (!AbsoluteTime.isExpired(p.timestampDue)) {
@@ -1336,6 +1334,10 @@ async function dispatchRequestInternal<Op extends 
WalletApiOperation>(
       await loadBackupRecovery(ws, req);
       return {};
     }
+    case WalletApiOperation.GetPlanForOperation: {
+      const req = codecForGetPlanForOperationRequest().decode(payload);
+      return await getPlanForOperation(ws, req);
+    }
     case WalletApiOperation.GetBackupInfo: {
       const resp = await getBackupInfo(ws);
       return resp;

-- 
To stop receiving notification emails like this one, please contact
gnunet@gnunet.org.



reply via email to

[Prev in Thread] Current Thread [Next in Thread]