diff options
-rw-r--r-- | packages/taler-harness/src/harness/harness.ts | 11 | ||||
-rw-r--r-- | packages/taler-harness/src/integrationtests/test-kyc.ts | 204 | ||||
-rw-r--r-- | packages/taler-harness/src/integrationtests/testrunner.ts | 50 | ||||
-rw-r--r-- | packages/taler-util/src/taler-types.ts | 15 | ||||
-rw-r--r-- | packages/taler-wallet-core/src/db.ts | 7 | ||||
-rw-r--r-- | packages/taler-wallet-core/src/operations/withdraw.ts | 73 |
6 files changed, 329 insertions, 31 deletions
diff --git a/packages/taler-harness/src/harness/harness.ts b/packages/taler-harness/src/harness/harness.ts index a9298637f..5b72cbc06 100644 --- a/packages/taler-harness/src/harness/harness.ts +++ b/packages/taler-harness/src/harness/harness.ts @@ -1081,6 +1081,17 @@ export class ExchangeService implements ExchangeServiceInterface { return this.exchangeConfig.httpPort; } + /** + * Run a function that modifies the existing exchange configuration. + * The modified exchange configuration will then be written to the + * file system. + */ + async modifyConfig(f: (config: Configuration) => Promise<void>): Promise<void> { + const config = Configuration.load(this.configFilename); + await f(config); + config.write(this.configFilename); + } + async addBankAccount( localName: string, exchangeBankAccount: HarnessExchangeBankAccount, diff --git a/packages/taler-harness/src/integrationtests/test-kyc.ts b/packages/taler-harness/src/integrationtests/test-kyc.ts new file mode 100644 index 000000000..40474fb6f --- /dev/null +++ b/packages/taler-harness/src/integrationtests/test-kyc.ts @@ -0,0 +1,204 @@ +/* + This file is part of GNU Taler + (C) 2020 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/> + */ + +/** + * Imports. + */ +import { Duration } from "@gnu-taler/taler-util"; +import { CoinConfig, defaultCoinConfig } from "../harness/denomStructures.js"; +import { + BankService, + ExchangeService, + getPayto, + GlobalTestState, + MerchantService, + setupDb, + WalletCli, +} from "../harness/harness.js"; +import { + withdrawViaBank, + makeTestPayment, + EnvOptions, + SimpleTestEnvironment, +} from "../harness/helpers.js"; + +export async function createKycTestkudosEnvironment( + t: GlobalTestState, + coinConfig: CoinConfig[] = defaultCoinConfig.map((x) => x("TESTKUDOS")), + opts: EnvOptions = {}, +): Promise<SimpleTestEnvironment> { + const db = await setupDb(t); + + const bank = await BankService.create(t, { + allowRegistrations: true, + currency: "TESTKUDOS", + database: db.connStr, + httpPort: 8082, + }); + + const exchange = ExchangeService.create(t, { + name: "testexchange-1", + currency: "TESTKUDOS", + httpPort: 8081, + database: db.connStr, + }); + + const merchant = await MerchantService.create(t, { + name: "testmerchant-1", + currency: "TESTKUDOS", + httpPort: 8083, + database: db.connStr, + }); + + const exchangeBankAccount = await bank.createExchangeAccount( + "myexchange", + "x", + ); + exchange.addBankAccount("1", exchangeBankAccount); + + bank.setSuggestedExchange(exchange, exchangeBankAccount.accountPaytoUri); + + await bank.start(); + + await bank.pingUntilAvailable(); + + const ageMaskSpec = opts.ageMaskSpec; + + if (ageMaskSpec) { + exchange.enableAgeRestrictions(ageMaskSpec); + // Enable age restriction for all coins. + exchange.addCoinConfigList( + coinConfig.map((x) => ({ + ...x, + name: `${x.name}-age`, + ageRestricted: true, + })), + ); + // For mixed age restrictions, we also offer coins without age restrictions + if (opts.mixedAgeRestriction) { + exchange.addCoinConfigList( + coinConfig.map((x) => ({ ...x, ageRestricted: false })), + ); + } + } else { + exchange.addCoinConfigList(coinConfig); + } + + await exchange.modifyConfig(async (config) => { + const myprov = "kyc-provider-myprov"; + config.setString(myprov, "cost", "0"); + config.setString(myprov, "logic", "oauth2"); + config.setString(myprov, "provided_checks", "dummy1"); + config.setString(myprov, "user_type", "individual"); + config.setString(myprov, "kyc_oauth2_validity", "forever"); + config.setString( + myprov, + "kyc_oauth2_auth_url", + "http://localhost:6666/oauth/v2/token", + ); + config.setString( + myprov, + "kyc_oauth2_login_url", + "http://localhost:6666/oauth/v2/login", + ); + config.setString( + myprov, + "kyc_oauth2_info_url", + "http://localhost:6666/oauth/v2/login", + ); + config.setString( + myprov, + "kyc_oauth2_client_id", + "taler-exchange", + ); + config.setString( + myprov, + "kyc_oauth2_client_secret", + "exchange-secret", + ); + config.setString( + myprov, + "kyc_oauth2_post_url", + "https://taler.com", + ); + + config.setString("kyc-legitimization-withdraw1", "operation_type", "withdraw"); + config.setString("kyc-legitimization-withdraw1", "required_checks", "dummy1"); + config.setString("kyc-legitimization-withdraw1", "timeframe", "1d"); + config.setString("kyc-legitimization-withdraw1", "threshold", "TESTKUDOS:5"); + }); + + await exchange.start(); + await exchange.pingUntilAvailable(); + + merchant.addExchange(exchange); + + await merchant.start(); + await merchant.pingUntilAvailable(); + + await merchant.addInstance({ + id: "default", + name: "Default Instance", + paytoUris: [getPayto("merchant-default")], + defaultWireTransferDelay: Duration.toTalerProtocolDuration( + Duration.fromSpec({ minutes: 1 }), + ), + }); + + await merchant.addInstance({ + id: "minst1", + name: "minst1", + paytoUris: [getPayto("minst1")], + defaultWireTransferDelay: Duration.toTalerProtocolDuration( + Duration.fromSpec({ minutes: 1 }), + ), + }); + + console.log("setup done!"); + + const wallet = new WalletCli(t); + + return { + commonDb: db, + exchange, + merchant, + wallet, + bank, + exchangeBankAccount, + }; +} + +export async function runKycTest(t: GlobalTestState) { + // Set up test environment + + const { wallet, bank, exchange, merchant } = + await createKycTestkudosEnvironment(t); + + // Withdraw digital cash into the wallet. + + await withdrawViaBank(t, { wallet, bank, exchange, amount: "TESTKUDOS:20" }); + + const order = { + summary: "Buy me!", + amount: "TESTKUDOS:5", + fulfillment_url: "taler://fulfillment-success/thx", + }; + + await makeTestPayment(t, { wallet, merchant, order }); + await wallet.runUntilDone(); +} + +runKycTest.suites = ["wallet"]; diff --git a/packages/taler-harness/src/integrationtests/testrunner.ts b/packages/taler-harness/src/integrationtests/testrunner.ts index 4b1c28bde..9e64a151a 100644 --- a/packages/taler-harness/src/integrationtests/testrunner.ts +++ b/packages/taler-harness/src/integrationtests/testrunner.ts @@ -96,6 +96,7 @@ import { runWalletBalanceTest } from "./test-wallet-balance.js"; import { runAgeRestrictionsMixedMerchantTest } from "./test-age-restrictions-mixed-merchant.js"; import { runWalletCryptoWorkerTest } from "./test-wallet-cryptoworker.js"; import { runWithdrawalHighTest } from "./test-withdrawal-high.js"; +import { runKycTest } from "./test-kyc.js"; /** * Test runner. @@ -113,75 +114,76 @@ interface TestMainFunction { const allTests: TestMainFunction[] = [ runAgeRestrictionsMerchantTest, - runAgeRestrictionsPeerTest, runAgeRestrictionsMixedMerchantTest, + runAgeRestrictionsPeerTest, runBankApiTest, runClaimLoopTest, runClauseSchnorrTest, - runWalletCryptoWorkerTest, - runDepositTest, runDenomUnofferedTest, + runDepositTest, runExchangeManagementTest, runExchangeTimetravelTest, runFeeRegressionTest, runForcedSelectionTest, - runLibeufinBasicTest, - runLibeufinKeyrotationTest, - runLibeufinTutorialTest, - runLibeufinRefundTest, - runLibeufinC5xTest, - runLibeufinNexusBalanceTest, - runLibeufinBadGatewayTest, - runLibeufinRefundMultipleUsersTest, - runLibeufinApiPermissionsTest, - runLibeufinApiFacadeTest, - runLibeufinApiFacadeBadRequestTest, + runKycTest, runLibeufinAnastasisFacadeTest, - runLibeufinApiSchedulingTest, - runLibeufinApiUsersTest, runLibeufinApiBankaccountTest, runLibeufinApiBankconnectionTest, - runLibeufinApiSandboxTransactionsTest, + runLibeufinApiFacadeBadRequestTest, + runLibeufinApiFacadeTest, + runLibeufinApiPermissionsTest, runLibeufinApiSandboxCamtTest, + runLibeufinApiSandboxTransactionsTest, + runLibeufinApiSchedulingTest, + runLibeufinApiUsersTest, + runLibeufinBadGatewayTest, + runLibeufinBasicTest, + runLibeufinC5xTest, + runLibeufinKeyrotationTest, + runLibeufinNexusBalanceTest, + runLibeufinRefundMultipleUsersTest, + runLibeufinRefundTest, runLibeufinSandboxWireTransferCliTest, + runLibeufinTutorialTest, runMerchantExchangeConfusionTest, - runMerchantInstancesTest, runMerchantInstancesDeleteTest, + runMerchantInstancesTest, runMerchantInstancesUrlsTest, runMerchantLongpollingTest, - runMerchantSpecPublicOrdersTest, runMerchantRefundApiTest, + runMerchantSpecPublicOrdersTest, runPaymentClaimTest, + runPaymentDemoTest, runPaymentFaultTest, runPaymentForgettableTest, runPaymentIdempotencyTest, runPaymentMultipleTest, runPaymentTest, - runPaymentDemoTest, runPaymentTransientTest, runPaymentZeroTest, runPayPaidTest, runPaywallFlowTest, - runPeerToPeerPushTest, runPeerToPeerPullTest, + runPeerToPeerPushTest, runRefundAutoTest, runRefundGoneTest, runRefundIncrementalTest, runRefundTest, runRevocationTest, runTestWithdrawalManualTest, - runWithdrawalFakebankTest, runTimetravelAutorefreshTest, runTimetravelWithdrawTest, runTippingTest, runWalletBackupBasicTest, runWalletBackupDoublespendTest, runWalletBalanceTest, - runWithdrawalHighTest, - runWallettestingTest, + runWalletCryptoWorkerTest, runWalletDblessTest, + runWallettestingTest, runWithdrawalAbortBankTest, runWithdrawalBankIntegratedTest, + runWithdrawalFakebankTest, + runWithdrawalHighTest, ]; export interface TestRunSpec { diff --git a/packages/taler-util/src/taler-types.ts b/packages/taler-util/src/taler-types.ts index 292ace94b..9251868e6 100644 --- a/packages/taler-util/src/taler-types.ts +++ b/packages/taler-util/src/taler-types.ts @@ -2027,3 +2027,18 @@ export interface ExchangeDepositRequest { h_age_commitment?: string; } + +export interface WalletKycUuid { + // UUID that the wallet should use when initiating + // the KYC check. + requirement_row: number; + + // Hash of the payto:// account URI for the wallet. + h_payto: string; +} + +export const codecForWalletKycUuid = (): Codec<WalletKycUuid> => + buildCodecForObject<WalletKycUuid>() + .property("requirement_row", codecForNumber()) + .property("h_payto", codecForString()) + .build("WalletKycUuid"); diff --git a/packages/taler-wallet-core/src/db.ts b/packages/taler-wallet-core/src/db.ts index 04fee9495..c56c3a9b5 100644 --- a/packages/taler-wallet-core/src/db.ts +++ b/packages/taler-wallet-core/src/db.ts @@ -1327,6 +1327,11 @@ export type WgInfo = | WgInfoBankPeerPush | WgInfoBankRecoup; + +export interface WithdrawalKycPendingInfo { + paytoHash: string; + requirementRow: number; +} /** * Group of withdrawal operations that need to be executed. * (Either for a normal withdrawal or from a tip.) @@ -1342,6 +1347,8 @@ export interface WithdrawalGroupRecord { wgInfo: WgInfo; + kycPending?: WithdrawalKycPendingInfo; + /** * Secret seed used to derive planchets. * Stored since planchets are created lazily. diff --git a/packages/taler-wallet-core/src/operations/withdraw.ts b/packages/taler-wallet-core/src/operations/withdraw.ts index 76bbec416..368cf3510 100644 --- a/packages/taler-wallet-core/src/operations/withdraw.ts +++ b/packages/taler-wallet-core/src/operations/withdraw.ts @@ -33,6 +33,7 @@ import { codecForBankWithdrawalOperationPostResponse, codecForReserveStatus, codecForTalerConfigResponse, + codecForWalletKycUuid, codecForWithdrawBatchResponse, codecForWithdrawOperationStatusResponse, codecForWithdrawResponse, @@ -75,6 +76,7 @@ import { WgInfo, WithdrawalGroupRecord, WithdrawalGroupStatus, + WithdrawalKycPendingInfo, WithdrawalRecordType, } from "../db.js"; import { @@ -530,8 +532,11 @@ async function processPlanchetExchangeRequest( const resp = await ws.http.postJson(reqUrl, reqBody); if (resp.status === HttpStatusCode.UnavailableForLegalReasons) { logger.info("withdrawal requires KYC"); + const respJson = await resp.json(); + const uuidResp = codecForWalletKycUuid().decode(respJson); + logger.info(`kyc uuid response: ${j2s(uuidResp)}`); await ws.db - .mktx((x) => [x.planchets]) + .mktx((x) => [x.planchets, x.withdrawalGroups]) .runReadWrite(async (tx) => { let planchet = await tx.planchets.indexes.byGroupAndIndex.get([ withdrawalGroup.withdrawalGroupId, @@ -541,7 +546,18 @@ async function processPlanchetExchangeRequest( return; } planchet.planchetStatus = PlanchetStatus.KycRequired; + const wg2 = await tx.withdrawalGroups.get( + withdrawalGroup.withdrawalGroupId, + ); + if (!wg2) { + return; + } + wg2.kycPending = { + paytoHash: uuidResp.h_payto, + requirementRow: uuidResp.requirement_row, + }; await tx.planchets.put(planchet); + await tx.withdrawalGroups.put(wg2); }); return; } @@ -1148,7 +1164,7 @@ export async function processWithdrawalGroup( let finishedForFirstTime = false; let errorsPerCoin: Record<number, TalerErrorDetail> = {}; - await ws.db + let res = await ws.db .mktx((x) => [x.coins, x.withdrawalGroups, x.planchets]) .runReadWrite(async (tx) => { const wg = await tx.withdrawalGroups.get(withdrawalGroupId); @@ -1177,13 +1193,56 @@ export async function processWithdrawalGroup( } await tx.withdrawalGroups.put(wg); + + return { + kycInfo: wg.kycPending, + }; }); + + if (!res) { + throw Error("withdrawal group does not exist anymore"); + } + + const { kycInfo } = res; + if (numKycRequired > 0) { - throw TalerError.fromDetail( - TalerErrorCode.WALLET_WITHDRAWAL_KYC_REQUIRED, - {}, - `KYC check required for withdrawal (not yet implemented in wallet-core)`, - ); + if (kycInfo) { + const url = new URL( + `kyc-check/${kycInfo.requirementRow}/${kycInfo.paytoHash}/individual`, + withdrawalGroup.exchangeBaseUrl, + ); + logger.info(`kyc url ${url.href}`); + const kycStatusReq = await ws.http.fetch(url.href, { + method: "GET", + }); + logger.warn("kyc requested, but already fulfilled"); + if (kycStatusReq.status === HttpStatusCode.Ok) { + return { + type: OperationAttemptResultType.Pending, + result: undefined, + }; + } else if (kycStatusReq.status === HttpStatusCode.Accepted) { + const kycStatus = await kycStatusReq.json(); + logger.info(`kyc status: ${j2s(kycStatus)}`); + throw TalerError.fromDetail( + TalerErrorCode.WALLET_WITHDRAWAL_KYC_REQUIRED, + { + kycUrl: kycStatus.kyc_url, + }, + `KYC check required for withdrawal`, + ); + } else { + throw Error( + `unexpected response from kyc-check (${kycStatusReq.status})`, + ); + } + } else { + throw TalerError.fromDetail( + TalerErrorCode.WALLET_WITHDRAWAL_KYC_REQUIRED, + {}, + `KYC check required for withdrawal (not yet implemented in wallet-core)`, + ); + } } if (numFinished != numTotalCoins) { throw TalerError.fromDetail( |