From a0d746ad8d80490f9c2f1e017ff0c6a56b7d435c Mon Sep 17 00:00:00 2001 From: Florian Dold Date: Thu, 23 Nov 2023 12:57:18 +0100 Subject: wallet-core: implement balance flags for UI badges --- packages/taler-util/src/wallet-types.ts | 17 ++- packages/taler-wallet-core/src/db.ts | 14 +++ .../taler-wallet-core/src/operations/balance.ts | 120 +++++++++++++++------ .../taler-wallet-core/src/operations/pending.ts | 23 ++-- 4 files changed, 123 insertions(+), 51 deletions(-) diff --git a/packages/taler-util/src/wallet-types.ts b/packages/taler-util/src/wallet-types.ts index 346528029..ba6f53cca 100644 --- a/packages/taler-util/src/wallet-types.ts +++ b/packages/taler-util/src/wallet-types.ts @@ -374,19 +374,31 @@ export const codecForAmountResponse = (): Codec => .property("rawAmount", codecForAmountString()) .build("AmountResponse"); +export enum BalanceFlag { + IncomingKyc = "incoming-kyc", + IncomingAml = "incoming-aml", + IncomingConfirmation = "incoming-confirmation", + OutgoingKyc = "outgoing-kyc", +} export interface WalletBalance { scopeInfo: ScopeInfo; available: AmountString; pendingIncoming: AmountString; pendingOutgoing: AmountString; - // Does the balance for this currency have a pending - // transaction? + /** + * Does the balance for this currency have a pending + * transaction? + * + * FIXME: Represent as a flag! + */ hasPendingTransactions: boolean; // Is there a pending transaction that would affect the balance // and requires user input? requiresUserInput: boolean; + + flags: BalanceFlag[]; } export const codecForScopeInfoGlobal = (): Codec => @@ -481,6 +493,7 @@ export const codecForBalance = (): Codec => .property("pendingIncoming", codecForAmountString()) .property("pendingOutgoing", codecForAmountString()) .property("requiresUserInput", codecForBoolean()) + .property("flags", codecForAny()) // FIXME .build("Balance"); export const codecForBalancesResponse = (): Codec => diff --git a/packages/taler-wallet-core/src/db.ts b/packages/taler-wallet-core/src/db.ts index 0cafae2a1..279b75bf5 100644 --- a/packages/taler-wallet-core/src/db.ts +++ b/packages/taler-wallet-core/src/db.ts @@ -19,6 +19,7 @@ */ import { Event, + GlobalIDB, IDBDatabase, IDBFactory, IDBObjectStore, @@ -325,6 +326,14 @@ export enum WithdrawalGroupStatus { AbortedBank = 0x0503_0002, } +/** + * Status range of nonfinal withdrawal groups. + */ +export const withdrawalGroupNonfinalRange = GlobalIDB.KeyRange.bound( + WithdrawalGroupStatus.PendingRegisteringBank, + WithdrawalGroupStatus.PendingAml, +); + /** * Extra info about a withdrawal that is used * with a bank-integrated withdrawal. @@ -1686,6 +1695,11 @@ export enum DepositOperationStatus { Aborted = 0x0503_0000, } +export const depositOperationNonfinalStatusRange = GlobalIDB.KeyRange.bound( + DepositOperationStatus.PendingDeposit, + DepositOperationStatus.PendingKyc, +); + export interface DepositTrackingInfo { // Raw wire transfer identifier of the deposit. wireTransferId: string; diff --git a/packages/taler-wallet-core/src/operations/balance.ts b/packages/taler-wallet-core/src/operations/balance.ts index 8034f78ea..6d396f6dd 100644 --- a/packages/taler-wallet-core/src/operations/balance.ts +++ b/packages/taler-wallet-core/src/operations/balance.ts @@ -54,6 +54,7 @@ import { AllowedExchangeInfo, AmountJson, Amounts, + BalanceFlag, BalancesResponse, canonicalizeBaseUrl, GetBalanceDetailRequest, @@ -62,8 +63,11 @@ import { ScopeType, } from "@gnu-taler/taler-util"; import { + depositOperationNonfinalStatusRange, + DepositOperationStatus, RefreshGroupRecord, WalletStoresV1, + withdrawalGroupNonfinalRange, WithdrawalGroupStatus, } from "../db.js"; import { InternalWalletState } from "../internal-wallet-state.js"; @@ -81,6 +85,10 @@ interface WalletBalance { available: AmountJson; pendingIncoming: AmountJson; pendingOutgoing: AmountJson; + flagIncomingKyc: boolean; + flagIncomingAml: boolean; + flagIncomingConfirmation: boolean; + flagOutgoingKyc: boolean; } /** @@ -109,6 +117,7 @@ export async function getBalancesInsideTransaction( coinAvailability: typeof WalletStoresV1.coinAvailability; refreshGroups: typeof WalletStoresV1.refreshGroups; withdrawalGroups: typeof WalletStoresV1.withdrawalGroups; + depositGroups: typeof WalletStoresV1.depositGroups; }>, ): Promise { const balanceStore: Record = {}; @@ -124,6 +133,10 @@ export async function getBalancesInsideTransaction( available: Amounts.zeroOfCurrency(currency), pendingIncoming: Amounts.zeroOfCurrency(currency), pendingOutgoing: Amounts.zeroOfCurrency(currency), + flagIncomingAml: false, + flagIncomingConfirmation: false, + flagIncomingKyc: false, + flagOutgoingKyc: false, }; } return balanceStore[currency]; @@ -145,42 +158,65 @@ export async function getBalancesInsideTransaction( ).amount; }); + await tx.withdrawalGroups.indexes.byStatus + .iter(withdrawalGroupNonfinalRange) + .forEach((wgRecord) => { + const b = initBalance( + Amounts.currencyOf(wgRecord.denomsSel.totalWithdrawCost), + ); + switch (wgRecord.status) { + case WithdrawalGroupStatus.AbortedBank: + case WithdrawalGroupStatus.AbortedExchange: + case WithdrawalGroupStatus.FailedAbortingBank: + case WithdrawalGroupStatus.FailedBankAborted: + case WithdrawalGroupStatus.Done: + // Does not count as pendingIncoming + return; + case WithdrawalGroupStatus.PendingReady: + case WithdrawalGroupStatus.AbortingBank: + case WithdrawalGroupStatus.PendingAml: + b.flagIncomingAml = true; + break; + case WithdrawalGroupStatus.PendingKyc: + b.flagIncomingKyc = true; + break; + case WithdrawalGroupStatus.PendingQueryingStatus: + case WithdrawalGroupStatus.SuspendedWaitConfirmBank: + case WithdrawalGroupStatus.SuspendedReady: + case WithdrawalGroupStatus.SuspendedRegisteringBank: + case WithdrawalGroupStatus.SuspendedKyc: + b.flagIncomingKyc = true; + break; + case WithdrawalGroupStatus.SuspendedAbortingBank: + case WithdrawalGroupStatus.SuspendedAml: + b.flagIncomingAml = true; + break; + case WithdrawalGroupStatus.PendingRegisteringBank: + case WithdrawalGroupStatus.PendingWaitConfirmBank: + b.flagIncomingConfirmation = true; + break; + case WithdrawalGroupStatus.SuspendedQueryingStatus: + break; + default: + assertUnreachable(wgRecord.status); + } + b.pendingIncoming = Amounts.add( + b.pendingIncoming, + wgRecord.denomsSel.totalCoinValue, + ).amount; + }); + // FIXME: Use indexing to filter out final transactions. - await tx.withdrawalGroups.iter().forEach((wgRecord) => { - switch (wgRecord.status) { - case WithdrawalGroupStatus.AbortedBank: - case WithdrawalGroupStatus.AbortedExchange: - case WithdrawalGroupStatus.FailedAbortingBank: - case WithdrawalGroupStatus.FailedBankAborted: - case WithdrawalGroupStatus.Done: - // Does not count as pendingIncoming - return; - case WithdrawalGroupStatus.PendingReady: - case WithdrawalGroupStatus.AbortingBank: - case WithdrawalGroupStatus.PendingAml: - case WithdrawalGroupStatus.PendingKyc: - case WithdrawalGroupStatus.PendingQueryingStatus: - case WithdrawalGroupStatus.SuspendedWaitConfirmBank: - case WithdrawalGroupStatus.SuspendedReady: - case WithdrawalGroupStatus.SuspendedRegisteringBank: - case WithdrawalGroupStatus.SuspendedKyc: - case WithdrawalGroupStatus.SuspendedAbortingBank: - case WithdrawalGroupStatus.SuspendedAml: - case WithdrawalGroupStatus.PendingRegisteringBank: - case WithdrawalGroupStatus.PendingWaitConfirmBank: - case WithdrawalGroupStatus.SuspendedQueryingStatus: - break; - default: - assertUnreachable(wgRecord.status); - } - const b = initBalance( - Amounts.currencyOf(wgRecord.denomsSel.totalWithdrawCost), - ); - b.pendingIncoming = Amounts.add( - b.pendingIncoming, - wgRecord.denomsSel.totalCoinValue, - ).amount; - }); + await tx.depositGroups.indexes.byStatus + .iter(depositOperationNonfinalStatusRange) + .forEach((dgRecord) => { + const b = initBalance(Amounts.currencyOf(dgRecord.amount)); + switch (dgRecord.operationStatus) { + case DepositOperationStatus.SuspendedKyc: + case DepositOperationStatus.PendingKyc: + b.flagOutgoingKyc = true; + } + }); const balancesResponse: BalancesResponse = { balances: [], @@ -190,6 +226,16 @@ export async function getBalancesInsideTransaction( .sort() .forEach((c) => { const v = balanceStore[c]; + const flags: BalanceFlag[] = []; + if (v.flagIncomingAml) { + flags.push(BalanceFlag.IncomingAml); + } + if (v.flagIncomingKyc) { + flags.push(BalanceFlag.IncomingKyc); + } + if (v.flagIncomingConfirmation) { + flags.push(BalanceFlag.IncomingConfirmation); + } balancesResponse.balances.push({ scopeInfo: { type: ScopeType.Global, @@ -198,8 +244,11 @@ export async function getBalancesInsideTransaction( available: Amounts.stringify(v.available), pendingIncoming: Amounts.stringify(v.pendingIncoming), pendingOutgoing: Amounts.stringify(v.pendingOutgoing), + // FIXME: This field is basically not implemented, do we even need it? hasPendingTransactions: false, + // FIXME: This field is basically not implemented, do we even need it? requiresUserInput: false, + flags, }); }); @@ -221,6 +270,7 @@ export async function getBalances( x.refreshGroups, x.purchases, x.withdrawalGroups, + x.depositGroups, ]) .runReadOnly(async (tx) => { return getBalancesInsideTransaction(ws, tx); diff --git a/packages/taler-wallet-core/src/operations/pending.ts b/packages/taler-wallet-core/src/operations/pending.ts index 7641dcf33..282f84ad7 100644 --- a/packages/taler-wallet-core/src/operations/pending.ts +++ b/packages/taler-wallet-core/src/operations/pending.ts @@ -49,10 +49,12 @@ import { WalletStoresV1, WithdrawalGroupRecord, WithdrawalGroupStatus, + depositOperationNonfinalStatusRange, timestampAbsoluteFromDb, timestampOptionalAbsoluteFromDb, timestampPreciseFromDb, timestampPreciseToDb, + withdrawalGroupNonfinalRange, } from "../db.js"; import { InternalWalletState } from "../internal-wallet-state.js"; import { @@ -205,12 +207,8 @@ export async function iterRecordsForWithdrawal( ): Promise { let withdrawalGroupRecords: WithdrawalGroupRecord[]; if (filter.onlyState === "nonfinal") { - const range = GlobalIDB.KeyRange.bound( - WithdrawalGroupStatus.PendingRegisteringBank, - WithdrawalGroupStatus.PendingAml, - ); withdrawalGroupRecords = await tx.withdrawalGroups.indexes.byStatus.getAll( - range, + withdrawalGroupNonfinalRange, ); } else { withdrawalGroupRecords = @@ -238,7 +236,7 @@ async function gatherWithdrawalPending( * kyc pending operation don't give lifeness * since the user need to complete kyc procedure */ - const userNeedToCompleteKYC = wsr.kycUrl !== undefined + const userNeedToCompleteKYC = wsr.kycUrl !== undefined; const now = AbsoluteTime.now(); if (!opr) { opr = { @@ -256,7 +254,7 @@ async function gatherWithdrawalPending( ws, opTag, timestampOptionalAbsoluteFromDb(opr.retryInfo?.nextRetry) ?? - AbsoluteTime.now(), + AbsoluteTime.now(), ), givesLifeness: !userNeedToCompleteKYC, withdrawalGroupId: wsr.withdrawalGroupId, @@ -276,10 +274,7 @@ export async function iterRecordsForDeposit( let dgs: DepositGroupRecord[]; if (filter.onlyState === "nonfinal") { dgs = await tx.depositGroups.indexes.byStatus.getAll( - GlobalIDB.KeyRange.bound( - DepositOperationStatus.PendingDeposit, - DepositOperationStatus.PendingKyc, - ), + depositOperationNonfinalStatusRange, ); } else { dgs = await tx.depositGroups.indexes.byStatus.getAll(); @@ -310,7 +305,7 @@ async function gatherDepositPending( * kyc pending operation don't give lifeness * since the user need to complete kyc procedure */ - const userNeedToCompleteKYC = dg.kycInfo !== undefined + const userNeedToCompleteKYC = dg.kycInfo !== undefined; const opId = TaskIdentifiers.forDeposit(dg); const retryRecord = await tx.operationRetries.get(opId); const timestampDue = @@ -554,7 +549,7 @@ async function gatherPeerPullInitiationPending( * kyc pending operation don't give lifeness * since the user need to complete kyc procedure */ - const userNeedToCompleteKYC = pi.kycUrl !== undefined + const userNeedToCompleteKYC = pi.kycUrl !== undefined; resp.pendingOperations.push({ type: PendingTaskType.PeerPullCredit, @@ -712,7 +707,7 @@ async function gatherPeerPushCreditPending( * kyc pending operation don't give lifeness * since the user need to complete kyc procedure */ - const userNeedToCompleteKYC = pi.kycUrl !== undefined + const userNeedToCompleteKYC = pi.kycUrl !== undefined; resp.pendingOperations.push({ type: PendingTaskType.PeerPushCredit, -- cgit v1.2.3