diff options
Diffstat (limited to 'packages/taler-wallet-core/src/kyc.ts')
-rw-r--r-- | packages/taler-wallet-core/src/kyc.ts | 252 |
1 files changed, 252 insertions, 0 deletions
diff --git a/packages/taler-wallet-core/src/kyc.ts b/packages/taler-wallet-core/src/kyc.ts new file mode 100644 index 000000000..b28816073 --- /dev/null +++ b/packages/taler-wallet-core/src/kyc.ts @@ -0,0 +1,252 @@ +/* + This file is part of GNU Taler + (C) 2024 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 { + AmountJson, + AmountLike, + Amounts, + AmountString, +} from "@gnu-taler/taler-util"; +import { ReadyExchangeSummary } from "./exchanges.js"; + +/** + * @fileoverview Helpers for KYC. + * @author Florian Dold <dold@taler.net> + */ + +export interface SimpleLimitInfo { + kycHardLimit: AmountString | undefined; + kycSoftLimit: AmountString | undefined; +} + +export interface MultiExchangeLimitInfo { + kycHardLimit: AmountString | undefined; + kycSoftLimit: AmountString | undefined; + + /** + * Exchanges that would require soft KYC. + */ + kycExchanges: string[]; +} + +/** + * Return the smallest given amount, where an undefined amount + * is interpreted the larger amount. + */ +function minDefAmount( + a: AmountLike | undefined, + b: AmountLike | undefined, +): AmountJson { + if (a == null) { + if (b == null) { + throw Error(); + } + return Amounts.jsonifyAmount(b); + } + if (b == null) { + if (a == null) { + throw Error(); + } + return Amounts.jsonifyAmount(a); + } + return Amounts.min(a, b); +} + +/** + * Add to an amount. + * Interprets the second argument as zero if not defined. + */ +function addDefAmount(a: AmountLike, b: AmountLike | undefined): AmountJson { + if (b == null) { + return Amounts.jsonifyAmount(a); + } + return Amounts.add(a, b).amount; +} + +export function getDepositLimitInfo( + exchanges: ReadyExchangeSummary[], + instructedAmount: AmountLike, +): MultiExchangeLimitInfo { + let kycHardLimit: AmountJson | undefined; + let kycSoftLimit: AmountJson | undefined; + let kycExchanges: string[] = []; + + // FIXME: Summing up the limits doesn't really make a lot of sense, + // as the funds at each exchange are limited (by the coins in the wallet), + // and thus an exchange where we don't have coins but that has a high + // KYC limit can't meaningfully contribute to the whole limit. + + for (const exchange of exchanges) { + const exchLim = getSingleExchangeDepositLimitInfo( + exchange, + instructedAmount, + ); + if (exchLim.kycSoftLimit) { + kycExchanges.push(exchange.exchangeBaseUrl); + kycSoftLimit = addDefAmount(exchLim.kycSoftLimit, kycSoftLimit); + } + if (exchLim.kycHardLimit) { + kycHardLimit = addDefAmount(exchLim.kycHardLimit, kycHardLimit); + } + } + + return { + kycHardLimit: kycHardLimit ? Amounts.stringify(kycHardLimit) : undefined, + kycSoftLimit: kycSoftLimit ? Amounts.stringify(kycSoftLimit) : undefined, + kycExchanges, + }; +} + +export function getSingleExchangeDepositLimitInfo( + exchange: ReadyExchangeSummary, + instructedAmount: AmountLike, +): SimpleLimitInfo { + let kycHardLimit: AmountJson | undefined; + let kycSoftLimit: AmountJson | undefined; + + for (let lim of exchange.hardLimits) { + switch (lim.operation_type) { + case "DEPOSIT": + case "AGGREGATE": + // FIXME: This should consider past deposits and KYC checks + kycHardLimit = minDefAmount(kycHardLimit, lim.threshold); + break; + } + } + + for (let limAmount of exchange.walletBalanceLimitWithoutKyc ?? []) { + kycSoftLimit = minDefAmount(kycSoftLimit, limAmount); + } + + for (let lim of exchange.zeroLimits) { + switch (lim.operation_type) { + case "DEPOSIT": + case "AGGREGATE": + kycSoftLimit = Amounts.zeroOfAmount(instructedAmount); + break; + } + } + + return { + kycHardLimit: kycHardLimit ? Amounts.stringify(kycHardLimit) : undefined, + kycSoftLimit: kycSoftLimit ? Amounts.stringify(kycSoftLimit) : undefined, + }; +} + +export function getPeerCreditLimitInfo( + exchange: ReadyExchangeSummary, + instructedAmount: AmountLike, +): SimpleLimitInfo { + let kycHardLimit: AmountJson | undefined; + let kycSoftLimit: AmountJson | undefined; + + for (let lim of exchange.hardLimits) { + switch (lim.operation_type) { + case "BALANCE": + case "MERGE": + // FIXME: This should consider past merges and KYC checks + kycHardLimit = minDefAmount(kycHardLimit, lim.threshold); + break; + } + } + + for (let limAmount of exchange.walletBalanceLimitWithoutKyc ?? []) { + kycSoftLimit = minDefAmount(kycSoftLimit, limAmount); + } + + for (let lim of exchange.zeroLimits) { + switch (lim.operation_type) { + case "BALANCE": + case "MERGE": + kycSoftLimit = Amounts.zeroOfAmount(instructedAmount); + break; + } + } + + return { + kycHardLimit: kycHardLimit ? Amounts.stringify(kycHardLimit) : undefined, + kycSoftLimit: kycSoftLimit ? Amounts.stringify(kycSoftLimit) : undefined, + }; +} + +export function checkWithdrawalHardLimitExceeded( + exchange: ReadyExchangeSummary, + instructedAmount: AmountLike, +): boolean { + const limitInfo = getWithdrawalLimitInfo(exchange, instructedAmount); + return ( + limitInfo.kycHardLimit != null && + Amounts.cmp(limitInfo.kycHardLimit, instructedAmount) <= 0 + ); +} + +export function checkPeerCreditHardLimitExceeded( + exchange: ReadyExchangeSummary, + instructedAmount: AmountLike, +): boolean { + const limitInfo = getPeerCreditLimitInfo(exchange, instructedAmount); + return ( + limitInfo.kycHardLimit != null && + Amounts.cmp(limitInfo.kycHardLimit, instructedAmount) <= 0 + ); +} + +export function checkDepositHardLimitExceeded( + exchanges: ReadyExchangeSummary[], + instructedAmount: AmountLike, +): boolean { + const limitInfo = getDepositLimitInfo(exchanges, instructedAmount); + return ( + limitInfo.kycHardLimit != null && + Amounts.cmp(limitInfo.kycHardLimit, instructedAmount) <= 0 + ); +} + +export function getWithdrawalLimitInfo( + exchange: ReadyExchangeSummary, + instructedAmount: AmountLike, +): SimpleLimitInfo { + let kycHardLimit: AmountJson | undefined; + let kycSoftLimit: AmountJson | undefined; + + for (let lim of exchange.hardLimits) { + switch (lim.operation_type) { + case "BALANCE": + case "WITHDRAW": + // FIXME: This should consider past withdrawals and KYC checks + kycHardLimit = minDefAmount(kycHardLimit, lim.threshold); + break; + } + } + + for (let limAmount of exchange.walletBalanceLimitWithoutKyc ?? []) { + kycSoftLimit = minDefAmount(kycSoftLimit, limAmount); + } + + for (let lim of exchange.zeroLimits) { + switch (lim.operation_type) { + case "BALANCE": + case "WITHDRAW": + kycSoftLimit = Amounts.zeroOfAmount(instructedAmount); + break; + } + } + + return { + kycHardLimit: kycHardLimit ? Amounts.stringify(kycHardLimit) : undefined, + kycSoftLimit: kycSoftLimit ? Amounts.stringify(kycSoftLimit) : undefined, + }; +} |