aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--packages/taler-harness/src/harness/harness.ts6
-rw-r--r--packages/taler-harness/src/harness/helpers.ts19
-rw-r--r--packages/taler-harness/src/integrationtests/test-kyc.ts199
-rw-r--r--packages/taler-util/src/notifications.ts10
-rw-r--r--packages/taler-wallet-cli/src/index.ts6
-rw-r--r--packages/taler-wallet-core/src/operations/deposits.ts40
-rw-r--r--packages/taler-wallet-core/src/operations/withdraw.ts22
-rw-r--r--packages/taler-wallet-core/src/remote.ts30
8 files changed, 275 insertions, 57 deletions
diff --git a/packages/taler-harness/src/harness/harness.ts b/packages/taler-harness/src/harness/harness.ts
index 3659ea538..275592091 100644
--- a/packages/taler-harness/src/harness/harness.ts
+++ b/packages/taler-harness/src/harness/harness.ts
@@ -2028,9 +2028,9 @@ export class WalletClient {
return getClientFromRemoteWallet(this.remoteWallet);
}
- waitForNotificationCond(
- cond: (n: WalletNotification) => boolean,
- ): Promise<void> {
+ waitForNotificationCond<T>(
+ cond: (n: WalletNotification) => T | undefined | false,
+ ): Promise<T> {
return this.waiter.waitForNotificationCond(cond);
}
}
diff --git a/packages/taler-harness/src/harness/helpers.ts b/packages/taler-harness/src/harness/helpers.ts
index 59a37e4b8..4c2ca80a7 100644
--- a/packages/taler-harness/src/harness/helpers.ts
+++ b/packages/taler-harness/src/harness/helpers.ts
@@ -53,9 +53,14 @@ import {
MerchantServiceInterface,
setupDb,
WalletCli,
+ WalletClient,
+ WalletService,
WithAuthorization,
} from "./harness.js";
+/**
+ * @deprecated
+ */
export interface SimpleTestEnvironment {
commonDb: DbInfo;
bank: BankService;
@@ -65,6 +70,20 @@ export interface SimpleTestEnvironment {
wallet: WalletCli;
}
+/**
+ * Improved version of the simple test environment,
+ * with the daemonized wallet.
+ */
+export interface SimpleTestEnvironmentNg {
+ commonDb: DbInfo;
+ bank: BankService;
+ exchange: ExchangeService;
+ exchangeBankAccount: HarnessExchangeBankAccount;
+ merchant: MerchantService;
+ walletClient: WalletClient;
+ walletService: WalletService;
+}
+
export interface EnvOptions {
/**
* If provided, enable age restrictions with the specified age mask string.
diff --git a/packages/taler-harness/src/integrationtests/test-kyc.ts b/packages/taler-harness/src/integrationtests/test-kyc.ts
index c652c86fa..b08db66f7 100644
--- a/packages/taler-harness/src/integrationtests/test-kyc.ts
+++ b/packages/taler-harness/src/integrationtests/test-kyc.ts
@@ -17,7 +17,13 @@
/**
* Imports.
*/
-import { Duration } from "@gnu-taler/taler-util";
+import { Duration, j2s, NotificationType } from "@gnu-taler/taler-util";
+import {
+ BankAccessApi,
+ BankApi,
+ NodeHttpLib,
+ WalletApiOperation,
+} from "@gnu-taler/taler-wallet-core";
import { CoinConfig, defaultCoinConfig } from "../harness/denomStructures.js";
import {
BankService,
@@ -26,20 +32,17 @@ import {
GlobalTestState,
MerchantService,
setupDb,
- WalletCli,
+ WalletClient,
+ WalletService,
} from "../harness/harness.js";
-import {
- withdrawViaBank,
- makeTestPayment,
- EnvOptions,
- SimpleTestEnvironment,
-} from "../harness/helpers.js";
+import { EnvOptions, SimpleTestEnvironmentNg } from "../harness/helpers.js";
+import * as http from "node:http";
export async function createKycTestkudosEnvironment(
t: GlobalTestState,
coinConfig: CoinConfig[] = defaultCoinConfig.map((x) => x("TESTKUDOS")),
opts: EnvOptions = {},
-): Promise<SimpleTestEnvironment> {
+): Promise<SimpleTestEnvironmentNg> {
const db = await setupDb(t);
const bank = await BankService.create(t, {
@@ -117,11 +120,11 @@ export async function createKycTestkudosEnvironment(
config.setString(
myprov,
"kyc_oauth2_info_url",
- "http://localhost:6666/oauth/v2/login",
+ "http://localhost:6666/oauth/v2/info",
);
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(myprov, "kyc_oauth2_post_url", "https://taler.net");
config.setString(
"kyc-legitimization-withdraw1",
@@ -167,40 +170,186 @@ export async function createKycTestkudosEnvironment(
),
});
- console.log("setup done!");
+ const walletService = new WalletService(t, {
+ name: "wallet",
+ useInMemoryDb: true,
+ });
+ await walletService.start();
+ await walletService.pingUntilAvailable();
- const wallet = new WalletCli(t);
+ const walletClient = new WalletClient({
+ unixPath: walletService.socketPath,
+ onNotification(n) {
+ console.log("got notification", n);
+ },
+ });
+ await walletClient.connect();
+ await walletClient.client.call(WalletApiOperation.InitWallet, {
+ skipDefaults: true,
+ });
+
+ console.log("setup done!");
return {
commonDb: db,
exchange,
merchant,
- wallet,
+ walletClient,
+ walletService,
bank,
exchangeBankAccount,
};
}
+interface TestfakeKycService {
+ stop: () => void;
+}
+
+function splitInTwoAt(s: string, separator: string): [string, string] {
+ const idx = s.indexOf(separator);
+ if (idx === -1) {
+ return [s, ""];
+ }
+ return [s.slice(0, idx), s.slice(idx + 1)];
+}
+
+/**
+ * Testfake for the kyc service that the exchange talks to.
+ */
+async function runTestfakeKycService(): Promise<TestfakeKycService> {
+ const server = http.createServer((req, res) => {
+ const requestUrl = req.url!;
+ console.log(`kyc: got ${req.method} request`, requestUrl);
+
+ const [path, query] = splitInTwoAt(requestUrl, "?");
+
+ const qp = new URLSearchParams(query);
+
+ if (path === "/oauth/v2/login") {
+ // Usually this would render some HTML page for the user to log in,
+ // but we return JSON here.
+ const redirUri = new URL(qp.get("redirect_uri")!);
+ redirUri.searchParams.set("code", "code_is_ok");
+ res.writeHead(200, { "Content-Type": "application/json" });
+ res.end(
+ JSON.stringify({
+ redirect_uri: redirUri.href,
+ }),
+ );
+ } else if (path === "/oauth/v2/token") {
+ let reqBody = "";
+ req.on("data", (x) => {
+ reqBody += x;
+ });
+
+ req.on("end", () => {
+ console.log("login request body:", reqBody);
+
+ res.writeHead(200, { "Content-Type": "application/json" });
+ // Normally, the access_token would also include which user we're trying
+ // to get info about, but we (for now) skip it in this test.
+ res.end(
+ JSON.stringify({
+ access_token: "exchange_access_token",
+ token_type: "Bearer",
+ }),
+ );
+ });
+ } else if (path === "/oauth/v2/info") {
+ console.log("authorization header:", req.headers.authorization);
+ res.writeHead(200, { "Content-Type": "application/json" });
+ res.end(
+ JSON.stringify({
+ status: "success",
+ data: {
+ id: "foobar",
+ },
+ }),
+ );
+ } else {
+ res.writeHead(400, { "Content-Type": "application/json" });
+ res.end(JSON.stringify({ code: 1, message: "bad request" }));
+ }
+ });
+ await new Promise<void>((resolve, reject) => {
+ server.listen(6666, () => resolve());
+ });
+ return {
+ stop() {
+ server.close();
+ },
+ };
+}
+
export async function runKycTest(t: GlobalTestState) {
// Set up test environment
- const { wallet, bank, exchange, merchant } =
+ const { walletClient, bank, exchange, merchant } =
await createKycTestkudosEnvironment(t);
+ const kycServer = await runTestfakeKycService();
+
// Withdraw digital cash into the wallet.
- await withdrawViaBank(t, { wallet, bank, exchange, amount: "TESTKUDOS:20" });
+ const amount = "TESTKUDOS:20";
+ const user = await BankApi.createRandomBankUser(bank);
+ const wop = await BankAccessApi.createWithdrawalOperation(bank, user, amount);
- const order = {
- summary: "Buy me!",
- amount: "TESTKUDOS:5",
- fulfillment_url: "taler://fulfillment-success/thx",
- };
+ // Hand it to the wallet
+
+ await walletClient.client.call(
+ WalletApiOperation.GetWithdrawalDetailsForUri,
+ {
+ talerWithdrawUri: wop.taler_withdraw_uri,
+ },
+ );
+
+ // Withdraw
+
+ const kycNotificationCond = walletClient.waitForNotificationCond((x) => {
+ if (x.type === NotificationType.WithdrawalKycRequested) {
+ return x;
+ }
+ return false;
+ });
+
+ const withdrawalDoneCond = walletClient.waitForNotificationCond(
+ (x) => x.type === NotificationType.WithdrawGroupFinished,
+ );
+
+ await walletClient.client.call(
+ WalletApiOperation.AcceptBankIntegratedWithdrawal,
+ {
+ exchangeBaseUrl: exchange.baseUrl,
+ talerWithdrawUri: wop.taler_withdraw_uri,
+ },
+ );
+
+ // Confirm it
+
+ await BankApi.confirmWithdrawalOperation(bank, user, wop);
+
+ const kycNotif = await kycNotificationCond;
+
+ console.log("got kyc notification:", j2s(kycNotif));
+
+ // We now simulate the user interacting with the KYC service,
+ // which would usually done in the browser.
+
+ const httpClient = new NodeHttpLib();
+ const kycServerResp = await httpClient.get(kycNotif.kycUrl);
+ const kycLoginResp = await kycServerResp.json();
+ console.log("kyc server resp:", j2s(kycLoginResp));
+ const kycProofUrl = kycLoginResp.redirect_uri;
+ const proofHttpResp = await httpClient.get(kycProofUrl);
+ console.log("proof resp status", proofHttpResp.status);
+ console.log("resp headers", proofHttpResp.headers.toJSON());
+
+ // Now that KYC is done, withdrawal should finally succeed.
+
+ await withdrawalDoneCond;
- await makeTestPayment(t, { wallet, merchant, order });
- await wallet.runUntilDone();
+ kycServer.stop();
}
runKycTest.suites = ["wallet"];
-// See bugs.taler.net/n/7599
-runKycTest.experimental = true;
diff --git a/packages/taler-util/src/notifications.ts b/packages/taler-util/src/notifications.ts
index bc1c4b71f..9d3ca32b0 100644
--- a/packages/taler-util/src/notifications.ts
+++ b/packages/taler-util/src/notifications.ts
@@ -62,6 +62,7 @@ export enum NotificationType {
PendingOperationProcessed = "pending-operation-processed",
ProposalRefused = "proposal-refused",
ReserveRegisteredWithBank = "reserve-registered-with-bank",
+ WithdrawalKycRequested = "withdrawal-kyc-requested",
DepositOperationError = "deposit-operation-error",
}
@@ -117,6 +118,12 @@ export interface RefreshMeltedNotification {
type: NotificationType.RefreshMelted;
}
+export interface WithdrawalKycRequested {
+ type: NotificationType.WithdrawalKycRequested;
+ transactionId: string;
+ kycUrl: string;
+}
+
export interface RefreshRevealedNotification {
type: NotificationType.RefreshRevealed;
}
@@ -285,4 +292,5 @@ export type WalletNotification =
| ProposalRefusedNotification
| ReserveRegisteredWithBankNotification
| ReserveNotYetFoundNotification
- | PayOperationSuccessNotification;
+ | PayOperationSuccessNotification
+ | WithdrawalKycRequested;
diff --git a/packages/taler-wallet-cli/src/index.ts b/packages/taler-wallet-cli/src/index.ts
index 6641dab09..7e942ede7 100644
--- a/packages/taler-wallet-cli/src/index.ts
+++ b/packages/taler-wallet-cli/src/index.ts
@@ -228,9 +228,9 @@ export interface WalletContext {
* Return a promise that resolves after the wallet has emitted a notification
* that meets the criteria of the "cond" predicate.
*/
- waitForNotificationCond(
- cond: (n: WalletNotification) => boolean,
- ): Promise<void>;
+ waitForNotificationCond<T>(
+ cond: (n: WalletNotification) => T | false | undefined,
+ ): Promise<T>;
}
async function createLocalWallet(
diff --git a/packages/taler-wallet-core/src/operations/deposits.ts b/packages/taler-wallet-core/src/operations/deposits.ts
index e97738b55..4ff6a65cd 100644
--- a/packages/taler-wallet-core/src/operations/deposits.ts
+++ b/packages/taler-wallet-core/src/operations/deposits.ts
@@ -38,8 +38,10 @@ import {
hashTruncate32,
hashWire,
HttpStatusCode,
+ j2s,
Logger,
MerchantContractTerms,
+ NotificationType,
parsePaytoUri,
PayCoinSelection,
PrepareDepositRequest,
@@ -61,7 +63,7 @@ import {
TransactionStatus,
} from "../db.js";
import { TalerError } from "../errors.js";
-import { checkKycStatus } from "../index.js";
+import { checkWithdrawalKycStatus, KycPendingInfo, KycUserType } from "../index.js";
import { InternalWalletState } from "../internal-wallet-state.js";
import { readSuccessResponseJsonOrThrow } from "../util/http.js";
import { OperationAttemptResult } from "../util/retries.js";
@@ -80,6 +82,40 @@ import { getTotalRefreshCost } from "./refresh.js";
*/
const logger = new Logger("deposits.ts");
+
+export async function checkDepositKycStatus(
+ ws: InternalWalletState,
+ exchangeUrl: string,
+ kycInfo: KycPendingInfo,
+ userType: KycUserType,
+): Promise<void> {
+ const url = new URL(
+ `kyc-check/${kycInfo.requirementRow}/${kycInfo.paytoHash}/${userType}`,
+ exchangeUrl,
+ );
+ logger.info(`kyc url ${url.href}`);
+ const kycStatusReq = await ws.http.fetch(url.href, {
+ method: "GET",
+ });
+ if (kycStatusReq.status === HttpStatusCode.Ok) {
+ logger.warn("kyc requested, but already fulfilled");
+ return;
+ } else if (kycStatusReq.status === HttpStatusCode.Accepted) {
+ const kycStatus = await kycStatusReq.json();
+ logger.info(`kyc status: ${j2s(kycStatus)}`);
+ // FIXME: This error code is totally wrong
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_WITHDRAWAL_KYC_REQUIRED,
+ {
+ kycUrl: kycStatus.kyc_url,
+ },
+ `KYC check required for deposit`,
+ );
+ } else {
+ throw Error(`unexpected response from kyc-check (${kycStatusReq.status})`);
+ }
+}
+
/**
* @see {processDepositGroup}
*/
@@ -162,7 +198,7 @@ export async function processDepositGroup(
const paytoHash = encodeCrock(
hashTruncate32(stringToBytes(depositGroup.wire.payto_uri + "\0")),
);
- await checkKycStatus(
+ await checkDepositKycStatus(
ws,
perm.exchange_url,
{ paytoHash, requirementRow },
diff --git a/packages/taler-wallet-core/src/operations/withdraw.ts b/packages/taler-wallet-core/src/operations/withdraw.ts
index 5c9854c0f..28754c77e 100644
--- a/packages/taler-wallet-core/src/operations/withdraw.ts
+++ b/packages/taler-wallet-core/src/operations/withdraw.ts
@@ -1250,12 +1250,7 @@ export async function processWithdrawalGroup(
if (numKycRequired > 0) {
if (kycInfo) {
- await checkKycStatus(
- ws,
- withdrawalGroup.exchangeBaseUrl,
- kycInfo,
- "individual",
- );
+ await checkWithdrawalKycStatus(ws, withdrawalGroup, kycInfo, "individual");
return {
type: OperationAttemptResultType.Pending,
result: undefined,
@@ -1293,12 +1288,13 @@ export async function processWithdrawalGroup(
};
}
-export async function checkKycStatus(
+export async function checkWithdrawalKycStatus(
ws: InternalWalletState,
- exchangeUrl: string,
+ wg: WithdrawalGroupRecord,
kycInfo: KycPendingInfo,
userType: KycUserType,
): Promise<void> {
+ const exchangeUrl = wg.exchangeBaseUrl;
const url = new URL(
`kyc-check/${kycInfo.requirementRow}/${kycInfo.paytoHash}/${userType}`,
exchangeUrl,
@@ -1307,12 +1303,20 @@ export async function checkKycStatus(
const kycStatusReq = await ws.http.fetch(url.href, {
method: "GET",
});
- logger.warn("kyc requested, but already fulfilled");
if (kycStatusReq.status === HttpStatusCode.Ok) {
+ logger.warn("kyc requested, but already fulfilled");
return;
} else if (kycStatusReq.status === HttpStatusCode.Accepted) {
const kycStatus = await kycStatusReq.json();
logger.info(`kyc status: ${j2s(kycStatus)}`);
+ ws.notify({
+ type: NotificationType.WithdrawalKycRequested,
+ kycUrl: kycStatus.kyc_url,
+ transactionId: makeTransactionId(
+ TransactionType.Withdrawal,
+ wg.withdrawalGroupId,
+ ),
+ });
throw TalerError.fromDetail(
TalerErrorCode.WALLET_WITHDRAWAL_KYC_REQUIRED,
{
diff --git a/packages/taler-wallet-core/src/remote.ts b/packages/taler-wallet-core/src/remote.ts
index 2628fea07..bc0be9d30 100644
--- a/packages/taler-wallet-core/src/remote.ts
+++ b/packages/taler-wallet-core/src/remote.ts
@@ -145,9 +145,14 @@ export function getClientFromRemoteWallet(
export interface WalletNotificationWaiter {
notify(wn: WalletNotification): void;
- waitForNotificationCond(
- cond: (n: WalletNotification) => boolean,
- ): Promise<void>;
+ waitForNotificationCond<T>(
+ cond: (n: WalletNotification) => T | false | undefined,
+ ): Promise<T>;
+}
+
+interface NotificationCondEntry<T> {
+ condition: (n: WalletNotification) => T | false | undefined;
+ promiseCapability: OpenedPromise<T>;
}
/**
@@ -157,22 +162,19 @@ export interface WalletNotificationWaiter {
export function makeNotificationWaiter(): WalletNotificationWaiter {
// Bookkeeping for waiting on notification conditions
let nextCondIndex = 1;
- const condMap: Map<
- number,
- {
- condition: (n: WalletNotification) => boolean;
- promiseCapability: OpenedPromise<void>;
- }
- > = new Map();
+ const condMap: Map<number, NotificationCondEntry<any>> = new Map();
function onNotification(n: WalletNotification) {
condMap.forEach((cond, condKey) => {
- if (cond.condition(n)) {
- cond.promiseCapability.resolve();
+ const res = cond.condition(n);
+ if (res) {
+ cond.promiseCapability.resolve(res);
}
});
}
- function waitForNotificationCond(cond: (n: WalletNotification) => boolean) {
- const promCap = openPromise<void>();
+ function waitForNotificationCond<T>(
+ cond: (n: WalletNotification) => T | false | undefined,
+ ) {
+ const promCap = openPromise<T>();
condMap.set(nextCondIndex++, {
condition: cond,
promiseCapability: promCap,