gnunet-svn
[Top][All Lists]
Advanced

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

[taler-wallet-core] branch master updated (4ac67bbcd -> 4e7967dba)


From: gnunet
Subject: [taler-wallet-core] branch master updated (4ac67bbcd -> 4e7967dba)
Date: Tue, 13 Jun 2023 21:46:38 +0200

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

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

    from 4ac67bbcd spelling
     new 671342818 get operation plan types
     new 8b74bda06 get operation plan impl, no test
     new 4e7967dba showing off information about operation plan

The 3 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 packages/taler-util/src/wallet-types.ts            | 159 ++++++-
 .../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 +-
 .../taler-wallet-webextension/src/pwa/index.html   |  28 +-
 .../src/wallet/DestinationSelection/index.ts       |   8 +-
 .../src/wallet/DestinationSelection/state.ts       |  90 +++-
 .../src/wallet/DestinationSelection/views.tsx      |  16 +-
 .../src/wallet/ExchangeAddPage.tsx                 |  29 +-
 10 files changed, 836 insertions(+), 42 deletions(-)

diff --git a/packages/taler-util/src/wallet-types.ts 
b/packages/taler-util/src/wallet-types.ts
index af02807a6..e33f9318d 100644
--- a/packages/taler-util/src/wallet-types.ts
+++ b/packages/taler-util/src/wallet-types.ts
@@ -71,7 +71,7 @@ import {
   codecForAbsoluteTime,
   codecForTimestamp,
 } from "./time.js";
