full recoup, untested/unfinished first attempt
This commit is contained in:
parent
6e2881fabf
commit
2c52046f0b
@ -30,12 +30,11 @@ import {
|
|||||||
RefreshSessionRecord,
|
RefreshSessionRecord,
|
||||||
TipPlanchet,
|
TipPlanchet,
|
||||||
WireFee,
|
WireFee,
|
||||||
WalletContractData,
|
|
||||||
} from "../../types/dbTypes";
|
} from "../../types/dbTypes";
|
||||||
|
|
||||||
import { CryptoWorker } from "./cryptoWorker";
|
import { CryptoWorker } from "./cryptoWorker";
|
||||||
|
|
||||||
import { ContractTerms, PaybackRequest, CoinDepositPermission } from "../../types/talerTypes";
|
import { RecoupRequest, CoinDepositPermission } from "../../types/talerTypes";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
BenchmarkResult,
|
BenchmarkResult,
|
||||||
@ -409,8 +408,8 @@ export class CryptoApi {
|
|||||||
return this.doRpc<boolean>("isValidWireAccount", 4, paytoUri, sig, masterPub);
|
return this.doRpc<boolean>("isValidWireAccount", 4, paytoUri, sig, masterPub);
|
||||||
}
|
}
|
||||||
|
|
||||||
createPaybackRequest(coin: CoinRecord): Promise<PaybackRequest> {
|
createRecoupRequest(coin: CoinRecord): Promise<RecoupRequest> {
|
||||||
return this.doRpc<PaybackRequest>("createPaybackRequest", 1, coin);
|
return this.doRpc<RecoupRequest>("createRecoupRequest", 1, coin);
|
||||||
}
|
}
|
||||||
|
|
||||||
createRefreshSession(
|
createRefreshSession(
|
||||||
|
@ -31,9 +31,10 @@ import {
|
|||||||
RefreshSessionRecord,
|
RefreshSessionRecord,
|
||||||
TipPlanchet,
|
TipPlanchet,
|
||||||
WireFee,
|
WireFee,
|
||||||
|
CoinSourceType,
|
||||||
} from "../../types/dbTypes";
|
} from "../../types/dbTypes";
|
||||||
|
|
||||||
import { CoinDepositPermission, ContractTerms, PaybackRequest } from "../../types/talerTypes";
|
import { CoinDepositPermission, RecoupRequest } from "../../types/talerTypes";
|
||||||
import {
|
import {
|
||||||
BenchmarkResult,
|
BenchmarkResult,
|
||||||
PlanchetCreationResult,
|
PlanchetCreationResult,
|
||||||
@ -73,7 +74,7 @@ enum SignaturePurpose {
|
|||||||
WALLET_COIN_MELT = 1202,
|
WALLET_COIN_MELT = 1202,
|
||||||
TEST = 4242,
|
TEST = 4242,
|
||||||
MERCHANT_PAYMENT_OK = 1104,
|
MERCHANT_PAYMENT_OK = 1104,
|
||||||
WALLET_COIN_PAYBACK = 1203,
|
WALLET_COIN_RECOUP = 1203,
|
||||||
WALLET_COIN_LINK = 1204,
|
WALLET_COIN_LINK = 1204,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -198,10 +199,10 @@ export class CryptoImplementation {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create and sign a message to request payback for a coin.
|
* Create and sign a message to recoup a coin.
|
||||||
*/
|
*/
|
||||||
createPaybackRequest(coin: CoinRecord): PaybackRequest {
|
createRecoupRequest(coin: CoinRecord): RecoupRequest {
|
||||||
const p = buildSigPS(SignaturePurpose.WALLET_COIN_PAYBACK)
|
const p = buildSigPS(SignaturePurpose.WALLET_COIN_RECOUP)
|
||||||
.put(decodeCrock(coin.coinPub))
|
.put(decodeCrock(coin.coinPub))
|
||||||
.put(decodeCrock(coin.denomPubHash))
|
.put(decodeCrock(coin.denomPubHash))
|
||||||
.put(decodeCrock(coin.blindingKey))
|
.put(decodeCrock(coin.blindingKey))
|
||||||
@ -209,12 +210,13 @@ export class CryptoImplementation {
|
|||||||
|
|
||||||
const coinPriv = decodeCrock(coin.coinPriv);
|
const coinPriv = decodeCrock(coin.coinPriv);
|
||||||
const coinSig = eddsaSign(p, coinPriv);
|
const coinSig = eddsaSign(p, coinPriv);
|
||||||
const paybackRequest: PaybackRequest = {
|
const paybackRequest: RecoupRequest = {
|
||||||
coin_blind_key_secret: coin.blindingKey,
|
coin_blind_key_secret: coin.blindingKey,
|
||||||
coin_pub: coin.coinPub,
|
coin_pub: coin.coinPub,
|
||||||
coin_sig: encodeCrock(coinSig),
|
coin_sig: encodeCrock(coinSig),
|
||||||
denom_pub: coin.denomPub,
|
denom_pub: coin.denomPub,
|
||||||
denom_sig: coin.denomSig,
|
denom_sig: coin.denomSig,
|
||||||
|
refreshed: (coin.coinSource.type === CoinSourceType.Refresh),
|
||||||
};
|
};
|
||||||
return paybackRequest;
|
return paybackRequest;
|
||||||
}
|
}
|
||||||
|
@ -365,6 +365,7 @@ advancedCli
|
|||||||
console.log(`coin ${coin.coinPub}`);
|
console.log(`coin ${coin.coinPub}`);
|
||||||
console.log(` status ${coin.status}`);
|
console.log(` status ${coin.status}`);
|
||||||
console.log(` exchange ${coin.exchangeBaseUrl}`);
|
console.log(` exchange ${coin.exchangeBaseUrl}`);
|
||||||
|
console.log(` denomPubHash ${coin.denomPubHash}`);
|
||||||
console.log(
|
console.log(
|
||||||
` remaining amount ${Amounts.toString(coin.currentAmount)}`,
|
` remaining amount ${Amounts.toString(coin.currentAmount)}`,
|
||||||
);
|
);
|
||||||
|
@ -31,6 +31,7 @@ import {
|
|||||||
WireFee,
|
WireFee,
|
||||||
ExchangeUpdateReason,
|
ExchangeUpdateReason,
|
||||||
ExchangeUpdatedEventRecord,
|
ExchangeUpdatedEventRecord,
|
||||||
|
CoinStatus,
|
||||||
} from "../types/dbTypes";
|
} from "../types/dbTypes";
|
||||||
import { canonicalizeBaseUrl } from "../util/helpers";
|
import { canonicalizeBaseUrl } from "../util/helpers";
|
||||||
import * as Amounts from "../util/amounts";
|
import * as Amounts from "../util/amounts";
|
||||||
@ -45,6 +46,7 @@ import {
|
|||||||
} from "./versions";
|
} from "./versions";
|
||||||
import { getTimestampNow } from "../util/time";
|
import { getTimestampNow } from "../util/time";
|
||||||
import { compare } from "../util/libtoolVersion";
|
import { compare } from "../util/libtoolVersion";
|
||||||
|
import { createRecoupGroup, processRecoupGroup } from "./recoup";
|
||||||
|
|
||||||
async function denominationRecordFromKeys(
|
async function denominationRecordFromKeys(
|
||||||
ws: InternalWalletState,
|
ws: InternalWalletState,
|
||||||
@ -61,6 +63,7 @@ async function denominationRecordFromKeys(
|
|||||||
feeRefund: Amounts.parseOrThrow(denomIn.fee_refund),
|
feeRefund: Amounts.parseOrThrow(denomIn.fee_refund),
|
||||||
feeWithdraw: Amounts.parseOrThrow(denomIn.fee_withdraw),
|
feeWithdraw: Amounts.parseOrThrow(denomIn.fee_withdraw),
|
||||||
isOffered: true,
|
isOffered: true,
|
||||||
|
isRevoked: false,
|
||||||
masterSig: denomIn.master_sig,
|
masterSig: denomIn.master_sig,
|
||||||
stampExpireDeposit: denomIn.stamp_expire_deposit,
|
stampExpireDeposit: denomIn.stamp_expire_deposit,
|
||||||
stampExpireLegal: denomIn.stamp_expire_legal,
|
stampExpireLegal: denomIn.stamp_expire_legal,
|
||||||
@ -189,6 +192,8 @@ async function updateExchangeWithKeys(
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let recoupGroupId: string | undefined = undefined;
|
||||||
|
|
||||||
await ws.db.runWithWriteTransaction(
|
await ws.db.runWithWriteTransaction(
|
||||||
[Stores.exchanges, Stores.denominations],
|
[Stores.exchanges, Stores.denominations],
|
||||||
async tx => {
|
async tx => {
|
||||||
@ -222,8 +227,46 @@ async function updateExchangeWithKeys(
|
|||||||
await tx.put(Stores.denominations, newDenom);
|
await tx.put(Stores.denominations, newDenom);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle recoup
|
||||||
|
const recoupDenomList = exchangeKeysJson.recoup ?? [];
|
||||||
|
const newlyRevokedCoinPubs: string[] = [];
|
||||||
|
for (const recoupDenomPubHash of recoupDenomList) {
|
||||||
|
const oldDenom = await tx.getIndexed(
|
||||||
|
Stores.denominations.denomPubHashIndex,
|
||||||
|
recoupDenomPubHash,
|
||||||
|
);
|
||||||
|
if (!oldDenom) {
|
||||||
|
// We never even knew about the revoked denomination, all good.
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (oldDenom.isRevoked) {
|
||||||
|
// We already marked the denomination as revoked,
|
||||||
|
// this implies we revoked all coins
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
oldDenom.isRevoked = true;
|
||||||
|
await tx.put(Stores.denominations, oldDenom);
|
||||||
|
const affectedCoins = await tx
|
||||||
|
.iterIndexed(Stores.coins.denomPubIndex)
|
||||||
|
.toArray();
|
||||||
|
for (const ac of affectedCoins) {
|
||||||
|
newlyRevokedCoinPubs.push(ac.coinPub);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (newlyRevokedCoinPubs.length != 0) {
|
||||||
|
await createRecoupGroup(ws, tx, newlyRevokedCoinPubs);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (recoupGroupId) {
|
||||||
|
// Asynchronously start recoup. This doesn't need to finish
|
||||||
|
// for the exchange update to be considered finished.
|
||||||
|
processRecoupGroup(ws, recoupGroupId).catch((e) => {
|
||||||
|
console.log("error while recouping coins:", e);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function updateExchangeFinalize(
|
async function updateExchangeFinalize(
|
||||||
|
@ -181,6 +181,7 @@ export async function getHistory(
|
|||||||
Stores.payEvents,
|
Stores.payEvents,
|
||||||
Stores.refundEvents,
|
Stores.refundEvents,
|
||||||
Stores.reserveUpdatedEvents,
|
Stores.reserveUpdatedEvents,
|
||||||
|
Stores.recoupGroups,
|
||||||
],
|
],
|
||||||
async tx => {
|
async tx => {
|
||||||
tx.iter(Stores.exchanges).forEach(exchange => {
|
tx.iter(Stores.exchanges).forEach(exchange => {
|
||||||
@ -485,6 +486,16 @@ export async function getHistory(
|
|||||||
amountRefundedInvalid: Amounts.toString(amountRefundedInvalid),
|
amountRefundedInvalid: Amounts.toString(amountRefundedInvalid),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
tx.iter(Stores.recoupGroups).forEach(rg => {
|
||||||
|
if (rg.timestampFinished) {
|
||||||
|
history.push({
|
||||||
|
type: HistoryEventType.FundsRecouped,
|
||||||
|
timestamp: rg.timestampFinished,
|
||||||
|
eventId: makeEventId(HistoryEventType.FundsRecouped, rg.recoupGroupId),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -405,6 +405,32 @@ async function gatherPurchasePending(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function gatherRecoupPending(
|
||||||
|
tx: TransactionHandle,
|
||||||
|
now: Timestamp,
|
||||||
|
resp: PendingOperationsResponse,
|
||||||
|
onlyDue: boolean = false,
|
||||||
|
): Promise<void> {
|
||||||
|
await tx.iter(Stores.recoupGroups).forEach(rg => {
|
||||||
|
if (rg.timestampFinished) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
resp.nextRetryDelay = updateRetryDelay(
|
||||||
|
resp.nextRetryDelay,
|
||||||
|
now,
|
||||||
|
rg.retryInfo.nextRetry,
|
||||||
|
);
|
||||||
|
if (onlyDue && rg.retryInfo.nextRetry.t_ms > now.t_ms) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
resp.pendingOperations.push({
|
||||||
|
type: PendingOperationType.Recoup,
|
||||||
|
givesLifeness: true,
|
||||||
|
recoupGroupId: rg.recoupGroupId,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export async function getPendingOperations(
|
export async function getPendingOperations(
|
||||||
ws: InternalWalletState,
|
ws: InternalWalletState,
|
||||||
{ onlyDue = false } = {},
|
{ onlyDue = false } = {},
|
||||||
@ -420,6 +446,7 @@ export async function getPendingOperations(
|
|||||||
Stores.proposals,
|
Stores.proposals,
|
||||||
Stores.tips,
|
Stores.tips,
|
||||||
Stores.purchases,
|
Stores.purchases,
|
||||||
|
Stores.recoupGroups,
|
||||||
],
|
],
|
||||||
async tx => {
|
async tx => {
|
||||||
const walletBalance = await getBalancesInsideTransaction(ws, tx);
|
const walletBalance = await getBalancesInsideTransaction(ws, tx);
|
||||||
@ -436,6 +463,7 @@ export async function getPendingOperations(
|
|||||||
await gatherProposalPending(tx, now, resp, onlyDue);
|
await gatherProposalPending(tx, now, resp, onlyDue);
|
||||||
await gatherTipPending(tx, now, resp, onlyDue);
|
await gatherTipPending(tx, now, resp, onlyDue);
|
||||||
await gatherPurchasePending(tx, now, resp, onlyDue);
|
await gatherPurchasePending(tx, now, resp, onlyDue);
|
||||||
|
await gatherRecoupPending(tx, now, resp, onlyDue);
|
||||||
return resp;
|
return resp;
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
This file is part of GNU Taler
|
This file is part of GNU Taler
|
||||||
(C) 2019 GNUnet e.V.
|
(C) 2019-2010 Taler Systems SA
|
||||||
|
|
||||||
GNU Taler is free software; you can redistribute it and/or modify it under the
|
GNU Taler is free software; you can redistribute it and/or modify it under the
|
||||||
terms of the GNU General Public License as published by the Free Software
|
terms of the GNU General Public License as published by the Free Software
|
||||||
@ -14,76 +14,358 @@
|
|||||||
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
|
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implementation of the recoup operation, which allows to recover the
|
||||||
|
* value of coins held in a revoked denomination.
|
||||||
|
*
|
||||||
|
* @author Florian Dold <dold@taler.net>
|
||||||
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Imports.
|
* Imports.
|
||||||
*/
|
*/
|
||||||
import {
|
|
||||||
Database
|
|
||||||
} from "../util/query";
|
|
||||||
import { InternalWalletState } from "./state";
|
import { InternalWalletState } from "./state";
|
||||||
import { Stores, TipRecord, CoinStatus } from "../types/dbTypes";
|
import {
|
||||||
|
Stores,
|
||||||
|
CoinStatus,
|
||||||
|
CoinSourceType,
|
||||||
|
CoinRecord,
|
||||||
|
WithdrawCoinSource,
|
||||||
|
RefreshCoinSource,
|
||||||
|
ReserveRecordStatus,
|
||||||
|
RecoupGroupRecord,
|
||||||
|
initRetryInfo,
|
||||||
|
updateRetryInfoTimeout,
|
||||||
|
} from "../types/dbTypes";
|
||||||
|
|
||||||
import { Logger } from "../util/logging";
|
import { codecForRecoupConfirmation } from "../types/talerTypes";
|
||||||
import { RecoupConfirmation, codecForRecoupConfirmation } from "../types/talerTypes";
|
|
||||||
import { updateExchangeFromUrl } from "./exchanges";
|
|
||||||
import { NotificationType } from "../types/notifications";
|
import { NotificationType } from "../types/notifications";
|
||||||
|
import { processReserve } from "./reserves";
|
||||||
|
|
||||||
const logger = new Logger("payback.ts");
|
import * as Amounts from "../util/amounts";
|
||||||
|
import { createRefreshGroup, processRefreshGroup } from "./refresh";
|
||||||
|
import { RefreshReason, OperationError } from "../types/walletTypes";
|
||||||
|
import { TransactionHandle } from "../util/query";
|
||||||
|
import { encodeCrock, getRandomBytes } from "../crypto/talerCrypto";
|
||||||
|
import { getTimestampNow } from "../util/time";
|
||||||
|
import { guardOperationException } from "./errors";
|
||||||
|
|
||||||
export async function recoup(
|
async function incrementRecoupRetry(
|
||||||
ws: InternalWalletState,
|
ws: InternalWalletState,
|
||||||
coinPub: string,
|
recoupGroupId: string,
|
||||||
|
err: OperationError | undefined,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
|
await ws.db.runWithWriteTransaction([Stores.recoupGroups], async tx => {
|
||||||
|
const r = await tx.get(Stores.recoupGroups, recoupGroupId);
|
||||||
|
if (!r) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!r.retryInfo) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
r.retryInfo.retryCounter++;
|
||||||
|
updateRetryInfoTimeout(r.retryInfo);
|
||||||
|
r.lastError = err;
|
||||||
|
await tx.put(Stores.recoupGroups, r);
|
||||||
|
});
|
||||||
|
ws.notify({ type: NotificationType.RecoupOperationError });
|
||||||
|
}
|
||||||
|
|
||||||
|
async function putGroupAsFinished(
|
||||||
|
tx: TransactionHandle,
|
||||||
|
recoupGroup: RecoupGroupRecord,
|
||||||
|
coinIdx: number,
|
||||||
|
): Promise<void> {
|
||||||
|
recoupGroup.recoupFinishedPerCoin[coinIdx] = true;
|
||||||
|
let allFinished = true;
|
||||||
|
for (const b of recoupGroup.recoupFinishedPerCoin) {
|
||||||
|
if (!b) {
|
||||||
|
allFinished = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (allFinished) {
|
||||||
|
recoupGroup.timestampFinished = getTimestampNow();
|
||||||
|
recoupGroup.retryInfo = initRetryInfo(false);
|
||||||
|
recoupGroup.lastError = undefined;
|
||||||
|
}
|
||||||
|
await tx.put(Stores.recoupGroups, recoupGroup);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function recoupTipCoin(
|
||||||
|
ws: InternalWalletState,
|
||||||
|
recoupGroupId: string,
|
||||||
|
coinIdx: number,
|
||||||
|
coin: CoinRecord,
|
||||||
|
): Promise<void> {
|
||||||
|
// We can't really recoup a coin we got via tipping.
|
||||||
|
// Thus we just put the coin to sleep.
|
||||||
|
// FIXME: somehow report this to the user
|
||||||
|
await ws.db.runWithWriteTransaction([Stores.recoupGroups], async tx => {
|
||||||
|
const recoupGroup = await tx.get(Stores.recoupGroups, recoupGroupId);
|
||||||
|
if (!recoupGroup) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (recoupGroup.recoupFinishedPerCoin[coinIdx]) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await putGroupAsFinished(tx, recoupGroup, coinIdx);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function recoupWithdrawCoin(
|
||||||
|
ws: InternalWalletState,
|
||||||
|
recoupGroupId: string,
|
||||||
|
coinIdx: number,
|
||||||
|
coin: CoinRecord,
|
||||||
|
cs: WithdrawCoinSource,
|
||||||
|
): Promise<void> {
|
||||||
|
const reservePub = cs.reservePub;
|
||||||
|
const reserve = await ws.db.get(Stores.reserves, reservePub);
|
||||||
|
if (!reserve) {
|
||||||
|
// FIXME: We should at least emit some pending operation / warning for this?
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ws.notify({
|
||||||
|
type: NotificationType.RecoupStarted,
|
||||||
|
});
|
||||||
|
|
||||||
|
const recoupRequest = await ws.cryptoApi.createRecoupRequest(coin);
|
||||||
|
const reqUrl = new URL(`/coins/${coin.coinPub}/recoup`, coin.exchangeBaseUrl);
|
||||||
|
const resp = await ws.http.postJson(reqUrl.href, recoupRequest);
|
||||||
|
if (resp.status !== 200) {
|
||||||
|
throw Error("recoup request failed");
|
||||||
|
}
|
||||||
|
const recoupConfirmation = codecForRecoupConfirmation().decode(
|
||||||
|
await resp.json(),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (recoupConfirmation.reserve_pub !== reservePub) {
|
||||||
|
throw Error(`Coin's reserve doesn't match reserve on recoup`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// FIXME: verify that our expectations about the amount match
|
||||||
|
|
||||||
|
await ws.db.runWithWriteTransaction(
|
||||||
|
[Stores.coins, Stores.reserves, Stores.recoupGroups],
|
||||||
|
async tx => {
|
||||||
|
const recoupGroup = await tx.get(Stores.recoupGroups, recoupGroupId);
|
||||||
|
if (!recoupGroup) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (recoupGroup.recoupFinishedPerCoin[coinIdx]) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const updatedCoin = await tx.get(Stores.coins, coin.coinPub);
|
||||||
|
if (!updatedCoin) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const updatedReserve = await tx.get(Stores.reserves, reserve.reservePub);
|
||||||
|
if (!updatedReserve) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
updatedCoin.status = CoinStatus.Dormant;
|
||||||
|
const currency = updatedCoin.currentAmount.currency;
|
||||||
|
updatedCoin.currentAmount = Amounts.getZero(currency);
|
||||||
|
updatedReserve.reserveStatus = ReserveRecordStatus.QUERYING_STATUS;
|
||||||
|
await tx.put(Stores.coins, updatedCoin);
|
||||||
|
await tx.put(Stores.reserves, updatedReserve);
|
||||||
|
await putGroupAsFinished(tx, recoupGroup, coinIdx);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
ws.notify({
|
||||||
|
type: NotificationType.RecoupFinished,
|
||||||
|
});
|
||||||
|
|
||||||
|
processReserve(ws, reserve.reservePub).catch(e => {
|
||||||
|
console.log("processing reserve after recoup failed:", e);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function recoupRefreshCoin(
|
||||||
|
ws: InternalWalletState,
|
||||||
|
recoupGroupId: string,
|
||||||
|
coinIdx: number,
|
||||||
|
coin: CoinRecord,
|
||||||
|
cs: RefreshCoinSource,
|
||||||
|
): Promise<void> {
|
||||||
|
ws.notify({
|
||||||
|
type: NotificationType.RecoupStarted,
|
||||||
|
});
|
||||||
|
|
||||||
|
const recoupRequest = await ws.cryptoApi.createRecoupRequest(coin);
|
||||||
|
const reqUrl = new URL(`/coins/${coin.coinPub}/recoup`, coin.exchangeBaseUrl);
|
||||||
|
const resp = await ws.http.postJson(reqUrl.href, recoupRequest);
|
||||||
|
if (resp.status !== 200) {
|
||||||
|
throw Error("recoup request failed");
|
||||||
|
}
|
||||||
|
const recoupConfirmation = codecForRecoupConfirmation().decode(
|
||||||
|
await resp.json(),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (recoupConfirmation.old_coin_pub != cs.oldCoinPub) {
|
||||||
|
throw Error(`Coin's oldCoinPub doesn't match reserve on recoup`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const refreshGroupId = await ws.db.runWithWriteTransaction(
|
||||||
|
[Stores.coins, Stores.reserves],
|
||||||
|
async tx => {
|
||||||
|
const recoupGroup = await tx.get(Stores.recoupGroups, recoupGroupId);
|
||||||
|
if (!recoupGroup) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (recoupGroup.recoupFinishedPerCoin[coinIdx]) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const oldCoin = await tx.get(Stores.coins, cs.oldCoinPub);
|
||||||
|
const updatedCoin = await tx.get(Stores.coins, coin.coinPub);
|
||||||
|
if (!updatedCoin) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!oldCoin) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
updatedCoin.status = CoinStatus.Dormant;
|
||||||
|
oldCoin.currentAmount = Amounts.add(
|
||||||
|
oldCoin.currentAmount,
|
||||||
|
updatedCoin.currentAmount,
|
||||||
|
).amount;
|
||||||
|
await tx.put(Stores.coins, updatedCoin);
|
||||||
|
await putGroupAsFinished(tx, recoupGroup, coinIdx);
|
||||||
|
return await createRefreshGroup(
|
||||||
|
tx,
|
||||||
|
[{ coinPub: oldCoin.coinPub }],
|
||||||
|
RefreshReason.Recoup,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (refreshGroupId) {
|
||||||
|
processRefreshGroup(ws, refreshGroupId.refreshGroupId).then(e => {
|
||||||
|
console.error("error while refreshing after recoup", e);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function resetRecoupGroupRetry(
|
||||||
|
ws: InternalWalletState,
|
||||||
|
recoupGroupId: string,
|
||||||
|
) {
|
||||||
|
await ws.db.mutate(Stores.recoupGroups, recoupGroupId, x => {
|
||||||
|
if (x.retryInfo.active) {
|
||||||
|
x.retryInfo = initRetryInfo();
|
||||||
|
}
|
||||||
|
return x;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function processRecoupGroup(
|
||||||
|
ws: InternalWalletState,
|
||||||
|
recoupGroupId: string,
|
||||||
|
forceNow: boolean = false,
|
||||||
|
): Promise<void> {
|
||||||
|
await ws.memoProcessRecoup.memo(recoupGroupId, async () => {
|
||||||
|
const onOpErr = (e: OperationError) =>
|
||||||
|
incrementRecoupRetry(ws, recoupGroupId, e);
|
||||||
|
return await guardOperationException(
|
||||||
|
async () => await processRecoupGroupImpl(ws, recoupGroupId, forceNow),
|
||||||
|
onOpErr,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function processRecoupGroupImpl(
|
||||||
|
ws: InternalWalletState,
|
||||||
|
recoupGroupId: string,
|
||||||
|
forceNow: boolean = false,
|
||||||
|
): Promise<void> {
|
||||||
|
if (forceNow) {
|
||||||
|
await resetRecoupGroupRetry(ws, recoupGroupId);
|
||||||
|
}
|
||||||
|
const recoupGroup = await ws.db.get(Stores.recoupGroups, recoupGroupId);
|
||||||
|
if (!recoupGroup) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (recoupGroup.timestampFinished) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const ps = recoupGroup.coinPubs.map((x, i) =>
|
||||||
|
processRecoup(ws, recoupGroupId, i),
|
||||||
|
);
|
||||||
|
await Promise.all(ps);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createRecoupGroup(
|
||||||
|
ws: InternalWalletState,
|
||||||
|
tx: TransactionHandle,
|
||||||
|
coinPubs: string[],
|
||||||
|
): Promise<string> {
|
||||||
|
const recoupGroupId = encodeCrock(getRandomBytes(32));
|
||||||
|
|
||||||
|
const recoupGroup: RecoupGroupRecord = {
|
||||||
|
recoupGroupId,
|
||||||
|
coinPubs: coinPubs,
|
||||||
|
lastError: undefined,
|
||||||
|
timestampFinished: undefined,
|
||||||
|
timestampStarted: getTimestampNow(),
|
||||||
|
retryInfo: initRetryInfo(),
|
||||||
|
recoupFinishedPerCoin: coinPubs.map(() => false),
|
||||||
|
};
|
||||||
|
|
||||||
|
for (let coinIdx = 0; coinIdx < coinPubs.length; coinIdx++) {
|
||||||
|
const coinPub = coinPubs[coinIdx];
|
||||||
|
const coin = await tx.get(Stores.coins, coinPub);
|
||||||
|
if (!coin) {
|
||||||
|
recoupGroup.recoupFinishedPerCoin[coinIdx] = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (Amounts.isZero(coin.currentAmount)) {
|
||||||
|
recoupGroup.recoupFinishedPerCoin[coinIdx] = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
coin.currentAmount = Amounts.getZero(coin.currentAmount.currency);
|
||||||
|
await tx.put(Stores.coins, coin);
|
||||||
|
}
|
||||||
|
|
||||||
|
await tx.put(Stores.recoupGroups, recoupGroup);
|
||||||
|
|
||||||
|
return recoupGroupId;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function processRecoup(
|
||||||
|
ws: InternalWalletState,
|
||||||
|
recoupGroupId: string,
|
||||||
|
coinIdx: number,
|
||||||
|
): Promise<void> {
|
||||||
|
const recoupGroup = await ws.db.get(Stores.recoupGroups, recoupGroupId);
|
||||||
|
if (!recoupGroup) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (recoupGroup.timestampFinished) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (recoupGroup.recoupFinishedPerCoin[coinIdx]) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const coinPub = recoupGroup.coinPubs[coinIdx];
|
||||||
|
|
||||||
let coin = await ws.db.get(Stores.coins, coinPub);
|
let coin = await ws.db.get(Stores.coins, coinPub);
|
||||||
if (!coin) {
|
if (!coin) {
|
||||||
throw Error(`Coin ${coinPub} not found, can't request payback`);
|
throw Error(`Coin ${coinPub} not found, can't request payback`);
|
||||||
}
|
}
|
||||||
const reservePub = coin.reservePub;
|
|
||||||
if (!reservePub) {
|
|
||||||
throw Error(`Can't request payback for a refreshed coin`);
|
|
||||||
}
|
|
||||||
const reserve = await ws.db.get(Stores.reserves, reservePub);
|
|
||||||
if (!reserve) {
|
|
||||||
throw Error(`Reserve of coin ${coinPub} not found`);
|
|
||||||
}
|
|
||||||
switch (coin.status) {
|
|
||||||
case CoinStatus.Dormant:
|
|
||||||
throw Error(`Can't do payback for coin ${coinPub} since it's dormant`);
|
|
||||||
}
|
|
||||||
coin.status = CoinStatus.Dormant;
|
|
||||||
// Even if we didn't get the payback yet, we suspend withdrawal, since
|
|
||||||
// technically we might update reserve status before we get the response
|
|
||||||
// from the reserve for the payback request.
|
|
||||||
reserve.hasPayback = true;
|
|
||||||
await ws.db.runWithWriteTransaction(
|
|
||||||
[Stores.coins, Stores.reserves],
|
|
||||||
async tx => {
|
|
||||||
await tx.put(Stores.coins, coin!!);
|
|
||||||
await tx.put(Stores.reserves, reserve);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
ws.notify({
|
|
||||||
type: NotificationType.PaybackStarted,
|
|
||||||
});
|
|
||||||
|
|
||||||
const paybackRequest = await ws.cryptoApi.createPaybackRequest(coin);
|
const cs = coin.coinSource;
|
||||||
const reqUrl = new URL("payback", coin.exchangeBaseUrl);
|
|
||||||
const resp = await ws.http.postJson(reqUrl.href, paybackRequest);
|
switch (cs.type) {
|
||||||
if (resp.status !== 200) {
|
case CoinSourceType.Tip:
|
||||||
throw Error();
|
return recoupTipCoin(ws, recoupGroupId, coinIdx, coin);
|
||||||
|
case CoinSourceType.Refresh:
|
||||||
|
return recoupRefreshCoin(ws, recoupGroupId, coinIdx, coin, cs);
|
||||||
|
case CoinSourceType.Withdraw:
|
||||||
|
return recoupWithdrawCoin(ws, recoupGroupId, coinIdx, coin, cs);
|
||||||
|
default:
|
||||||
|
throw Error("unknown coin source type");
|
||||||
}
|
}
|
||||||
const paybackConfirmation = codecForRecoupConfirmation().decode(await resp.json());
|
|
||||||
if (paybackConfirmation.reserve_pub !== coin.reservePub) {
|
|
||||||
throw Error(`Coin's reserve doesn't match reserve on payback`);
|
|
||||||
}
|
|
||||||
coin = await ws.db.get(Stores.coins, coinPub);
|
|
||||||
if (!coin) {
|
|
||||||
throw Error(`Coin ${coinPub} not found, can't confirm payback`);
|
|
||||||
}
|
|
||||||
coin.status = CoinStatus.Dormant;
|
|
||||||
await ws.db.put(Stores.coins, coin);
|
|
||||||
ws.notify({
|
|
||||||
type: NotificationType.PaybackFinished,
|
|
||||||
});
|
|
||||||
await updateExchangeFromUrl(ws, coin.exchangeBaseUrl, true);
|
|
||||||
}
|
}
|
||||||
|
@ -26,6 +26,7 @@ import {
|
|||||||
initRetryInfo,
|
initRetryInfo,
|
||||||
updateRetryInfoTimeout,
|
updateRetryInfoTimeout,
|
||||||
RefreshGroupRecord,
|
RefreshGroupRecord,
|
||||||
|
CoinSourceType,
|
||||||
} from "../types/dbTypes";
|
} from "../types/dbTypes";
|
||||||
import { amountToPretty } from "../util/helpers";
|
import { amountToPretty } from "../util/helpers";
|
||||||
import { Database, TransactionHandle } from "../util/query";
|
import { Database, TransactionHandle } from "../util/query";
|
||||||
@ -407,10 +408,11 @@ async function refreshReveal(
|
|||||||
denomPubHash: denom.denomPubHash,
|
denomPubHash: denom.denomPubHash,
|
||||||
denomSig,
|
denomSig,
|
||||||
exchangeBaseUrl: refreshSession.exchangeBaseUrl,
|
exchangeBaseUrl: refreshSession.exchangeBaseUrl,
|
||||||
reservePub: undefined,
|
|
||||||
status: CoinStatus.Fresh,
|
status: CoinStatus.Fresh,
|
||||||
coinIndex: -1,
|
coinSource: {
|
||||||
withdrawSessionId: "",
|
type: CoinSourceType.Refresh,
|
||||||
|
oldCoinPub: refreshSession.meltCoinPub,
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
coins.push(coin);
|
coins.push(coin);
|
||||||
|
@ -103,7 +103,6 @@ export async function createReserve(
|
|||||||
amountWithdrawCompleted: Amounts.getZero(currency),
|
amountWithdrawCompleted: Amounts.getZero(currency),
|
||||||
amountWithdrawRemaining: Amounts.getZero(currency),
|
amountWithdrawRemaining: Amounts.getZero(currency),
|
||||||
exchangeBaseUrl: canonExchange,
|
exchangeBaseUrl: canonExchange,
|
||||||
hasPayback: false,
|
|
||||||
amountInitiallyRequested: req.amount,
|
amountInitiallyRequested: req.amount,
|
||||||
reservePriv: keypair.priv,
|
reservePriv: keypair.priv,
|
||||||
reservePub: keypair.pub,
|
reservePub: keypair.pub,
|
||||||
|
@ -39,6 +39,7 @@ export class InternalWalletState {
|
|||||||
> = new AsyncOpMemoSingle();
|
> = new AsyncOpMemoSingle();
|
||||||
memoGetBalance: AsyncOpMemoSingle<WalletBalance> = new AsyncOpMemoSingle();
|
memoGetBalance: AsyncOpMemoSingle<WalletBalance> = new AsyncOpMemoSingle();
|
||||||
memoProcessRefresh: AsyncOpMemoMap<void> = new AsyncOpMemoMap();
|
memoProcessRefresh: AsyncOpMemoMap<void> = new AsyncOpMemoMap();
|
||||||
|
memoProcessRecoup: AsyncOpMemoMap<void> = new AsyncOpMemoMap();
|
||||||
cryptoApi: CryptoApi;
|
cryptoApi: CryptoApi;
|
||||||
|
|
||||||
listeners: NotificationListener[] = [];
|
listeners: NotificationListener[] = [];
|
||||||
|
@ -24,6 +24,7 @@ import {
|
|||||||
PlanchetRecord,
|
PlanchetRecord,
|
||||||
initRetryInfo,
|
initRetryInfo,
|
||||||
updateRetryInfoTimeout,
|
updateRetryInfoTimeout,
|
||||||
|
CoinSourceType,
|
||||||
} from "../types/dbTypes";
|
} from "../types/dbTypes";
|
||||||
import * as Amounts from "../util/amounts";
|
import * as Amounts from "../util/amounts";
|
||||||
import {
|
import {
|
||||||
@ -48,6 +49,7 @@ import {
|
|||||||
timestampCmp,
|
timestampCmp,
|
||||||
timestampSubtractDuraction,
|
timestampSubtractDuraction,
|
||||||
} from "../util/time";
|
} from "../util/time";
|
||||||
|
import { Store } from "../util/query";
|
||||||
|
|
||||||
const logger = new Logger("withdraw.ts");
|
const logger = new Logger("withdraw.ts");
|
||||||
|
|
||||||
@ -229,10 +231,13 @@ async function processPlanchet(
|
|||||||
denomPubHash: planchet.denomPubHash,
|
denomPubHash: planchet.denomPubHash,
|
||||||
denomSig,
|
denomSig,
|
||||||
exchangeBaseUrl: withdrawalSession.exchangeBaseUrl,
|
exchangeBaseUrl: withdrawalSession.exchangeBaseUrl,
|
||||||
reservePub: planchet.reservePub,
|
|
||||||
status: CoinStatus.Fresh,
|
status: CoinStatus.Fresh,
|
||||||
|
coinSource: {
|
||||||
|
type: CoinSourceType.Withdraw,
|
||||||
coinIndex: coinIdx,
|
coinIndex: coinIdx,
|
||||||
withdrawSessionId: withdrawalSessionId,
|
reservePub: planchet.reservePub,
|
||||||
|
withdrawSessionId: withdrawalSessionId
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let withdrawSessionFinished = false;
|
let withdrawSessionFinished = false;
|
||||||
@ -449,15 +454,16 @@ async function processWithdrawCoin(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const coin = await ws.db.getIndexed(Stores.coins.byWithdrawalWithIdx, [
|
const planchet = withdrawalSession.planchets[coinIndex];
|
||||||
withdrawalSessionId,
|
|
||||||
coinIndex,
|
if (planchet) {
|
||||||
]);
|
const coin = await ws.db.get(Stores.coins, planchet.coinPub);
|
||||||
|
|
||||||
if (coin) {
|
if (coin) {
|
||||||
console.log("coin already exists");
|
console.log("coin already exists");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!withdrawalSession.planchets[coinIndex]) {
|
if (!withdrawalSession.planchets[coinIndex]) {
|
||||||
const key = `${withdrawalSessionId}-${coinIndex}`;
|
const key = `${withdrawalSessionId}-${coinIndex}`;
|
||||||
|
@ -33,10 +33,7 @@ import {
|
|||||||
} from "./talerTypes";
|
} from "./talerTypes";
|
||||||
|
|
||||||
import { Index, Store } from "../util/query";
|
import { Index, Store } from "../util/query";
|
||||||
import {
|
import { OperationError, RefreshReason } from "./walletTypes";
|
||||||
OperationError,
|
|
||||||
RefreshReason,
|
|
||||||
} from "./walletTypes";
|
|
||||||
import { ReserveTransaction } from "./ReserveTransaction";
|
import { ReserveTransaction } from "./ReserveTransaction";
|
||||||
import { Timestamp, Duration, getTimestampNow } from "../util/time";
|
import { Timestamp, Duration, getTimestampNow } from "../util/time";
|
||||||
|
|
||||||
@ -133,7 +130,6 @@ export function initRetryInfo(
|
|||||||
return info;
|
return info;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A reserve record as stored in the wallet's database.
|
* A reserve record as stored in the wallet's database.
|
||||||
*/
|
*/
|
||||||
@ -196,12 +192,6 @@ export interface ReserveRecord {
|
|||||||
*/
|
*/
|
||||||
amountInitiallyRequested: AmountJson;
|
amountInitiallyRequested: AmountJson;
|
||||||
|
|
||||||
/**
|
|
||||||
* We got some payback to this reserve. We'll cease to automatically
|
|
||||||
* withdraw money from it.
|
|
||||||
*/
|
|
||||||
hasPayback: boolean;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Wire information (as payto URI) for the bank account that
|
* Wire information (as payto URI) for the bank account that
|
||||||
* transfered funds for this reserve.
|
* transfered funds for this reserve.
|
||||||
@ -386,6 +376,8 @@ export interface DenominationRecord {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Did we verify the signature on the denomination?
|
* Did we verify the signature on the denomination?
|
||||||
|
*
|
||||||
|
* FIXME: Rename to "verificationStatus"?
|
||||||
*/
|
*/
|
||||||
status: DenominationStatus;
|
status: DenominationStatus;
|
||||||
|
|
||||||
@ -396,6 +388,13 @@ export interface DenominationRecord {
|
|||||||
*/
|
*/
|
||||||
isOffered: boolean;
|
isOffered: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Did the exchange revoke the denomination?
|
||||||
|
* When this field is set to true in the database, the same transaction
|
||||||
|
* should also mark all affected coins as revoked.
|
||||||
|
*/
|
||||||
|
isRevoked: boolean;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Base URL of the exchange.
|
* Base URL of the exchange.
|
||||||
*/
|
*/
|
||||||
@ -577,7 +576,7 @@ export interface RefreshPlanchetRecord {
|
|||||||
/**
|
/**
|
||||||
* Status of a coin.
|
* Status of a coin.
|
||||||
*/
|
*/
|
||||||
export enum CoinStatus {
|
export const enum CoinStatus {
|
||||||
/**
|
/**
|
||||||
* Withdrawn and never shown to anybody.
|
* Withdrawn and never shown to anybody.
|
||||||
*/
|
*/
|
||||||
@ -588,26 +587,47 @@ export enum CoinStatus {
|
|||||||
Dormant = "dormant",
|
Dormant = "dormant",
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum CoinSource {
|
export const enum CoinSourceType {
|
||||||
Withdraw = "withdraw",
|
Withdraw = "withdraw",
|
||||||
Refresh = "refresh",
|
Refresh = "refresh",
|
||||||
Tip = "tip",
|
Tip = "tip",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface WithdrawCoinSource {
|
||||||
|
type: CoinSourceType.Withdraw;
|
||||||
|
withdrawSessionId: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Index of the coin in the withdrawal session.
|
||||||
|
*/
|
||||||
|
coinIndex: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reserve public key for the reserve we got this coin from.
|
||||||
|
*/
|
||||||
|
reservePub: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RefreshCoinSource {
|
||||||
|
type: CoinSourceType.Refresh;
|
||||||
|
oldCoinPub: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TipCoinSource {
|
||||||
|
type: CoinSourceType.Tip;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CoinSource = WithdrawCoinSource | RefreshCoinSource | TipCoinSource;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* CoinRecord as stored in the "coins" data store
|
* CoinRecord as stored in the "coins" data store
|
||||||
* of the wallet database.
|
* of the wallet database.
|
||||||
*/
|
*/
|
||||||
export interface CoinRecord {
|
export interface CoinRecord {
|
||||||
/**
|
/**
|
||||||
* Withdraw session ID, or "" (empty string) if withdrawn via refresh.
|
* Where did the coin come from? Used for recouping coins.
|
||||||
*/
|
*/
|
||||||
withdrawSessionId: string;
|
coinSource: CoinSource;
|
||||||
|
|
||||||
/**
|
|
||||||
* Index of the coin in the withdrawal session.
|
|
||||||
*/
|
|
||||||
coinIndex: number;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Public key of the coin.
|
* Public key of the coin.
|
||||||
@ -658,12 +678,6 @@ export interface CoinRecord {
|
|||||||
*/
|
*/
|
||||||
blindingKey: string;
|
blindingKey: string;
|
||||||
|
|
||||||
/**
|
|
||||||
* Reserve public key for the reserve we got this coin from,
|
|
||||||
* or zero when we got the coin from refresh.
|
|
||||||
*/
|
|
||||||
reservePub: string | undefined;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Status of the coin.
|
* Status of the coin.
|
||||||
*/
|
*/
|
||||||
@ -1285,6 +1299,11 @@ export type WithdrawalSource = WithdrawalSourceTip | WithdrawalSourceReserve;
|
|||||||
export interface WithdrawalSessionRecord {
|
export interface WithdrawalSessionRecord {
|
||||||
withdrawSessionId: string;
|
withdrawSessionId: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Withdrawal source. Fields that don't apply to the respective
|
||||||
|
* withdrawal source type must be null (i.e. can't be absent),
|
||||||
|
* otherwise the IndexedDB indexing won't like us.
|
||||||
|
*/
|
||||||
source: WithdrawalSource;
|
source: WithdrawalSource;
|
||||||
|
|
||||||
exchangeBaseUrl: string;
|
exchangeBaseUrl: string;
|
||||||
@ -1343,6 +1362,46 @@ export interface BankWithdrawUriRecord {
|
|||||||
reservePub: string;
|
reservePub: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Status of recoup operations that were grouped together.
|
||||||
|
*
|
||||||
|
* The remaining amount of involved coins should be set to zero
|
||||||
|
* in the same transaction that inserts the RecoupGroupRecord.
|
||||||
|
*/
|
||||||
|
export interface RecoupGroupRecord {
|
||||||
|
/**
|
||||||
|
* Unique identifier for the recoup group record.
|
||||||
|
*/
|
||||||
|
recoupGroupId: string;
|
||||||
|
|
||||||
|
timestampStarted: Timestamp;
|
||||||
|
|
||||||
|
timestampFinished: Timestamp | undefined;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Public keys that identify the coins being recouped
|
||||||
|
* as part of this session.
|
||||||
|
*
|
||||||
|
* (Structured like this to enable multiEntry indexing in IndexedDB.)
|
||||||
|
*/
|
||||||
|
coinPubs: string[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Array of flags to indicate whether the recoup finished on each individual coin.
|
||||||
|
*/
|
||||||
|
recoupFinishedPerCoin: boolean[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retry info.
|
||||||
|
*/
|
||||||
|
retryInfo: RetryInfo;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Last error that occured, if any.
|
||||||
|
*/
|
||||||
|
lastError: OperationError | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
export const enum ImportPayloadType {
|
export const enum ImportPayloadType {
|
||||||
CoreSchema = "core-schema",
|
CoreSchema = "core-schema",
|
||||||
}
|
}
|
||||||
@ -1398,11 +1457,6 @@ export namespace Stores {
|
|||||||
"denomPubIndex",
|
"denomPubIndex",
|
||||||
"denomPub",
|
"denomPub",
|
||||||
);
|
);
|
||||||
byWithdrawalWithIdx = new Index<any, CoinRecord>(
|
|
||||||
this,
|
|
||||||
"planchetsByWithdrawalWithIdxIndex",
|
|
||||||
["withdrawSessionId", "coinIndex"],
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class ProposalsStore extends Store<ProposalRecord> {
|
class ProposalsStore extends Store<ProposalRecord> {
|
||||||
@ -1540,6 +1594,9 @@ export namespace Stores {
|
|||||||
export const refreshGroups = new Store<RefreshGroupRecord>("refreshGroups", {
|
export const refreshGroups = new Store<RefreshGroupRecord>("refreshGroups", {
|
||||||
keyPath: "refreshGroupId",
|
keyPath: "refreshGroupId",
|
||||||
});
|
});
|
||||||
|
export const recoupGroups = new Store<RecoupGroupRecord>("recoupGroups", {
|
||||||
|
keyPath: "recoupGroupId",
|
||||||
|
});
|
||||||
export const reserves = new ReservesStore();
|
export const reserves = new ReservesStore();
|
||||||
export const purchases = new PurchasesStore();
|
export const purchases = new PurchasesStore();
|
||||||
export const tips = new TipsStore();
|
export const tips = new TipsStore();
|
||||||
|
@ -348,19 +348,7 @@ export interface HistoryFundsDepositedToSelfEvent {
|
|||||||
* converted funds in these denominations to new funds.
|
* converted funds in these denominations to new funds.
|
||||||
*/
|
*/
|
||||||
export interface HistoryFundsRecoupedEvent {
|
export interface HistoryFundsRecoupedEvent {
|
||||||
type: HistoryEventType.FundsDepositedToSelf;
|
type: HistoryEventType.FundsRecouped;
|
||||||
|
|
||||||
exchangeBaseUrl: string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Amount that the wallet managed to recover.
|
|
||||||
*/
|
|
||||||
amountRecouped: string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Amount that was lost due to fees.
|
|
||||||
*/
|
|
||||||
amountLost: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -26,8 +26,8 @@ export const enum NotificationType {
|
|||||||
ProposalAccepted = "proposal-accepted",
|
ProposalAccepted = "proposal-accepted",
|
||||||
ProposalDownloaded = "proposal-downloaded",
|
ProposalDownloaded = "proposal-downloaded",
|
||||||
RefundsSubmitted = "refunds-submitted",
|
RefundsSubmitted = "refunds-submitted",
|
||||||
PaybackStarted = "payback-started",
|
RecoupStarted = "payback-started",
|
||||||
PaybackFinished = "payback-finished",
|
RecoupFinished = "payback-finished",
|
||||||
RefreshRevealed = "refresh-revealed",
|
RefreshRevealed = "refresh-revealed",
|
||||||
RefreshMelted = "refresh-melted",
|
RefreshMelted = "refresh-melted",
|
||||||
RefreshStarted = "refresh-started",
|
RefreshStarted = "refresh-started",
|
||||||
@ -44,6 +44,7 @@ export const enum NotificationType {
|
|||||||
RefundFinished = "refund-finished",
|
RefundFinished = "refund-finished",
|
||||||
ExchangeOperationError = "exchange-operation-error",
|
ExchangeOperationError = "exchange-operation-error",
|
||||||
RefreshOperationError = "refresh-operation-error",
|
RefreshOperationError = "refresh-operation-error",
|
||||||
|
RecoupOperationError = "refresh-operation-error",
|
||||||
RefundApplyOperationError = "refund-apply-error",
|
RefundApplyOperationError = "refund-apply-error",
|
||||||
RefundStatusOperationError = "refund-status-error",
|
RefundStatusOperationError = "refund-status-error",
|
||||||
ProposalOperationError = "proposal-error",
|
ProposalOperationError = "proposal-error",
|
||||||
@ -82,11 +83,11 @@ export interface RefundsSubmittedNotification {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface PaybackStartedNotification {
|
export interface PaybackStartedNotification {
|
||||||
type: NotificationType.PaybackStarted;
|
type: NotificationType.RecoupStarted;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PaybackFinishedNotification {
|
export interface PaybackFinishedNotification {
|
||||||
type: NotificationType.PaybackFinished;
|
type: NotificationType.RecoupFinished;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RefreshMeltedNotification {
|
export interface RefreshMeltedNotification {
|
||||||
|
@ -58,6 +58,7 @@ export type PendingOperationInfo = PendingOperationInfoCommon &
|
|||||||
| PendingTipChoiceOperation
|
| PendingTipChoiceOperation
|
||||||
| PendingTipPickupOperation
|
| PendingTipPickupOperation
|
||||||
| PendingWithdrawOperation
|
| PendingWithdrawOperation
|
||||||
|
| PendingRecoupOperation
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -200,6 +201,11 @@ export interface PendingRefundApplyOperation {
|
|||||||
numRefundsDone: number;
|
numRefundsDone: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface PendingRecoupOperation {
|
||||||
|
type: PendingOperationType.Recoup;
|
||||||
|
recoupGroupId: string;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Status of an ongoing withdrawal operation.
|
* Status of an ongoing withdrawal operation.
|
||||||
*/
|
*/
|
||||||
|
@ -38,7 +38,12 @@ import {
|
|||||||
codecForBoolean,
|
codecForBoolean,
|
||||||
makeCodecForMap,
|
makeCodecForMap,
|
||||||
} from "../util/codec";
|
} from "../util/codec";
|
||||||
import { Timestamp, codecForTimestamp, Duration, codecForDuration } from "../util/time";
|
import {
|
||||||
|
Timestamp,
|
||||||
|
codecForTimestamp,
|
||||||
|
Duration,
|
||||||
|
codecForDuration,
|
||||||
|
} from "../util/time";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Denomination as found in the /keys response from the exchange.
|
* Denomination as found in the /keys response from the exchange.
|
||||||
@ -141,7 +146,7 @@ export class Auditor {
|
|||||||
/**
|
/**
|
||||||
* Request that we send to the exchange to get a payback.
|
* Request that we send to the exchange to get a payback.
|
||||||
*/
|
*/
|
||||||
export interface PaybackRequest {
|
export interface RecoupRequest {
|
||||||
/**
|
/**
|
||||||
* Denomination public key of the coin we want to get
|
* Denomination public key of the coin we want to get
|
||||||
* paid back.
|
* paid back.
|
||||||
@ -168,6 +173,11 @@ export interface PaybackRequest {
|
|||||||
* Signature made by the coin, authorizing the payback.
|
* Signature made by the coin, authorizing the payback.
|
||||||
*/
|
*/
|
||||||
coin_sig: string;
|
coin_sig: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Was the coin refreshed (and thus the recoup should go to the old coin)?
|
||||||
|
*/
|
||||||
|
refreshed: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -175,9 +185,15 @@ export interface PaybackRequest {
|
|||||||
*/
|
*/
|
||||||
export class RecoupConfirmation {
|
export class RecoupConfirmation {
|
||||||
/**
|
/**
|
||||||
* public key of the reserve that will receive the payback.
|
* Public key of the reserve that will receive the payback.
|
||||||
*/
|
*/
|
||||||
reserve_pub: string;
|
reserve_pub?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Public key of the old coin that will receive the recoup,
|
||||||
|
* provided if refreshed was true.
|
||||||
|
*/
|
||||||
|
old_coin_pub?: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* How much will the exchange pay back (needed by wallet in
|
* How much will the exchange pay back (needed by wallet in
|
||||||
@ -575,7 +591,7 @@ export class TipResponse {
|
|||||||
* Element of the payback list that the
|
* Element of the payback list that the
|
||||||
* exchange gives us in /keys.
|
* exchange gives us in /keys.
|
||||||
*/
|
*/
|
||||||
export class Payback {
|
export class Recoup {
|
||||||
/**
|
/**
|
||||||
* The hash of the denomination public key for which the payback is offered.
|
* The hash of the denomination public key for which the payback is offered.
|
||||||
*/
|
*/
|
||||||
@ -607,9 +623,9 @@ export class ExchangeKeysJson {
|
|||||||
list_issue_date: Timestamp;
|
list_issue_date: Timestamp;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* List of paybacks for compromised denominations.
|
* List of revoked denominations.
|
||||||
*/
|
*/
|
||||||
payback?: Payback[];
|
recoup?: Recoup[];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Short-lived signing keys used to sign online
|
* Short-lived signing keys used to sign online
|
||||||
@ -764,7 +780,10 @@ export const codecForAuditor = () =>
|
|||||||
makeCodecForObject<Auditor>()
|
makeCodecForObject<Auditor>()
|
||||||
.property("auditor_pub", codecForString)
|
.property("auditor_pub", codecForString)
|
||||||
.property("auditor_url", codecForString)
|
.property("auditor_url", codecForString)
|
||||||
.property("denomination_keys", makeCodecForList(codecForAuditorDenomSig()))
|
.property(
|
||||||
|
"denomination_keys",
|
||||||
|
makeCodecForList(codecForAuditorDenomSig()),
|
||||||
|
)
|
||||||
.build("Auditor"),
|
.build("Auditor"),
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -851,9 +870,9 @@ export const codecForTipResponse = () =>
|
|||||||
.build("TipResponse"),
|
.build("TipResponse"),
|
||||||
);
|
);
|
||||||
|
|
||||||
export const codecForPayback = () =>
|
export const codecForRecoup = () =>
|
||||||
typecheckedCodec<Payback>(
|
typecheckedCodec<Recoup>(
|
||||||
makeCodecForObject<Payback>()
|
makeCodecForObject<Recoup>()
|
||||||
.property("h_denom_pub", codecForString)
|
.property("h_denom_pub", codecForString)
|
||||||
.build("Payback"),
|
.build("Payback"),
|
||||||
);
|
);
|
||||||
@ -865,13 +884,12 @@ export const codecForExchangeKeysJson = () =>
|
|||||||
.property("master_public_key", codecForString)
|
.property("master_public_key", codecForString)
|
||||||
.property("auditors", makeCodecForList(codecForAuditor()))
|
.property("auditors", makeCodecForList(codecForAuditor()))
|
||||||
.property("list_issue_date", codecForTimestamp)
|
.property("list_issue_date", codecForTimestamp)
|
||||||
.property("payback", makeCodecOptional(makeCodecForList(codecForPayback())))
|
.property("recoup", makeCodecOptional(makeCodecForList(codecForRecoup())))
|
||||||
.property("signkeys", codecForAny)
|
.property("signkeys", codecForAny)
|
||||||
.property("version", codecForString)
|
.property("version", codecForString)
|
||||||
.build("KeysJson"),
|
.build("KeysJson"),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
export const codecForWireFeesJson = () =>
|
export const codecForWireFeesJson = () =>
|
||||||
typecheckedCodec<WireFeesJson>(
|
typecheckedCodec<WireFeesJson>(
|
||||||
makeCodecForObject<WireFeesJson>()
|
makeCodecForObject<WireFeesJson>()
|
||||||
@ -895,7 +913,10 @@ export const codecForExchangeWireJson = () =>
|
|||||||
typecheckedCodec<ExchangeWireJson>(
|
typecheckedCodec<ExchangeWireJson>(
|
||||||
makeCodecForObject<ExchangeWireJson>()
|
makeCodecForObject<ExchangeWireJson>()
|
||||||
.property("accounts", makeCodecForList(codecForAccountInfo()))
|
.property("accounts", makeCodecForList(codecForAccountInfo()))
|
||||||
.property("fees", makeCodecForMap(makeCodecForList(codecForWireFeesJson())))
|
.property(
|
||||||
|
"fees",
|
||||||
|
makeCodecForMap(makeCodecForList(codecForWireFeesJson())),
|
||||||
|
)
|
||||||
.build("ExchangeWireJson"),
|
.build("ExchangeWireJson"),
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -919,13 +940,12 @@ export const codecForCheckPaymentResponse = () =>
|
|||||||
.build("CheckPaymentResponse"),
|
.build("CheckPaymentResponse"),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
export const codecForWithdrawOperationStatusResponse = () =>
|
export const codecForWithdrawOperationStatusResponse = () =>
|
||||||
typecheckedCodec<WithdrawOperationStatusResponse>(
|
typecheckedCodec<WithdrawOperationStatusResponse>(
|
||||||
makeCodecForObject<WithdrawOperationStatusResponse>()
|
makeCodecForObject<WithdrawOperationStatusResponse>()
|
||||||
.property("selection_done", codecForBoolean)
|
.property("selection_done", codecForBoolean)
|
||||||
.property("transfer_done", codecForBoolean)
|
.property("transfer_done", codecForBoolean)
|
||||||
.property("amount",codecForString)
|
.property("amount", codecForString)
|
||||||
.property("sender_wire", makeCodecOptional(codecForString))
|
.property("sender_wire", makeCodecOptional(codecForString))
|
||||||
.property("suggested_exchange", makeCodecOptional(codecForString))
|
.property("suggested_exchange", makeCodecOptional(codecForString))
|
||||||
.property("confirm_transfer_url", makeCodecOptional(codecForString))
|
.property("confirm_transfer_url", makeCodecOptional(codecForString))
|
||||||
@ -945,11 +965,11 @@ export const codecForTipPickupGetResponse = () =>
|
|||||||
.build("TipPickupGetResponse"),
|
.build("TipPickupGetResponse"),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
export const codecForRecoupConfirmation = () =>
|
export const codecForRecoupConfirmation = () =>
|
||||||
typecheckedCodec<RecoupConfirmation>(
|
typecheckedCodec<RecoupConfirmation>(
|
||||||
makeCodecForObject<RecoupConfirmation>()
|
makeCodecForObject<RecoupConfirmation>()
|
||||||
.property("reserve_pub", codecForString)
|
.property("reserve_pub", makeCodecOptional(codecForString))
|
||||||
|
.property("old_coin_pub", makeCodecOptional(codecForString))
|
||||||
.property("amount", codecForString)
|
.property("amount", codecForString)
|
||||||
.property("timestamp", codecForTimestamp)
|
.property("timestamp", codecForTimestamp)
|
||||||
.property("exchange_sig", codecForString)
|
.property("exchange_sig", codecForString)
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
This file is part of TALER
|
This file is part of GNU Taler
|
||||||
(C) 2015-2017 GNUnet e.V. and INRIA
|
(C) 2015-2020 Taler Systems SA
|
||||||
|
|
||||||
TALER is free software; you can redistribute it and/or modify it under the
|
TALER is free software; you can redistribute it and/or modify it under the
|
||||||
terms of the GNU General Public License as published by the Free Software
|
terms of the GNU General Public License as published by the Free Software
|
||||||
@ -20,6 +20,8 @@
|
|||||||
* These types are defined in a separate file make tree shaking easier, since
|
* These types are defined in a separate file make tree shaking easier, since
|
||||||
* some components use these types (via RPC) but do not depend on the wallet
|
* some components use these types (via RPC) but do not depend on the wallet
|
||||||
* code directly.
|
* code directly.
|
||||||
|
*
|
||||||
|
* @author Florian Dold <dold@taler.net>
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -271,6 +271,14 @@ export class TransactionHandle {
|
|||||||
return new ResultStream<T>(req);
|
return new ResultStream<T>(req);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
iterIndexed<S extends IDBValidKey,T>(
|
||||||
|
index: Index<S, T>,
|
||||||
|
key?: any,
|
||||||
|
): ResultStream<T> {
|
||||||
|
const req = this.tx.objectStore(index.storeName).index(index.indexName).openCursor(key);
|
||||||
|
return new ResultStream<T>(req);
|
||||||
|
}
|
||||||
|
|
||||||
delete<T>(store: Store<T>, key: any): Promise<void> {
|
delete<T>(store: Store<T>, key: any): Promise<void> {
|
||||||
const req = this.tx.objectStore(store.name).delete(key);
|
const req = this.tx.objectStore(store.name).delete(key);
|
||||||
return requestToPromise(req);
|
return requestToPromise(req);
|
||||||
|
@ -95,7 +95,6 @@ import { getHistory } from "./operations/history";
|
|||||||
import { getPendingOperations } from "./operations/pending";
|
import { getPendingOperations } from "./operations/pending";
|
||||||
import { getBalances } from "./operations/balance";
|
import { getBalances } from "./operations/balance";
|
||||||
import { acceptTip, getTipStatus, processTip } from "./operations/tip";
|
import { acceptTip, getTipStatus, processTip } from "./operations/tip";
|
||||||
import { recoup } from "./operations/recoup";
|
|
||||||
import { TimerGroup } from "./util/timer";
|
import { TimerGroup } from "./util/timer";
|
||||||
import { AsyncCondition } from "./util/promiseUtils";
|
import { AsyncCondition } from "./util/promiseUtils";
|
||||||
import { AsyncOpMemoSingle } from "./util/asyncMemo";
|
import { AsyncOpMemoSingle } from "./util/asyncMemo";
|
||||||
@ -113,6 +112,7 @@ import {
|
|||||||
applyRefund,
|
applyRefund,
|
||||||
} from "./operations/refund";
|
} from "./operations/refund";
|
||||||
import { durationMin, Duration } from "./util/time";
|
import { durationMin, Duration } from "./util/time";
|
||||||
|
import { processRecoupGroup } from "./operations/recoup";
|
||||||
|
|
||||||
const builtinCurrencies: CurrencyRecord[] = [
|
const builtinCurrencies: CurrencyRecord[] = [
|
||||||
{
|
{
|
||||||
@ -217,6 +217,9 @@ export class Wallet {
|
|||||||
case PendingOperationType.RefundApply:
|
case PendingOperationType.RefundApply:
|
||||||
await processPurchaseApplyRefund(this.ws, pending.proposalId, forceNow);
|
await processPurchaseApplyRefund(this.ws, pending.proposalId, forceNow);
|
||||||
break;
|
break;
|
||||||
|
case PendingOperationType.Recoup:
|
||||||
|
await processRecoupGroup(this.ws, pending.recoupGroupId, forceNow);
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
assertUnreachable(pending);
|
assertUnreachable(pending);
|
||||||
}
|
}
|
||||||
@ -577,10 +580,6 @@ export class Wallet {
|
|||||||
return await this.db.iter(Stores.coins).toArray();
|
return await this.db.iter(Stores.coins).toArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
async getPaybackReserves(): Promise<ReserveRecord[]> {
|
|
||||||
return await this.db.iter(Stores.reserves).filter(r => r.hasPayback);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stop ongoing processing.
|
* Stop ongoing processing.
|
||||||
*/
|
*/
|
||||||
|
@ -106,14 +106,6 @@ export interface MessageMap {
|
|||||||
request: { exchangeBaseUrl: string };
|
request: { exchangeBaseUrl: string };
|
||||||
response: dbTypes.ReserveRecord[];
|
response: dbTypes.ReserveRecord[];
|
||||||
};
|
};
|
||||||
"get-payback-reserves": {
|
|
||||||
request: {};
|
|
||||||
response: dbTypes.ReserveRecord[];
|
|
||||||
};
|
|
||||||
"withdraw-payback-reserve": {
|
|
||||||
request: { reservePub: string };
|
|
||||||
response: dbTypes.ReserveRecord[];
|
|
||||||
};
|
|
||||||
"get-denoms": {
|
"get-denoms": {
|
||||||
request: { exchangeBaseUrl: string };
|
request: { exchangeBaseUrl: string };
|
||||||
response: dbTypes.DenominationRecord[];
|
response: dbTypes.DenominationRecord[];
|
||||||
|
@ -25,49 +25,11 @@
|
|||||||
*/
|
*/
|
||||||
import { ReserveRecord } from "../../types/dbTypes";
|
import { ReserveRecord } from "../../types/dbTypes";
|
||||||
import { renderAmount, registerMountPage } from "../renderHtml";
|
import { renderAmount, registerMountPage } from "../renderHtml";
|
||||||
import { getPaybackReserves, withdrawPaybackReserve } from "../wxApi";
|
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
|
||||||
function Payback() {
|
function Payback() {
|
||||||
const [reserves, setReserves] = useState<ReserveRecord[] | null>(null);
|
return <div>not implemented</div>;
|
||||||
|
|
||||||
useState(() => {
|
|
||||||
const update = async () => {
|
|
||||||
const r = await getPaybackReserves();
|
|
||||||
setReserves(r);
|
|
||||||
};
|
|
||||||
|
|
||||||
const port = chrome.runtime.connect();
|
|
||||||
port.onMessage.addListener((msg: any) => {
|
|
||||||
if (msg.notify) {
|
|
||||||
console.log("got notified");
|
|
||||||
update();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!reserves) {
|
|
||||||
return <span>loading ...</span>;
|
|
||||||
}
|
|
||||||
if (reserves.length === 0) {
|
|
||||||
return <span>No reserves with payback available.</span>;
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
{reserves.map(r => (
|
|
||||||
<div>
|
|
||||||
<h2>Reserve for ${renderAmount(r.amountWithdrawRemaining)}</h2>
|
|
||||||
<ul>
|
|
||||||
<li>Exchange: ${r.exchangeBaseUrl}</li>
|
|
||||||
</ul>
|
|
||||||
<button onClick={() => withdrawPaybackReserve(r.reservePub)}>
|
|
||||||
Withdraw again
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
registerMountPage(() => <Payback />);
|
registerMountPage(() => <Payback />);
|
||||||
|
@ -18,7 +18,6 @@
|
|||||||
* Interface to the wallet through WebExtension messaging.
|
* Interface to the wallet through WebExtension messaging.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Imports.
|
* Imports.
|
||||||
*/
|
*/
|
||||||
@ -28,7 +27,6 @@ import {
|
|||||||
CurrencyRecord,
|
CurrencyRecord,
|
||||||
DenominationRecord,
|
DenominationRecord,
|
||||||
ExchangeRecord,
|
ExchangeRecord,
|
||||||
PlanchetRecord,
|
|
||||||
ReserveRecord,
|
ReserveRecord,
|
||||||
} from "../types/dbTypes";
|
} from "../types/dbTypes";
|
||||||
import {
|
import {
|
||||||
@ -44,7 +42,6 @@ import {
|
|||||||
|
|
||||||
import { MessageMap, MessageType } from "./messages";
|
import { MessageMap, MessageType } from "./messages";
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Response with information about available version upgrades.
|
* Response with information about available version upgrades.
|
||||||
*/
|
*/
|
||||||
@ -66,7 +63,6 @@ export interface UpgradeResponse {
|
|||||||
oldDbVersion: string;
|
oldDbVersion: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Error thrown when the function from the backend (via RPC) threw an error.
|
* Error thrown when the function from the backend (via RPC) threw an error.
|
||||||
*/
|
*/
|
||||||
@ -78,19 +74,22 @@ export class WalletApiError extends Error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
async function callBackend<T extends MessageType>(
|
async function callBackend<T extends MessageType>(
|
||||||
type: T,
|
type: T,
|
||||||
detail: MessageMap[T]["request"],
|
detail: MessageMap[T]["request"],
|
||||||
): Promise<MessageMap[T]["response"]> {
|
): Promise<MessageMap[T]["response"]> {
|
||||||
return new Promise<MessageMap[T]["response"]>((resolve, reject) => {
|
return new Promise<MessageMap[T]["response"]>((resolve, reject) => {
|
||||||
chrome.runtime.sendMessage({ type, detail }, (resp) => {
|
chrome.runtime.sendMessage({ type, detail }, resp => {
|
||||||
if (chrome.runtime.lastError) {
|
if (chrome.runtime.lastError) {
|
||||||
console.log("Error calling backend");
|
console.log("Error calling backend");
|
||||||
reject(new Error(`Error contacting backend: chrome.runtime.lastError.message`));
|
reject(
|
||||||
|
new Error(
|
||||||
|
`Error contacting backend: chrome.runtime.lastError.message`,
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if (typeof resp === "object" && resp && resp.error) {
|
if (typeof resp === "object" && resp && resp.error) {
|
||||||
console.warn("response error:", resp)
|
console.warn("response error:", resp);
|
||||||
const e = new WalletApiError(resp.error.message, resp.error);
|
const e = new WalletApiError(resp.error.message, resp.error);
|
||||||
reject(e);
|
reject(e);
|
||||||
} else {
|
} else {
|
||||||
@ -100,42 +99,38 @@ async function callBackend<T extends MessageType>(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Query the wallet for the coins that would be used to withdraw
|
* Query the wallet for the coins that would be used to withdraw
|
||||||
* from a given reserve.
|
* from a given reserve.
|
||||||
*/
|
*/
|
||||||
export function getReserveCreationInfo(baseUrl: string,
|
export function getReserveCreationInfo(
|
||||||
amount: AmountJson): Promise<ExchangeWithdrawDetails> {
|
baseUrl: string,
|
||||||
|
amount: AmountJson,
|
||||||
|
): Promise<ExchangeWithdrawDetails> {
|
||||||
return callBackend("reserve-creation-info", { baseUrl, amount });
|
return callBackend("reserve-creation-info", { baseUrl, amount });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get all exchanges the wallet knows about.
|
* Get all exchanges the wallet knows about.
|
||||||
*/
|
*/
|
||||||
export function getExchanges(): Promise<ExchangeRecord[]> {
|
export function getExchanges(): Promise<ExchangeRecord[]> {
|
||||||
return callBackend("get-exchanges", { });
|
return callBackend("get-exchanges", {});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get all currencies the exchange knows about.
|
* Get all currencies the exchange knows about.
|
||||||
*/
|
*/
|
||||||
export function getCurrencies(): Promise<CurrencyRecord[]> {
|
export function getCurrencies(): Promise<CurrencyRecord[]> {
|
||||||
return callBackend("get-currencies", { });
|
return callBackend("get-currencies", {});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get information about a specific exchange.
|
* Get information about a specific exchange.
|
||||||
*/
|
*/
|
||||||
export function getExchangeInfo(baseUrl: string): Promise<ExchangeRecord> {
|
export function getExchangeInfo(baseUrl: string): Promise<ExchangeRecord> {
|
||||||
return callBackend("exchange-info", {baseUrl});
|
return callBackend("exchange-info", { baseUrl });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Replace an existing currency record with the one given. The currency to
|
* Replace an existing currency record with the one given. The currency to
|
||||||
* replace is specified inside the currency record.
|
* replace is specified inside the currency record.
|
||||||
@ -144,7 +139,6 @@ export function updateCurrency(currencyRecord: CurrencyRecord): Promise<void> {
|
|||||||
return callBackend("update-currency", { currencyRecord });
|
return callBackend("update-currency", { currencyRecord });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get all reserves the wallet has at an exchange.
|
* Get all reserves the wallet has at an exchange.
|
||||||
*/
|
*/
|
||||||
@ -152,23 +146,6 @@ export function getReserves(exchangeBaseUrl: string): Promise<ReserveRecord[]> {
|
|||||||
return callBackend("get-reserves", { exchangeBaseUrl });
|
return callBackend("get-reserves", { exchangeBaseUrl });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get all reserves for which a payback is available.
|
|
||||||
*/
|
|
||||||
export function getPaybackReserves(): Promise<ReserveRecord[]> {
|
|
||||||
return callBackend("get-payback-reserves", { });
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Withdraw the payback that is available for a reserve.
|
|
||||||
*/
|
|
||||||
export function withdrawPaybackReserve(reservePub: string): Promise<ReserveRecord[]> {
|
|
||||||
return callBackend("withdraw-payback-reserve", { reservePub });
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get all coins withdrawn from the given exchange.
|
* Get all coins withdrawn from the given exchange.
|
||||||
*/
|
*/
|
||||||
@ -176,15 +153,15 @@ export function getCoins(exchangeBaseUrl: string): Promise<CoinRecord[]> {
|
|||||||
return callBackend("get-coins", { exchangeBaseUrl });
|
return callBackend("get-coins", { exchangeBaseUrl });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get all denoms offered by the given exchange.
|
* Get all denoms offered by the given exchange.
|
||||||
*/
|
*/
|
||||||
export function getDenoms(exchangeBaseUrl: string): Promise<DenominationRecord[]> {
|
export function getDenoms(
|
||||||
|
exchangeBaseUrl: string,
|
||||||
|
): Promise<DenominationRecord[]> {
|
||||||
return callBackend("get-denoms", { exchangeBaseUrl });
|
return callBackend("get-denoms", { exchangeBaseUrl });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Start refreshing a coin.
|
* Start refreshing a coin.
|
||||||
*/
|
*/
|
||||||
@ -192,15 +169,16 @@ export function refresh(coinPub: string): Promise<void> {
|
|||||||
return callBackend("refresh-coin", { coinPub });
|
return callBackend("refresh-coin", { coinPub });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Pay for a proposal.
|
* Pay for a proposal.
|
||||||
*/
|
*/
|
||||||
export function confirmPay(proposalId: string, sessionId: string | undefined): Promise<ConfirmPayResult> {
|
export function confirmPay(
|
||||||
|
proposalId: string,
|
||||||
|
sessionId: string | undefined,
|
||||||
|
): Promise<ConfirmPayResult> {
|
||||||
return callBackend("confirm-pay", { proposalId, sessionId });
|
return callBackend("confirm-pay", { proposalId, sessionId });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Mark a reserve as confirmed.
|
* Mark a reserve as confirmed.
|
||||||
*/
|
*/
|
||||||
@ -212,13 +190,17 @@ export function confirmReserve(reservePub: string): Promise<void> {
|
|||||||
* Check upgrade information
|
* Check upgrade information
|
||||||
*/
|
*/
|
||||||
export function checkUpgrade(): Promise<UpgradeResponse> {
|
export function checkUpgrade(): Promise<UpgradeResponse> {
|
||||||
return callBackend("check-upgrade", { });
|
return callBackend("check-upgrade", {});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a reserve.
|
* Create a reserve.
|
||||||
*/
|
*/
|
||||||
export function createReserve(args: { amount: AmountJson, exchange: string, senderWire?: string }): Promise<any> {
|
export function createReserve(args: {
|
||||||
|
amount: AmountJson;
|
||||||
|
exchange: string;
|
||||||
|
senderWire?: string;
|
||||||
|
}): Promise<any> {
|
||||||
return callBackend("create-reserve", args);
|
return callBackend("create-reserve", args);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -226,42 +208,45 @@ export function createReserve(args: { amount: AmountJson, exchange: string, send
|
|||||||
* Reset database
|
* Reset database
|
||||||
*/
|
*/
|
||||||
export function resetDb(): Promise<void> {
|
export function resetDb(): Promise<void> {
|
||||||
return callBackend("reset-db", { });
|
return callBackend("reset-db", {});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get balances for all currencies/exchanges.
|
* Get balances for all currencies/exchanges.
|
||||||
*/
|
*/
|
||||||
export function getBalance(): Promise<WalletBalance> {
|
export function getBalance(): Promise<WalletBalance> {
|
||||||
return callBackend("balances", { });
|
return callBackend("balances", {});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get possible sender wire infos for getting money
|
* Get possible sender wire infos for getting money
|
||||||
* wired from an exchange.
|
* wired from an exchange.
|
||||||
*/
|
*/
|
||||||
export function getSenderWireInfos(): Promise<SenderWireInfos> {
|
export function getSenderWireInfos(): Promise<SenderWireInfos> {
|
||||||
return callBackend("get-sender-wire-infos", { });
|
return callBackend("get-sender-wire-infos", {});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return coins to a bank account.
|
* Return coins to a bank account.
|
||||||
*/
|
*/
|
||||||
export function returnCoins(args: { amount: AmountJson, exchange: string, senderWire: object }): Promise<void> {
|
export function returnCoins(args: {
|
||||||
|
amount: AmountJson;
|
||||||
|
exchange: string;
|
||||||
|
senderWire: object;
|
||||||
|
}): Promise<void> {
|
||||||
return callBackend("return-coins", args);
|
return callBackend("return-coins", args);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Look up a purchase in the wallet database from
|
* Look up a purchase in the wallet database from
|
||||||
* the contract terms hash.
|
* the contract terms hash.
|
||||||
*/
|
*/
|
||||||
export function getPurchaseDetails(contractTermsHash: string): Promise<PurchaseDetails> {
|
export function getPurchaseDetails(
|
||||||
|
contractTermsHash: string,
|
||||||
|
): Promise<PurchaseDetails> {
|
||||||
return callBackend("get-purchase-details", { contractTermsHash });
|
return callBackend("get-purchase-details", { contractTermsHash });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the status of processing a tip.
|
* Get the status of processing a tip.
|
||||||
*/
|
*/
|
||||||
@ -276,7 +261,6 @@ export function acceptTip(talerTipUri: string): Promise<void> {
|
|||||||
return callBackend("accept-tip", { talerTipUri });
|
return callBackend("accept-tip", { talerTipUri });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Download a refund and accept it.
|
* Download a refund and accept it.
|
||||||
*/
|
*/
|
||||||
@ -291,7 +275,6 @@ export function abortFailedPayment(contractTermsHash: string) {
|
|||||||
return callBackend("abort-failed-payment", { contractTermsHash });
|
return callBackend("abort-failed-payment", { contractTermsHash });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Abort a failed payment and try to get a refund.
|
* Abort a failed payment and try to get a refund.
|
||||||
*/
|
*/
|
||||||
@ -302,8 +285,14 @@ export function benchmarkCrypto(repetitions: number): Promise<BenchmarkResult> {
|
|||||||
/**
|
/**
|
||||||
* Get details about a withdraw operation.
|
* Get details about a withdraw operation.
|
||||||
*/
|
*/
|
||||||
export function getWithdrawDetails(talerWithdrawUri: string, maybeSelectedExchange: string | undefined) {
|
export function getWithdrawDetails(
|
||||||
return callBackend("get-withdraw-details", { talerWithdrawUri, maybeSelectedExchange });
|
talerWithdrawUri: string,
|
||||||
|
maybeSelectedExchange: string | undefined,
|
||||||
|
) {
|
||||||
|
return callBackend("get-withdraw-details", {
|
||||||
|
talerWithdrawUri,
|
||||||
|
maybeSelectedExchange,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -316,8 +305,14 @@ export function preparePay(talerPayUri: string) {
|
|||||||
/**
|
/**
|
||||||
* Get details about a withdraw operation.
|
* Get details about a withdraw operation.
|
||||||
*/
|
*/
|
||||||
export function acceptWithdrawal(talerWithdrawUri: string, selectedExchange: string) {
|
export function acceptWithdrawal(
|
||||||
return callBackend("accept-withdrawal", { talerWithdrawUri, selectedExchange });
|
talerWithdrawUri: string,
|
||||||
|
selectedExchange: string,
|
||||||
|
) {
|
||||||
|
return callBackend("accept-withdrawal", {
|
||||||
|
talerWithdrawUri,
|
||||||
|
selectedExchange,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -148,15 +148,6 @@ async function handleMessage(
|
|||||||
}
|
}
|
||||||
return needsWallet().getReserves(detail.exchangeBaseUrl);
|
return needsWallet().getReserves(detail.exchangeBaseUrl);
|
||||||
}
|
}
|
||||||
case "get-payback-reserves": {
|
|
||||||
return needsWallet().getPaybackReserves();
|
|
||||||
}
|
|
||||||
case "withdraw-payback-reserve": {
|
|
||||||
if (typeof detail.reservePub !== "string") {
|
|
||||||
return Promise.reject(Error("reservePub missing"));
|
|
||||||
}
|
|
||||||
throw Error("not implemented");
|
|
||||||
}
|
|
||||||
case "get-coins": {
|
case "get-coins": {
|
||||||
if (typeof detail.exchangeBaseUrl !== "string") {
|
if (typeof detail.exchangeBaseUrl !== "string") {
|
||||||
return Promise.reject(Error("exchangBaseUrl missing"));
|
return Promise.reject(Error("exchangBaseUrl missing"));
|
||||||
|
Loading…
Reference in New Issue
Block a user