aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFlorian Dold <florian@dold.me>2024-09-11 14:37:36 +0200
committerFlorian Dold <florian@dold.me>2024-09-11 14:37:36 +0200
commite308a6a83a95735481134452bc4d4240783eec53 (patch)
treedfa5ec262a1a54ac8d2e88159986a66199cb0803
parent3db5ec833d76284bbc7526c1a6941885198d048d (diff)
wallet-core: exposed KYC limits WIP
-rw-r--r--packages/taler-harness/src/integrationtests/test-kyc-threshold-withdrawal.ts25
-rw-r--r--packages/taler-util/src/types-taler-exchange.ts56
-rw-r--r--packages/taler-util/src/types-taler-wallet.ts56
-rw-r--r--packages/taler-wallet-core/src/db.ts6
-rw-r--r--packages/taler-wallet-core/src/denomSelection.ts6
-rw-r--r--packages/taler-wallet-core/src/exchanges.ts10
-rw-r--r--packages/taler-wallet-core/src/withdraw.ts25
7 files changed, 115 insertions, 69 deletions
diff --git a/packages/taler-harness/src/integrationtests/test-kyc-threshold-withdrawal.ts b/packages/taler-harness/src/integrationtests/test-kyc-threshold-withdrawal.ts
index 7861fae97..bf3c2dbb2 100644
--- a/packages/taler-harness/src/integrationtests/test-kyc-threshold-withdrawal.ts
+++ b/packages/taler-harness/src/integrationtests/test-kyc-threshold-withdrawal.ts
@@ -18,8 +18,6 @@
* Imports.
*/
import {
- encodeCrock,
- hashPaytoUri,
NotificationType,
TalerCorebankApiClient,
TransactionMajorState,
@@ -145,6 +143,14 @@ async function createKycTestkudosEnvironment(
config.setString("KYC-RULE-R1", "timeframe", "1d");
config.setString("KYC-RULE-R1", "next_measures", "M1");
+ config.setString("KYC-RULE-R1", "operation_type", "withdraw");
+ config.setString("KYC-RULE-R1", "enabled", "yes");
+ config.setString("KYC-RULE-R1", "exposed", "yes");
+ config.setString("KYC-RULE-R1", "is_and_combinator", "yes");
+ config.setString("KYC-RULE-R1", "threshold", "TESTKUDOS:300");
+ config.setString("KYC-RULE-R1", "timeframe", "1d");
+ config.setString("KYC-RULE-R1", "next_measures", "verboten");
+
config.setString("KYC-MEASURE-M1", "check_name", "C1");
config.setString("KYC-MEASURE-M1", "context", "{}");
config.setString("KYC-MEASURE-M1", "program", "P1");
@@ -226,13 +232,24 @@ export async function runKycThresholdWithdrawalTest(t: GlobalTestState) {
// Hand it to the wallet
- await walletClient.client.call(
+ const withdrawalUrlInfo = await walletClient.client.call(
WalletApiOperation.GetWithdrawalDetailsForUri,
{
talerWithdrawUri: wop.taler_withdraw_uri,
},
);
+ const withdrawalAmountInfo = await walletClient.call(
+ WalletApiOperation.GetWithdrawalDetailsForAmount,
+ {
+ amount: withdrawalUrlInfo.amount!,
+ exchangeBaseUrl: withdrawalUrlInfo.possibleExchanges[0].exchangeBaseUrl,
+ },
+ );
+
+ // t.assertTrue(!!withdrawalAmountInfo.kycHardLimit);
+ // t.assertAmountEquals(withdrawalAmountInfo.kycHardLimit, "TESTKUDOS:300");
+
// Withdraw
const acceptResp = await walletClient.client.call(
@@ -265,7 +282,6 @@ export async function runKycThresholdWithdrawalTest(t: GlobalTestState) {
await kycNotificationCond;
-
const txDet = await walletClient.call(WalletApiOperation.GetTransactionById, {
transactionId: withdrawalTxId,
});
@@ -275,7 +291,6 @@ export async function runKycThresholdWithdrawalTest(t: GlobalTestState) {
const kycPaytoHash = txDet.kycPaytoHash;
t.assertTrue(!!kycPaytoHash);
-
await postAmlDecisionNoRules(t, {
amlPriv: amlKeypair.priv,
amlPub: amlKeypair.pub,
diff --git a/packages/taler-util/src/types-taler-exchange.ts b/packages/taler-util/src/types-taler-exchange.ts
index 678b0b6c8..7c879b52e 100644
--- a/packages/taler-util/src/types-taler-exchange.ts
+++ b/packages/taler-util/src/types-taler-exchange.ts
@@ -454,6 +454,21 @@ export interface ExchangeKeysJson {
// Optional option, if not given there is no limit.
// Currency must match currency.
wallet_balance_limit_without_kyc?: AmountString[];
+
+ // Array of limits that apply to all accounts.
+ // All of the given limits will be hard limits.
+ // Wallets and merchants are expected to obey them
+ // and not even allow the user to cross them.
+ // Since protocol **v21**.
+ hard_limits?: AccountLimit[];
+
+ // Array of limits with a soft threshold of zero
+ // that apply to all accounts without KYC.
+ // Wallets and merchants are expected to trigger
+ // a KYC process before attempting any zero-limited
+ // operations.
+ // Since protocol **v21**.
+ zero_limits?: ZeroLimitedOperation[];
}
export interface ExchangeMeltRequest {
@@ -877,6 +892,8 @@ export const codecForExchangeKeysJson = (): Codec<ExchangeKeysJson> =>
.property("global_fees", codecForList(codecForGlobalFees()))
.property("accounts", codecForList(codecForExchangeWireAccount()))
.property("wire_fees", codecForMap(codecForList(codecForWireFeesJson())))
+ .property("zero_limits", codecOptional(codecForList(codecForZeroLimitedOperation())))
+ .property("hard_limits", codecOptional(codecForList(codecForAccountLimit())))
.property("denominations", codecForList(codecForNgDenominations))
.property(
"wallet_balance_limit_without_kyc",
@@ -1687,11 +1704,12 @@ export interface AccountKycStatus {
// request that would cause it to exceed hard limits).
limits?: AccountLimit[];
}
+
+export type LimitOperationType = "WITHDRAW" | "DEPOSIT" | "MERGE" | "AGGREGATE" | "BALANCE" | "REFUND" | "CLOSE" | "TRANSACTION";
+
export interface AccountLimit {
// Operation that is limited.
- // Must be one of "WITHDRAW", "DEPOSIT", "P2P-RECEIVE"
- // or "WALLET-BALANCE".
- operation_type: "WITHDRAW" | "DEPOSIT" | "P2P-RECEIVE" | "WALLET-BALANCE";
+ operation_type: LimitOperationType;
// Timeframe during which the limit applies.
timeframe: RelativeTime;
@@ -2104,8 +2122,7 @@ export interface ExchangeKeysResponse {
extensions_sig?: EddsaSignature;
}
-interface ZeroLimitedOperation {
-
+export interface ZeroLimitedOperation {
// Operation that is limited to an amount of
// zero until the client has passed some KYC check.
// Must be one of "WITHDRAW", "DEPOSIT",
@@ -2113,7 +2130,6 @@ interface ZeroLimitedOperation {
// (reserve) "CLOSE", "AGGREGATE",
// "TRANSACTION" or "REFUND".
operation_type: string;
-
}
interface ExtensionManifest {
@@ -2486,7 +2502,7 @@ export const codecForLegitimizationNeededResponse =
.property("hint", codecOptional(codecForString()))
.property("h_payto", codecForString())
.property("account_pub", codecOptional(codecForString()))
- .property("requirement_row", (codecForNumber()))
+ .property("requirement_row", codecForNumber())
.property("bad_kyc_auth", codecOptional(codecForBoolean()))
.build("TalerExchangeApi.LegitimizationNeededResponse");
@@ -2497,22 +2513,36 @@ export const codecForAccountKycStatus = (): Codec<AccountKycStatus> =>
.property("limits", codecOptional(codecForList(codecForAccountLimit())))
.build("TalerExchangeApi.AccountKycStatus");
+export const codecForOperationType = codecForEither(
+ codecForConstString("WITHDRAW"),
+ codecForConstString("DEPOSIT"),
+ codecForConstString("MERGE"),
+ codecForConstString("BALANCE"),
+ codecForConstString("CLOSE"),
+ codecForConstString("AGGREGATE"),
+ codecForConstString("TRANSACTION"),
+ codecForConstString("REFUND"),
+);
+
export const codecForAccountLimit = (): Codec<AccountLimit> =>
buildCodecForObject<AccountLimit>()
.property(
"operation_type",
- codecForEither(
- codecForConstString("WITHDRAW"),
- codecForConstString("DEPOSIT"),
- codecForConstString("P2P-RECEIVE"),
- codecForConstString("WALLET-BALANCE"),
- ),
+ codecForOperationType,
)
.property("timeframe", codecForDuration)
.property("threshold", codecForAmountString())
.property("soft_limit", codecOptional(codecForBoolean()))
.build("TalerExchangeApi.AccountLimit");
+export const codecForZeroLimitedOperation = (): Codec<ZeroLimitedOperation> =>
+ buildCodecForObject<ZeroLimitedOperation>()
+ .property(
+ "operation_type",
+ codecForOperationType
+ )
+ .build("TalerExchangeApi.ZeroLimitedOperation");
+
export const codecForKycCheckPublicInformation =
(): Codec<KycCheckPublicInformation> =>
buildCodecForObject<KycCheckPublicInformation>()
diff --git a/packages/taler-util/src/types-taler-wallet.ts b/packages/taler-util/src/types-taler-wallet.ts
index 798197834..0461b74f9 100644
--- a/packages/taler-util/src/types-taler-wallet.ts
+++ b/packages/taler-util/src/types-taler-wallet.ts
@@ -57,7 +57,6 @@ import {
WithdrawalOperationStatus,
canonicalizeBaseUrl,
} from "./index.js";
-import { VersionMatchResult } from "./libtool-version.js";
import { PaytoString, PaytoUri, codecForPaytoString } from "./payto.js";
import { QrCodeSpec } from "./qr.js";
import { AgeCommitmentProof } from "./taler-crypto.js";
@@ -1507,6 +1506,20 @@ export interface WithdrawalDetailsForAmount {
* Scope info of the currency withdrawn.
*/
scopeInfo: ScopeInfo;
+
+ /**
+ * KYC soft limit.
+ *
+ * Withdrawals over that amount will require KYC.
+ */
+ kycSoftLimit?: AmountString;
+
+ /**
+ * KYC soft limits.
+ *
+ * Withdrawals over that amount will be denied.
+ */
+ kycHardLimit?: AmountString;
}
export interface DenomSelItem {
@@ -1520,13 +1533,12 @@ export interface DenomSelItem {
}
/**
- * Selected denominations withn some extra info.
+ * Selected denominations with some extra info.
*/
export interface DenomSelectionState {
totalCoinValue: AmountString;
totalWithdrawCost: AmountString;
selectedDenoms: DenomSelItem[];
- earliestDepositExpiration: TalerProtocolTimestamp;
hasDenomWithAgeRestriction: boolean;
}
@@ -1556,30 +1568,6 @@ export interface ExchangeWithdrawalDetails {
termsOfServiceAccepted: boolean;
/**
- * The earliest deposit expiration of the selected coins.
- */
- earliestDepositExpiration: TalerProtocolTimestamp;
-
- /**
- * Result of checking the wallet's version
- * against the exchange's version.
- *
- * Older exchanges don't return version information.
- */
- versionMatch: VersionMatchResult | undefined;
-
- /**
- * Libtool-style version string for the exchange or "unknown"
- * for older exchanges.
- */
- exchangeVersion: string;
-
- /**
- * Libtool-style version string for the wallet.
- */
- walletVersion: string;
-
- /**
* Amount that will be subtracted from the reserve's balance.
*/
withdrawalAmountRaw: AmountString;
@@ -1597,6 +1585,20 @@ export interface ExchangeWithdrawalDetails {
ageRestrictionOptions?: number[];
scopeInfo: ScopeInfo;
+
+ /**
+ * KYC soft limit.
+ *
+ * Withdrawals over that amount will require KYC.
+ */
+ kycSoftLimit?: AmountString;
+
+ /**
+ * KYC soft limits.
+ *
+ * Withdrawals over that amount will be denied.
+ */
+ kycHardLimit?: AmountString;
}
export interface GetExchangeTosResult {
diff --git a/packages/taler-wallet-core/src/db.ts b/packages/taler-wallet-core/src/db.ts
index a42548bb3..7f3b24250 100644
--- a/packages/taler-wallet-core/src/db.ts
+++ b/packages/taler-wallet-core/src/db.ts
@@ -29,6 +29,7 @@ import {
} from "@gnu-taler/idb-bridge";
import {
AbsoluteTime,
+ AccountLimit,
AgeCommitmentProof,
AmountString,
Amounts,
@@ -61,6 +62,7 @@ import {
UnblindedSignature,
WireInfo,
WithdrawalExchangeAccountDetails,
+ ZeroLimitedOperation,
codecForAny,
j2s,
stringifyScopeInfo,
@@ -624,6 +626,10 @@ export interface ExchangeDetailsRecord {
ageMask?: number;
walletBalanceLimits?: AmountString[];
+
+ hardLimits?: AccountLimit[];
+
+ zeroLimits?: ZeroLimitedOperation[];
}
export interface ExchangeDetailsPointer {
diff --git a/packages/taler-wallet-core/src/denomSelection.ts b/packages/taler-wallet-core/src/denomSelection.ts
index ecc1fa881..9e62857bf 100644
--- a/packages/taler-wallet-core/src/denomSelection.ts
+++ b/packages/taler-wallet-core/src/denomSelection.ts
@@ -123,9 +123,6 @@ export function selectWithdrawalDenominations(
selectedDenoms,
totalCoinValue: Amounts.stringify(totalCoinValue),
totalWithdrawCost: Amounts.stringify(totalWithdrawCost),
- earliestDepositExpiration: AbsoluteTime.toProtocolTimestamp(
- earliestDepositExpiration,
- ),
hasDenomWithAgeRestriction,
};
}
@@ -191,9 +188,6 @@ export function selectForcedWithdrawalDenominations(
selectedDenoms,
totalCoinValue: Amounts.stringify(totalCoinValue),
totalWithdrawCost: Amounts.stringify(totalWithdrawCost),
- earliestDepositExpiration: AbsoluteTime.toProtocolTimestamp(
- earliestDepositExpiration,
- ),
hasDenomWithAgeRestriction,
};
}
diff --git a/packages/taler-wallet-core/src/exchanges.ts b/packages/taler-wallet-core/src/exchanges.ts
index 2c0386d0c..4c2677482 100644
--- a/packages/taler-wallet-core/src/exchanges.ts
+++ b/packages/taler-wallet-core/src/exchanges.ts
@@ -26,6 +26,7 @@
import {
AbsoluteTime,
AccountKycStatus,
+ AccountLimit,
AgeRestriction,
Amount,
AmountLike,
@@ -91,6 +92,7 @@ import {
WireFeeMap,
WireFeesJson,
WireInfo,
+ ZeroLimitedOperation,
assertUnreachable,
checkDbInvariant,
checkLogicInvariant,
@@ -854,6 +856,8 @@ export interface ExchangeKeysDownloadResult {
wireFees: { [methodName: string]: WireFeesJson[] };
currencySpecification?: CurrencySpecification;
walletBalanceLimits: AmountString[] | undefined;
+ hardLimits: AccountLimit[] | undefined;
+ zeroLimits: ZeroLimitedOperation[] | undefined;
}
/**
@@ -1019,6 +1023,8 @@ async function downloadExchangeKeysInfo(
currencySpecification: exchangeKeysJsonUnchecked.currency_specification,
walletBalanceLimits:
exchangeKeysJsonUnchecked.wallet_balance_limit_without_kyc,
+ hardLimits: exchangeKeysJsonUnchecked.hard_limits,
+ zeroLimits: exchangeKeysJsonUnchecked.zero_limits,
};
}
@@ -1259,6 +1265,8 @@ export interface ReadyExchangeSummary {
protocolVersionRange: string;
tosAcceptedTimestamp: TalerPreciseTimestamp | undefined;
scopeInfo: ScopeInfo;
+ zeroLimits: ZeroLimitedOperation[];
+ hardLimits: AccountLimit[];
}
/**
@@ -1423,6 +1431,8 @@ async function waitReadyExchange(
exchange.tosAcceptedTimestamp,
),
scopeInfo,
+ hardLimits: exchangeDetails.hardLimits ?? [],
+ zeroLimits: exchangeDetails.zeroLimits ?? [],
};
if (options.expectedMasterPub) {
diff --git a/packages/taler-wallet-core/src/withdraw.ts b/packages/taler-wallet-core/src/withdraw.ts
index c98c16f65..5cc0f228f 100644
--- a/packages/taler-wallet-core/src/withdraw.ts
+++ b/packages/taler-wallet-core/src/withdraw.ts
@@ -2190,14 +2190,6 @@ async function redenominateWithdrawal(
.toString(),
hasDenomWithAgeRestriction:
prevHasDenomWithAgeRestriction || newSel.hasDenomWithAgeRestriction,
- earliestDepositExpiration: AbsoluteTime.toProtocolTimestamp(
- AbsoluteTime.min(
- prevEarliestDepositExpiration,
- AbsoluteTime.fromProtocolTimestamp(
- newSel.earliestDepositExpiration,
- ),
- ),
- ),
};
wg.denomsSel = mergedSel;
if (logger.shouldLogTrace()) {
@@ -2574,14 +2566,10 @@ export async function getExchangeWithdrawalInfo(
}
const ret: ExchangeWithdrawalDetails = {
- earliestDepositExpiration: selectedDenoms.earliestDepositExpiration,
exchangePaytoUris: paytoUris,
exchangeWireAccounts,
exchangeCreditAccountDetails: withdrawalAccountsList,
- exchangeVersion: exchange.protocolVersionRange || "unknown",
selectedDenoms,
- versionMatch,
- walletVersion: WALLET_EXCHANGE_PROTOCOL_VERSION,
termsOfServiceAccepted: tosAccepted,
withdrawalAmountEffective: Amounts.stringify(selectedDenoms.totalCoinValue),
withdrawalAmountRaw: Amounts.stringify(instructedAmount),
@@ -2595,11 +2583,6 @@ export async function getExchangeWithdrawalInfo(
return ret;
}
-export interface GetWithdrawalDetailsForUriOpts {
- restrictAge?: number;
- notifyChangeFromPendingTimeoutMs?: number;
-}
-
async function getWithdrawalDetailsForBankInfo(
wex: WalletExecutionContext,
info: BankWithdrawDetails,
@@ -2629,6 +2612,12 @@ async function getWithdrawalDetailsForBankInfo(
} else {
const listExchangesResp = await listExchanges(wex);
+ for (const exchange of listExchangesResp.exchanges) {
+ if (exchange.currency !== currency) {
+ continue;
+ }
+ }
+
possibleExchanges = listExchangesResp.exchanges.filter((x) => {
return (
x.currency === currency &&
@@ -3993,7 +3982,7 @@ export async function getWithdrawalDetailsForAmount(
);
}
-async function internalGetWithdrawalDetailsForAmount(
+export async function internalGetWithdrawalDetailsForAmount(
wex: WalletExecutionContext,
req: GetWithdrawalDetailsForAmountRequest,
): Promise<WithdrawalDetailsForAmount> {