-import { OrderShortInfo } from "./transactions-types.js";
+import { OrderShortInfo, TransactionType } from "./transactions-types.js";
 
 /**
  * Identifier for a transaction in the wallet.
@@ -159,6 +159,163 @@ export const codecForGetBalanceDetailRequest =
       .property("currency", codecForString())
       .build("GetBalanceDetailRequest");
 
+export type GetPlanForOperationRequest =
+  | GetPlanForWithdrawRequest
+  | GetPlanForDepositRequest;
+// | GetPlanForPushDebitRequest
+// | GetPlanForPullCreditRequest
+// | GetPlanForPaymentRequest
+// | GetPlanForTipRequest
+// | GetPlanForRefundRequest
+// | GetPlanForPullDebitRequest
+// | GetPlanForPushCreditRequest;
+
+interface GetPlanForWalletInitiatedOperation {
+  instructedAmount: AmountString;
+  mode: "raw" | "effective";
+}
+
+interface GetPlanToCompleteOperation {
+  instructedAmount: AmountString;
+}
+
+const codecForGetPlanForWalletInitiatedOperation = <
+  T extends GetPlanForWalletInitiatedOperation,
+>() =>
+  buildCodecForObject<T>()
+    .property(
+      "mode",
+      codecForEither(
+        codecForConstString("raw"),
+        codecForConstString("effective"),
+      ),
+    )
+    .property("instructedAmount", codecForAmountString());
+
+interface GetPlanForWithdrawRequest extends GetPlanForWalletInitiatedOperation 
{
+  type: TransactionType.Withdrawal;
+  exchangeUrl?: string;
+}
+interface GetPlanForDepositRequest extends GetPlanForWalletInitiatedOperation {
+  type: TransactionType.Deposit;
+  account: string; //payto string
+}
+interface GetPlanForPushDebitRequest
+  extends GetPlanForWalletInitiatedOperation {
+  type: TransactionType.PeerPushDebit;
+}
+
+interface GetPlanForPullCreditRequest
+  extends GetPlanForWalletInitiatedOperation {
+  type: TransactionType.PeerPullCredit;
+  exchangeUrl: string;
+}
+
+const codecForGetPlanForWithdrawRequest =
+  codecForGetPlanForWalletInitiatedOperation<GetPlanForWithdrawRequest>()
+    .property("type", codecForConstString(TransactionType.Withdrawal))
+    .property("exchangeUrl", codecOptional(codecForString()))
+    .build("GetPlanForWithdrawRequest");
+
+const codecForGetPlanForDepositRequest =
+  codecForGetPlanForWalletInitiatedOperation<GetPlanForDepositRequest>()
+    .property("type", codecForConstString(TransactionType.Deposit))
+    .property("account", codecForString())
+    .build("GetPlanForDepositRequest");
+
+const codecForGetPlanForPushDebitRequest =
+  codecForGetPlanForWalletInitiatedOperation<GetPlanForPushDebitRequest>()
+    .property("type", codecForConstString(TransactionType.PeerPushDebit))
+    .build("GetPlanForPushDebitRequest");
+
+const codecForGetPlanForPullCreditRequest =
+  codecForGetPlanForWalletInitiatedOperation<GetPlanForPullCreditRequest>()
+    .property("type", codecForConstString(TransactionType.PeerPullCredit))
+    .property("exchangeUrl", codecForString())
+    .build("GetPlanForPullCreditRequest");
+
+interface GetPlanForPaymentRequest extends GetPlanToCompleteOperation {
+  type: TransactionType.Payment;
+  wireMethod: string;
+  ageRestriction: number;
+  maxDepositFee: AmountString;
+  maxWireFee: AmountString;
+}
+
+// interface GetPlanForTipRequest extends GetPlanForOperationBase {
+//   type: TransactionType.Tip;
+// }
+// interface GetPlanForRefundRequest extends GetPlanForOperationBase {
+//   type: TransactionType.Refund;
+// }
+interface GetPlanForPullDebitRequest extends GetPlanToCompleteOperation {
+  type: TransactionType.PeerPullDebit;
+}
+interface GetPlanForPushCreditRequest extends GetPlanToCompleteOperation {
+  type: TransactionType.PeerPushCredit;
+}
+
+const codecForGetPlanForPaymentRequest =
+  buildCodecForObject<GetPlanForPaymentRequest>()
+    .property("type", codecForConstString(TransactionType.Payment))
+    .property("maxDepositFee", codecForAmountString())
+    .property("maxWireFee", codecForAmountString())
+    .build("GetPlanForPaymentRequest");
+
+const codecForGetPlanForPullDebitRequest =
+  buildCodecForObject<GetPlanForPullDebitRequest>()
+    .property("type", codecForConstString(TransactionType.PeerPullDebit))
+    .build("GetPlanForPullDebitRequest");
+
+const codecForGetPlanForPushCreditRequest =
+  buildCodecForObject<GetPlanForPushCreditRequest>()
+    .property("type", codecForConstString(TransactionType.PeerPushCredit))
+    .build("GetPlanForPushCreditRequest");
+
+export const codecForGetPlanForOperationRequest =
+  (): Codec<GetPlanForOperationRequest> =>
+    buildCodecForUnion<GetPlanForOperationRequest>()
+      .discriminateOn("type")
+      .alternative(
+        TransactionType.Withdrawal,
+        codecForGetPlanForWithdrawRequest,
+      )
+      .alternative(TransactionType.Deposit, codecForGetPlanForDepositRequest)
+      // .alternative(
+      //   TransactionType.PeerPushDebit,
+      //   codecForGetPlanForPushDebitRequest,
+      // )
+      // .alternative(
+      //   TransactionType.PeerPullCredit,
+      //   codecForGetPlanForPullCreditRequest,
+      // )
+      // .alternative(TransactionType.Payment, 
codecForGetPlanForPaymentRequest)
+      // .alternative(
+      //   TransactionType.PeerPullDebit,
+      //   codecForGetPlanForPullDebitRequest,
+      // )
+      // .alternative(
+      //   TransactionType.PeerPushCredit,
+      //   codecForGetPlanForPushCreditRequest,
+      // )
+      .build("GetPlanForOperationRequest");
+
+export interface GetPlanForOperationResponse {
+  effectiveAmount: AmountString;
+  rawAmount: AmountString;
+  counterPartyAmount?: AmountString;
+  details: any;
+}
+
+export const codecForGetPlanForOperationResponse =
+  (): Codec<GetPlanForOperationResponse> =>
+    buildCodecForObject<GetPlanForOperationResponse>()
+      .property("effectiveAmount", codecForAmountString())
+      .property("rawAmount", codecForAmountString())
+      .property("details", codecForAny())
+      .property("counterPartyAmount", codecOptional(codecForAmountString()))
+      .build("GetPlanForOperationResponse");
+
 export interface Balance {
   scopeInfo: ScopeInfo;
   available: AmountString;
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;
diff --git a/packages/taler-wallet-webextension/src/pwa/index.html 
b/packages/taler-wallet-webextension/src/pwa/index.html
index 4ffbe5205..c150ee68d 100644
--- a/packages/taler-wallet-webextension/src/pwa/index.html
+++ b/packages/taler-wallet-webextension/src/pwa/index.html
@@ -23,7 +23,6 @@
       function openPopup() {
         document.getElementById("popup-overlay").style.display = "flex";
         window.frames["popup"].location = "popup.html";
-        window.frames["popup"];
       }
       function closePopup() {
         document.getElementById("popup-overlay").style.display = "none";
@@ -37,6 +36,9 @@
       function closeWallet() {
         redirectWallet("about:blank");
       }
+      function reloadWallet() {
+        window.frames["wallet"].location.reload()
+      }
       function openPage() {
         window.frames["other"].location =
           document.getElementById("page-url").value;
@@ -44,15 +46,19 @@
     </script>
     <button value="asd" onclick="openPopup()">open popup</button>
     <button value="asd" onclick="closeWallet();openWallet()">
-      reload wallet page
+      restart
+    </button>
+    <button value="asd" onclick="reloadWallet()">
+      refresh
     </button>
     <br />
     <iframe
       id="wallet-window"
       name="wallet"
       src="wallet.html"
-      width="1000"
-      height="100%"
+      style="height: calc(100% - 30px)"
+      width="850"
+      height="90%"
     >
     </iframe>
     <!-- <input id="page-url" type="text" />
@@ -70,14 +76,14 @@
       height="325"
     >
     </iframe> -->
-    <hr />
-    <div class="overlay" id="popup-overlay">
+    <div class="overlay" id="popup-overlay" onclick="closePopup()">
+      
       <iframe
-        id="popup-window"
-        name="popup"
-        src="about:blank"
-        width="500"
-        height="325"
+      id="popup-window"
+      name="popup"
+      src="about:blank"
+      width="500"
+      height="325"
       >
       </iframe>
     </div>
diff --git 
a/packages/taler-wallet-webextension/src/wallet/DestinationSelection/index.ts 
b/packages/taler-wallet-webextension/src/wallet/DestinationSelection/index.ts
index 9a0ba1d88..e3f4a89cb 100644
--- 
a/packages/taler-wallet-webextension/src/wallet/DestinationSelection/index.ts
+++ 
b/packages/taler-wallet-webextension/src/wallet/DestinationSelection/index.ts
@@ -17,7 +17,11 @@
 import { ErrorAlertView } from "../../components/CurrentAlerts.js";
 import { Loading } from "../../components/Loading.js";
 import { ErrorAlert } from "../../context/alert.js";
-import { AmountFieldHandler, ButtonHandler } from "../../mui/handlers.js";
+import {
+  AmountFieldHandler,
+  ButtonHandler,
+  ToggleHandler,
+} from "../../mui/handlers.js";
 import { compose, StateViewMap } from "../../utils/index.js";
 import { useComponentState } from "./state.js";
 import { ReadyView, SelectCurrencyView } from "./views.js";
@@ -71,6 +75,8 @@ export namespace State {
     goToBank: ButtonHandler;
     goToWallet: ButtonHandler;
     amountHandler: AmountFieldHandler;
+    amounts: any;
+    mode: ToggleHandler;
   }
 }
 
diff --git 
a/packages/taler-wallet-webextension/src/wallet/DestinationSelection/state.ts 
b/packages/taler-wallet-webextension/src/wallet/DestinationSelection/state.ts
index b76543b46..b6d4f4cc2 100644
--- 
a/packages/taler-wallet-webextension/src/wallet/DestinationSelection/state.ts
+++ 
b/packages/taler-wallet-webextension/src/wallet/DestinationSelection/state.ts
@@ -14,9 +14,9 @@
  GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
  */
 
