aboutsummaryrefslogtreecommitdiff
path: root/packages/taler-wallet-webextension/src/wallet/ExchangeSelection/state.ts
diff options
context:
space:
mode:
Diffstat (limited to 'packages/taler-wallet-webextension/src/wallet/ExchangeSelection/state.ts')
-rw-r--r--packages/taler-wallet-webextension/src/wallet/ExchangeSelection/state.ts427
1 files changed, 8 insertions, 419 deletions
diff --git a/packages/taler-wallet-webextension/src/wallet/ExchangeSelection/state.ts b/packages/taler-wallet-webextension/src/wallet/ExchangeSelection/state.ts
index 28d34578a..352952da0 100644
--- a/packages/taler-wallet-webextension/src/wallet/ExchangeSelection/state.ts
+++ b/packages/taler-wallet-webextension/src/wallet/ExchangeSelection/state.ts
@@ -15,11 +15,12 @@
*/
-import { AbsoluteTime, AmountJson, Amounts, DenominationInfo, TalerProtocolTimestamp } from "@gnu-taler/taler-util";
+import { FeeDescription, OperationMap } from "@gnu-taler/taler-util";
+import { createDenominationPairTimeline } from "@gnu-taler/taler-wallet-core";
import { useState } from "preact/hooks";
import { useAsyncAsHook } from "../../hooks/useAsyncAsHook.js";
import * as wxApi from "../../wxApi.js";
-import { FeeDescription, FeeDescriptionPair, OperationMap, Props, State } from "./index.js";
+import { Props, State } from "./index.js";
export function useComponentState(
{ onCancel, onSelection, currency }: Props,
@@ -63,46 +64,6 @@ export function useComponentState(
}
}
- let nextFeeUpdate = TalerProtocolTimestamp.never();
-
- nextFeeUpdate = Object.values(selected.wireInfo.feesForType).reduce(
- (prev, cur) => {
- return cur.reduce((p, c) => nearestTimestamp(p, c.endStamp), prev);
- },
- nextFeeUpdate,
- );
-
- nextFeeUpdate = selected.denominations.reduce((prev, cur) => {
- return [
- cur.stampExpireWithdraw,
- cur.stampExpireLegal,
- cur.stampExpireDeposit,
- ].reduce(nearestTimestamp, prev);
- }, nextFeeUpdate);
-
- const timeline: OperationMap<FeeDescription[]> = {
- deposit: createDenominationTimeline(
- selected.denominations,
- "stampExpireDeposit",
- "feeDeposit",
- ),
- refresh: createDenominationTimeline(
- selected.denominations,
- "stampExpireWithdraw",
- "feeRefresh",
- ),
- refund: createDenominationTimeline(
- selected.denominations,
- "stampExpireWithdraw",
- "feeRefund",
- ),
- withdraw: createDenominationTimeline(
- selected.denominations,
- "stampExpireWithdraw",
- "feeWithdraw",
- ),
- };
-
const exchangeMap = exchanges.reduce((prev, cur, idx) => ({ ...prev, [cur.exchangeBaseUrl]: String(idx) }), {} as Record<string, string>)
if (!original) {
@@ -117,42 +78,19 @@ export function useComponentState(
}
},
error: undefined,
- nextFeeUpdate: AbsoluteTime.fromTimestamp(nextFeeUpdate),
onClose: {
onClick: onCancel
},
selected,
- timeline
+ timeline: selected.feesDescription
}
}
- const originalTimeline: OperationMap<FeeDescription[]> = {
- deposit: createDenominationTimeline(
- original.denominations,
- "stampExpireDeposit",
- "feeDeposit",
- ),
- refresh: createDenominationTimeline(
- original.denominations,
- "stampExpireWithdraw",
- "feeRefresh",
- ),
- refund: createDenominationTimeline(
- original.denominations,
- "stampExpireWithdraw",
- "feeRefund",
- ),
- withdraw: createDenominationTimeline(
- original.denominations,
- "stampExpireWithdraw",
- "feeWithdraw",
- ),
- };
const pairTimeline: OperationMap<FeeDescription[]> = {
- deposit: createDenominationPairTimeline(timeline.deposit, originalTimeline.deposit),
- refresh: createDenominationPairTimeline(timeline.refresh, originalTimeline.refresh),
- refund: createDenominationPairTimeline(timeline.refund, originalTimeline.refund),
- withdraw: createDenominationPairTimeline(timeline.withdraw, originalTimeline.withdraw),
+ deposit: createDenominationPairTimeline(selected.feesDescription.deposit, original.feesDescription.deposit),
+ refresh: createDenominationPairTimeline(selected.feesDescription.refresh, original.feesDescription.refresh),
+ refund: createDenominationPairTimeline(selected.feesDescription.refund, original.feesDescription.refund),
+ withdraw: createDenominationPairTimeline(selected.feesDescription.withdraw, original.feesDescription.withdraw),
}
return {
@@ -165,7 +103,6 @@ export function useComponentState(
}
},
error: undefined,
- nextFeeUpdate: AbsoluteTime.fromTimestamp(nextFeeUpdate),
onReset: {
onClick: async () => {
setValue(String(initialValue))
@@ -182,351 +119,3 @@ export function useComponentState(
}
-function nearestTimestamp(
- first: TalerProtocolTimestamp,
- second: TalerProtocolTimestamp,
-): TalerProtocolTimestamp {
- const f = AbsoluteTime.fromTimestamp(first);
- const s = AbsoluteTime.fromTimestamp(second);
- const a = AbsoluteTime.min(f, s);
- return AbsoluteTime.toTimestamp(a);
-}
-
-
-
-export interface TimePoint {
- type: "start" | "end";
- moment: AbsoluteTime;
- denom: DenominationInfo;
-}
-
-/**
- * Given a list of denominations with the same value and same period of time:
- * return the one that will be used.
- * The best denomination is the one that will minimize the fee cost.
- *
- * @param list denominations of same value
- * @returns
- */
-function selectBestForOverlappingDenominations(
- list: DenominationInfo[],
-): DenominationInfo | undefined {
- let minDeposit: DenominationInfo | undefined = undefined;
- list.forEach((e) => {
- if (minDeposit === undefined) {
- minDeposit = e;
- return;
- }
- if (Amounts.cmp(minDeposit.feeDeposit, e.feeDeposit) > -1) {
- minDeposit = e;
- }
- });
- return minDeposit;
-}
-
-type PropsWithReturnType<T extends object, F> = Exclude<
- {
- [K in keyof T]: T[K] extends F ? K : never;
- }[keyof T],
- undefined
->;
-
-/**
- * Takes two list and create one with one timeline.
- * For any element in the position "p" on the left or right "list", then
- * list[p].until should be equal to list[p+1].from
- *
- * @see {createDenominationTimeline}
- *
- * @param left list denominations @type {FeeDescription}
- * @param right list denominations @type {FeeDescription}
- * @returns list of pairs for the same time
- */
-export function createDenominationPairTimeline(left: FeeDescription[], right: FeeDescription[]): FeeDescriptionPair[] {
- //both list empty, discarded
- if (left.length === 0 && right.length === 0) return [];
-
- const pairList: FeeDescriptionPair[] = [];
-
- let li = 0;
- let ri = 0;
-
- while (li < left.length && ri < right.length) {
- const currentValue = Amounts.cmp(left[li].value, right[ri].value) < 0 ? left[li].value : right[ri].value;
-
- let ll = 0 //left length (until next value)
- while (li + ll < left.length && Amounts.cmp(left[li + ll].value, currentValue) === 0) {
- ll++
- }
- let rl = 0 //right length (until next value)
- while (ri + rl < right.length && Amounts.cmp(right[ri + rl].value, currentValue) === 0) {
- rl++
- }
- const leftIsEmpty = ll === 0
- const rightIsEmpty = rl === 0
- //check which start after, add gap so both list starts at the same time
- // one list may be empty
- const leftStarts: AbsoluteTime =
- leftIsEmpty ? { t_ms: "never" } : left[li].from;
- const rightStarts: AbsoluteTime =
- rightIsEmpty ? { t_ms: "never" } : right[ri].from;
-
- //first time cut is the smallest time
- let timeCut: AbsoluteTime = leftStarts;
-
- if (AbsoluteTime.cmp(leftStarts, rightStarts) < 0) {
- const ends =
- rightIsEmpty ? left[li + ll - 1].until : right[0].from;
-
- right.splice(ri, 0, {
- from: leftStarts,
- until: ends,
- value: left[li].value,
- });
- rl++;
-
- timeCut = leftStarts
- }
- if (AbsoluteTime.cmp(leftStarts, rightStarts) > 0) {
- const ends =
- leftIsEmpty ? right[ri + rl - 1].until : left[0].from;
-
- left.splice(li, 0, {
- from: rightStarts,
- until: ends,
- value: right[ri].value,
- });
- ll++;
-
- timeCut = rightStarts
- }
-
- //check which ends sooner, add gap so both list ends at the same time
- // here both list are non empty
- const leftEnds: AbsoluteTime = left[li + ll - 1].until;
- const rightEnds: AbsoluteTime = right[ri + rl - 1].until;
-
- if (AbsoluteTime.cmp(leftEnds, rightEnds) > 0) {
- right.splice(ri + rl, 0, {
- from: rightEnds,
- until: leftEnds,
- value: left[0].value,
- });
- rl++;
-
- }
- if (AbsoluteTime.cmp(leftEnds, rightEnds) < 0) {
- left.splice(li + ll, 0, {
- from: leftEnds,
- until: rightEnds,
- value: right[0].value,
- });
- ll++;
- }
-
- //now both lists are non empty and (starts,ends) at the same time
- while (li < left.length && ri < right.length && Amounts.cmp(left[li].value, right[ri].value) === 0) {
-
- if (AbsoluteTime.cmp(left[li].from, timeCut) !== 0 && AbsoluteTime.cmp(right[ri].from, timeCut) !== 0) {
- // timeCut comes from the latest "until" (expiration from the previous)
- // and this value comes from the latest left or right
- // it should be the same as the "from" from one of the latest left or right
- // otherwise it means that there is missing a gap object in the middle
- // the list is not complete and the behavior is undefined
- throw Error('one of the list is not completed: list[i].until !== list[i+1].from')
- }
-
- pairList.push({
- left: left[li].fee,
- right: right[ri].fee,
- from: timeCut,
- until: AbsoluteTime.never(),
- value: currentValue,
- });
-
- if (left[li].until.t_ms === right[ri].until.t_ms) {
- timeCut = left[li].until;
- ri++;
- li++;
- } else if (left[li].until.t_ms < right[ri].until.t_ms) {
- timeCut = left[li].until;
- li++;
- } else if (left[li].until.t_ms > right[ri].until.t_ms) {
- timeCut = right[ri].until;
- ri++;
- }
- pairList[pairList.length - 1].until = timeCut
-
- if (li < left.length && Amounts.cmp(left[li].value, pairList[pairList.length - 1].value) !== 0) {
- //value changed, should break
- //this if will catch when both (left and right) change at the same time
- //if just one side changed it will catch in the while condition
- break;
- }
-
- }
-
- }
- //one of the list left or right can still have elements
- if (li < left.length) {
- let timeCut = pairList.length > 0 && Amounts.cmp(pairList[pairList.length - 1].value, left[li].value) === 0 ? pairList[pairList.length - 1].until : left[li].from;
- while (li < left.length) {
- pairList.push({
- left: left[li].fee,
- right: undefined,
- from: timeCut,
- until: left[li].until,
- value: left[li].value,
- })
- timeCut = left[li].until
- li++;
- }
- }
- if (ri < right.length) {
- let timeCut = pairList.length > 0 && Amounts.cmp(pairList[pairList.length - 1].value, right[ri].value) === 0 ? pairList[pairList.length - 1].until : right[ri].from;
- while (ri < right.length) {
- pairList.push({
- right: right[ri].fee,
- left: undefined,
- from: timeCut,
- until: right[ri].until,
- value: right[ri].value,
- })
- timeCut = right[ri].until
- ri++;
- }
- }
- return pairList
-}
-
-/**
- * Create a usage timeline with the denominations given.
- *
- * If there are multiple denominations that can be used, the list will
- * contain the one that minimize the fee cost. @see selectBestForOverlappingDenominations
- *
- * @param list list of denominations
- * @param periodProp property of element of the list that will be used as end of the usage period
- * @param feeProp property of the element of the list that will be used as fee reference
- * @returns list of @type {FeeDescription} sorted by usage period
- */
-export function createDenominationTimeline(
- list: DenominationInfo[],
- periodProp: PropsWithReturnType<DenominationInfo, TalerProtocolTimestamp>,
- feeProp: PropsWithReturnType<DenominationInfo, AmountJson>,
-): FeeDescription[] {
- const points = list
- .reduce((ps, denom) => {
- //exclude denoms with bad configuration
- if (denom.stampStart.t_s >= denom[periodProp].t_s) {
- throw Error(`denom ${denom.denomPubHash} has start after the end`);
- // return ps;
- }
- ps.push({
- type: "start",
- moment: AbsoluteTime.fromTimestamp(denom.stampStart),
- denom,
- });
- ps.push({
- type: "end",
- moment: AbsoluteTime.fromTimestamp(denom[periodProp]),
- denom,
- });
- return ps;
- }, [] as TimePoint[])
- .sort((a, b) => {
- const v = Amounts.cmp(a.denom.value, b.denom.value);
- if (v != 0) return v;
- const t = AbsoluteTime.cmp(a.moment, b.moment);
- if (t != 0) return t;
- if (a.type === b.type) return 0;
- return a.type === "start" ? 1 : -1;
- });
-
- const activeAtTheSameTime: DenominationInfo[] = [];
- return points.reduce((result, cursor, idx) => {
- const hash = cursor.denom.denomPubHash;
- if (!hash)
- throw Error(
- `denomination without hash ${JSON.stringify(
- cursor.denom,
- undefined,
- 2,
- )}`,
- );
-
- let prev = result.length > 0 ? result[result.length - 1] : undefined;
- const prevHasSameValue =
- prev && Amounts.cmp(prev.value, cursor.denom.value) === 0;
- if (prev) {
- if (prevHasSameValue) {
- prev.until = cursor.moment;
-
- if (prev.from.t_ms === prev.until.t_ms) {
- result.pop();
- prev = result[result.length - 1];
- }
- } else {
- // the last end adds a gap that we have to remove
- result.pop();
- }
- }
-
- //update the activeAtTheSameTime list
- if (cursor.type === "end") {
- const loc = activeAtTheSameTime.findIndex((v) => v.denomPubHash === hash);
- if (loc === -1) {
- throw Error(`denomination ${hash} has an end but no start`);
- }
- activeAtTheSameTime.splice(loc, 1);
- } else if (cursor.type === "start") {
- activeAtTheSameTime.push(cursor.denom);
- } else {
- const exhaustiveCheck: never = cursor.type;
- throw new Error(`not TimePoint defined for type: ${exhaustiveCheck}`);
- }
-
- if (idx == points.length - 1) {
- //this is the last element in the list, prevent adding
- //a gap in the end
- if (cursor.type !== "end") {
- throw Error(
- `denomination ${hash} starts after ending or doesn't have an ending`,
- );
- }
- if (activeAtTheSameTime.length > 0) {
- throw Error(
- `there are ${activeAtTheSameTime.length} denominations without ending`,
- );
- }
- return result;
- }
-
- const current = selectBestForOverlappingDenominations(activeAtTheSameTime);
-
- if (current) {
- if (
- prev === undefined || //is the first
- !prev.fee || //is a gap
- Amounts.cmp(prev.fee, current[feeProp]) !== 0 // prev has the same fee
- ) {
- result.push({
- value: cursor.denom.value,
- from: cursor.moment,
- until: AbsoluteTime.never(), //not yet known
- fee: current[feeProp],
- });
- } else {
- prev.until = cursor.moment;
- }
- } else {
- result.push({
- value: cursor.denom.value,
- from: cursor.moment,
- until: AbsoluteTime.never(), //not yet known
- });
- }
-
- return result;
- }, [] as FeeDescription[]);
-}