aboutsummaryrefslogtreecommitdiff
path: root/packages
diff options
context:
space:
mode:
authorFlorian Dold <florian@dold.me>2024-08-19 15:52:51 +0200
committerFlorian Dold <florian@dold.me>2024-08-19 15:52:51 +0200
commite7971ba0abaf272f3293c8b3b5b46be89e27ddb1 (patch)
treeae500ad382450a932b30b1fe443b0b6001964766 /packages
parent296afa3d9ccc7f8102dab1a3ae9b48c8ca84537e (diff)
downloadwallet-core-e7971ba0abaf272f3293c8b3b5b46be89e27ddb1.tar.xz
wallet-core: update KYC implementation for peer-push, add test
Diffstat (limited to 'packages')
-rw-r--r--packages/taler-harness/src/integrationtests/test-kyc-peer-push.ts347
-rw-r--r--packages/taler-harness/src/integrationtests/testrunner.ts2
-rw-r--r--packages/taler-wallet-core/src/pay-peer-push-credit.ts69
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 {