-import { Amounts } from "@gnu-taler/taler-util";
+import { Amounts, TransactionType } from "@gnu-taler/taler-util";
 import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
-import { useState } from "preact/hooks";
+import { useEffect, useState } from "preact/hooks";
 import { alertFromError, useAlertContext } from "../../context/alert.js";
 import { useBackendContext } from "../../context/backend.js";
 import { useTranslationContext } from "@gnu-taler/web-util/browser";
@@ -34,19 +34,77 @@ export function useComponentState(props: Props): 
RecursiveState<State> {
 
   const hook = useAsyncAsHook(async () => {
     if (!parsedInitialAmount) return undefined;
-    const resp = await api.wallet.call(WalletApiOperation.GetBalanceDetail, {
+    const balance = await api.wallet.call(WalletApiOperation.GetBalanceDetail, 
{
       currency: parsedInitialAmount.currency,
     });
-    return resp;
+    return { balance };
   });
 
-  const total = hook && !hook.hasError ? hook.response : undefined;
+  const info = hook && !hook.hasError ? hook.response : undefined;
 
   // const initialCurrency = parsedInitialAmount?.currency;
 
   const [amount, setAmount] = useState(
     !parsedInitialAmount ? undefined : parsedInitialAmount,
   );
+  const [rawMode, setRawMode] = useState(false);
+
+  const [fee, setFee] = useState<any>({});
+  useEffect(() => {
+    if (!amount) return;
+
+    // const type = TransactionType.Deposit
+    [
+      TransactionType.Deposit as const,
+      TransactionType.Withdrawal as const,
+    ].forEach((type) => {
+      Promise.all([
+        api.wallet.call(WalletApiOperation.GetPlanForOperation, {
+          type,
+          mode: "effective",
+          account: "payto://iban/DE123",
+          instructedAmount: Amounts.stringify(amount),
+        }),
+        api.wallet.call(WalletApiOperation.GetPlanForOperation, {
+          type,
+          mode: "raw",
+          account: "payto://iban/DE123",
+          instructedAmount: Amounts.stringify(amount),
+        }),
+      ]).then(([effective1, raw1]) => {
+        Promise.all([
+          api.wallet.call(WalletApiOperation.GetPlanForOperation, {
+            type,
+            mode: "raw",
+            instructedAmount: effective1.rawAmount,
+            account: "payto://iban/DE123",
+          }),
+          api.wallet.call(WalletApiOperation.GetPlanForOperation, {
+            type,
+            mode: "effective",
+            instructedAmount: raw1.effectiveAmount,
+            account: "payto://iban/DE123",
+          }),
+        ]).then(([effective2, raw2]) => {
+          setFee({
+            ...fee,
+            [type]: {
+              effective: effective1,
+              raw: raw1,
+              // effective: {
+              //   // first: effective1,
+              //   // second: effective2,
+              // },
+              // raw: {
+              //   // first: raw1,
+              //   // second: raw2,
+              // },
+            },
+          });
+        });
+      });
+    });
+  }, [amount?.value, amount?.fraction, rawMode]);
 
   //FIXME: get this information from wallet
   // eslint-disable-next-line no-constant-condition
@@ -118,6 +176,15 @@ export function useComponentState(props: Props): 
RecursiveState<State> {
       return {
         status: "ready",
         error: undefined,
+        amounts: fee,
+        mode: {
+          button: {
+            onClick: pushAlertOnError(async () => {
+              setRawMode(!rawMode);
+            }),
+          },
+          value: rawMode,
+        },
         previous,
         selectCurrency: {
           onClick: pushAlertOnError(async () => {
@@ -133,10 +200,10 @@ export function useComponentState(props: Props): 
RecursiveState<State> {
         },
         sendAll: {
           onClick:
-            total === undefined
+            info === undefined
               ? undefined
               : pushAlertOnError(async () => {
-                  setAmount(total.balanceMerchantDepositable);
+                  setAmount(info.balance.balanceMerchantDepositable);
                 }),
         },
         goToWallet: {
@@ -156,7 +223,16 @@ export function useComponentState(props: Props): 
RecursiveState<State> {
       return {
         status: "ready",
         error: undefined,
+        amounts: fee,
         previous,
+        mode: {
+          button: {
+            onClick: pushAlertOnError(async () => {
+              setRawMode(!rawMode);
+            }),
+          },
+          value: rawMode,
+        },
         selectCurrency: {
           onClick: pushAlertOnError(async () => {
             setAmount(undefined);
diff --git 
a/packages/taler-wallet-webextension/src/wallet/DestinationSelection/views.tsx 
b/packages/taler-wallet-webextension/src/wallet/DestinationSelection/views.tsx
index 47c5ffea1..2c862202a 100644
--- 
a/packages/taler-wallet-webextension/src/wallet/DestinationSelection/views.tsx
+++ 
b/packages/taler-wallet-webextension/src/wallet/DestinationSelection/views.tsx
@@ -34,6 +34,8 @@ import arrowIcon from "../../svg/chevron-down.inline.svg";
 import bankIcon from "../../svg/ri-bank-line.inline.svg";
 import { assertUnreachable } from "../../utils/index.js";
 import { Contact, State } from "./index.js";
+import { useEffect } from "preact/hooks";
+import { Checkbox } from "../../components/Checkbox.js";
 
 export function SelectCurrencyView({
   currencies,
@@ -192,6 +194,8 @@ export function ReadyView(props: State.Ready): VNode {
 export function ReadyGetView({
   amountHandler,
   goToBank,
+  amounts,
+  mode,
   goToWallet,
   selectCurrency,
   previous,
@@ -201,14 +205,22 @@ export function ReadyGetView({
   return (
     <Container>
       <h1>
-        <i18n.Translate>Specify the amount and the origin</i18n.Translate>
+        <i18n.Translate>Specify the amount and the origin2</i18n.Translate>
       </h1>
+      <pre>{JSON.stringify(amounts["withdrawal"], undefined, 2)}</pre>
       <Grid container columns={2} justifyContent="space-between">
         <AmountField
           label={i18n.str`Amount`}
           required
           handler={amountHandler}
         />
+        <Checkbox
+          label={i18n.str`Raw mode`}
+          name="rawMode"
+          enabled={mode.value!}
+          onToggle={mode.button.onClick!}
+        />
+
         <Button onClick={selectCurrency.onClick}>
           <i18n.Translate>Change currency</i18n.Translate>
         </Button>
@@ -281,6 +293,7 @@ export function ReadyGetView({
 }
 export function ReadySendView({
   amountHandler,
+  amounts,
   goToBank,
   goToWallet,
   previous,
@@ -293,6 +306,7 @@ export function ReadySendView({
       <h1>
         <i18n.Translate>Specify the amount and the destination</i18n.Translate>
       </h1>
+      <pre>{JSON.stringify(amounts["deposit"], undefined, 2)}</pre>
 
       <Grid container columns={2} justifyContent="space-between">
         <AmountField
diff --git a/packages/taler-wallet-webextension/src/wallet/ExchangeAddPage.tsx 
b/packages/taler-wallet-webextension/src/wallet/ExchangeAddPage.tsx
index d8a7c6090..9be12fb28 100644
--- a/packages/taler-wallet-webextension/src/wallet/ExchangeAddPage.tsx
+++ b/packages/taler-wallet-webextension/src/wallet/ExchangeAddPage.tsx
@@ -59,14 +59,29 @@ export function ExchangeAddPage({ currency, onBack }: 
Props): VNode {
           if (found) {
             throw Error("This exchange is already known");
           }
-          return queryToSlashKeys(url);
+          return {
+            name: "1",
+            version: "15:0:0",
+            currency: "ARS",
+          };
         }}
-        onConfirm={(url) =>
-          queryToSlashKeys<TalerConfigResponse>(url)
-            .then((config) => {
-              setVerifying({ url, config });
-            })
-            .catch((e) => e.message)
+        onConfirm={
+          async (url) => {
+            setVerifying({
+              url,
+              config: {
+                name: "1",
+                version: "15:0:0",
+                currency: "ARS",
+              },
+            });
+            return undefined;
+          }
+          // queryToSlashKeys<TalerConfigResponse>(url)
+          //   .then((config) => {
+          //     setVerifying({ url, config });
+          //   })
+          //   .catch((e) => e.message)
         }
       />
     );

-- 
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]