diff options
3 files changed, 387 insertions, 31 deletions
diff --git a/packages/taler-harness/src/integrationtests/test-kyc-peer-push.ts b/packages/taler-harness/src/integrationtests/test-kyc-peer-push.ts new file mode 100644 index 000000000..fcd5d7aa8 --- /dev/null +++ b/packages/taler-harness/src/integrationtests/test-kyc-peer-push.ts @@ -0,0 +1,347 @@ +/* + 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 { + AbsoluteTime, + AmountString, + Duration, + j2s, + TalerCorebankApiClient, + TransactionMajorState, + TransactionMinorState, + TransactionType, +} from "@gnu-taler/taler-util"; +import { + createSyncCryptoApi, + EddsaKeypair, + WalletApiOperation, +} from "@gnu-taler/taler-wallet-core"; +import { CoinConfig, defaultCoinConfig } from "../harness/denomStructures.js"; +import { + BankService, + DbInfo, + ExchangeService, + generateRandomPayto, + GlobalTestState, + HarnessExchangeBankAccount, + setupDb, + WalletClient, + WalletService, +} from "../harness/harness.js"; +import { + createWalletDaemonWithClient, + EnvOptions, + postAmlDecisionNoRules, + withdrawViaBankV3, +} from "../harness/helpers.js"; + +interface KycTestEnv { + commonDb: DbInfo; + bankClient: TalerCorebankApiClient; + exchange: ExchangeService; + exchangeBankAccount: HarnessExchangeBankAccount; + walletClient: WalletClient; + walletService: WalletService; + amlKeypair: EddsaKeypair; +} + +async function createKycTestkudosEnvironment( + t: GlobalTestState, + coinConfig: CoinConfig[] = defaultCoinConfig.map((x) => x("TESTKUDOS")), + opts: EnvOptions = {}, +): Promise<KycTestEnv> { + 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, + }); + + let receiverName = "Exchange"; + let exchangeBankUsername = "exchange"; + let exchangeBankPassword = "mypw"; + let exchangePaytoUri = generateRandomPayto(exchangeBankUsername); + + await exchange.addBankAccount("1", { + accountName: exchangeBankUsername, + accountPassword: exchangeBankPassword, + wireGatewayApiBaseUrl: new URL( + "accounts/exchange/taler-wire-gateway/", + bank.baseUrl, + ).href, + accountPaytoUri: exchangePaytoUri, + }); + + bank.setSuggestedExchange(exchange, exchangePaytoUri); + + await bank.start(); + + await bank.pingUntilAvailable(); + + const bankClient = new TalerCorebankApiClient(bank.corebankApiBaseUrl, { + auth: { + username: "admin", + password: "adminpw", + }, + }); + + await bankClient.registerAccountExtended({ + name: receiverName, + password: exchangeBankPassword, + username: exchangeBankUsername, + is_taler_exchange: true, + payto_uri: exchangePaytoUri, + }); + + 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) => { + config.setString("exchange", "enable_kyc", "yes"); + + config.setString("KYC-RULE-R1", "operation_type", "merge"); + 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:5"); + config.setString("KYC-RULE-R1", "timeframe", "1d"); + config.setString("KYC-RULE-R1", "next_measures", "M1"); + + config.setString("KYC-MEASURE-M1", "check_name", "C1"); + config.setString("KYC-MEASURE-M1", "context", "{}"); + config.setString("KYC-MEASURE-M1", "program", "P1"); + + config.setString("AML-PROGRAM-P1", "command", "/bin/true"); + config.setString("AML-PROGRAM-P1", "enabled", "true"); + config.setString("AML-PROGRAM-P1", "description", "this does nothing"); + config.setString("AML-PROGRAM-P1", "fallback", "M1"); + + config.setString("KYC-CHECK-C1", "type", "INFO"); + config.setString("KYC-CHECK-C1", "description", "my check!"); + config.setString("KYC-CHECK-C1", "fallback", "M1"); + }); + + await exchange.start(); + + const cryptoApi = createSyncCryptoApi(); + const amlKeypair = await cryptoApi.createEddsaKeypair({}); + + await exchange.enableAmlAccount(amlKeypair.pub, "Alice"); + + const walletService = new WalletService(t, { + name: "wallet", + useInMemoryDb: true, + }); + await walletService.start(); + await walletService.pingUntilAvailable(); + + const walletClient = new WalletClient({ + name: "wallet", + unixPath: walletService.socketPath, + onNotification(n) { + console.log("got notification", n); + }, + }); + await walletClient.connect(); + await walletClient.client.call(WalletApiOperation.InitWallet, { + config: { + testing: { + skipDefaults: true, + }, + }, + }); + + console.log("setup done!"); + + return { + commonDb: db, + exchange, + amlKeypair, + walletClient, + walletService, + bankClient, + exchangeBankAccount: { + accountName: "", + accountPassword: "", + accountPaytoUri: "", + wireGatewayApiBaseUrl: "", + }, + }; +} + +export async function runKycPeerPushTest(t: GlobalTestState) { + // Set up test environment + + const { walletClient, bankClient, exchange, amlKeypair } = + await createKycTestkudosEnvironment(t); + + // Origin wallet for the p2p transaction. + const w0 = await createWalletDaemonWithClient(t, { + name: "w0", + }); + + // Withdraw digital cash into the wallet. + + const wres = await withdrawViaBankV3(t, { + bankClient, + amount: "TESTKUDOS:20", + exchange: exchange, + walletClient: w0.walletClient, + }); + + await wres.withdrawalFinishedCond; + + const pushDebitRes = await doPeerPushDebit(t, { + walletClient: w0.walletClient, + amount: "TESTKUDOS:10", + summary: "Test1", + }); + + const prepRes = await walletClient.call( + WalletApiOperation.PreparePeerPushCredit, + { + talerUri: pushDebitRes.talerUri, + }, + ); + + console.log("prepRes", j2s(prepRes)); + + await walletClient.call(WalletApiOperation.ConfirmPeerPushCredit, { + transactionId: prepRes.transactionId, + }); + + await walletClient.call(WalletApiOperation.TestingWaitTransactionState, { + transactionId: prepRes.transactionId, + txState: { + major: TransactionMajorState.Pending, + minor: TransactionMinorState.MergeKycRequired, + }, + }); + + const txDet = await walletClient.call(WalletApiOperation.GetTransactionById, { + transactionId: prepRes.transactionId, + }); + + console.log("tx details", j2s(txDet)); + + const kycPaytoHash = txDet.kycPaytoHash; + + t.assertTrue(!!kycPaytoHash); + + await postAmlDecisionNoRules(t, { + amlPriv: amlKeypair.priv, + amlPub: amlKeypair.pub, + exchangeBaseUrl: exchange.baseUrl, + paytoHash: kycPaytoHash, + }); + + await walletClient.call(WalletApiOperation.TestingWaitTransactionState, { + transactionId: prepRes.transactionId, + txState: { + major: TransactionMajorState.Done, + }, + }); +} + +/** + * Initiate a push debit transaction, wait until the transaction + * is ready. + */ +async function doPeerPushDebit( + t: GlobalTestState, + args: { + walletClient: WalletClient; + amount: AmountString; + summary?: string; + }, +): Promise<{ + transactionId: string; + talerUri: string; +}> { + const purse_expiration = AbsoluteTime.toProtocolTimestamp( + AbsoluteTime.addDuration( + AbsoluteTime.now(), + Duration.fromSpec({ days: 2 }), + ), + ); + const initRet = await args.walletClient.call( + WalletApiOperation.InitiatePeerPushDebit, + { + partialContractTerms: { + amount: args.amount, + summary: args.summary ?? "Test P2P Payment", + purse_expiration, + }, + }, + ); + + await args.walletClient.call(WalletApiOperation.TestingWaitTransactionState, { + transactionId: initRet.transactionId, + txState: { + major: TransactionMajorState.Pending, + minor: TransactionMinorState.Ready, + }, + }); + + const txDet = await args.walletClient.call( + WalletApiOperation.GetTransactionById, + { + transactionId: initRet.transactionId, + }, + ); + + t.assertTrue(txDet.type === TransactionType.PeerPushDebit); + const talerUri = txDet.talerUri; + t.assertTrue(!!talerUri); + + return { + transactionId: initRet.transactionId, + talerUri, + }; +} + +runKycPeerPushTest.suites = ["wallet"]; diff --git a/packages/taler-harness/src/integrationtests/testrunner.ts b/packages/taler-harness/src/integrationtests/testrunner.ts index 629a9029a..d50b3e1ad 100644 --- a/packages/taler-harness/src/integrationtests/testrunner.ts +++ b/packages/taler-harness/src/integrationtests/testrunner.ts @@ -48,6 +48,7 @@ import { runExchangeTimetravelTest } from "./test-exchange-timetravel.js"; import { runFeeRegressionTest } from "./test-fee-regression.js"; import { runForcedSelectionTest } from "./test-forced-selection.js"; import { runKycExchangeWalletTest } from "./test-kyc-exchange-wallet.js"; +import { runKycPeerPushTest } from "./test-kyc-peer-push.js"; import { runKycThresholdWithdrawalTest } from "./test-kyc-threshold-withdrawal.js"; import { runKycTest } from "./test-kyc.js"; import { runLibeufinBankTest } from "./test-libeufin-bank.js"; @@ -248,6 +249,7 @@ const allTests: TestMainFunction[] = [ runWithdrawalIdempotentTest, runKycThresholdWithdrawalTest, runKycExchangeWalletTest, + runKycPeerPushTest, ]; export interface TestRunSpec { diff --git a/packages/taler-wallet-core/src/pay-peer-push-credit.ts b/packages/taler-wallet-core/src/pay-peer-push-credit.ts index 830e2bd1b..b7cead3fa 100644 --- a/packages/taler-wallet-core/src/pay-peer-push-credit.ts +++ b/packages/taler-wallet-core/src/pay-peer-push-credit.ts @@ -27,7 +27,6 @@ import { PeerContractTerms, PreparePeerPushCreditRequest, PreparePeerPushCreditResponse, - TalerErrorCode, TalerPreciseTimestamp, Transaction, TransactionAction, @@ -49,7 +48,6 @@ import { encodeCrock, getRandomBytes, j2s, - makeErrorDetail, parsePayPushUri, talerPaytoFromExchangeReserve, } from "@gnu-taler/taler-util"; @@ -59,7 +57,6 @@ import { TaskIdStr, TaskIdentifiers, TaskRunResult, - TaskRunResultType, TombstoneTag, TransactionContext, constructTaskIdentifier, @@ -67,7 +64,6 @@ import { } from "./common.js"; import { KycPendingInfo, - KycUserType, OperationRetryRecord, PeerPushCreditStatus, PeerPushPaymentIncomingRecord, @@ -199,7 +195,8 @@ export class PeerPushCreditTransactionContext implements TransactionContext { tag: TransactionType.PeerPushCredit, peerPushCreditId: pushInc.peerPushCreditId, }), - kycUrl: pushInc.kycUrl, + kycUrl: wg.kycUrl, + kycPaytoHash: wg.kycPending?.paytoHash, ...(wgRetryRecord?.lastError ? { error: wgRetryRecord.lastError } : {}), }; } @@ -221,6 +218,7 @@ export class PeerPushCreditTransactionContext implements TransactionContext { summary: peerContractTerms.summary, }, kycUrl: pushInc.kycUrl, + kycPaytoHash: pushInc.kycInfo?.paytoHash, timestamp: timestampPreciseFromDb(pushInc.timestamp), transactionId: constructTransactionIdentifier({ tag: TransactionType.PeerPushCredit, @@ -662,13 +660,20 @@ async function longpollKycStatus( peerPushCreditId: string, exchangeUrl: string, kycInfo: KycPendingInfo, - userType: KycUserType, ): Promise<TaskRunResult> { const ctx = new PeerPushCreditTransactionContext(wex, peerPushCreditId); - const url = new URL( - `kyc-check/${kycInfo.requirementRow}/${kycInfo.paytoHash}/${userType}`, - exchangeUrl, - ); + + // FIXME: What if this changes? Should be part of the p2p record + const mergeReserveInfo = await getMergeReserveInfo(wex, { + exchangeBaseUrl: exchangeUrl, + }); + + const sigResp = await wex.cryptoApi.signWalletKycAuth({ + accountPriv: mergeReserveInfo.reservePriv, + accountPub: mergeReserveInfo.reservePub, + }); + + const url = new URL(`kyc-check/${kycInfo.requirementRow}`, exchangeUrl); logger.info(`kyc url ${url.href}`); const kycStatusRes = await wex.ws.runLongpollQueueing( wex, @@ -677,6 +682,9 @@ async function longpollKycStatus( url.searchParams.set("timeout_ms", `${timeoutMs}`); return await wex.http.fetch(url.href, { method: "GET", + headers: { + ["Account-Owner-Signature"]: sigResp.sig, + }, cancellationToken: wex.cancellationToken, }); }, @@ -684,8 +692,6 @@ async function longpollKycStatus( if ( kycStatusRes.status === HttpStatusCode.Ok || - //FIXME: NoContent is not expected https://docs.taler.net/core/api-exchange.html#post--purses-$PURSE_PUB-merge - // remove after the exchange is fixed or clarified kycStatusRes.status === HttpStatusCode.NoContent ) { const transitionInfo = await wex.db.runReadWriteTx( @@ -709,7 +715,7 @@ async function longpollKycStatus( notifyTransition(wex, ctx.transactionId, transitionInfo); return TaskRunResult.progress(); } else if (kycStatusRes.status === HttpStatusCode.Accepted) { - // FIXME: Do we have to update the URL here? + // Access token / URL stays the same, just long-poll again. return TaskRunResult.longpollReturnedPending(); } else { throw Error(`unexpected response from kyc-check (${kycStatusRes.status})`); @@ -731,22 +737,34 @@ async function processPeerPushCreditKycRequired( ); const { peerPushCreditId } = peerInc; - const userType = "individual"; + // FIXME: What if this changes? Should be part of the p2p record + const mergeReserveInfo = await getMergeReserveInfo(wex, { + exchangeBaseUrl: peerInc.exchangeBaseUrl, + }); + + const sigResp = await wex.cryptoApi.signWalletKycAuth({ + accountPriv: mergeReserveInfo.reservePriv, + accountPub: mergeReserveInfo.reservePub, + }); + const url = new URL( - `kyc-check/${kycPending.requirement_row}/${kycPending.h_payto}/${userType}`, + `kyc-check/${kycPending.requirement_row}`, peerInc.exchangeBaseUrl, ); logger.info(`kyc url ${url.href}`); const kycStatusRes = await wex.http.fetch(url.href, { method: "GET", + headers: { + ["Account-Owner-Signature"]: sigResp.sig, + }, cancellationToken: wex.cancellationToken, }); + logger.info(`kyc result status ${kycStatusRes.status}`); + if ( kycStatusRes.status === HttpStatusCode.Ok || - //FIXME: NoContent is not expected https://docs.taler.net/core/api-exchange.html#post--purses-$PURSE_PUB-merge - // remove after the exchange is fixed or clarified kycStatusRes.status === HttpStatusCode.NoContent ) { logger.warn("kyc requested, but already fulfilled"); @@ -774,20 +792,9 @@ async function processPeerPushCreditKycRequired( const newTxState = computePeerPushCreditTransactionState(peerInc); await tx.peerPushCredit.put(peerInc); await ctx.updateTransactionMeta(tx); - // We'll remove this eventually! New clients should rely on the - // kycUrl field of the transaction, not the error code. - const res: TaskRunResult = { - type: TaskRunResultType.Error, - errorDetail: makeErrorDetail( - TalerErrorCode.WALLET_WITHDRAWAL_KYC_REQUIRED, - { - kycUrl: kycStatus.kyc_url, - }, - ), - }; return { transitionInfo: { oldTxState, newTxState }, - result: res, + result: TaskRunResult.progress(), }; }, ); @@ -808,6 +815,7 @@ async function handlePendingMerge( const amount = Amounts.parseOrThrow(contractTerms.amount); + // FIXME: What if this changes? Should be part of the p2p record const mergeReserveInfo = await getMergeReserveInfo(wex, { exchangeBaseUrl: peerInc.exchangeBaseUrl, }); @@ -1047,7 +1055,6 @@ export async function processPeerPushCredit( peerPushCreditId, peerInc.exchangeBaseUrl, peerInc.kycInfo, - "individual", ); } @@ -1133,7 +1140,7 @@ export function computePeerPushCreditTransactionState( case PeerPushCreditStatus.PendingMergeKycRequired: return { major: TransactionMajorState.Pending, - minor: TransactionMinorState.KycRequired, + minor: TransactionMinorState.MergeKycRequired, }; case PeerPushCreditStatus.PendingWithdrawing: return { |