towards new recoup API

This commit is contained in:
Florian Dold 2022-01-11 12:48:32 +01:00
parent fb22009ec4
commit a05e891d6e
No known key found for this signature in database
GPG Key ID: D2E4F00F29D02A4B
10 changed files with 317 additions and 172 deletions

View File

@ -479,6 +479,7 @@ export enum TalerSignaturePurpose {
MERCHANT_CONTRACT = 1101, MERCHANT_CONTRACT = 1101,
WALLET_COIN_RECOUP = 1203, WALLET_COIN_RECOUP = 1203,
WALLET_COIN_LINK = 1204, WALLET_COIN_LINK = 1204,
WALLET_COIN_RECOUP_REFRESH = 1206,
EXCHANGE_CONFIRM_RECOUP = 1039, EXCHANGE_CONFIRM_RECOUP = 1039,
EXCHANGE_CONFIRM_RECOUP_REFRESH = 1041, EXCHANGE_CONFIRM_RECOUP_REFRESH = 1041,
ANASTASIS_POLICY_UPLOAD = 1400, ANASTASIS_POLICY_UPLOAD = 1400,

View File

@ -46,7 +46,7 @@ import {
Duration, Duration,
codecForDuration, codecForDuration,
} from "./time.js"; } from "./time.js";
import { codecForAmountString } from "./amounts.js"; import { Amounts, codecForAmountString } from "./amounts.js";
/** /**
* Denomination as found in the /keys response from the exchange. * Denomination as found in the /keys response from the exchange.
@ -149,9 +149,6 @@ export class ExchangeAuditor {
denomination_keys: AuditorDenomSig[]; denomination_keys: AuditorDenomSig[];
} }
/**
* Request that we send to the exchange to get a payback.
*/
export interface RecoupRequest { export interface RecoupRequest {
/** /**
* Hashed enomination public key of the coin we want to get * Hashed enomination public key of the coin we want to get
@ -161,16 +158,11 @@ export interface RecoupRequest {
/** /**
* Signature over the coin public key by the denomination. * Signature over the coin public key by the denomination.
* *
* The string variant is for the legacy exchange protocol. * The string variant is for the legacy exchange protocol.
*/ */
denom_sig: UnblindedSignature | string; denom_sig: UnblindedSignature | string;
/**
* Coin public key of the coin we want to refund.
*/
coin_pub: string;
/** /**
* Blinding key that was used during withdraw, * Blinding key that was used during withdraw,
* used to prove that we were actually withdrawing the coin. * used to prove that we were actually withdrawing the coin.
@ -178,14 +170,45 @@ export interface RecoupRequest {
coin_blind_key_secret: string; coin_blind_key_secret: string;
/** /**
* Signature made by the coin, authorizing the payback. * Signature of TALER_RecoupRequestPS created with the coin's private key.
*/ */
coin_sig: string; coin_sig: string;
/** /**
* Was the coin refreshed (and thus the recoup should go to the old coin)? * Amount being recouped.
*/ */
refreshed: boolean; amount: AmountString;
}
export interface RecoupRefreshRequest {
/**
* Hashed enomination public key of the coin we want to get
* paid back.
*/
denom_pub_hash: string;
/**
* Signature over the coin public key by the denomination.
*
* The string variant is for the legacy exchange protocol.
*/
denom_sig: UnblindedSignature | string;
/**
* Coin's blinding factor.
*/
coin_blind_key_secret: string;
/**
* Signature of TALER_RecoupRefreshRequestPS created with
* the coin's private key.
*/
coin_sig: string;
/**
* Amount being recouped.
*/
amount: AmountString;
} }
/** /**
@ -1131,10 +1154,11 @@ export const codecForLegacyRsaDenominationPubKey = () =>
.property("rsa_public_key", codecForString()) .property("rsa_public_key", codecForString())
.build("LegacyRsaDenominationPubKey"); .build("LegacyRsaDenominationPubKey");
export const codecForBankWithdrawalOperationPostResponse = (): Codec<BankWithdrawalOperationPostResponse> => export const codecForBankWithdrawalOperationPostResponse =
buildCodecForObject<BankWithdrawalOperationPostResponse>() (): Codec<BankWithdrawalOperationPostResponse> =>
.property("transfer_done", codecForBoolean()) buildCodecForObject<BankWithdrawalOperationPostResponse>()
.build("BankWithdrawalOperationPostResponse"); .property("transfer_done", codecForBoolean())
.build("BankWithdrawalOperationPostResponse");
export type AmountString = string; export type AmountString = string;
export type Base32String = string; export type Base32String = string;
@ -1213,8 +1237,8 @@ export const codecForTax = (): Codec<Tax> =>
.property("tax", codecForString()) .property("tax", codecForString())
.build("Tax"); .build("Tax");
export const codecForInternationalizedString = (): Codec<InternationalizedString> => export const codecForInternationalizedString =
codecForMap(codecForString()); (): Codec<InternationalizedString> => codecForMap(codecForString());
export const codecForProduct = (): Codec<Product> => export const codecForProduct = (): Codec<Product> =>
buildCodecForObject<Product>() buildCodecForObject<Product>()
@ -1262,30 +1286,33 @@ export const codecForContractTerms = (): Codec<ContractTerms> =>
.property("extra", codecForAny()) .property("extra", codecForAny())
.build("ContractTerms"); .build("ContractTerms");
export const codecForMerchantRefundPermission = (): Codec<MerchantAbortPayRefundDetails> => export const codecForMerchantRefundPermission =
buildCodecForObject<MerchantAbortPayRefundDetails>() (): Codec<MerchantAbortPayRefundDetails> =>
.property("refund_amount", codecForAmountString()) buildCodecForObject<MerchantAbortPayRefundDetails>()
.property("refund_fee", codecForAmountString()) .property("refund_amount", codecForAmountString())
.property("coin_pub", codecForString()) .property("refund_fee", codecForAmountString())
.property("rtransaction_id", codecForNumber()) .property("coin_pub", codecForString())
.property("exchange_http_status", codecForNumber()) .property("rtransaction_id", codecForNumber())
.property("exchange_code", codecOptional(codecForNumber())) .property("exchange_http_status", codecForNumber())
.property("exchange_reply", codecOptional(codecForAny())) .property("exchange_code", codecOptional(codecForNumber()))
.property("exchange_sig", codecOptional(codecForString())) .property("exchange_reply", codecOptional(codecForAny()))
.property("exchange_pub", codecOptional(codecForString())) .property("exchange_sig", codecOptional(codecForString()))
.build("MerchantRefundPermission"); .property("exchange_pub", codecOptional(codecForString()))
.build("MerchantRefundPermission");
export const codecForMerchantRefundResponse = (): Codec<MerchantRefundResponse> => export const codecForMerchantRefundResponse =
buildCodecForObject<MerchantRefundResponse>() (): Codec<MerchantRefundResponse> =>
.property("merchant_pub", codecForString()) buildCodecForObject<MerchantRefundResponse>()
.property("h_contract_terms", codecForString()) .property("merchant_pub", codecForString())
.property("refunds", codecForList(codecForMerchantRefundPermission())) .property("h_contract_terms", codecForString())
.build("MerchantRefundResponse"); .property("refunds", codecForList(codecForMerchantRefundPermission()))
.build("MerchantRefundResponse");
export const codecForMerchantBlindSigWrapperV1 = (): Codec<MerchantBlindSigWrapperV1> => export const codecForMerchantBlindSigWrapperV1 =
buildCodecForObject<MerchantBlindSigWrapperV1>() (): Codec<MerchantBlindSigWrapperV1> =>
.property("blind_sig", codecForString()) buildCodecForObject<MerchantBlindSigWrapperV1>()
.build("BlindSigWrapper"); .property("blind_sig", codecForString())
.build("BlindSigWrapper");
export const codecForMerchantTipResponseV1 = (): Codec<MerchantTipResponseV1> => export const codecForMerchantTipResponseV1 = (): Codec<MerchantTipResponseV1> =>
buildCodecForObject<MerchantTipResponseV1>() buildCodecForObject<MerchantTipResponseV1>()
@ -1365,17 +1392,18 @@ export const codecForCheckPaymentResponse = (): Codec<CheckPaymentResponse> =>
.property("contract_url", codecOptional(codecForString())) .property("contract_url", codecOptional(codecForString()))
.build("CheckPaymentResponse"); .build("CheckPaymentResponse");
export const codecForWithdrawOperationStatusResponse = (): Codec<WithdrawOperationStatusResponse> => export const codecForWithdrawOperationStatusResponse =
buildCodecForObject<WithdrawOperationStatusResponse>() (): Codec<WithdrawOperationStatusResponse> =>
.property("selection_done", codecForBoolean()) buildCodecForObject<WithdrawOperationStatusResponse>()
.property("transfer_done", codecForBoolean()) .property("selection_done", codecForBoolean())
.property("aborted", codecForBoolean()) .property("transfer_done", codecForBoolean())
.property("amount", codecForString()) .property("aborted", codecForBoolean())
.property("sender_wire", codecOptional(codecForString())) .property("amount", codecForString())
.property("suggested_exchange", codecOptional(codecForString())) .property("sender_wire", codecOptional(codecForString()))
.property("confirm_transfer_url", codecOptional(codecForString())) .property("suggested_exchange", codecOptional(codecForString()))
.property("wire_types", codecForList(codecForString())) .property("confirm_transfer_url", codecOptional(codecForString()))
.build("WithdrawOperationStatusResponse"); .property("wire_types", codecForList(codecForString()))
.build("WithdrawOperationStatusResponse");
export const codecForTipPickupGetResponse = (): Codec<TipPickupGetResponse> => export const codecForTipPickupGetResponse = (): Codec<TipPickupGetResponse> =>
buildCodecForObject<TipPickupGetResponse>() buildCodecForObject<TipPickupGetResponse>()
@ -1419,60 +1447,67 @@ export const codecForExchangeRevealItem = (): Codec<ExchangeRevealItem> =>
) )
.build("ExchangeRevealItem"); .build("ExchangeRevealItem");
export const codecForExchangeRevealResponse = (): Codec<ExchangeRevealResponse> => export const codecForExchangeRevealResponse =
buildCodecForObject<ExchangeRevealResponse>() (): Codec<ExchangeRevealResponse> =>
.property("ev_sigs", codecForList(codecForExchangeRevealItem())) buildCodecForObject<ExchangeRevealResponse>()
.build("ExchangeRevealResponse"); .property("ev_sigs", codecForList(codecForExchangeRevealItem()))
.build("ExchangeRevealResponse");
export const codecForMerchantCoinRefundSuccessStatus = (): Codec<MerchantCoinRefundSuccessStatus> => export const codecForMerchantCoinRefundSuccessStatus =
buildCodecForObject<MerchantCoinRefundSuccessStatus>() (): Codec<MerchantCoinRefundSuccessStatus> =>
.property("type", codecForConstString("success")) buildCodecForObject<MerchantCoinRefundSuccessStatus>()
.property("coin_pub", codecForString()) .property("type", codecForConstString("success"))
.property("exchange_status", codecForConstNumber(200)) .property("coin_pub", codecForString())
.property("exchange_sig", codecForString()) .property("exchange_status", codecForConstNumber(200))
.property("rtransaction_id", codecForNumber()) .property("exchange_sig", codecForString())
.property("refund_amount", codecForString()) .property("rtransaction_id", codecForNumber())
.property("exchange_pub", codecForString()) .property("refund_amount", codecForString())
.property("execution_time", codecForTimestamp) .property("exchange_pub", codecForString())
.build("MerchantCoinRefundSuccessStatus"); .property("execution_time", codecForTimestamp)
.build("MerchantCoinRefundSuccessStatus");
export const codecForMerchantCoinRefundFailureStatus = (): Codec<MerchantCoinRefundFailureStatus> => export const codecForMerchantCoinRefundFailureStatus =
buildCodecForObject<MerchantCoinRefundFailureStatus>() (): Codec<MerchantCoinRefundFailureStatus> =>
.property("type", codecForConstString("failure")) buildCodecForObject<MerchantCoinRefundFailureStatus>()
.property("coin_pub", codecForString()) .property("type", codecForConstString("failure"))
.property("exchange_status", codecForNumber()) .property("coin_pub", codecForString())
.property("rtransaction_id", codecForNumber()) .property("exchange_status", codecForNumber())
.property("refund_amount", codecForString()) .property("rtransaction_id", codecForNumber())
.property("exchange_code", codecOptional(codecForNumber())) .property("refund_amount", codecForString())
.property("exchange_reply", codecOptional(codecForAny())) .property("exchange_code", codecOptional(codecForNumber()))
.property("execution_time", codecForTimestamp) .property("exchange_reply", codecOptional(codecForAny()))
.build("MerchantCoinRefundFailureStatus"); .property("execution_time", codecForTimestamp)
.build("MerchantCoinRefundFailureStatus");
export const codecForMerchantCoinRefundStatus = (): Codec<MerchantCoinRefundStatus> => export const codecForMerchantCoinRefundStatus =
buildCodecForUnion<MerchantCoinRefundStatus>() (): Codec<MerchantCoinRefundStatus> =>
.discriminateOn("type") buildCodecForUnion<MerchantCoinRefundStatus>()
.alternative("success", codecForMerchantCoinRefundSuccessStatus()) .discriminateOn("type")
.alternative("failure", codecForMerchantCoinRefundFailureStatus()) .alternative("success", codecForMerchantCoinRefundSuccessStatus())
.build("MerchantCoinRefundStatus"); .alternative("failure", codecForMerchantCoinRefundFailureStatus())
.build("MerchantCoinRefundStatus");
export const codecForMerchantOrderStatusPaid = (): Codec<MerchantOrderStatusPaid> => export const codecForMerchantOrderStatusPaid =
buildCodecForObject<MerchantOrderStatusPaid>() (): Codec<MerchantOrderStatusPaid> =>
.property("refund_amount", codecForString()) buildCodecForObject<MerchantOrderStatusPaid>()
.property("refunded", codecForBoolean()) .property("refund_amount", codecForString())
.build("MerchantOrderStatusPaid"); .property("refunded", codecForBoolean())
.build("MerchantOrderStatusPaid");
export const codecForMerchantOrderRefundPickupResponse = (): Codec<MerchantOrderRefundResponse> => export const codecForMerchantOrderRefundPickupResponse =
buildCodecForObject<MerchantOrderRefundResponse>() (): Codec<MerchantOrderRefundResponse> =>
.property("merchant_pub", codecForString()) buildCodecForObject<MerchantOrderRefundResponse>()
.property("refund_amount", codecForString()) .property("merchant_pub", codecForString())
.property("refunds", codecForList(codecForMerchantCoinRefundStatus())) .property("refund_amount", codecForString())
.build("MerchantOrderRefundPickupResponse"); .property("refunds", codecForList(codecForMerchantCoinRefundStatus()))
.build("MerchantOrderRefundPickupResponse");
export const codecForMerchantOrderStatusUnpaid = (): Codec<MerchantOrderStatusUnpaid> => export const codecForMerchantOrderStatusUnpaid =
buildCodecForObject<MerchantOrderStatusUnpaid>() (): Codec<MerchantOrderStatusUnpaid> =>
.property("taler_pay_uri", codecForString()) buildCodecForObject<MerchantOrderStatusUnpaid>()
.property("already_paid_order_id", codecOptional(codecForString())) .property("taler_pay_uri", codecForString())
.build("MerchantOrderStatusUnpaid"); .property("already_paid_order_id", codecOptional(codecForString()))
.build("MerchantOrderStatusUnpaid");
export interface AbortRequest { export interface AbortRequest {
// hash of the order's contract terms (this is used to authenticate the // hash of the order's contract terms (this is used to authenticate the
@ -1550,28 +1585,31 @@ export interface MerchantAbortPayRefundSuccessStatus {
exchange_pub: string; exchange_pub: string;
} }
export const codecForMerchantAbortPayRefundSuccessStatus = (): Codec<MerchantAbortPayRefundSuccessStatus> => export const codecForMerchantAbortPayRefundSuccessStatus =
buildCodecForObject<MerchantAbortPayRefundSuccessStatus>() (): Codec<MerchantAbortPayRefundSuccessStatus> =>
.property("exchange_pub", codecForString()) buildCodecForObject<MerchantAbortPayRefundSuccessStatus>()
.property("exchange_sig", codecForString()) .property("exchange_pub", codecForString())
.property("exchange_status", codecForConstNumber(200)) .property("exchange_sig", codecForString())
.property("type", codecForConstString("success")) .property("exchange_status", codecForConstNumber(200))
.build("MerchantAbortPayRefundSuccessStatus"); .property("type", codecForConstString("success"))
.build("MerchantAbortPayRefundSuccessStatus");
export const codecForMerchantAbortPayRefundFailureStatus = (): Codec<MerchantAbortPayRefundFailureStatus> => export const codecForMerchantAbortPayRefundFailureStatus =
buildCodecForObject<MerchantAbortPayRefundFailureStatus>() (): Codec<MerchantAbortPayRefundFailureStatus> =>
.property("exchange_code", codecForNumber()) buildCodecForObject<MerchantAbortPayRefundFailureStatus>()
.property("exchange_reply", codecForAny()) .property("exchange_code", codecForNumber())
.property("exchange_status", codecForNumber()) .property("exchange_reply", codecForAny())
.property("type", codecForConstString("failure")) .property("exchange_status", codecForNumber())
.build("MerchantAbortPayRefundFailureStatus"); .property("type", codecForConstString("failure"))
.build("MerchantAbortPayRefundFailureStatus");
export const codecForMerchantAbortPayRefundStatus = (): Codec<MerchantAbortPayRefundStatus> => export const codecForMerchantAbortPayRefundStatus =
buildCodecForUnion<MerchantAbortPayRefundStatus>() (): Codec<MerchantAbortPayRefundStatus> =>
.discriminateOn("type") buildCodecForUnion<MerchantAbortPayRefundStatus>()
.alternative("success", codecForMerchantAbortPayRefundSuccessStatus()) .discriminateOn("type")
.alternative("failure", codecForMerchantAbortPayRefundFailureStatus()) .alternative("success", codecForMerchantAbortPayRefundSuccessStatus())
.build("MerchantAbortPayRefundStatus"); .alternative("failure", codecForMerchantAbortPayRefundFailureStatus())
.build("MerchantAbortPayRefundStatus");
export interface TalerConfigResponse { export interface TalerConfigResponse {
name: string; name: string;
@ -1614,13 +1652,13 @@ export interface MerchantConfigResponse {
version: string; version: string;
} }
export const codecForMerchantConfigResponse = (): Codec<MerchantConfigResponse> => export const codecForMerchantConfigResponse =
buildCodecForObject<MerchantConfigResponse>() (): Codec<MerchantConfigResponse> =>
.property("currency", codecForString()) buildCodecForObject<MerchantConfigResponse>()
.property("name", codecForString()) .property("currency", codecForString())
.property("version", codecForString()) .property("name", codecForString())
.build("MerchantConfigResponse"); .property("version", codecForString())
.build("MerchantConfigResponse");
export enum ExchangeProtocolVersion { export enum ExchangeProtocolVersion {
V9 = 9, V9 = 9,

View File

@ -2031,9 +2031,9 @@ export class WalletCli {
`wallet-${self.name}`, `wallet-${self.name}`,
`taler-wallet-cli ${ `taler-wallet-cli ${
self.timetravelArg ?? "" self.timetravelArg ?? ""
} --no-throttle --wallet-db '${self.dbfile}' api '${op}' ${shellWrap( } --no-throttle -LTRACE --wallet-db '${
JSON.stringify(payload), self.dbfile
)}`, }' api '${op}' ${shellWrap(JSON.stringify(payload))}`,
); );
console.log("--- wallet core response ---"); console.log("--- wallet core response ---");
console.log(resp); console.log(resp);
@ -2080,6 +2080,7 @@ export class WalletCli {
[ [
"--no-throttle", "--no-throttle",
...this.timetravelArgArr, ...this.timetravelArgArr,
"-LTRACE",
"--wallet-db", "--wallet-db",
this.dbfile, this.dbfile,
"run-until-done", "run-until-done",
@ -2095,6 +2096,7 @@ export class WalletCli {
"taler-wallet-cli", "taler-wallet-cli",
[ [
"--no-throttle", "--no-throttle",
"-LTRACE",
...this.timetravelArgArr, ...this.timetravelArgArr,
"--wallet-db", "--wallet-db",
this.dbfile, this.dbfile,

View File

@ -249,6 +249,7 @@ walletCli
.action(async (args) => { .action(async (args) => {
await withWallet(args, async (wallet) => { await withWallet(args, async (wallet) => {
let requestJson; let requestJson;
logger.info(`handling 'api' request (${args.api.operation})`);
try { try {
requestJson = JSON.parse(args.api.request); requestJson = JSON.parse(args.api.request);
} catch (e) { } catch (e) {
@ -293,12 +294,6 @@ walletCli
}); });
}); });
async function asyncSleep(milliSeconds: number): Promise<void> {
return new Promise<void>((resolve, reject) => {
setTimeout(() => resolve(), milliSeconds);
});
}
walletCli walletCli
.subcommand("runPendingOpt", "run-pending", { .subcommand("runPendingOpt", "run-pending", {
help: "Run pending operations.", help: "Run pending operations.",
@ -330,6 +325,7 @@ walletCli
.maybeOption("maxRetries", ["--max-retries"], clk.INT) .maybeOption("maxRetries", ["--max-retries"], clk.INT)
.action(async (args) => { .action(async (args) => {
await withWallet(args, async (wallet) => { await withWallet(args, async (wallet) => {
logger.info("running until pending operations are finished");
await wallet.ws.runTaskLoop({ await wallet.ws.runTaskLoop({
maxRetries: args.finishPendingOpt.maxRetries, maxRetries: args.finishPendingOpt.maxRetries,
stopWhenDone: true, stopWhenDone: true,

View File

@ -27,7 +27,13 @@
/** /**
* Imports. * Imports.
*/ */
import { AmountJson, DenominationPubKey, ExchangeProtocolVersion } from "@gnu-taler/taler-util"; import {
AmountJson,
AmountString,
DenominationPubKey,
ExchangeProtocolVersion,
UnblindedSignature,
} from "@gnu-taler/taler-util";
export interface RefreshNewDenomInfo { export interface RefreshNewDenomInfo {
count: number; count: number;
@ -140,3 +146,29 @@ export interface SignTrackTransactionRequest {
merchantPriv: string; merchantPriv: string;
merchantPub: string; merchantPub: string;
} }
/**
* Request to create a recoup request payload.
*/
export interface CreateRecoupReqRequest {
coinPub: string;
coinPriv: string;
blindingKey: string;
denomPub: DenominationPubKey;
denomPubHash: string;
denomSig: UnblindedSignature;
recoupAmount: AmountJson;
}
/**
* Request to create a recoup-refresh request payload.
*/
export interface CreateRecoupRefreshReqRequest {
coinPub: string;
coinPriv: string;
blindingKey: string;
denomPub: DenominationPubKey;
denomPubHash: string;
denomSig: UnblindedSignature;
recoupAmount: AmountJson;
}

View File

@ -26,7 +26,11 @@ import { CoinRecord, DenominationRecord, WireFee } from "../../db.js";
import { CryptoWorker } from "./cryptoWorkerInterface.js"; import { CryptoWorker } from "./cryptoWorkerInterface.js";
import { RecoupRequest, CoinDepositPermission } from "@gnu-taler/taler-util"; import {
CoinDepositPermission,
RecoupRefreshRequest,
RecoupRequest,
} from "@gnu-taler/taler-util";
import { import {
BenchmarkResult, BenchmarkResult,
@ -39,6 +43,8 @@ import {
import * as timer from "../../util/timer.js"; import * as timer from "../../util/timer.js";
import { Logger } from "@gnu-taler/taler-util"; import { Logger } from "@gnu-taler/taler-util";
import { import {
CreateRecoupRefreshReqRequest,
CreateRecoupReqRequest,
DerivedRefreshSession, DerivedRefreshSession,
DerivedTipPlanchet, DerivedTipPlanchet,
DeriveRefreshSessionRequest, DeriveRefreshSessionRequest,
@ -421,8 +427,18 @@ export class CryptoApi {
); );
} }
createRecoupRequest(coin: CoinRecord): Promise<RecoupRequest> { createRecoupRequest(req: CreateRecoupReqRequest): Promise<RecoupRequest> {
return this.doRpc<RecoupRequest>("createRecoupRequest", 1, coin); return this.doRpc<RecoupRequest>("createRecoupRequest", 1, req);
}
createRecoupRefreshRequest(
req: CreateRecoupRefreshReqRequest,
): Promise<RecoupRefreshRequest> {
return this.doRpc<RecoupRefreshRequest>(
"createRecoupRefreshRequest",
1,
req,
);
} }
deriveRefreshSession( deriveRefreshSession(

View File

@ -25,12 +25,7 @@
*/ */
// FIXME: Crypto should not use DB Types! // FIXME: Crypto should not use DB Types!
import { import { DenominationRecord, WireFee } from "../../db.js";
CoinRecord,
DenominationRecord,
WireFee,
CoinSourceType,
} from "../../db.js";
import { import {
buildSigPS, buildSigPS,
@ -39,6 +34,7 @@ import {
ExchangeProtocolVersion, ExchangeProtocolVersion,
FreshCoin, FreshCoin,
hashDenomPub, hashDenomPub,
RecoupRefreshRequest,
RecoupRequest, RecoupRequest,
RefreshPlanchetInfo, RefreshPlanchetInfo,
TalerSignaturePurpose, TalerSignaturePurpose,
@ -78,6 +74,8 @@ import { Timestamp, timestampTruncateToSecond } from "@gnu-taler/taler-util";
import { Logger } from "@gnu-taler/taler-util"; import { Logger } from "@gnu-taler/taler-util";
import { import {
CreateRecoupRefreshReqRequest,
CreateRecoupReqRequest,
DerivedRefreshSession, DerivedRefreshSession,
DerivedTipPlanchet, DerivedTipPlanchet,
DeriveRefreshSessionRequest, DeriveRefreshSessionRequest,
@ -261,33 +259,64 @@ export class CryptoImplementation {
/** /**
* Create and sign a message to recoup a coin. * Create and sign a message to recoup a coin.
*/ */
createRecoupRequest(coin: CoinRecord): RecoupRequest { createRecoupRequest(req: CreateRecoupReqRequest): RecoupRequest {
const p = buildSigPS(TalerSignaturePurpose.WALLET_COIN_RECOUP) const p = buildSigPS(TalerSignaturePurpose.WALLET_COIN_RECOUP)
.put(decodeCrock(coin.coinPub)) .put(decodeCrock(req.denomPubHash))
.put(decodeCrock(coin.denomPubHash)) .put(decodeCrock(req.blindingKey))
.put(decodeCrock(coin.blindingKey)) .put(amountToBuffer(Amounts.jsonifyAmount(req.recoupAmount)))
.build(); .build();
const coinPriv = decodeCrock(coin.coinPriv); const coinPriv = decodeCrock(req.coinPriv);
const coinSig = eddsaSign(p, coinPriv); const coinSig = eddsaSign(p, coinPriv);
if (coin.denomPub.cipher === DenomKeyType.LegacyRsa) { if (req.denomPub.cipher === DenomKeyType.LegacyRsa) {
const paybackRequest: RecoupRequest = { const paybackRequest: RecoupRequest = {
coin_blind_key_secret: coin.blindingKey, coin_blind_key_secret: req.blindingKey,
coin_pub: coin.coinPub,
coin_sig: encodeCrock(coinSig), coin_sig: encodeCrock(coinSig),
denom_pub_hash: coin.denomPubHash, denom_pub_hash: req.denomPubHash,
denom_sig: coin.denomSig.rsa_signature, denom_sig: req.denomSig.rsa_signature,
refreshed: coin.coinSource.type === CoinSourceType.Refresh, amount: Amounts.stringify(req.recoupAmount),
}; };
return paybackRequest; return paybackRequest;
} else { } else {
const paybackRequest: RecoupRequest = { const paybackRequest: RecoupRequest = {
coin_blind_key_secret: coin.blindingKey, coin_blind_key_secret: req.blindingKey,
coin_pub: coin.coinPub,
coin_sig: encodeCrock(coinSig), coin_sig: encodeCrock(coinSig),
denom_pub_hash: coin.denomPubHash, denom_pub_hash: req.denomPubHash,
denom_sig: coin.denomSig, denom_sig: req.denomSig,
refreshed: coin.coinSource.type === CoinSourceType.Refresh, amount: Amounts.stringify(req.recoupAmount),
};
return paybackRequest;
}
}
/**
* Create and sign a message to recoup a coin.
*/
createRecoupRefreshRequest(req: CreateRecoupRefreshReqRequest): RecoupRefreshRequest {
const p = buildSigPS(TalerSignaturePurpose.WALLET_COIN_RECOUP_REFRESH)
.put(decodeCrock(req.denomPubHash))
.put(decodeCrock(req.blindingKey))
.put(amountToBuffer(Amounts.jsonifyAmount(req.recoupAmount)))
.build();
const coinPriv = decodeCrock(req.coinPriv);
const coinSig = eddsaSign(p, coinPriv);
if (req.denomPub.cipher === DenomKeyType.LegacyRsa) {
const paybackRequest: RecoupRefreshRequest = {
coin_blind_key_secret: req.blindingKey,
coin_sig: encodeCrock(coinSig),
denom_pub_hash: req.denomPubHash,
denom_sig: req.denomSig.rsa_signature,
amount: Amounts.stringify(req.recoupAmount),
};
return paybackRequest;
} else {
const paybackRequest: RecoupRefreshRequest = {
coin_blind_key_secret: req.blindingKey,
coin_sig: encodeCrock(coinSig),
denom_pub_hash: req.denomPubHash,
denom_sig: req.denomSig,
amount: Amounts.stringify(req.recoupAmount),
}; };
return paybackRequest; return paybackRequest;
} }

View File

@ -651,7 +651,7 @@ async function updateExchangeFromUrlImpl(
logger.trace("denom already revoked"); logger.trace("denom already revoked");
continue; continue;
} }
logger.trace("revoking denom", recoupInfo.h_denom_pub); logger.info("revoking denom", recoupInfo.h_denom_pub);
oldDenom.isRevoked = true; oldDenom.isRevoked = true;
await tx.denominations.put(oldDenom); await tx.denominations.put(oldDenom);
const affectedCoins = await tx.coins.indexes.byDenomPubHash const affectedCoins = await tx.coins.indexes.byDenomPubHash
@ -662,7 +662,7 @@ async function updateExchangeFromUrlImpl(
} }
} }
if (newlyRevokedCoinPubs.length != 0) { if (newlyRevokedCoinPubs.length != 0) {
logger.trace("recouping coins", newlyRevokedCoinPubs); logger.info("recouping coins", newlyRevokedCoinPubs);
recoupGroupId = await ws.recoupOps.createRecoupGroup( recoupGroupId = await ws.recoupOps.createRecoupGroup(
ws, ws,
tx, tx,

View File

@ -28,6 +28,7 @@ import {
Amounts, Amounts,
codecForRecoupConfirmation, codecForRecoupConfirmation,
getTimestampNow, getTimestampNow,
j2s,
NotificationType, NotificationType,
RefreshReason, RefreshReason,
TalerErrorDetails, TalerErrorDetails,
@ -107,7 +108,7 @@ async function putGroupAsFinished(
} }
} }
if (allFinished) { if (allFinished) {
logger.trace("all recoups of recoup group are finished"); logger.info("all recoups of recoup group are finished");
recoupGroup.timestampFinished = getTimestampNow(); recoupGroup.timestampFinished = getTimestampNow();
recoupGroup.retryInfo = initRetryInfo(); recoupGroup.retryInfo = initRetryInfo();
recoupGroup.lastError = undefined; recoupGroup.lastError = undefined;
@ -178,8 +179,17 @@ async function recoupWithdrawCoin(
type: NotificationType.RecoupStarted, type: NotificationType.RecoupStarted,
}); });
const recoupRequest = await ws.cryptoApi.createRecoupRequest(coin); const recoupRequest = await ws.cryptoApi.createRecoupRequest({
blindingKey: coin.blindingKey,
coinPriv: coin.coinPriv,
coinPub: coin.coinPub,
denomPub: coin.denomPub,
denomPubHash: coin.denomPubHash,
denomSig: coin.denomSig,
recoupAmount: coin.currentAmount,
});
const reqUrl = new URL(`/coins/${coin.coinPub}/recoup`, coin.exchangeBaseUrl); const reqUrl = new URL(`/coins/${coin.coinPub}/recoup`, coin.exchangeBaseUrl);
logger.trace(`requesting recoup via ${reqUrl.href}`);
const resp = await ws.http.postJson(reqUrl.href, recoupRequest, { const resp = await ws.http.postJson(reqUrl.href, recoupRequest, {
timeout: getReserveRequestTimeout(reserve), timeout: getReserveRequestTimeout(reserve),
}); });
@ -188,6 +198,8 @@ async function recoupWithdrawCoin(
codecForRecoupConfirmation(), codecForRecoupConfirmation(),
); );
logger.trace(`got recoup confirmation ${j2s(recoupConfirmation)}`);
if (recoupConfirmation.reserve_pub !== reservePub) { if (recoupConfirmation.reserve_pub !== reservePub) {
throw Error(`Coin's reserve doesn't match reserve on recoup`); throw Error(`Coin's reserve doesn't match reserve on recoup`);
} }
@ -249,7 +261,15 @@ async function recoupRefreshCoin(
type: NotificationType.RecoupStarted, type: NotificationType.RecoupStarted,
}); });
const recoupRequest = await ws.cryptoApi.createRecoupRequest(coin); const recoupRequest = await ws.cryptoApi.createRecoupRefreshRequest({
blindingKey: coin.blindingKey,
coinPriv: coin.coinPriv,
coinPub: coin.coinPub,
denomPub: coin.denomPub,
denomPubHash: coin.denomPubHash,
denomSig: coin.denomSig,
recoupAmount: coin.currentAmount,
});
const reqUrl = new URL(`/coins/${coin.coinPub}/recoup`, coin.exchangeBaseUrl); const reqUrl = new URL(`/coins/${coin.coinPub}/recoup`, coin.exchangeBaseUrl);
logger.trace(`making recoup request for ${coin.coinPub}`); logger.trace(`making recoup request for ${coin.coinPub}`);
@ -359,9 +379,14 @@ async function processRecoupGroupImpl(
logger.trace("recoup group finished"); logger.trace("recoup group finished");
return; return;
} }
const ps = recoupGroup.coinPubs.map((x, i) => const ps = recoupGroup.coinPubs.map(async (x, i) => {
processRecoup(ws, recoupGroupId, i), try {
); processRecoup(ws, recoupGroupId, i);
} catch (e) {
logger.warn(`processRecoup failed: ${e}`);
throw e;
}
});
await Promise.all(ps); await Promise.all(ps);
const reserveSet = new Set<string>(); const reserveSet = new Set<string>();

View File

@ -30,6 +30,7 @@ import {
encodeCrock, encodeCrock,
getRandomBytes, getRandomBytes,
getTimestampNow, getTimestampNow,
j2s,
Logger, Logger,
NotificationType, NotificationType,
randomBytes, randomBytes,
@ -538,6 +539,7 @@ async function updateReserve(
resp, resp,
codecForReserveStatus(), codecForReserveStatus(),
); );
if (result.isError) { if (result.isError) {
if ( if (
resp.status === 404 && resp.status === 404 &&
@ -555,6 +557,8 @@ async function updateReserve(
} }
} }
logger.trace(`got reserve status ${j2s(result.response)}`);
const reserveInfo = result.response; const reserveInfo = result.response;
const balance = Amounts.parseOrThrow(reserveInfo.balance); const balance = Amounts.parseOrThrow(reserveInfo.balance);
const currency = balance.currency; const currency = balance.currency;
@ -635,8 +639,10 @@ async function updateReserve(
} }
} }
const remainingAmount = Amounts.sub(amountReservePlus, amountReserveMinus) const remainingAmount = Amounts.sub(
.amount; amountReservePlus,
amountReserveMinus,
).amount;
const denomSelInfo = selectWithdrawalDenominations( const denomSelInfo = selectWithdrawalDenominations(
remainingAmount, remainingAmount,
denoms, denoms,