commit 3c04a0703f3d3a3b05c68d470539aa8f0b0b5dc7
parent 357f9d6eef887a5616c67c58dffaea78f1e5eb41
Author: Antoine A <>
Date: Thu, 24 Apr 2025 10:44:45 +0200
wallet-core: clean P2P and KYC status
Diffstat:
6 files changed, 173 insertions(+), 180 deletions(-)
diff --git a/packages/kyc-ui/src/pages/TriggerKyc.tsx b/packages/kyc-ui/src/pages/TriggerKyc.tsx
@@ -13,6 +13,7 @@
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/>
*/
+import { CancellationToken } from "@gnu-taler/taler-util";
import {
AbsoluteTime,
AccessToken,
@@ -90,44 +91,43 @@ export function TriggerKyc({ onKycStarted }: Props): VNode {
const paytoHash = kycAccount;
async function check() {
const { signingKey } = await accountPromise;
- const result = await lib.exchange.checkKycStatus(signingKey, paytoHash);
- if (result.type === "ok") {
- if (result.body) {
- onKycStarted(result.body.access_token);
- } else {
+ const result = await lib.exchange.checkKycStatus(signingKey, paytoHash, CancellationToken.CONTINUE);
+ switch (result.case) {
+ case "ok":
console.log("empty body");
- }
- } else {
- switch (result.case) {
- case HttpStatusCode.Forbidden: {
- notify({
- type: "error",
- title: i18n.str`could not create token`,
- description: i18n.str`access denied`,
- when: AbsoluteTime.now(),
- });
+ break;
+ case HttpStatusCode.Ok:
+ case HttpStatusCode.Accepted:
+ onKycStarted(result.body.access_token);
+ break
+ case HttpStatusCode.Forbidden: {
+ notify({
+ type: "error",
+ title: i18n.str`could not create token`,
+ description: i18n.str`access denied`,
+ when: AbsoluteTime.now(),
+ });
- break;
- }
- case HttpStatusCode.NotFound: {
- notify({
- type: "error",
- title: i18n.str`could not create token`,
- description: i18n.str`not found`,
- when: AbsoluteTime.now(),
- });
+ break;
+ }
+ case HttpStatusCode.NotFound: {
+ notify({
+ type: "error",
+ title: i18n.str`could not create token`,
+ description: i18n.str`not found`,
+ when: AbsoluteTime.now(),
+ });
- break;
- }
- case HttpStatusCode.Conflict: {
- notify({
- type: "error",
- title: i18n.str`could not create token`,
- description: i18n.str`conflict`,
- when: AbsoluteTime.now(),
- });
- break;
- }
+ break;
+ }
+ case HttpStatusCode.Conflict: {
+ notify({
+ type: "error",
+ title: i18n.str`could not create token`,
+ description: i18n.str`conflict`,
+ when: AbsoluteTime.now(),
+ });
+ break;
}
}
}
diff --git a/packages/taler-harness/src/index.ts b/packages/taler-harness/src/index.ts
@@ -23,6 +23,7 @@ import {
AmountString,
Amounts,
BalancesResponse,
+ CancellationToken,
Configuration,
Duration,
EddsaPrivP,
@@ -919,9 +920,9 @@ deploymentCli
contact_data:
email || phone
? {
- email: email,
- phone: phone,
- }
+ email: email,
+ phone: phone,
+ }
: undefined,
});
@@ -1266,10 +1267,10 @@ deploymentCli
credit_facade_credentials:
bankUser && bankPassword
? {
- type: "basic",
- username: bankUser,
- password: bankPassword,
- }
+ type: "basic",
+ username: bankUser,
+ password: bankPassword,
+ }
: undefined,
},
);
@@ -1746,6 +1747,7 @@ merchantCli
const kyc_status = await exchangeApi.checkKycStatus(
merchant_priv,
encodeCrock(hashNormalizedPaytoUri(parsePaytoUri(payto_uri)!)),
+ CancellationToken.CONTINUE
);
return {
payto_uri,
diff --git a/packages/taler-util/src/http-client/exchange.ts b/packages/taler-util/src/http-client/exchange.ts
@@ -32,7 +32,6 @@ import {
opEmptySuccess,
opFixedSuccess,
opKnownAlternativeFailure,
- opKnownFailure,
opKnownHttpFailure,
opSuccessFromHttp,
opUnknownFailure,
@@ -48,6 +47,7 @@ import {
codecForTalerCommonConfigResponse,
} from "../types-taler-common.js";
import {
+ AccountKycStatus,
AmlDecisionRequest,
BatchWithdrawResponse,
ExchangeKycUploadFormRequest,
@@ -55,6 +55,7 @@ import {
ExchangeMergeConflictResponse,
ExchangeMergeSuccessResponse,
ExchangePurseMergeRequest,
+ ExchangeReservePurseRequest,
ExchangeVersionResponse,
KycRequirementInformationId,
LegitimizationNeededResponse,
@@ -157,12 +158,11 @@ export class TalerExchangeHttpClient {
case HttpStatusCode.Ok:
const buffer = await resp.bytes();
const uintar = new Uint8Array(buffer);
-
return opFixedSuccess(uintar);
case HttpStatusCode.NotFound:
return opKnownHttpFailure(resp.status, resp);
default:
- return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ return opUnknownHttpFailure(resp);
}
}
/**
@@ -214,7 +214,7 @@ export class TalerExchangeHttpClient {
case HttpStatusCode.NotFound:
return opKnownHttpFailure(resp.status, resp);
default:
- return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ return opUnknownHttpFailure(resp);
}
}
@@ -232,7 +232,7 @@ export class TalerExchangeHttpClient {
case HttpStatusCode.Ok:
return opSuccessFromHttp(resp, codecForExchangeKeysResponse());
default:
- return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ return opUnknownHttpFailure(resp);
}
}
@@ -394,13 +394,9 @@ export class TalerExchangeHttpClient {
codecForAny() as Codec<BatchWithdrawResponse>,
);
case HttpStatusCode.Forbidden:
- return opKnownHttpFailure(resp.status, resp);
case HttpStatusCode.BadRequest:
- return opKnownHttpFailure(resp.status, resp);
case HttpStatusCode.NotFound:
- return opKnownHttpFailure(resp.status, resp);
case HttpStatusCode.Conflict:
- return opKnownHttpFailure(resp.status, resp);
case HttpStatusCode.Gone:
return opKnownHttpFailure(resp.status, resp);
case HttpStatusCode.UnavailableForLegalReasons:
@@ -410,7 +406,7 @@ export class TalerExchangeHttpClient {
codecForLegitimizationNeededResponse(),
);
default:
- return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ return opUnknownHttpFailure(resp);
}
}
@@ -581,11 +577,11 @@ export class TalerExchangeHttpClient {
*
* https://docs.taler.net/core/api-exchange.html#post--purses-$PURSE_PUB-merge
*/
- async postPurseMerge(args: {
- pursePub: string;
- body: ExchangePurseMergeRequest;
- cancellationToken: CancellationToken;
- }): Promise<
+ async postPurseMerge(
+ pursePub: string,
+ body: ExchangePurseMergeRequest,
+ cancellationToken: CancellationToken
+ ): Promise<
| OperationOk<ExchangeMergeSuccessResponse>
| OperationAlternative<
HttpStatusCode.UnavailableForLegalReasons,
@@ -598,14 +594,14 @@ export class TalerExchangeHttpClient {
| OperationFail<HttpStatusCode.NotFound>
| OperationFail<HttpStatusCode.Gone>
> {
- const mergePurseUrl = new URL(
- `purses/${args.pursePub}/merge`,
+ const url = new URL(
+ `purses/${pursePub}/merge`,
this.baseUrl,
);
- const resp = await this.httpLib.fetch(mergePurseUrl.href, {
+ const resp = await this.httpLib.fetch(url.href, {
method: "POST",
- body: args.body,
- cancellationToken: args.cancellationToken
+ body,
+ cancellationToken
});
switch (resp.status) {
case HttpStatusCode.Ok:
@@ -634,8 +630,39 @@ export class TalerExchangeHttpClient {
* https://docs.taler.net/core/api-exchange.html#post--reserves-$RESERVE_PUB-purse
*
*/
- async createPurseFromReserve(): Promise<never> {
- throw Error("not yet implemented");
+ async createPurseFromReserve(
+ pursePub: string,
+ body: ExchangeReservePurseRequest,
+ cancellationToken: CancellationToken,
+ ): Promise<
+ | OperationOk<void>
+ | OperationAlternative<
+ HttpStatusCode.UnavailableForLegalReasons,
+ LegitimizationNeededResponse
+ >
+ > {
+ const url = new URL(
+ `reserves/${pursePub}/purse`,
+ this.baseUrl,
+ );
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "POST",
+ body,
+ cancellationToken
+ });
+ switch (resp.status) {
+ case HttpStatusCode.Ok:
+ // FIXME: parse PurseCreateSuccessResponse
+ return opSuccessFromHttp(resp, codecForAny());
+ case HttpStatusCode.UnavailableForLegalReasons:
+ return opKnownAlternativeFailure(
+ resp,
+ resp.status,
+ codecForLegitimizationNeededResponse(),
+ );
+ default:
+ return opUnknownHttpFailure(resp);
+ }
}
/**
@@ -699,17 +726,25 @@ export class TalerExchangeHttpClient {
}
/**
- * https://docs.taler.net/core/api-exchange.html#get--kyc-check-$H_PAYTO
+ * https://docs.taler.net/core/api-exchange.html#get--kyc-check-$H_NORMALIZED_PAYTO
*
*/
async checkKycStatus(
- signingKey: EddsaPrivP,
+ signingKey: EddsaPrivP | string,
paytoHash: string,
+ cancellationToken: CancellationToken,
params: {
timeout?: number;
awaitAuth?: boolean;
} = {},
- ) {
+ ): Promise<
+ | OperationOk<void>
+ | OperationAlternative<HttpStatusCode.Ok, AccountKycStatus>
+ | OperationAlternative<HttpStatusCode.Accepted, AccountKycStatus>
+ | OperationFail<HttpStatusCode.Forbidden>
+ | OperationFail<HttpStatusCode.NotFound>
+ | OperationFail<HttpStatusCode.Conflict>
+ > {
const url = new URL(`kyc-check/${paytoHash}`, this.baseUrl);
if (params.timeout !== undefined) {
@@ -719,28 +754,27 @@ export class TalerExchangeHttpClient {
url.searchParams.set("await_auth", params.awaitAuth ? "YES" : "NO");
}
+ const signature = typeof signingKey === 'string' ? signingKey : encodeCrock(signKycAuth(signingKey));
+
const resp = await this.httpLib.fetch(url.href, {
- method: "GET",
headers: {
- "Account-Owner-Signature": encodeCrock(signKycAuth(signingKey)),
+ "Account-Owner-Signature": signature,
},
+ cancellationToken
});
switch (resp.status) {
case HttpStatusCode.Ok:
- return opSuccessFromHttp(resp, codecForAccountKycStatus());
case HttpStatusCode.Accepted:
- return opSuccessFromHttp(resp, codecForAccountKycStatus());
+ return opKnownAlternativeFailure(resp, resp.status, codecForAccountKycStatus());
case HttpStatusCode.NoContent:
return opFixedSuccess(undefined);
case HttpStatusCode.Forbidden:
- return opKnownHttpFailure(resp.status, resp);
case HttpStatusCode.NotFound:
- return opKnownHttpFailure(resp.status, resp);
case HttpStatusCode.Conflict:
return opKnownHttpFailure(resp.status, resp);
default:
- return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ return opUnknownHttpFailure(resp);
}
}
diff --git a/packages/taler-wallet-core/src/pay-peer-pull-credit.ts b/packages/taler-wallet-core/src/pay-peer-pull-credit.ts
@@ -31,6 +31,7 @@ import {
ScopeType,
TalerErrorCode,
TalerErrorDetail,
+ TalerExchangeHttpClient,
TalerPreciseTimestamp,
TalerProtocolTimestamp,
TalerUriAction,
@@ -46,8 +47,6 @@ import {
assertUnreachable,
checkDbInvariant,
codecForAccountKycStatus,
- codecForAny,
- codecForLegitimizationNeededResponse,
encodeCrock,
getRandomBytes,
j2s,
@@ -364,10 +363,7 @@ export class PeerPullCreditTransactionContext implements TransactionContext {
exchangeBaseUrl: wsr.exchangeBaseUrl,
contractPriv: wsr.wgInfo.contractPriv,
}),
- transactionId: constructTransactionIdentifier({
- tag: TransactionType.PeerPullCredit,
- pursePub: pullCredit.pursePub,
- }),
+ transactionId: this.transactionId,
abortReason: pullCredit.abortReason,
failReason: pullCredit.failReason,
// FIXME: Is this the KYC URL of the withdrawal group?!
@@ -402,10 +398,7 @@ export class PeerPullCreditTransactionContext implements TransactionContext {
exchangeBaseUrl: pullCredit.exchangeBaseUrl,
contractPriv: pullCredit.contractPriv,
}),
- transactionId: constructTransactionIdentifier({
- tag: TransactionType.PeerPullCredit,
- pursePub: pullCredit.pursePub,
- }),
+ transactionId: this.transactionId,
kycUrl,
kycAccessToken: pullCredit.kycAccessToken,
kycPaytoHash: pullCredit.kycPaytoHash,
@@ -633,9 +626,7 @@ async function queryPurseForPeerPullCredit(
const reserve = await wex.db.runReadOnlyTx(
{ storeNames: ["reserves"] },
- async (tx) => {
- return await tx.reserves.get(pullIni.mergeReserveRowId);
- },
+ (tx) => tx.reserves.get(pullIni.mergeReserveRowId)
);
if (!reserve) {
@@ -848,29 +839,31 @@ async function handlePeerPullCreditCreatePurse(
econtract: econtractResp.econtract,
};
- logger.info(`reserve purse request: ${j2s(reservePurseReqBody)}`);
-
- const reservePurseMergeUrl = new URL(
- `reserves/${mergeReserve.reservePub}/purse`,
+ const exchangeClient = new TalerExchangeHttpClient(
pullIni.exchangeBaseUrl,
+ wex.http,
);
- const httpResp = await cancelableFetch(wex, reservePurseMergeUrl, {
- method: "POST",
- body: reservePurseReqBody,
- });
+ logger.info(`reserve purse request: ${j2s(reservePurseReqBody)}`);
- if (httpResp.status === HttpStatusCode.UnavailableForLegalReasons) {
- const respJson = await httpResp.json();
- const kycPending = codecForLegitimizationNeededResponse().decode(respJson);
- logger.info(`kyc uuid response: ${j2s(kycPending)}`);
- return processPeerPullCreditKycRequired(wex, pullIni, kycPending.h_payto);
+ const httpResp = await exchangeClient.createPurseFromReserve(
+ mergeReserve.reservePub,
+ reservePurseReqBody,
+ wex.cancellationToken
+ )
+
+ switch (httpResp.case) {
+ case "ok":
+ logger.info(`reserve merge response: ${j2s((await httpResp).body)}`);
+ break;
+ case HttpStatusCode.UnavailableForLegalReasons: {
+ logger.info(`kyc uuid response: ${j2s(httpResp.body)}`);
+ return processPeerPullCreditKycRequired(wex, pullIni, httpResp.body.h_payto);
+ }
+ default:
+ assertUnreachable(httpResp);
}
- const resp = await readSuccessResponseJsonOrThrow(httpResp, codecForAny());
-
- logger.info(`reserve merge response: ${j2s(resp)}`);
-
await ctx.transition({}, async (rec, _) => {
rec.status = PeerPullPaymentCreditStatus.PendingReady
return TransitionResultType.Transition
@@ -894,19 +887,12 @@ export async function processPeerPullCredit(
throw Error("peer pull payment initiation not found in database");
}
- const retryTag = constructTaskIdentifier({
- tag: PendingTaskType.PeerPullCredit,
- pursePub,
- });
-
- logger.trace(`processing ${retryTag}, status=${pullIni.status}`);
-
const ctx = new PeerPullCreditTransactionContext(wex, pullIni.pursePub);
+ logger.trace(`processing ${ctx.taskId}, status=${pullIni.status}`);
switch (pullIni.status) {
- case PeerPullPaymentCreditStatus.Done: {
+ case PeerPullPaymentCreditStatus.Done:
return TaskRunResult.finished();
- }
case PeerPullPaymentCreditStatus.PendingReady:
return queryPurseForPeerPullCredit(wex, pullIni);
case PeerPullPaymentCreditStatus.PendingMergeKycRequired: {
@@ -966,8 +952,7 @@ async function processPeerPullCreditBalanceKyc(
);
if (checkRes.result === "ok") {
return checkRes;
- }
- if (
+ } else if (
peerInc.status === PeerPullPaymentCreditStatus.PendingBalanceKycInit &&
checkRes.walletKycStatus === ExchangeWalletKycStatus.Legi
) {
@@ -979,13 +964,10 @@ async function processPeerPullCreditBalanceKyc(
});
return undefined;
},
- filterNotification(notif) {
- return (
- (notif.type === NotificationType.ExchangeStateTransition &&
- notif.exchangeBaseUrl === exchangeBaseUrl) ||
- notif.type === NotificationType.BalanceChange
- );
- },
+ filterNotification: (notif) =>
+ (notif.type === NotificationType.ExchangeStateTransition &&
+ notif.exchangeBaseUrl === exchangeBaseUrl) ||
+ notif.type === NotificationType.BalanceChange
});
if (ret.result === "ok") {
@@ -1026,43 +1008,32 @@ async function processPeerPullCreditKycRequired(
accountPub: mergeReserveInfo.reservePub,
});
- const url = new URL(`kyc-check/${kycPayoHash}`, peerIni.exchangeBaseUrl);
+ const exchangeClient = new TalerExchangeHttpClient(
+ peerIni.exchangeBaseUrl,
+ wex.http
+ )
+ const res = await exchangeClient.checkKycStatus(sigResp.sig, kycPayoHash, wex.cancellationToken);
- logger.info(`kyc url ${url.href}`);
- const kycStatusRes = await cancelableFetch(wex, url, {
- headers: {
- ["Account-Owner-Signature"]: sigResp.sig,
- }
- });
-
- if (
- kycStatusRes.status === HttpStatusCode.Ok ||
- kycStatusRes.status === HttpStatusCode.NoContent
- ) {
- logger.warn("kyc requested, but already fulfilled");
- return TaskRunResult.backoff();
- } else if (kycStatusRes.status === HttpStatusCode.Accepted) {
- const kycStatus = await readResponseJsonOrThrow(
- kycStatusRes,
- codecForAccountKycStatus(),
- );
- logger.info(`kyc status: ${j2s(kycStatus)}`);
- const info = await ctx.transition({}, async (rec) => {
- rec.kycPaytoHash = kycPayoHash;
- logger.info(
- `setting peer-pull-credit kyc payto hash to ${kycPayoHash}`,
- );
- rec.kycAccessToken = kycStatus.access_token;
- rec.status = PeerPullPaymentCreditStatus.PendingMergeKycRequired;
- return TransitionResultType.Transition
- })
- if (info == null) {
- return TaskRunResult.finished()
- } else {
+ switch (res.case) {
+ case "ok":
+ case HttpStatusCode.Ok: // FIXME: voluntary check ?
+ logger.warn("kyc requested, but already fulfilled");
+ return TaskRunResult.finished();
+ case HttpStatusCode.Accepted: {
+ logger.info(`kyc status: ${j2s(res.body)}`);
+ await ctx.transition({}, async (rec) => {
+ rec.kycPaytoHash = kycPayoHash;
+ logger.info(
+ `setting peer-pull-credit kyc payto hash to ${kycPayoHash}`,
+ );
+ rec.kycAccessToken = res.body.access_token;
+ rec.status = PeerPullPaymentCreditStatus.PendingMergeKycRequired;
+ return TransitionResultType.Transition
+ })
return TaskRunResult.progress()
}
- } else {
- throw Error(`unexpected response from kyc-check (${kycStatusRes.status})`);
+ default:
+ throw Error(`unexpected response from kyc-check (${res.case})`);
}
}
diff --git a/packages/taler-wallet-core/src/pay-peer-pull-debit.ts b/packages/taler-wallet-core/src/pay-peer-pull-debit.ts
@@ -186,10 +186,7 @@ export class PeerPullDebitTransactionContext implements TransactionContext {
abortReason: pi.abortReason,
failReason: pi.failReason,
timestamp: timestampPreciseFromDb(pi.timestampCreated),
- transactionId: constructTransactionIdentifier({
- tag: TransactionType.PeerPullDebit,
- peerPullDebitId: pi.peerPullDebitId,
- }),
+ transactionId: this.transactionId,
...(ort?.lastError ? { error: ort.lastError } : {}),
};
}
diff --git a/packages/taler-wallet-core/src/pay-peer-push-credit.ts b/packages/taler-wallet-core/src/pay-peer-push-credit.ts
@@ -296,10 +296,7 @@ export class PeerPushCreditTransactionContext implements TransactionContext {
summary: peerContractTerms.summary,
},
timestamp: timestampPreciseFromDb(wg.timestampStart),
- transactionId: constructTransactionIdentifier({
- tag: TransactionType.PeerPushCredit,
- peerPushCreditId: pushInc.peerPushCreditId,
- }),
+ transactionId: this.transactionId,
abortReason: pushInc.abortReason,
failReason: pushInc.failReason,
kycUrl,
@@ -327,10 +324,7 @@ export class PeerPushCreditTransactionContext implements TransactionContext {
kycUrl,
kycPaytoHash: pushInc.kycPaytoHash,
timestamp: timestampPreciseFromDb(pushInc.timestamp),
- transactionId: constructTransactionIdentifier({
- tag: TransactionType.PeerPushCredit,
- peerPushCreditId: pushInc.peerPushCreditId,
- }),
+ transactionId: this.transactionId,
abortReason: pushInc.abortReason,
failReason: pushInc.failReason,
...(pushRetryRecord?.lastError
@@ -753,15 +747,10 @@ async function processPeerPushCreditKycRequired(
peerInc: PeerPushPaymentIncomingRecord,
kycPending: LegitimizationNeededResponse,
): Promise<TaskRunResult> {
- const transactionId = constructTransactionIdentifier({
- tag: TransactionType.PeerPushCredit,
- peerPushCreditId: peerInc.peerPushCreditId,
- });
const ctx = new PeerPushCreditTransactionContext(
wex,
peerInc.peerPushCreditId,
);
- const { peerPushCreditId } = peerInc;
// FIXME: What if this changes? Should be part of the p2p record
const mergeReserveInfo = await getMergeReserveInfo(wex, {
@@ -802,7 +791,7 @@ async function processPeerPushCreditKycRequired(
const { transitionInfo, result } = await wex.db.runReadWriteTx(
{ storeNames: ["peerPushCredit", "transactionsMeta"] },
async (tx) => {
- const peerInc = await tx.peerPushCredit.get(peerPushCreditId);
+ const peerInc = await tx.peerPushCredit.get(ctx.peerPushCreditId);
if (!peerInc) {
return {
transitionInfo: undefined,
@@ -826,7 +815,7 @@ async function processPeerPushCreditKycRequired(
};
},
);
- notifyTransition(wex, transactionId, transitionInfo);
+ notifyTransition(wex, ctx.transactionId, transitionInfo);
return result;
} else {
throw Error(`unexpected response from kyc-check (${kycStatusRes.status})`);
@@ -903,11 +892,11 @@ async function handlePendingMerge(
reserve_sig: sigRes.accountSig,
};
- const mergeResp = await exchangeClient.postPurseMerge({
- pursePub: peerInc.pursePub,
- cancellationToken: wex.cancellationToken,
- body: mergeReq,
- });
+ const mergeResp = await exchangeClient.postPurseMerge(
+ peerInc.pursePub,
+ mergeReq,
+ wex.cancellationToken,
+ );
logger.trace(`merge request: ${j2s(mergeReq)}`);