implement latest recoup protocol
This commit is contained in:
parent
dbdad96b27
commit
dc596f1f4d
@ -173,11 +173,6 @@ export interface RecoupRequest {
|
|||||||
* Signature of TALER_RecoupRequestPS created with the coin's private key.
|
* Signature of TALER_RecoupRequestPS created with the coin's private key.
|
||||||
*/
|
*/
|
||||||
coin_sig: string;
|
coin_sig: string;
|
||||||
|
|
||||||
/**
|
|
||||||
* Amount being recouped.
|
|
||||||
*/
|
|
||||||
amount: AmountString;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RecoupRefreshRequest {
|
export interface RecoupRefreshRequest {
|
||||||
@ -204,11 +199,6 @@ export interface RecoupRefreshRequest {
|
|||||||
* the coin's private key.
|
* the coin's private key.
|
||||||
*/
|
*/
|
||||||
coin_sig: string;
|
coin_sig: string;
|
||||||
|
|
||||||
/**
|
|
||||||
* Amount being recouped.
|
|
||||||
*/
|
|
||||||
amount: AmountString;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -157,7 +157,6 @@ export interface CreateRecoupReqRequest {
|
|||||||
denomPub: DenominationPubKey;
|
denomPub: DenominationPubKey;
|
||||||
denomPubHash: string;
|
denomPubHash: string;
|
||||||
denomSig: UnblindedSignature;
|
denomSig: UnblindedSignature;
|
||||||
recoupAmount: AmountJson;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -170,5 +169,4 @@ export interface CreateRecoupRefreshReqRequest {
|
|||||||
denomPub: DenominationPubKey;
|
denomPub: DenominationPubKey;
|
||||||
denomPubHash: string;
|
denomPubHash: string;
|
||||||
denomSig: UnblindedSignature;
|
denomSig: UnblindedSignature;
|
||||||
recoupAmount: AmountJson;
|
|
||||||
}
|
}
|
||||||
|
@ -263,7 +263,6 @@ export class CryptoImplementation {
|
|||||||
const p = buildSigPS(TalerSignaturePurpose.WALLET_COIN_RECOUP)
|
const p = buildSigPS(TalerSignaturePurpose.WALLET_COIN_RECOUP)
|
||||||
.put(decodeCrock(req.denomPubHash))
|
.put(decodeCrock(req.denomPubHash))
|
||||||
.put(decodeCrock(req.blindingKey))
|
.put(decodeCrock(req.blindingKey))
|
||||||
.put(amountToBuffer(Amounts.jsonifyAmount(req.recoupAmount)))
|
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
const coinPriv = decodeCrock(req.coinPriv);
|
const coinPriv = decodeCrock(req.coinPriv);
|
||||||
@ -274,7 +273,6 @@ export class CryptoImplementation {
|
|||||||
coin_sig: encodeCrock(coinSig),
|
coin_sig: encodeCrock(coinSig),
|
||||||
denom_pub_hash: req.denomPubHash,
|
denom_pub_hash: req.denomPubHash,
|
||||||
denom_sig: req.denomSig.rsa_signature,
|
denom_sig: req.denomSig.rsa_signature,
|
||||||
amount: Amounts.stringify(req.recoupAmount),
|
|
||||||
};
|
};
|
||||||
return paybackRequest;
|
return paybackRequest;
|
||||||
} else {
|
} else {
|
||||||
@ -283,7 +281,6 @@ export class CryptoImplementation {
|
|||||||
coin_sig: encodeCrock(coinSig),
|
coin_sig: encodeCrock(coinSig),
|
||||||
denom_pub_hash: req.denomPubHash,
|
denom_pub_hash: req.denomPubHash,
|
||||||
denom_sig: req.denomSig,
|
denom_sig: req.denomSig,
|
||||||
amount: Amounts.stringify(req.recoupAmount),
|
|
||||||
};
|
};
|
||||||
return paybackRequest;
|
return paybackRequest;
|
||||||
}
|
}
|
||||||
@ -292,33 +289,32 @@ export class CryptoImplementation {
|
|||||||
/**
|
/**
|
||||||
* Create and sign a message to recoup a coin.
|
* Create and sign a message to recoup a coin.
|
||||||
*/
|
*/
|
||||||
createRecoupRefreshRequest(req: CreateRecoupRefreshReqRequest): RecoupRefreshRequest {
|
createRecoupRefreshRequest(
|
||||||
|
req: CreateRecoupRefreshReqRequest,
|
||||||
|
): RecoupRefreshRequest {
|
||||||
const p = buildSigPS(TalerSignaturePurpose.WALLET_COIN_RECOUP_REFRESH)
|
const p = buildSigPS(TalerSignaturePurpose.WALLET_COIN_RECOUP_REFRESH)
|
||||||
.put(decodeCrock(req.denomPubHash))
|
.put(decodeCrock(req.denomPubHash))
|
||||||
.put(decodeCrock(req.blindingKey))
|
.put(decodeCrock(req.blindingKey))
|
||||||
.put(amountToBuffer(Amounts.jsonifyAmount(req.recoupAmount)))
|
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
const coinPriv = decodeCrock(req.coinPriv);
|
const coinPriv = decodeCrock(req.coinPriv);
|
||||||
const coinSig = eddsaSign(p, coinPriv);
|
const coinSig = eddsaSign(p, coinPriv);
|
||||||
if (req.denomPub.cipher === DenomKeyType.LegacyRsa) {
|
if (req.denomPub.cipher === DenomKeyType.LegacyRsa) {
|
||||||
const paybackRequest: RecoupRefreshRequest = {
|
const recoupRequest: RecoupRefreshRequest = {
|
||||||
coin_blind_key_secret: req.blindingKey,
|
coin_blind_key_secret: req.blindingKey,
|
||||||
coin_sig: encodeCrock(coinSig),
|
coin_sig: encodeCrock(coinSig),
|
||||||
denom_pub_hash: req.denomPubHash,
|
denom_pub_hash: req.denomPubHash,
|
||||||
denom_sig: req.denomSig.rsa_signature,
|
denom_sig: req.denomSig.rsa_signature,
|
||||||
amount: Amounts.stringify(req.recoupAmount),
|
|
||||||
};
|
};
|
||||||
return paybackRequest;
|
return recoupRequest;
|
||||||
} else {
|
} else {
|
||||||
const paybackRequest: RecoupRefreshRequest = {
|
const recoupRequest: RecoupRefreshRequest = {
|
||||||
coin_blind_key_secret: req.blindingKey,
|
coin_blind_key_secret: req.blindingKey,
|
||||||
coin_sig: encodeCrock(coinSig),
|
coin_sig: encodeCrock(coinSig),
|
||||||
denom_pub_hash: req.denomPubHash,
|
denom_pub_hash: req.denomPubHash,
|
||||||
denom_sig: req.denomSig,
|
denom_sig: req.denomSig,
|
||||||
amount: Amounts.stringify(req.recoupAmount),
|
|
||||||
};
|
};
|
||||||
return paybackRequest;
|
return recoupRequest;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -42,7 +42,6 @@ import {
|
|||||||
import { RetryInfo } from "./util/retries.js";
|
import { RetryInfo } from "./util/retries.js";
|
||||||
import { PayCoinSelection } from "./util/coinSelection.js";
|
import { PayCoinSelection } from "./util/coinSelection.js";
|
||||||
import { Event, IDBDatabase } from "@gnu-taler/idb-bridge";
|
import { Event, IDBDatabase } from "@gnu-taler/idb-bridge";
|
||||||
import { PendingTaskInfo } from "./pending-types.js";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Name of the Taler database. This is effectively the major
|
* Name of the Taler database. This is effectively the major
|
||||||
@ -140,7 +139,7 @@ export interface ReserveRecord {
|
|||||||
reservePriv: string;
|
reservePriv: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The exchange base URL.
|
* The exchange base URL for the reserve.
|
||||||
*/
|
*/
|
||||||
exchangeBaseUrl: string;
|
exchangeBaseUrl: string;
|
||||||
|
|
||||||
@ -154,8 +153,6 @@ export interface ReserveRecord {
|
|||||||
*/
|
*/
|
||||||
timestampCreated: Timestamp;
|
timestampCreated: Timestamp;
|
||||||
|
|
||||||
operationStatus: OperationStatus;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Time when the information about this reserve was posted to the bank.
|
* Time when the information about this reserve was posted to the bank.
|
||||||
*
|
*
|
||||||
@ -206,13 +203,17 @@ export interface ReserveRecord {
|
|||||||
*/
|
*/
|
||||||
initialDenomSel: DenomSelectionState;
|
initialDenomSel: DenomSelectionState;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Current status of the reserve.
|
||||||
|
*/
|
||||||
reserveStatus: ReserveRecordStatus;
|
reserveStatus: ReserveRecordStatus;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Was a reserve query requested? If so, query again instead
|
* Is there any work to be done for this reserve?
|
||||||
* of going into dormant status.
|
*
|
||||||
|
* FIXME: Technically redundant, since the reserveStatus would indicate this.
|
||||||
*/
|
*/
|
||||||
requestedQuery: boolean;
|
operationStatus: OperationStatus;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Time of the last successful status query.
|
* Time of the last successful status query.
|
||||||
|
@ -457,7 +457,6 @@ export async function importBackup(
|
|||||||
exchangeBaseUrl: backupExchangeDetails.base_url,
|
exchangeBaseUrl: backupExchangeDetails.base_url,
|
||||||
reservePub,
|
reservePub,
|
||||||
reservePriv: backupReserve.reserve_priv,
|
reservePriv: backupReserve.reserve_priv,
|
||||||
requestedQuery: false,
|
|
||||||
bankInfo,
|
bankInfo,
|
||||||
timestampCreated: backupReserve.timestamp_created,
|
timestampCreated: backupReserve.timestamp_created,
|
||||||
timestampBankConfirmed:
|
timestampBankConfirmed:
|
||||||
|
@ -43,6 +43,7 @@ import {
|
|||||||
ReserveRecordStatus,
|
ReserveRecordStatus,
|
||||||
WithdrawCoinSource,
|
WithdrawCoinSource,
|
||||||
WalletStoresV1,
|
WalletStoresV1,
|
||||||
|
OperationStatus,
|
||||||
} from "../db.js";
|
} from "../db.js";
|
||||||
|
|
||||||
import { readSuccessResponseJsonOrThrow } from "../util/http.js";
|
import { readSuccessResponseJsonOrThrow } from "../util/http.js";
|
||||||
@ -186,7 +187,6 @@ async function recoupWithdrawCoin(
|
|||||||
denomPub: coin.denomPub,
|
denomPub: coin.denomPub,
|
||||||
denomPubHash: coin.denomPubHash,
|
denomPubHash: coin.denomPubHash,
|
||||||
denomSig: coin.denomSig,
|
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}`);
|
logger.trace(`requesting recoup via ${reqUrl.href}`);
|
||||||
@ -233,13 +233,9 @@ async function recoupWithdrawCoin(
|
|||||||
updatedCoin.status = CoinStatus.Dormant;
|
updatedCoin.status = CoinStatus.Dormant;
|
||||||
const currency = updatedCoin.currentAmount.currency;
|
const currency = updatedCoin.currentAmount.currency;
|
||||||
updatedCoin.currentAmount = Amounts.getZero(currency);
|
updatedCoin.currentAmount = Amounts.getZero(currency);
|
||||||
if (updatedReserve.reserveStatus === ReserveRecordStatus.DORMANT) {
|
updatedReserve.reserveStatus = ReserveRecordStatus.QUERYING_STATUS;
|
||||||
updatedReserve.reserveStatus = ReserveRecordStatus.QUERYING_STATUS;
|
updatedReserve.retryInfo = initRetryInfo();
|
||||||
updatedReserve.retryInfo = initRetryInfo();
|
updatedReserve.operationStatus = OperationStatus.Pending;
|
||||||
} else {
|
|
||||||
updatedReserve.requestedQuery = true;
|
|
||||||
updatedReserve.retryInfo = initRetryInfo();
|
|
||||||
}
|
|
||||||
await tx.coins.put(updatedCoin);
|
await tx.coins.put(updatedCoin);
|
||||||
await tx.reserves.put(updatedReserve);
|
await tx.reserves.put(updatedReserve);
|
||||||
await putGroupAsFinished(ws, tx, recoupGroup, coinIdx);
|
await putGroupAsFinished(ws, tx, recoupGroup, coinIdx);
|
||||||
@ -268,9 +264,11 @@ async function recoupRefreshCoin(
|
|||||||
denomPub: coin.denomPub,
|
denomPub: coin.denomPub,
|
||||||
denomPubHash: coin.denomPubHash,
|
denomPubHash: coin.denomPubHash,
|
||||||
denomSig: coin.denomSig,
|
denomSig: coin.denomSig,
|
||||||
recoupAmount: coin.currentAmount,
|
|
||||||
});
|
});
|
||||||
const reqUrl = new URL(`/coins/${coin.coinPub}/recoup`, coin.exchangeBaseUrl);
|
const reqUrl = new URL(
|
||||||
|
`/coins/${coin.coinPub}/recoup-refresh`,
|
||||||
|
coin.exchangeBaseUrl,
|
||||||
|
);
|
||||||
logger.trace(`making recoup request for ${coin.coinPub}`);
|
logger.trace(`making recoup request for ${coin.coinPub}`);
|
||||||
|
|
||||||
const resp = await ws.http.postJson(reqUrl.href, recoupRequest);
|
const resp = await ws.http.postJson(reqUrl.href, recoupRequest);
|
||||||
@ -381,7 +379,7 @@ async function processRecoupGroupImpl(
|
|||||||
}
|
}
|
||||||
const ps = recoupGroup.coinPubs.map(async (x, i) => {
|
const ps = recoupGroup.coinPubs.map(async (x, i) => {
|
||||||
try {
|
try {
|
||||||
processRecoup(ws, recoupGroupId, i);
|
await processRecoup(ws, recoupGroupId, i);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.warn(`processRecoup failed: ${e}`);
|
logger.warn(`processRecoup failed: ${e}`);
|
||||||
throw e;
|
throw e;
|
||||||
@ -408,7 +406,7 @@ async function processRecoupGroupImpl(
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (const r of reserveSet.values()) {
|
for (const r of reserveSet.values()) {
|
||||||
processReserve(ws, r).catch((e) => {
|
processReserve(ws, r, true).catch((e) => {
|
||||||
logger.error(`processing reserve ${r} after recoup failed`);
|
logger.error(`processing reserve ${r} after recoup failed`);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -460,6 +458,9 @@ export async function createRecoupGroup(
|
|||||||
return recoupGroupId;
|
return recoupGroupId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run the recoup protocol for a single coin in a recoup group.
|
||||||
|
*/
|
||||||
async function processRecoup(
|
async function processRecoup(
|
||||||
ws: InternalWalletState,
|
ws: InternalWalletState,
|
||||||
recoupGroupId: string,
|
recoupGroupId: string,
|
||||||
@ -486,7 +487,7 @@ async function processRecoup(
|
|||||||
|
|
||||||
const coin = await tx.coins.get(coinPub);
|
const coin = await tx.coins.get(coinPub);
|
||||||
if (!coin) {
|
if (!coin) {
|
||||||
throw Error(`Coin ${coinPub} not found, can't request payback`);
|
throw Error(`Coin ${coinPub} not found, can't request recoup`);
|
||||||
}
|
}
|
||||||
return coin;
|
return coin;
|
||||||
});
|
});
|
||||||
|
@ -155,7 +155,6 @@ export async function createReserve(
|
|||||||
retryInfo: initRetryInfo(),
|
retryInfo: initRetryInfo(),
|
||||||
lastError: undefined,
|
lastError: undefined,
|
||||||
currency: req.amount.currency,
|
currency: req.amount.currency,
|
||||||
requestedQuery: false,
|
|
||||||
operationStatus: OperationStatus.Pending,
|
operationStatus: OperationStatus.Pending,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -255,7 +254,6 @@ export async function forceQueryReserve(
|
|||||||
reserve.operationStatus = OperationStatus.Pending;
|
reserve.operationStatus = OperationStatus.Pending;
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
reserve.requestedQuery = true;
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
reserve.retryInfo = initRetryInfo();
|
reserve.retryInfo = initRetryInfo();
|
||||||
|
@ -164,7 +164,7 @@ export async function readUnexpectedResponseDetails(
|
|||||||
}
|
}
|
||||||
return makeErrorDetails(
|
return makeErrorDetails(
|
||||||
TalerErrorCode.WALLET_UNEXPECTED_REQUEST_ERROR,
|
TalerErrorCode.WALLET_UNEXPECTED_REQUEST_ERROR,
|
||||||
"Unexpected error code in response",
|
`Unexpected HTTP status (${httpResponse.status}) in response`,
|
||||||
{
|
{
|
||||||
requestUrl: httpResponse.requestUrl,
|
requestUrl: httpResponse.requestUrl,
|
||||||
httpStatusCode: httpResponse.status,
|
httpStatusCode: httpResponse.status,
|
||||||
@ -220,7 +220,7 @@ export function throwUnexpectedRequestError(
|
|||||||
throw new OperationFailedError(
|
throw new OperationFailedError(
|
||||||
makeErrorDetails(
|
makeErrorDetails(
|
||||||
TalerErrorCode.WALLET_UNEXPECTED_REQUEST_ERROR,
|
TalerErrorCode.WALLET_UNEXPECTED_REQUEST_ERROR,
|
||||||
"Unexpected error code in response",
|
`Unexpected HTTP status ${httpResponse.status} in response`,
|
||||||
{
|
{
|
||||||
requestUrl: httpResponse.requestUrl,
|
requestUrl: httpResponse.requestUrl,
|
||||||
httpStatusCode: httpResponse.status,
|
httpStatusCode: httpResponse.status,
|
||||||
|
Loading…
Reference in New Issue
Block a user