aboutsummaryrefslogtreecommitdiff
path: root/packages/taler-wallet-core/src/util/denominations.ts
diff options
context:
space:
mode:
authorFlorian Dold <florian@dold.me>2024-02-21 13:01:23 +0100
committerFlorian Dold <florian@dold.me>2024-02-21 13:01:23 +0100
commit612b85c18fc17af412d08e075e1fddaa67aa7bf0 (patch)
tree2209cb052c94f70145d33271b7711e39728ad72b /packages/taler-wallet-core/src/util/denominations.ts
parent06635c195816121ed7d90cf7bd3834850b674564 (diff)
downloadwallet-core-612b85c18fc17af412d08e075e1fddaa67aa7bf0.tar.xz
move helpers to util
Diffstat (limited to 'packages/taler-wallet-core/src/util/denominations.ts')
-rw-r--r--packages/taler-wallet-core/src/util/denominations.ts477
1 files changed, 0 insertions, 477 deletions
diff --git a/packages/taler-wallet-core/src/util/denominations.ts b/packages/taler-wallet-core/src/util/denominations.ts
deleted file mode 100644
index 9557c078a..000000000
--- a/packages/taler-wallet-core/src/util/denominations.ts
+++ /dev/null
@@ -1,477 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2021 Taler Systems S.A.
-
- GNU Taler is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
-import {
- AbsoluteTime,
- AmountJson,
- Amounts,
- AmountString,
- DenominationInfo,
- Duration,
- durationFromSpec,
- FeeDescription,
- FeeDescriptionPair,
- TalerProtocolTimestamp,
- TimePoint,
-} from "@gnu-taler/taler-util";
-import { DenominationRecord, timestampProtocolFromDb } from "../db.js";
-
-/**
- * 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
- */
-export function selectBestForOverlappingDenominations<
- T extends DenominationInfo,
->(list: T[]): T | undefined {
- let minDeposit: DenominationInfo | undefined = undefined;
- //TODO: improve denomination selection, this is a trivial implementation
- list.forEach((e) => {
- if (minDeposit === undefined) {
- minDeposit = e;
- return;
- }
- if (Amounts.cmp(minDeposit.feeDeposit, e.feeDeposit) > -1) {
- minDeposit = e;
- }
- });
- return minDeposit;
-}
-
-export function selectMinimumFee<T extends { fee: AmountString }>(
- list: T[],
-): T | undefined {
- let minFee: T | undefined = undefined;
- //TODO: improve denomination selection, this is a trivial implementation
- list.forEach((e) => {
- if (minFee === undefined) {
- minFee = e;
- return;
- }
- if (Amounts.cmp(minFee.fee, e.fee) > -1) {
- minFee = e;
- }
- });
- return minFee;
-}
-
-type PropsWithReturnType<T extends object, F> = Exclude<
- {
- [K in keyof T]: T[K] extends F ? K : never;
- }[keyof T],
- undefined
->;
-
-/**
- * Takes two timelines and create one to compare them.
- *
- * For both lists the next condition should be true:
- * for any element in the position "idx" then
- * list[idx].until === list[idx+1].from
- *
- * @see {createTimeline}
- *
- * @param left list denominations @type {FeeDescription}
- * @param right list denominations @type {FeeDescription}
- * @returns list of pairs for the same time
- */
-export function createPairTimeline(
- left: FeeDescription[],
- right: FeeDescription[],
-): FeeDescriptionPair[] {
- //FIXME: we need to create a copy of the array because
- //this algorithm is using splice, remove splice and
- //remove this array duplication
- left = [...left];
- right = [...right];
-
- //both list empty, discarded
- if (left.length === 0 && right.length === 0) return [];
-
- const pairList: FeeDescriptionPair[] = [];
-
- let li = 0; //left list index
- let ri = 0; //right list index
-
- while (li < left.length && ri < right.length) {
- const currentGroup =
- Number.parseFloat(left[li].group) < Number.parseFloat(right[ri].group)
- ? left[li].group
- : right[ri].group;
- const lgs = li; //left group start index
- const rgs = ri; //right group start index
-
- let lgl = 0; //left group length (until next value)
- while (li + lgl < left.length && left[li + lgl].group === currentGroup) {
- lgl++;
- }
- let rgl = 0; //right group length (until next value)
- while (ri + rgl < right.length && right[ri + rgl].group === currentGroup) {
- rgl++;
- }
- const leftGroupIsEmpty = lgl === 0;
- const rightGroupIsEmpty = rgl === 0;
- //check which start after, add gap so both list starts at the same time
- // one list may be empty
- const leftStartTime: AbsoluteTime = leftGroupIsEmpty
- ? AbsoluteTime.never()
- : left[li].from;
- const rightStartTime: AbsoluteTime = rightGroupIsEmpty
- ? AbsoluteTime.never()
- : right[ri].from;
-
- //first time cut is the smallest time
- let timeCut: AbsoluteTime = leftStartTime;
-
- if (AbsoluteTime.cmp(leftStartTime, rightStartTime) < 0) {
- const ends = rightGroupIsEmpty ? left[li + lgl - 1].until : right[0].from;
-
- right.splice(ri, 0, {
- from: leftStartTime,
- until: ends,
- group: left[li].group,
- });
- rgl++;
-
- timeCut = leftStartTime;
- }
- if (AbsoluteTime.cmp(leftStartTime, rightStartTime) > 0) {
- const ends = leftGroupIsEmpty ? right[ri + rgl - 1].until : left[0].from;
-
- left.splice(li, 0, {
- from: rightStartTime,
- until: ends,
- group: right[ri].group,
- });
- lgl++;
-
- timeCut = rightStartTime;
- }
-
- //check which ends sooner, add gap so both list ends at the same time
- // here both list are non empty
- const leftEndTime: AbsoluteTime = left[li + lgl - 1].until;
- const rightEndTime: AbsoluteTime = right[ri + rgl - 1].until;
-
- if (AbsoluteTime.cmp(leftEndTime, rightEndTime) > 0) {
- right.splice(ri + rgl, 0, {
- from: rightEndTime,
- until: leftEndTime,
- group: left[0].group,
- });
- rgl++;
- }
- if (AbsoluteTime.cmp(leftEndTime, rightEndTime) < 0) {
- left.splice(li + lgl, 0, {
- from: leftEndTime,
- until: rightEndTime,
- group: right[0].group,
- });
- lgl++;
- }
-
- //now both lists are non empty and (starts,ends) at the same time
- while (li < lgs + lgl && ri < rgs + rgl) {
- 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(),
- group: currentGroup,
- });
-
- 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 && left[li].group !== currentGroup) ||
- // (ri < right.length && right[ri].group !== currentGroup)
- // ) {
- // //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 &&
- pairList[pairList.length - 1].group === left[li].group
- ? 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,
- group: left[li].group,
- });
- timeCut = left[li].until;
- li++;
- }
- }
- if (ri < right.length) {
- let timeCut =
- pairList.length > 0 &&
- pairList[pairList.length - 1].group === right[ri].group
- ? 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,
- group: right[ri].group,
- });
- timeCut = right[ri].until;
- ri++;
- }
- }
- return pairList;
-}
-
-/**
- * Create a usage timeline with the entity given.
- *
- * If there are multiple entities that can be used in the same period,
- * the list will contain the one that minimize the fee cost.
- * @see selectBestForOverlappingDenominations
- *
- * @param list list of entities
- * @param idProp property used for identification
- * @param periodStartProp property of element of the list that will be used as start of the usage period
- * @param periodEndProp 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
- * @param groupProp property of the element of the list that will be used for grouping
- * @returns list of @type {FeeDescription} sorted by usage period
- */
-export function createTimeline<Type extends object>(
- list: Type[],
- idProp: PropsWithReturnType<Type, string>,
- periodStartProp: PropsWithReturnType<Type, TalerProtocolTimestamp>,
- periodEndProp: PropsWithReturnType<Type, TalerProtocolTimestamp>,
- feeProp: PropsWithReturnType<Type, AmountString>,
- groupProp: PropsWithReturnType<Type, string> | undefined,
- selectBestForOverlapping: (l: Type[]) => Type | undefined,
-): FeeDescription[] {
- /**
- * First we create a list with with point in the timeline sorted
- * by time and categorized by starting or ending.
- */
- const sortedPointsInTime = list
- .reduce((ps, denom) => {
- //exclude denoms with bad configuration
- const id = denom[idProp] as string;
- const stampStart = denom[periodStartProp] as TalerProtocolTimestamp;
- const stampEnd = denom[periodEndProp] as TalerProtocolTimestamp;
- const fee = denom[feeProp] as AmountJson;
- const group = !groupProp ? "" : (denom[groupProp] as string);
-
- if (!id) {
- throw Error(
- `denomination without hash ${JSON.stringify(denom, undefined, 2)}`,
- );
- }
- if (stampStart.t_s >= stampEnd.t_s) {
- throw Error(`denom ${id} has start after the end`);
- }
- ps.push({
- type: "start",
- fee: Amounts.stringify(fee),
- group,
- id,
- moment: AbsoluteTime.fromProtocolTimestamp(stampStart),
- denom,
- });
- ps.push({
- type: "end",
- fee: Amounts.stringify(fee),
- group,
- id,
- moment: AbsoluteTime.fromProtocolTimestamp(stampEnd),
- denom,
- });
- return ps;
- }, [] as TimePoint<Type>[])
- .sort((a, b) => {
- const v = a.group == b.group ? 0 : a.group > b.group ? 1 : -1;
- 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: Type[] = [];
- return sortedPointsInTime.reduce((result, cursor, idx) => {
- /**
- * Now that we have move one step forward, we should
- * update the previous element ending period with the
- * current start time.
- */
- let prev = result.length > 0 ? result[result.length - 1] : undefined;
- const prevHasSameValue = prev && prev.group == cursor.group;
- 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();
- }
- }
-
- /**
- * With the current moment in the iteration we
- * should keep updated which entities are current
- * active in this period of time.
- */
- if (cursor.type === "end") {
- const loc = activeAtTheSameTime.findIndex((v) => v[idProp] === cursor.id);
- if (loc === -1) {
- throw Error(`denomination ${cursor.id} 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 == sortedPointsInTime.length - 1) {
- /**
- * This is the last element in the list, if we continue
- * a gap will normally be added which is not necessary.
- * Also, the last element should be ending and the list of active
- * element should be empty
- */
- if (cursor.type !== "end") {
- throw Error(
- `denomination ${cursor.id} 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 = selectBestForOverlapping(activeAtTheSameTime);
-
- if (current) {
- /**
- * We have a candidate to add in the list, check that we are
- * not adding a duplicate.
- * Next element in the list will defined the ending.
- */
- const currentFee = current[feeProp] as AmountJson;
- if (
- prev === undefined || //is the first
- !prev.fee || //is a gap
- Amounts.cmp(prev.fee, currentFee) !== 0 // prev has different fee
- ) {
- result.push({
- group: cursor.group,
- from: cursor.moment,
- until: AbsoluteTime.never(), //not yet known
- fee: Amounts.stringify(currentFee),
- });
- } else {
- prev.until = cursor.moment;
- }
- } else {
- /**
- * No active element in this period of time, so we add a gap (no fee)
- * Next element in the list will defined the ending.
- */
- result.push({
- group: cursor.group,
- from: cursor.moment,
- until: AbsoluteTime.never(), //not yet known
- });
- }
-
- return result;
- }, [] as FeeDescription[]);
-}
-
-/**
- * Check if a denom is withdrawable based on the expiration time,
- * revocation and offered state.
- */
-export function isWithdrawableDenom(
- d: DenominationRecord,
- denomselAllowLate?: boolean,
-): boolean {
- const now = AbsoluteTime.now();
- const start = AbsoluteTime.fromProtocolTimestamp(
- timestampProtocolFromDb(d.stampStart),
- );
- const withdrawExpire = AbsoluteTime.fromProtocolTimestamp(
- timestampProtocolFromDb(d.stampExpireWithdraw),
- );
- const started = AbsoluteTime.cmp(now, start) >= 0;
- let lastPossibleWithdraw: AbsoluteTime;
- if (denomselAllowLate) {
- lastPossibleWithdraw = start;
- } else {
- lastPossibleWithdraw = AbsoluteTime.subtractDuraction(
- withdrawExpire,
- durationFromSpec({ minutes: 5 }),
- );
- }
- const remaining = Duration.getRemaining(lastPossibleWithdraw, now);
- const stillOkay = remaining.d_ms !== 0;
- return started && stillOkay && !d.isRevoked && d.isOffered;
-}