commit 4d75a0c5f43bf68ed1b7bb3b7847b38fc5329abb
parent b7ebb1e567c59da6dc9a5baa74e48a8b52abb39b
Author: Christian Grothoff <christian@grothoff.org>
Date: Tue, 13 Oct 2026 00:26:38 +0200
add starting point for POST /withdraw-reveal (age-restricted withdrawal)
Diffstat:
2 files changed, 80 insertions(+), 0 deletions(-)
diff --git a/packages/taler-util/src/http-client/exchange-client.ts b/packages/taler-util/src/http-client/exchange-client.ts
@@ -76,6 +76,8 @@ import {
ExchangeRefundSuccessResponse,
ExchangeReservePurseRequest,
ExchangeRevealMeltResponseV2,
+ ExchangeRevealWithdrawRequest,
+ ExchangeRevealWithdrawResponse,
ExchangeTransferList,
ExchangeWithdrawRequest,
ExchangeWithdrawResponse,
@@ -115,6 +117,7 @@ import {
codecForExchangeMergeSuccessResponse,
codecForExchangePurseStatus,
codecForExchangeRevealMeltResponseV2,
+ codecForExchangeRevealWithdrawResponse,
codecForExchangeRefundSuccessResponse,
codecForExchangeTransferList,
codecForExchangeWithdrawResponse,
@@ -2178,6 +2181,48 @@ export class TalerExchangeHttpClient {
}
}
+ /**
+ * Request: POST /reveal-withdraw
+ *
+ * https://docs.taler.net/core/api-exchange.html#post--reveal-withdraw
+ *
+ * Second step of an age-restricted withdrawal: discloses the kappa-1
+ * batches that the exchange did not select. The request identifies the
+ * withdraw operation by ``reserve_pub`` and ``planchets_h``; both are
+ * required since protocol v42.
+ */
+ async postRevealWithdraw(args: {
+ body: ExchangeRevealWithdrawRequest;
+ }): Promise<
+ | OperationOk<ExchangeRevealWithdrawResponse>
+ | OperationFail<HttpStatusCode.BadRequest>
+ | OperationFail<HttpStatusCode.NotFound>
+ | OperationFail<HttpStatusCode.PayloadTooLarge>
+ | OperationFail<HttpStatusCode.InternalServerError>
+ | OperationFail<HttpStatusCode.ServiceUnavailable>
+ > {
+ const url = new URL(`reveal-withdraw`, this.baseUrl);
+ const resp = await this.fetch(url, {
+ method: "POST",
+ body: args.body,
+ });
+ switch (resp.status) {
+ case HttpStatusCode.Ok:
+ return opSuccessFromHttp(
+ resp,
+ codecForExchangeRevealWithdrawResponse(),
+ );
+ case HttpStatusCode.BadRequest:
+ case HttpStatusCode.NotFound:
+ case HttpStatusCode.PayloadTooLarge:
+ case HttpStatusCode.InternalServerError:
+ case HttpStatusCode.ServiceUnavailable:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownHttpFailure(resp);
+ }
+ }
+
async postBlindingPrepare(args: {
body: BlindingPrepareRequest;
}): Promise<
diff --git a/packages/taler-util/src/types-taler-exchange.ts b/packages/taler-util/src/types-taler-exchange.ts
@@ -1009,6 +1009,29 @@ export interface ExchangeRefreshRevealRequestV2 {
age_commitment?: Edx25519PublicKeyEnc[];
}
+/**
+ * Docs name: RevealWithdrawRequest
+ */
+export interface ExchangeRevealWithdrawRequest {
+ // Public key of the reserve that the original call to /withdraw was
+ // made from. Together with planchets_h, this identifies the withdraw
+ // operation.
+ //
+ // @since protocol **v42**. Required: without it the exchange cannot
+ // look the operation up by its primary key.
+ reserve_pub: EddsaPublicKey;
+
+ // Running hash of all blinded planchets from the previous
+ // call to /withdraw.
+ planchets_h: HashCode;
+
+ // Array of (kappa - 1) disclosed batch secrets, from which for each
+ // of the n coins in a batch their coin master secret is derived, and
+ // from that in turn their private key, blinding, nonce (for
+ // Clause-Schnorr) and age restriction.
+ disclosed_batch_seeds: HashCode[];
+}
+
export const codecForRecoup = (): Codec<Recoup> =>
buildCodecForObject<Recoup>()
.property("h_denom_pub", codecForString())
@@ -1226,6 +1249,18 @@ export const codecForExchangeRevealMeltResponseV2 =
.property("ev_sigs", codecForList(codecForBlindedDenominationSignature()))
.build("ExchangeRevealMeltResponseV2");
+/**
+ * Response to a POST /reveal-withdraw request. Structurally identical to
+ * {@link ExchangeWithdrawResponse} (an array of blinded signatures).
+ */
+export type ExchangeRevealWithdrawResponse = ExchangeWithdrawResponse;
+
+export const codecForExchangeRevealWithdrawResponse =
+ (): Codec<ExchangeRevealWithdrawResponse> =>
+ buildCodecForObject<ExchangeRevealWithdrawResponse>()
+ .property("ev_sigs", codecForList(codecForBlindedDenominationSignature()))
+ .build("ExchangeRevealWithdrawResponse");
+
export const codecForExchangeMeltResponse = (): Codec<ExchangeMeltResponse> =>
buildCodecForObject<ExchangeMeltResponse>()
.property("exchange_pub", codecForEddsaPublicKey())