aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/crypto/workers/cryptoApi.ts4
-rw-r--r--src/crypto/workers/cryptoImplementation.ts12
-rw-r--r--src/headless/helpers.ts6
-rw-r--r--src/headless/integrationtest.ts38
-rw-r--r--src/headless/taler-wallet-cli.ts32
-rw-r--r--src/operations/balance.ts4
-rw-r--r--src/operations/exchanges.ts2
-rw-r--r--src/operations/history.ts46
-rw-r--r--src/operations/pending.ts7
-rw-r--r--src/operations/recoup.ts4
-rw-r--r--src/operations/refresh.ts5
-rw-r--r--src/operations/refund.ts6
-rw-r--r--src/operations/reserves.ts235
-rw-r--r--src/operations/tip.ts22
-rw-r--r--src/operations/withdraw.ts202
-rw-r--r--src/types/dbTypes.ts70
-rw-r--r--src/types/history.ts16
-rw-r--r--src/types/notifications.ts28
-rw-r--r--src/types/pending.ts3
-rw-r--r--src/types/types-test.ts18
-rw-r--r--src/types/walletTypes.ts1
-rw-r--r--src/util/amounts.ts20
-rw-r--r--src/util/reserveHistoryUtil-test.ts286
-rw-r--r--src/util/reserveHistoryUtil.ts384
-rw-r--r--src/wallet.ts29
-rw-r--r--src/webex/pages/popup.tsx4
-rw-r--r--src/webex/pages/return-coins.tsx4
27 files changed, 1062 insertions, 426 deletions
diff --git a/src/crypto/workers/cryptoApi.ts b/src/crypto/workers/cryptoApi.ts
index ab97e1274..d3b12e26d 100644
--- a/src/crypto/workers/cryptoApi.ts
+++ b/src/crypto/workers/cryptoApi.ts
@@ -359,8 +359,8 @@ export class CryptoApi {
return this.doRpc<string>("hashString", 1, str);
}
- hashDenomPub(denomPub: string): Promise<string> {
- return this.doRpc<string>("hashDenomPub", 1, denomPub);
+ hashEncoded(encodedBytes: string): Promise<string> {
+ return this.doRpc<string>("hashEncoded", 1, encodedBytes);
}
isValidDenom(denom: DenominationRecord, masterPub: string): Promise<boolean> {
diff --git a/src/crypto/workers/cryptoImplementation.ts b/src/crypto/workers/cryptoImplementation.ts
index 156c72ba0..eef8f5955 100644
--- a/src/crypto/workers/cryptoImplementation.ts
+++ b/src/crypto/workers/cryptoImplementation.ts
@@ -49,8 +49,7 @@ import {
PlanchetCreationRequest,
DepositInfo,
} from "../../types/walletTypes";
-import { AmountJson } from "../../util/amounts";
-import * as Amounts from "../../util/amounts";
+import { AmountJson, Amounts } from "../../util/amounts";
import * as timer from "../../util/timer";
import {
encodeCrock,
@@ -199,6 +198,7 @@ export class CryptoImplementation {
denomPubHash: encodeCrock(denomPubHash),
reservePub: encodeCrock(reservePub),
withdrawSig: encodeCrock(sig),
+ coinEvHash: encodeCrock(evHash),
};
return planchet;
}
@@ -367,7 +367,7 @@ export class CryptoImplementation {
const s: CoinDepositPermission = {
coin_pub: depositInfo.coinPub,
coin_sig: encodeCrock(coinSig),
- contribution: Amounts.toString(depositInfo.spendAmount),
+ contribution: Amounts.stringify(depositInfo.spendAmount),
denom_pub: depositInfo.denomPub,
exchange_url: depositInfo.exchangeBaseUrl,
ub_sig: depositInfo.denomSig,
@@ -491,10 +491,10 @@ export class CryptoImplementation {
}
/**
- * Hash a denomination public key.
+ * Hash a crockford encoded value.
*/
- hashDenomPub(denomPub: string): string {
- return encodeCrock(hash(decodeCrock(denomPub)));
+ hashEncoded(encodedBytes: string): string {
+ return encodeCrock(hash(decodeCrock(encodedBytes)));
}
signCoinLink(
diff --git a/src/headless/helpers.ts b/src/headless/helpers.ts
index fb3d800d4..92452e78f 100644
--- a/src/headless/helpers.ts
+++ b/src/headless/helpers.ts
@@ -35,6 +35,7 @@ import { Database } from "../util/query";
import { NodeHttpLib } from "./NodeHttpLib";
import { Logger } from "../util/logging";
import { SynchronousCryptoWorkerFactory } from "../crypto/workers/synchronousWorker";
+import { WithdrawalSourceType } from "../types/dbTypes";
const logger = new Logger("helpers.ts");
@@ -165,8 +166,9 @@ export async function withdrawTestBalance(
});
myWallet.addNotificationListener((n) => {
if (
- n.type === NotificationType.ReserveDepleted &&
- n.reservePub === reservePub
+ n.type === NotificationType.WithdrawGroupFinished &&
+ n.withdrawalSource.type === WithdrawalSourceType.Reserve &&
+ n.withdrawalSource.reservePub === reservePub
) {
resolve();
}
diff --git a/src/headless/integrationtest.ts b/src/headless/integrationtest.ts
index 191e48ff6..6e45b76e2 100644
--- a/src/headless/integrationtest.ts
+++ b/src/headless/integrationtest.ts
@@ -22,9 +22,9 @@ import { getDefaultNodeWallet, withdrawTestBalance } from "./helpers";
import { MerchantBackendConnection } from "./merchant";
import { Logger } from "../util/logging";
import { NodeHttpLib } from "./NodeHttpLib";
-import * as Amounts from "../util/amounts";
import { Wallet } from "../wallet";
import { Configuration } from "../util/talerconfig";
+import { Amounts, AmountJson } from "../util/amounts";
const logger = new Logger("integrationtest.ts");
@@ -127,31 +127,31 @@ export async function runIntegrationTest(args: IntegrationTestArgs) {
await myWallet.runUntilDone();
console.log("withdrawing test balance for refund");
- const withdrawAmountTwo: Amounts.AmountJson = {
+ const withdrawAmountTwo: AmountJson = {
currency,
value: 18,
fraction: 0,
};
- const spendAmountTwo: Amounts.AmountJson = {
+ const spendAmountTwo: AmountJson = {
currency,
value: 7,
fraction: 0,
};
- const refundAmount: Amounts.AmountJson = {
+ const refundAmount: AmountJson = {
currency,
value: 6,
fraction: 0,
};
- const spendAmountThree: Amounts.AmountJson = {
+ const spendAmountThree: AmountJson = {
currency,
value: 3,
fraction: 0,
};
await withdrawTestBalance(
myWallet,
- Amounts.toString(withdrawAmountTwo),
+ Amounts.stringify(withdrawAmountTwo),
args.bankBaseUrl,
args.exchangeBaseUrl,
);
@@ -162,14 +162,14 @@ export async function runIntegrationTest(args: IntegrationTestArgs) {
let { orderId: refundOrderId } = await makePayment(
myWallet,
myMerchant,
- Amounts.toString(spendAmountTwo),
+ Amounts.stringify(spendAmountTwo),
"order that will be refunded",
);
const refundUri = await myMerchant.refund(
refundOrderId,
"test refund",
- Amounts.toString(refundAmount),
+ Amounts.stringify(refundAmount),
);
console.log("refund URI", refundUri);
@@ -182,7 +182,7 @@ export async function runIntegrationTest(args: IntegrationTestArgs) {
await makePayment(
myWallet,
myMerchant,
- Amounts.toString(spendAmountThree),
+ Amounts.stringify(spendAmountThree),
"payment after refund",
);
@@ -240,7 +240,7 @@ export async function runIntegrationTestBasic(cfg: Configuration) {
logger.info("withdrawing test balance");
await withdrawTestBalance(
myWallet,
- Amounts.toString(parsedWithdrawAmount),
+ Amounts.stringify(parsedWithdrawAmount),
bankBaseUrl,
exchangeBaseUrl,
);
@@ -258,7 +258,7 @@ export async function runIntegrationTestBasic(cfg: Configuration) {
await makePayment(
myWallet,
myMerchant,
- Amounts.toString(parsedSpendAmount),
+ Amounts.stringify(parsedSpendAmount),
"hello world",
);
@@ -266,24 +266,24 @@ export async function runIntegrationTestBasic(cfg: Configuration) {
await myWallet.runUntilDone();
console.log("withdrawing test balance for refund");
- const withdrawAmountTwo: Amounts.AmountJson = {
+ const withdrawAmountTwo: AmountJson = {
currency,
value: 18,
fraction: 0,
};
- const spendAmountTwo: Amounts.AmountJson = {
+ const spendAmountTwo: AmountJson = {
currency,
value: 7,
fraction: 0,
};
- const refundAmount: Amounts.AmountJson = {
+ const refundAmount: AmountJson = {
currency,
value: 6,
fraction: 0,
};
- const spendAmountThree: Amounts.AmountJson = {
+ const spendAmountThree: AmountJson = {
currency,
value: 3,
fraction: 0,
@@ -291,7 +291,7 @@ export async function runIntegrationTestBasic(cfg: Configuration) {
await withdrawTestBalance(
myWallet,
- Amounts.toString(withdrawAmountTwo),
+ Amounts.stringify(withdrawAmountTwo),
bankBaseUrl,
exchangeBaseUrl,
);
@@ -302,14 +302,14 @@ export async function runIntegrationTestBasic(cfg: Configuration) {
let { orderId: refundOrderId } = await makePayment(
myWallet,
myMerchant,
- Amounts.toString(spendAmountTwo),
+ Amounts.stringify(spendAmountTwo),
"order that will be refunded",
);
const refundUri = await myMerchant.refund(
refundOrderId,
"test refund",
- Amounts.toString(refundAmount),
+ Amounts.stringify(refundAmount),
);
console.log("refund URI", refundUri);
@@ -322,7 +322,7 @@ export async function runIntegrationTestBasic(cfg: Configuration) {
await makePayment(
myWallet,
myMerchant,
- Amounts.toString(spendAmountThree),
+ Amounts.stringify(spendAmountThree),
"payment after refund",
);
diff --git a/src/headless/taler-wallet-cli.ts b/src/headless/taler-wallet-cli.ts
index 45ab819a7..d183ef316 100644
--- a/src/headless/taler-wallet-cli.ts
+++ b/src/headless/taler-wallet-cli.ts
@@ -24,7 +24,7 @@ import qrcodeGenerator = require("qrcode-generator");
import * as clk from "./clk";
import { BridgeIDBFactory, MemoryBackend } from "idb-bridge";
import { Logger } from "../util/logging";
-import * as Amounts from "../util/amounts";
+import { Amounts } from "../util/amounts";
import { decodeCrock } from "../crypto/talerCrypto";
import { OperationFailedAndReportedError } from "../operations/errors";
import { Bank } from "./bank";
@@ -190,7 +190,7 @@ walletCli
} else {
const currencies = Object.keys(balance.byCurrency).sort();
for (const c of currencies) {
- console.log(Amounts.toString(balance.byCurrency[c].available));
+ console.log(Amounts.stringify(balance.byCurrency[c].available));
}
}
});
@@ -356,6 +356,32 @@ advancedCli
fs.writeFileSync(1, decodeCrock(enc.trim()));
});
+const reservesCli = advancedCli.subcommand("reserves", "reserves", {
+ help: "Manage reserves.",
+});
+
+reservesCli
+ .subcommand("list", "list", {
+ help: "List reserves.",
+ })
+ .action(async (args) => {
+ await withWallet(args, async (wallet) => {
+ const reserves = await wallet.getReserves();
+ console.log(JSON.stringify(reserves, undefined, 2));
+ });
+ });
+
+reservesCli
+ .subcommand("update", "update", {
+ help: "Update reserve status via exchange.",
+ })
+ .requiredArgument("reservePub", clk.STRING)
+ .action(async (args) => {
+ await withWallet(args, async (wallet) => {
+ await wallet.updateReserve(args.update.reservePub);
+ });
+ });
+
advancedCli
.subcommand("payPrepare", "pay-prepare", {
help: "Claim an order but don't pay yet.",
@@ -464,7 +490,7 @@ advancedCli
console.log(` exchange ${coin.exchangeBaseUrl}`);
console.log(` denomPubHash ${coin.denomPubHash}`);
console.log(
- ` remaining amount ${Amounts.toString(coin.currentAmount)}`,
+ ` remaining amount ${Amounts.stringify(coin.currentAmount)}`,
);
}
});
diff --git a/src/operations/balance.ts b/src/operations/balance.ts
index 8858e8b43..7c2d0e3fe 100644
--- a/src/operations/balance.ts
+++ b/src/operations/balance.ts
@@ -106,7 +106,7 @@ export async function getBalancesInsideTransaction(
}
});
- await tx.iter(Stores.withdrawalSession).forEach((wds) => {
+ await tx.iter(Stores.withdrawalGroups).forEach((wds) => {
let w = wds.totalCoinValue;
for (let i = 0; i < wds.planchets.length; i++) {
if (wds.withdrawn[i]) {
@@ -150,7 +150,7 @@ export async function getBalances(
Stores.refreshGroups,
Stores.reserves,
Stores.purchases,
- Stores.withdrawalSession,
+ Stores.withdrawalGroups,
],
async (tx) => {
return getBalancesInsideTransaction(ws, tx);
diff --git a/src/operations/exchanges.ts b/src/operations/exchanges.ts
index 3aaf77468..b9806bb62 100644
--- a/src/operations/exchanges.ts
+++ b/src/operations/exchanges.ts
@@ -53,7 +53,7 @@ async function denominationRecordFromKeys(
exchangeBaseUrl: string,
denomIn: Denomination,
): Promise<DenominationRecord> {
- const denomPubHash = await ws.cryptoApi.hashDenomPub(denomIn.denom_pub);
+ const denomPubHash = await ws.cryptoApi.hashEncoded(denomIn.denom_pub);
const d: DenominationRecord = {
denomPub: denomIn.denom_pub,
denomPubHash,
diff --git a/src/operations/history.ts b/src/operations/history.ts
index 1b4172526..848739334 100644
--- a/src/operations/history.ts
+++ b/src/operations/history.ts
@@ -26,7 +26,7 @@ import {
PlanchetRecord,
CoinRecord,
} from "../types/dbTypes";
-import * as Amounts from "../util/amounts";
+import { Amounts } from "../util/amounts";
import { AmountJson } from "../util/amounts";
import {
HistoryQuery,
@@ -42,6 +42,7 @@ import {
import { assertUnreachable } from "../util/assertUnreachable";
import { TransactionHandle, Store } from "../util/query";
import { timestampCmp } from "../util/time";
+import { summarizeReserveHistory } from "../util/reserveHistoryUtil";
/**
* Create an event ID from the type and the primary key for the event.
@@ -58,7 +59,7 @@ function getOrderShortInfo(
return undefined;
}
return {
- amount: Amounts.toString(download.contractData.amount),
+ amount: Amounts.stringify(download.contractData.amount),
fulfillmentUrl: download.contractData.fulfillmentUrl,
orderId: download.contractData.orderId,
merchantBaseUrl: download.contractData.merchantBaseUrl,
@@ -176,7 +177,7 @@ export async function getHistory(
Stores.refreshGroups,
Stores.reserves,
Stores.tips,
- Stores.withdrawalSession,
+ Stores.withdrawalGroups,
Stores.payEvents,
Stores.refundEvents,
Stores.reserveUpdatedEvents,
@@ -208,7 +209,7 @@ export async function getHistory(
});
});
- tx.iter(Stores.withdrawalSession).forEach((wsr) => {
+ tx.iter(Stores.withdrawalGroups).forEach((wsr) => {
if (wsr.timestampFinish) {
const cs: PlanchetRecord[] = [];
wsr.planchets.forEach((x) => {
@@ -221,7 +222,7 @@ export async function getHistory(
if (historyQuery?.extraDebug) {
verboseDetails = {
coins: cs.map((x) => ({
- value: Amounts.toString(x.coinValue),
+ value: Amounts.stringify(x.coinValue),
denomPub: x.denomPub,
})),
};
@@ -229,13 +230,13 @@ export async function getHistory(
history.push({
type: HistoryEventType.Withdrawn,
- withdrawSessionId: wsr.withdrawSessionId,
+ withdrawalGroupId: wsr.withdrawalGroupId,
eventId: makeEventId(
HistoryEventType.Withdrawn,
- wsr.withdrawSessionId,
+ wsr.withdrawalGroupId,
),
- amountWithdrawnEffective: Amounts.toString(wsr.totalCoinValue),
- amountWithdrawnRaw: Amounts.toString(wsr.rawWithdrawalAmount),
+ amountWithdrawnEffective: Amounts.stringify(wsr.totalCoinValue),
+ amountWithdrawnRaw: Amounts.stringify(wsr.rawWithdrawalAmount),
exchangeBaseUrl: wsr.exchangeBaseUrl,
timestamp: wsr.timestampFinish,
withdrawalSource: wsr.source,
@@ -283,7 +284,7 @@ export async function getHistory(
coins.push({
contribution: x.contribution,
denomPub: c.denomPub,
- value: Amounts.toString(d.value),
+ value: Amounts.stringify(d.value),
});
}
verboseDetails = { coins };
@@ -301,7 +302,7 @@ export async function getHistory(
sessionId: pe.sessionId,
timestamp: pe.timestamp,
numCoins: purchase.payReq.coins.length,
- amountPaidWithFees: Amounts.toString(amountPaidWithFees),
+ amountPaidWithFees: Amounts.stringify(amountPaidWithFees),
verboseDetails,
});
});
@@ -364,7 +365,7 @@ export async function getHistory(
}
outputCoins.push({
denomPub: d.denomPub,
- value: Amounts.toString(d.value),
+ value: Amounts.stringify(d.value),
});
}
}
@@ -378,8 +379,8 @@ export async function getHistory(
eventId: makeEventId(HistoryEventType.Refreshed, rg.refreshGroupId),
timestamp: rg.timestampFinished,
refreshReason: rg.reason,
- amountRefreshedEffective: Amounts.toString(amountRefreshedEffective),
- amountRefreshedRaw: Amounts.toString(amountRefreshedRaw),
+ amountRefreshedEffective: Amounts.stringify(amountRefreshedEffective),
+ amountRefreshedRaw: Amounts.stringify(amountRefreshedRaw),
numInputCoins,
numOutputCoins,
numRefreshedInputCoins,
@@ -403,21 +404,22 @@ export async function getHistory(
type: ReserveType.Manual,
};
}
+ const s = summarizeReserveHistory(reserve.reserveTransactions, reserve.currency);
history.push({
type: HistoryEventType.ReserveBalanceUpdated,
eventId: makeEventId(
HistoryEventType.ReserveBalanceUpdated,
ru.reserveUpdateId,
),
- amountExpected: ru.amountExpected,
- amountReserveBalance: ru.amountReserveBalance,
timestamp: ru.timestamp,
- newHistoryTransactions: ru.newHistoryTransactions,
reserveShortInfo: {
exchangeBaseUrl: reserve.exchangeBaseUrl,
reserveCreationDetail,
reservePub: reserve.reservePub,
},
+ reserveAwaitedAmount: Amounts.stringify(s.awaitedReserveAmount),
+ reserveBalance: Amounts.stringify(s.computedReserveBalance),
+ reserveUnclaimedAmount: Amounts.stringify(s.unclaimedReserveAmount),
});
});
@@ -428,7 +430,7 @@ export async function getHistory(
eventId: makeEventId(HistoryEventType.TipAccepted, tip.tipId),
timestamp: tip.acceptedTimestamp,
tipId: tip.tipId,
- tipAmountRaw: Amounts.toString(tip.amount),
+ tipAmountRaw: Amounts.stringify(tip.amount),
});
}
});
@@ -488,9 +490,9 @@ export async function getHistory(
refundGroupId: re.refundGroupId,
orderShortInfo,
timestamp: re.timestamp,
- amountRefundedEffective: Amounts.toString(amountRefundedEffective),
- amountRefundedRaw: Amounts.toString(amountRefundedRaw),
- amountRefundedInvalid: Amounts.toString(amountRefundedInvalid),
+ amountRefundedEffective: Amounts.stringify(amountRefundedEffective),
+ amountRefundedRaw: Amounts.stringify(amountRefundedRaw),
+ amountRefundedInvalid: Amounts.stringify(amountRefundedInvalid),
});
});
@@ -499,7 +501,7 @@ export async function getHistory(
let verboseDetails: any = undefined;
if (historyQuery?.extraDebug) {
verboseDetails = {
- oldAmountPerCoin: rg.oldAmountPerCoin.map(Amounts.toString),
+ oldAmountPerCoin: rg.oldAmountPerCoin.map(Amounts.stringify),
};
}
diff --git a/src/operations/pending.ts b/src/operations/pending.ts
index adf47b151..b0bb9a7c3 100644
--- a/src/operations/pending.ts
+++ b/src/operations/pending.ts
@@ -243,7 +243,7 @@ async function gatherWithdrawalPending(
resp: PendingOperationsResponse,
onlyDue: boolean = false,
): Promise<void> {
- await tx.iter(Stores.withdrawalSession).forEach((wsr) => {
+ await tx.iter(Stores.withdrawalGroups).forEach((wsr) => {
if (wsr.timestampFinish) {
return;
}
@@ -266,7 +266,8 @@ async function gatherWithdrawalPending(
numCoinsTotal,
numCoinsWithdrawn,
source: wsr.source,
- withdrawSessionId: wsr.withdrawSessionId,
+ withdrawalGroupId: wsr.withdrawalGroupId,
+ lastError: wsr.lastError,
});
});
}
@@ -444,7 +445,7 @@ export async function getPendingOperations(
Stores.reserves,
Stores.refreshGroups,
Stores.coins,
- Stores.withdrawalSession,
+ Stores.withdrawalGroups,
Stores.proposals,
Stores.tips,
Stores.purchases,
diff --git a/src/operations/recoup.ts b/src/operations/recoup.ts
index 4c6eaf3b6..e13ae3c1f 100644
--- a/src/operations/recoup.ts
+++ b/src/operations/recoup.ts
@@ -42,7 +42,7 @@ import { codecForRecoupConfirmation } from "../types/talerTypes";
import { NotificationType } from "../types/notifications";
import { forceQueryReserve } from "./reserves";
-import * as Amounts from "../util/amounts";
+import { Amounts } from "../util/amounts";
import { createRefreshGroup, processRefreshGroup } from "./refresh";
import { RefreshReason, OperationError } from "../types/walletTypes";
import { TransactionHandle } from "../util/query";
@@ -266,7 +266,7 @@ async function recoupRefreshCoin(
).amount;
console.log(
"recoup: setting old coin amount to",
- Amounts.toString(oldCoin.currentAmount),
+ Amounts.stringify(oldCoin.currentAmount),
);
recoupGroup.scheduleRefreshCoins.push(oldCoin.coinPub);
await tx.put(Stores.coins, revokedCoin);
diff --git a/src/operations/refresh.ts b/src/operations/refresh.ts
index 5628263ec..be4f5c5af 100644
--- a/src/operations/refresh.ts
+++ b/src/operations/refresh.ts
@@ -14,8 +14,7 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-import { AmountJson } from "../util/amounts";
-import * as Amounts from "../util/amounts";
+import { Amounts, AmountJson } from "../util/amounts";
import {
DenominationRecord,
Stores,
@@ -239,7 +238,7 @@ async function refreshMelt(
denom_pub_hash: coin.denomPubHash,
denom_sig: coin.denomSig,
rc: refreshSession.hash,
- value_with_fee: Amounts.toString(refreshSession.amountRefreshInput),
+ value_with_fee: Amounts.stringify(refreshSession.amountRefreshInput),
};
logger.trace(`melt request for coin:`, meltReq);
const resp = await ws.http.postJson(reqUrl.href, meltReq);
diff --git a/src/operations/refund.ts b/src/operations/refund.ts
index 7552fc11c..f0fec4065 100644
--- a/src/operations/refund.ts
+++ b/src/operations/refund.ts
@@ -41,7 +41,7 @@ import {
import { NotificationType } from "../types/notifications";
import { parseRefundUri } from "../util/taleruri";
import { createRefreshGroup, getTotalRefreshCost } from "./refresh";
-import * as Amounts from "../util/amounts";
+import { Amounts } from "../util/amounts";
import {
MerchantRefundPermission,
MerchantRefundResponse,
@@ -476,7 +476,7 @@ async function processPurchaseApplyRefundImpl(
`commiting refund ${perm.merchant_sig} to coin ${c.coinPub}`,
);
logger.trace(
- `coin amount before is ${Amounts.toString(c.currentAmount)}`,
+ `coin amount before is ${Amounts.stringify(c.currentAmount)}`,
);
logger.trace(`refund amount (via merchant) is ${perm.refund_amount}`);
logger.trace(`refund fee (via merchant) is ${perm.refund_fee}`);
@@ -486,7 +486,7 @@ async function processPurchaseApplyRefundImpl(
c.currentAmount = Amounts.add(c.currentAmount, refundAmount).amount;
c.currentAmount = Amounts.sub(c.currentAmount, refundFee).amount;
logger.trace(
- `coin amount after is ${Amounts.toString(c.currentAmount)}`,
+ `coin amount after is ${Amounts.stringify(c.currentAmount)}`,
);
await tx.put(Stores.coins, c);
};
diff --git a/src/operations/reserves.ts b/src/operations/reserves.ts
index 5cf189d3b..2ef902ef2 100644
--- a/src/operations/reserves.ts
+++ b/src/operations/reserves.ts
@@ -28,14 +28,17 @@ import {
ReserveRecord,
CurrencyRecord,
Stores,
- WithdrawalSessionRecord,
+ WithdrawalGroupRecord,
initRetryInfo,
updateRetryInfoTimeout,
ReserveUpdatedEventRecord,
+ WalletReserveHistoryItemType,
+ DenominationRecord,
+ PlanchetRecord,
+ WithdrawalSourceType,
} from "../types/dbTypes";
-import { TransactionAbort } from "../util/query";
import { Logger } from "../util/logging";
-import * as Amounts from "../util/amounts";
+import { Amounts } from "../util/amounts";
import {
updateExchangeFromUrl,
getExchangeTrust,
@@ -50,7 +53,7 @@ import { encodeCrock, getRandomBytes } from "../crypto/talerCrypto";
import { randomBytes } from "../crypto/primitives/nacl-fast";
import {
getVerifiedWithdrawDenomList,
- processWithdrawSession,
+ processWithdrawGroup,
getBankWithdrawalInfo,
} from "./withdraw";
import {
@@ -61,6 +64,10 @@ import {
import { NotificationType } from "../types/notifications";
import { codecForReserveStatus } from "../types/ReserveStatus";
import { getTimestampNow } from "../util/time";
+import {
+ reconcileReserveHistory,
+ summarizeReserveHistory,
+} from "../util/reserveHistoryUtil";
const logger = new Logger("reserves.ts");
@@ -98,11 +105,7 @@ export async function createReserve(
const reserveRecord: ReserveRecord = {
timestampCreated: now,
- amountWithdrawAllocated: Amounts.getZero(currency),
- amountWithdrawCompleted: Amounts.getZero(currency),
- amountWithdrawRemaining: Amounts.getZero(currency),
exchangeBaseUrl: canonExchange,
- amountInitiallyRequested: req.amount,
reservePriv: keypair.priv,
reservePub: keypair.pub,
senderWire: req.senderWire,
@@ -115,8 +118,14 @@ export async function createReserve(
retryInfo: initRetryInfo(),
lastError: undefined,
reserveTransactions: [],
+ currency: req.amount.currency,
};
+ reserveRecord.reserveTransactions.push({
+ type: WalletReserveHistoryItemType.Credit,
+ expectedAmount: req.amount,
+ });
+
const senderWire = req.senderWire;
if (senderWire) {
const rec = {
@@ -460,6 +469,7 @@ async function updateReserve(
const respJson = await resp.json();
const reserveInfo = codecForReserveStatus().decode(respJson);
const balance = Amounts.parseOrThrow(reserveInfo.balance);
+ const currency = balance.currency;
await ws.db.runWithWriteTransaction(
[Stores.reserves, Stores.reserveUpdatedEvents],
async (tx) => {
@@ -477,60 +487,41 @@ async function updateReserve(
const reserveUpdateId = encodeCrock(getRandomBytes(32));
- // FIXME: check / compare history!
- if (!r.lastSuccessfulStatusQuery) {
- // FIXME: check if this matches initial expectations
- r.amountWithdrawRemaining = balance;
+ const reconciled = reconcileReserveHistory(
+ r.reserveTransactions,
+ reserveInfo.history,
+ );
+
+ console.log("reconciled history:", JSON.stringify(reconciled, undefined, 2));
+
+ const summary = summarizeReserveHistory(
+ reconciled.updatedLocalHistory,
+ currency,
+ );
+ console.log("summary", summary);
+
+ if (
+ reconciled.newAddedItems.length + reconciled.newMatchedItems.length !=
+ 0
+ ) {
const reserveUpdate: ReserveUpdatedEventRecord = {
reservePub: r.reservePub,
timestamp: getTimestampNow(),
- amountReserveBalance: Amounts.toString(balance),
- amountExpected: Amounts.toString(reserve.amountInitiallyRequested),
+ amountReserveBalance: Amounts.stringify(balance),
+ amountExpected: Amounts.stringify(summary.awaitedReserveAmount),
newHistoryTransactions,
reserveUpdateId,
};
await tx.put(Stores.reserveUpdatedEvents, reserveUpdate);
r.reserveStatus = ReserveRecordStatus.WITHDRAWING;
+ r.retryInfo = initRetryInfo();
} else {
- const expectedBalance = Amounts.add(
- r.amountWithdrawRemaining,
- Amounts.sub(r.amountWithdrawAllocated, r.amountWithdrawCompleted)
- .amount,
- );
- const cmp = Amounts.cmp(balance, expectedBalance.amount);
- if (cmp == 0) {
- // Nothing changed, go back to sleep!
- r.reserveStatus = ReserveRecordStatus.DORMANT;
- } else if (cmp > 0) {
- const extra = Amounts.sub(balance, expectedBalance.amount).amount;
- r.amountWithdrawRemaining = Amounts.add(
- r.amountWithdrawRemaining,
- extra,
- ).amount;
- r.reserveStatus = ReserveRecordStatus.WITHDRAWING;
- } else {
- // We're missing some money.
- r.reserveStatus = ReserveRecordStatus.DORMANT;
- }
- if (r.reserveStatus !== ReserveRecordStatus.DORMANT) {
- const reserveUpdate: ReserveUpdatedEventRecord = {
- reservePub: r.reservePub,
- timestamp: getTimestampNow(),
- amountReserveBalance: Amounts.toString(balance),
- amountExpected: Amounts.toString(expectedBalance.amount),
- newHistoryTransactions,
- reserveUpdateId,
- };
- await tx.put(Stores.reserveUpdatedEvents, reserveUpdate);
- }
- }
- r.lastSuccessfulStatusQuery = getTimestampNow();
- if (r.reserveStatus == ReserveRecordStatus.DORMANT) {
+ r.reserveStatus = ReserveRecordStatus.DORMANT;
r.retryInfo = initRetryInfo(false);
- } else {
- r.retryInfo = initRetryInfo();
}
- r.reserveTransactions = reserveInfo.history;
+ r.lastSuccessfulStatusQuery = getTimestampNow();
+ r.reserveTransactions = reconciled.updatedLocalHistory;
+ r.lastError = undefined;
await tx.put(Stores.reserves, r);
},
);
@@ -607,6 +598,33 @@ export async function confirmReserve(
});
}
+async function makePlanchet(
+ ws: InternalWalletState,
+ reserve: ReserveRecord,
+ denom: DenominationRecord,
+): Promise<PlanchetRecord> {
+ const r = await ws.cryptoApi.createPlanchet({
+ denomPub: denom.denomPub,
+ feeWithdraw: denom.feeWithdraw,
+ reservePriv: reserve.reservePriv,
+ reservePub: reserve.reservePub,
+ value: denom.value,
+ });
+ return {
+ blindingKey: r.blindingKey,
+ coinEv: r.coinEv,
+ coinPriv: r.coinPriv,
+ coinPub: r.coinPub,
+ coinValue: r.coinValue,
+ denomPub: r.denomPub,
+ denomPubHash: r.denomPubHash,
+ isFromTip: false,
+ reservePub: r.reservePub,
+ withdrawSig: r.withdrawSig,
+ coinEvHash: r.coinEvHash,
+ };
+}
+
/**
* Withdraw coins from a reserve until it is empty.
*
@@ -626,7 +644,12 @@ async function depleteReserve(
}
logger.trace(`depleting reserve ${reservePub}`);
- const withdrawAmount = reserve.amountWithdrawRemaining;
+ const summary = summarizeReserveHistory(
+ reserve.reserveTransactions,
+ reserve.currency,
+ );
+
+ const withdrawAmount = summary.unclaimedReserveAmount;
logger.trace(`getting denom list`);
@@ -637,36 +660,47 @@ async function depleteReserve(
);
logger.trace(`got denom list`);
if (denomsForWithdraw.length === 0) {
- const m = `Unable to withdraw from reserve, no denominations are available to withdraw.`;
- const opErr = {
- type: "internal",
- message: m,
- details: {},
- };
- await incrementReserveRetry(ws, reserve.reservePub, opErr);
- console.log(m);
- throw new OperationFailedAndReportedError(opErr);
+ // Only complain about inability to withdraw if we
+ // didn't withdraw before.
+ if (Amounts.isZero(summary.withdrawnAmount)) {
+ const m = `Unable to withdraw from reserve, no denominations are available to withdraw.`;
+ const opErr = {
+ type: "internal",
+ message: m,
+ details: {},
+ };
+ await incrementReserveRetry(ws, reserve.reservePub, opErr);
+ console.log(m);
+ throw new OperationFailedAndReportedError(opErr);
+ }
+ return;
}
logger.trace("selected denominations");
- const withdrawalSessionId = encodeCrock(randomBytes(32));
+ const withdrawalGroupId = encodeCrock(randomBytes(32));
const totalCoinValue = Amounts.sum(denomsForWithdraw.map((x) => x.value))
.amount;
- const withdrawalRecord: WithdrawalSessionRecord = {
- withdrawSessionId: withdrawalSessionId,
+ const planchets: PlanchetRecord[] = [];
+ for (const d of denomsForWithdraw) {
+ const p = await makePlanchet(ws, reserve, d);
+ planchets.push(p);
+ }
+
+ const withdrawalRecord: WithdrawalGroupRecord = {
+ withdrawalGroupId: withdrawalGroupId,
exchangeBaseUrl: reserve.exchangeBaseUrl,
source: {
- type: "reserve",
+ type: WithdrawalSourceType.Reserve,
reservePub: reserve.reservePub,
},
rawWithdrawalAmount: withdrawAmount,
timestampStart: getTimestampNow(),
denoms: denomsForWithdraw.map((x) => x.denomPub),
withdrawn: denomsForWithdraw.map((x) => false),
- planchets: denomsForWithdraw.map((x) => undefined),
+ planchets,
totalCoinValue,
retryInfo: initRetryInfo(),
lastErrorPerCoin: {},
@@ -679,53 +713,54 @@ async function depleteReserve(
const totalWithdrawAmount = Amounts.add(totalCoinValue, totalCoinWithdrawFee)
.amount;
- function mutateReserve(r: ReserveRecord): ReserveRecord {
- const remaining = Amounts.sub(
- r.amountWithdrawRemaining,
- totalWithdrawAmount,
- );
- if (remaining.saturated) {
- console.error("can't create planchets, saturated");
- throw TransactionAbort;
- }
- const allocated = Amounts.add(
- r.amountWithdrawAllocated,
- totalWithdrawAmount,
- );
- if (allocated.saturated) {
- console.error("can't create planchets, saturated");
- throw TransactionAbort;
- }
- r.amountWithdrawRemaining = remaining.amount;
- r.amountWithdrawAllocated = allocated.amount;
- r.reserveStatus = ReserveRecordStatus.DORMANT;
- r.retryInfo = initRetryInfo(false);
- return r;
- }
-
const success = await ws.db.runWithWriteTransaction(
- [Stores.withdrawalSession, Stores.reserves],
+ [Stores.withdrawalGroups, Stores.reserves],
async (tx) => {
- const myReserve = await tx.get(Stores.reserves, reservePub);
- if (!myReserve) {
+ const newReserve = await tx.get(Stores.reserves, reservePub);
+ if (!newReserve) {
return false;
}
- if (myReserve.reserveStatus !== ReserveRecordStatus.WITHDRAWING) {
+ if (newReserve.reserveStatus !== ReserveRecordStatus.WITHDRAWING) {
return false;
}
- await tx.mutate(Stores.reserves, reserve.reservePub, mutateReserve);
- await tx.put(Stores.withdrawalSession, withdrawalRecord);
+ const newSummary = summarizeReserveHistory(
+ newReserve.reserveTransactions,
+ newReserve.currency,
+ );
+ if (
+ Amounts.cmp(newSummary.unclaimedReserveAmount, totalWithdrawAmount) < 0
+ ) {
+ // Something must have happened concurrently!
+ logger.error(
+ "aborting withdrawal session, likely concurrent withdrawal happened",
+ );
+ return false;
+ }
+ for (let i = 0; i < planchets.length; i++) {
+ const amt = Amounts.add(
+ denomsForWithdraw[i].value,
+ denomsForWithdraw[i].feeWithdraw,
+ ).amount;
+ newReserve.reserveTransactions.push({
+ type: WalletReserveHistoryItemType.Withdraw,
+ expectedAmount: amt,
+ });
+ }
+ newReserve.reserveStatus = ReserveRecordStatus.DORMANT;
+ newReserve.retryInfo = initRetryInfo(false);
+ await tx.put(Stores.reserves, newReserve);
+ await tx.put(Stores.withdrawalGroups, withdrawalRecord);
return true;
},
);
if (success) {
- console.log("processing new withdraw session");
+ console.log("processing new withdraw group");
ws.notify({
- type: NotificationType.WithdrawSessionCreated,
- withdrawSessionId: withdrawalSessionId,
+ type: NotificationType.WithdrawGroupCreated,
+ withdrawalGroupId: withdrawalGroupId,
});
- await processWithdrawSession(ws, withdrawalSessionId);
+ await processWithdrawGroup(ws, withdrawalGroupId);
} else {
console.trace("withdraw session already existed");
}
diff --git a/src/operations/tip.ts b/src/operations/tip.ts
index 3636dd247..d3c98d288 100644
--- a/src/operations/tip.ts
+++ b/src/operations/tip.ts
@@ -28,14 +28,15 @@ import * as Amounts from "../util/amounts";
import {
Stores,
PlanchetRecord,
- WithdrawalSessionRecord,
+ WithdrawalGroupRecord,
initRetryInfo,
updateRetryInfoTimeout,
+ WithdrawalSourceType,
} from "../types/dbTypes";
import {
getExchangeWithdrawalInfo,
getVerifiedWithdrawDenomList,
- processWithdrawSession,
+ processWithdrawGroup,
} from "./withdraw";
import { updateExchangeFromUrl } from "./exchanges";
import { getRandomBytes, encodeCrock } from "../crypto/talerCrypto";
@@ -246,8 +247,10 @@ async function processTipImpl(
const planchets: PlanchetRecord[] = [];
+
for (let i = 0; i < tipRecord.planchets.length; i++) {
const tipPlanchet = tipRecord.planchets[i];
+ const coinEvHash = await ws.cryptoApi.hashEncoded(tipPlanchet.coinEv);
const planchet: PlanchetRecord = {
blindingKey: tipPlanchet.blindingKey,
coinEv: tipPlanchet.coinEv,
@@ -259,22 +262,23 @@ async function processTipImpl(
reservePub: response.reserve_pub,
withdrawSig: response.reserve_sigs[i].reserve_sig,
isFromTip: true,
+ coinEvHash,
};
planchets.push(planchet);
}
- const withdrawalSessionId = encodeCrock(getRandomBytes(32));
+ const withdrawalGroupId = encodeCrock(getRandomBytes(32));
- const withdrawalSession: WithdrawalSessionRecord = {
+ const withdrawalGroup: WithdrawalGroupRecord = {
denoms: planchets.map((x) => x.denomPub),
exchangeBaseUrl: tipRecord.exchangeUrl,
planchets: planchets,
source: {
- type: "tip",
+ type: WithdrawalSourceType.Tip,
tipId: tipRecord.tipId,
},
timestampStart: getTimestampNow(),
- withdrawSessionId: withdrawalSessionId,
+ withdrawalGroupId: withdrawalGroupId,
rawWithdrawalAmount: tipRecord.amount,
withdrawn: planchets.map((x) => false),
totalCoinValue: Amounts.sum(planchets.map((p) => p.coinValue)).amount,
@@ -285,7 +289,7 @@ async function processTipImpl(
};
await ws.db.runWithWriteTransaction(
- [Stores.tips, Stores.withdrawalSession],
+ [Stores.tips, Stores.withdrawalGroups],
async (tx) => {
const tr = await tx.get(Stores.tips, tipId);
if (!tr) {
@@ -298,11 +302,11 @@ async function processTipImpl(
tr.retryInfo = initRetryInfo(false);
await tx.put(Stores.tips, tr);
- await tx.put(Stores.withdrawalSession, withdrawalSession);
+ await tx.put(Stores.withdrawalGroups, withdrawalGroup);
},
);
- await processWithdrawSession(ws, withdrawalSessionId);
+ await processWithdrawGroup(ws, withdrawalGroupId);
return;
}
diff --git a/src/operations/withdraw.ts b/src/operations/withdraw.ts
index 4d8af9fc0..48d70db20 100644
--- a/src/operations/withdraw.ts
+++ b/src/operations/withdraw.ts
@@ -52,6 +52,7 @@ import {
timestampCmp,
timestampSubtractDuraction,
} from "../util/time";
+import { summarizeReserveHistory, ReserveHistorySummary } from "../util/reserveHistoryUtil";
const logger = new Logger("withdraw.ts");
@@ -158,29 +159,29 @@ async function getPossibleDenoms(
*/
async function processPlanchet(
ws: InternalWalletState,
- withdrawalSessionId: string,
+ withdrawalGroupId: string,
coinIdx: number,
): Promise<void> {
- const withdrawalSession = await ws.db.get(
- Stores.withdrawalSession,
- withdrawalSessionId,
+ const withdrawalGroup = await ws.db.get(
+ Stores.withdrawalGroups,
+ withdrawalGroupId,
);
- if (!withdrawalSession) {
+ if (!withdrawalGroup) {
return;
}
- if (withdrawalSession.withdrawn[coinIdx]) {
+ if (withdrawalGroup.withdrawn[coinIdx]) {
return;
}
- if (withdrawalSession.source.type === "reserve") {
+ if (withdrawalGroup.source.type === "reserve") {
}
- const planchet = withdrawalSession.planchets[coinIdx];
+ const planchet = withdrawalGroup.planchets[coinIdx];
if (!planchet) {
console.log("processPlanchet: planchet not found");
return;
}
const exchange = await ws.db.get(
Stores.exchanges,
- withdrawalSession.exchangeBaseUrl,
+ withdrawalGroup.exchangeBaseUrl,
);
if (!exchange) {
console.error("db inconsistent: exchange for planchet not found");
@@ -188,7 +189,7 @@ async function processPlanchet(
}
const denom = await ws.db.get(Stores.denominations, [
- withdrawalSession.exchangeBaseUrl,
+ withdrawalGroup.exchangeBaseUrl,
planchet.denomPub,
]);
@@ -232,24 +233,24 @@ async function processPlanchet(
denomPub: planchet.denomPub,
denomPubHash: planchet.denomPubHash,
denomSig,
- exchangeBaseUrl: withdrawalSession.exchangeBaseUrl,
+ exchangeBaseUrl: withdrawalGroup.exchangeBaseUrl,
status: CoinStatus.Fresh,
coinSource: {
type: CoinSourceType.Withdraw,
coinIndex: coinIdx,
reservePub: planchet.reservePub,
- withdrawSessionId: withdrawalSessionId,
+ withdrawalGroupId: withdrawalGroupId,
},
suspended: false,
};
- let withdrawSessionFinished = false;
- let reserveDepleted = false;
+ let withdrawalGroupFinished = false;
+ let summary: ReserveHistorySummary | undefined = undefined;
const success = await ws.db.runWithWriteTransaction(
- [Stores.coins, Stores.withdrawalSession, Stores.reserves],
+ [Stores.coins, Stores.withdrawalGroups, Stores.reserves],
async (tx) => {
- const ws = await tx.get(Stores.withdrawalSession, withdrawalSessionId);
+ const ws = await tx.get(Stores.withdrawalGroups, withdrawalGroupId);
if (!ws) {
return false;
}
@@ -269,23 +270,13 @@ async function processPlanchet(
ws.timestampFinish = getTimestampNow();
ws.lastError = undefined;
ws.retryInfo = initRetryInfo(false);
- withdrawSessionFinished = true;
+ withdrawalGroupFinished = true;
}
- await tx.put(Stores.withdrawalSession, ws);
+ await tx.put(Stores.withdrawalGroups, ws);
if (!planchet.isFromTip) {
const r = await tx.get(Stores.reserves, planchet.reservePub);
if (r) {
- r.amountWithdrawCompleted = Amounts.add(
- r.amountWithdrawCompleted,
- Amounts.add(denom.value, denom.feeWithdraw).amount,
- ).amount;
- if (
- Amounts.cmp(r.amountWithdrawCompleted, r.amountWithdrawAllocated) ==
- 0
- ) {
- reserveDepleted = true;
- }
- await tx.put(Stores.reserves, r);
+ summary = summarizeReserveHistory(r.reserveTransactions, r.currency);
}
}
await tx.add(Stores.coins, coin);
@@ -299,17 +290,10 @@ async function processPlanchet(
});
}
- if (withdrawSessionFinished) {
+ if (withdrawalGroupFinished) {
ws.notify({
- type: NotificationType.WithdrawSessionFinished,
- withdrawSessionId: withdrawalSessionId,
- });
- }
-
- if (reserveDepleted && withdrawalSession.source.type === "reserve") {
- ws.notify({
- type: NotificationType.ReserveDepleted,
- reservePub: withdrawalSession.source.reservePub,
+ type: NotificationType.WithdrawGroupFinished,
+ withdrawalSource: withdrawalGroup.source,
});
}
}
@@ -383,113 +367,15 @@ export async function getVerifiedWithdrawDenomList(
return selectedDenoms;
}
-async function makePlanchet(
- ws: InternalWalletState,
- withdrawalSessionId: string,
- coinIndex: number,
-): Promise<void> {
- const withdrawalSession = await ws.db.get(
- Stores.withdrawalSession,
- withdrawalSessionId,
- );
- if (!withdrawalSession) {
- return;
- }
- const src = withdrawalSession.source;
- if (src.type !== "reserve") {
- throw Error("invalid state");
- }
- const reserve = await ws.db.get(Stores.reserves, src.reservePub);
- if (!reserve) {
- return;
- }
- const denom = await ws.db.get(Stores.denominations, [
- withdrawalSession.exchangeBaseUrl,
- withdrawalSession.denoms[coinIndex],
- ]);
- if (!denom) {
- return;
- }
- const r = await ws.cryptoApi.createPlanchet({
- denomPub: denom.denomPub,
- feeWithdraw: denom.feeWithdraw,
- reservePriv: reserve.reservePriv,
- reservePub: reserve.reservePub,
- value: denom.value,
- });
- const newPlanchet: PlanchetRecord = {
- blindingKey: r.blindingKey,
- coinEv: r.coinEv,
- coinPriv: r.coinPriv,
- coinPub: r.coinPub,
- coinValue: r.coinValue,
- denomPub: r.denomPub,
- denomPubHash: r.denomPubHash,
- isFromTip: false,
- reservePub: r.reservePub,
- withdrawSig: r.withdrawSig,
- };
- await ws.db.runWithWriteTransaction(
- [Stores.withdrawalSession],
- async (tx) => {
- const myWs = await tx.get(Stores.withdrawalSession, withdrawalSessionId);
- if (!myWs) {
- return;
- }
- if (myWs.planchets[coinIndex]) {
- return;
- }
- myWs.planchets[coinIndex] = newPlanchet;
- await tx.put(Stores.withdrawalSession, myWs);
- },
- );
-}
-
-async function processWithdrawCoin(
- ws: InternalWalletState,
- withdrawalSessionId: string,
- coinIndex: number,
-) {
- logger.trace("starting withdraw for coin", coinIndex);
- const withdrawalSession = await ws.db.get(
- Stores.withdrawalSession,
- withdrawalSessionId,
- );
- if (!withdrawalSession) {
- console.log("ws doesn't exist");
- return;
- }
-
- const planchet = withdrawalSession.planchets[coinIndex];
-
- if (planchet) {
- const coin = await ws.db.get(Stores.coins, planchet.coinPub);
-
- if (coin) {
- console.log("coin already exists");
- return;
- }
- }
-
- if (!withdrawalSession.planchets[coinIndex]) {
- const key = `${withdrawalSessionId}-${coinIndex}`;
- await ws.memoMakePlanchet.memo(key, async () => {
- logger.trace("creating planchet for coin", coinIndex);
- return makePlanchet(ws, withdrawalSessionId, coinIndex);
- });
- }
- await processPlanchet(ws, withdrawalSessionId, coinIndex);
-}
-
async function incrementWithdrawalRetry(
ws: InternalWalletState,
- withdrawalSessionId: string,
+ withdrawalGroupId: string,
err: OperationError | undefined,
): Promise<void> {
await ws.db.runWithWriteTransaction(
- [Stores.withdrawalSession],
+ [Stores.withdrawalGroups],
async (tx) => {
- const wsr = await tx.get(Stores.withdrawalSession, withdrawalSessionId);
+ const wsr = await tx.get(Stores.withdrawalGroups, withdrawalGroupId);
if (!wsr) {
return;
}
@@ -499,30 +385,30 @@ async function incrementWithdrawalRetry(
wsr.retryInfo.retryCounter++;
updateRetryInfoTimeout(wsr.retryInfo);
wsr.lastError = err;
- await tx.put(Stores.withdrawalSession, wsr);
+ await tx.put(Stores.withdrawalGroups, wsr);
},
);
ws.notify({ type: NotificationType.WithdrawOperationError });
}
-export async function processWithdrawSession(
+export async function processWithdrawGroup(
ws: InternalWalletState,
- withdrawalSessionId: string,
+ withdrawalGroupId: string,
forceNow: boolean = false,
): Promise<void> {
const onOpErr = (e: OperationError) =>
- incrementWithdrawalRetry(ws, withdrawalSessionId, e);
+ incrementWithdrawalRetry(ws, withdrawalGroupId, e);
await guardOperationException(
- () => processWithdrawSessionImpl(ws, withdrawalSessionId, forceNow),
+ () => processWithdrawGroupImpl(ws, withdrawalGroupId, forceNow),
onOpErr,
);
}
-async function resetWithdrawSessionRetry(
+async function resetWithdrawalGroupRetry(
ws: InternalWalletState,
- withdrawalSessionId: string,
+ withdrawalGroupId: string,
) {
- await ws.db.mutate(Stores.withdrawalSession, withdrawalSessionId, (x) => {
+ await ws.db.mutate(Stores.withdrawalGroups, withdrawalGroupId, (x) => {
if (x.retryInfo.active) {
x.retryInfo = initRetryInfo();
}
@@ -530,26 +416,26 @@ async function resetWithdrawSessionRetry(
});
}
-async function processWithdrawSessionImpl(
+async function processWithdrawGroupImpl(
ws: InternalWalletState,
- withdrawalSessionId: string,
+ withdrawalGroupId: string,
forceNow: boolean,
): Promise<void> {
- logger.trace("processing withdraw session", withdrawalSessionId);
+ logger.trace("processing withdraw group", withdrawalGroupId);
if (forceNow) {
- await resetWithdrawSessionRetry(ws, withdrawalSessionId);
+ await resetWithdrawalGroupRetry(ws, withdrawalGroupId);
}
- const withdrawalSession = await ws.db.get(
- Stores.withdrawalSession,
- withdrawalSessionId,
+ const withdrawalGroup = await ws.db.get(
+ Stores.withdrawalGroups,
+ withdrawalGroupId,
);
- if (!withdrawalSession) {
+ if (!withdrawalGroup) {
logger.trace("withdraw session doesn't exist");
return;
}
- const ps = withdrawalSession.denoms.map((d, i) =>
- processWithdrawCoin(ws, withdrawalSessionId, i),
+ const ps = withdrawalGroup.denoms.map((d, i) =>
+ processPlanchet(ws, withdrawalGroupId, i),
);
await Promise.all(ps);
return;
diff --git a/src/types/dbTypes.ts b/src/types/dbTypes.ts
index 9c2b3ca3e..b87ada115 100644
--- a/src/types/dbTypes.ts
+++ b/src/types/dbTypes.ts
@@ -151,7 +151,7 @@ export interface WalletReserveHistoryCreditItem {
/**
* Amount we expect to see credited.
*/
- expectedAmount?: string;
+ expectedAmount?: AmountJson;
/**
* Item from the reserve transaction history that this
@@ -161,7 +161,15 @@ export interface WalletReserveHistoryCreditItem {
}
export interface WalletReserveHistoryWithdrawItem {
- expectedAmount?: string;
+ expectedAmount?: AmountJson;
+
+ /**
+ * Hash of the blinded coin.
+ *
+ * When this value is set, it indicates that a withdrawal is active
+ * in the wallet for the
+ */
+ expectedCoinEvHash?: string;
type: WalletReserveHistoryItemType.Withdraw;
@@ -188,7 +196,7 @@ export interface WalletReserveHistoryRecoupItem {
/**
* Amount we expect to see recouped.
*/
- expectedAmount?: string;
+ expectedAmount?: AmountJson;
/**
* Item from the reserve transaction history that this
@@ -223,6 +231,11 @@ export interface ReserveRecord {
exchangeBaseUrl: string;
/**
+ * Currency of the reserve.
+ */
+ currency: string;
+
+ /**
* Time when the reserve was created.
*/
timestampCreated: Timestamp;
@@ -237,35 +250,14 @@ export interface ReserveRecord {
timestampReserveInfoPosted: Timestamp | undefined;
/**
- * Time when the reserve was confirmed.
+ * Time when the reserve was confirmed, either manually by the user
+ * or by the bank.
*
- * Set to 0 if not confirmed yet.
+ * Set to undefined if not confirmed yet.
*/
timestampConfirmed: Timestamp | undefined;
/**
- * Amount that's still available for withdrawing
- * from this reserve.
- */
- amountWithdrawRemaining: AmountJson;
-
- /**
- * Amount allocated for withdrawing.
- * The corresponding withdraw operation may or may not
- * have been completed yet.
- */
- amountWithdrawAllocated: AmountJson;
-
- amountWithdrawCompleted: AmountJson;
-
- /**
- * Amount requested when the reserve was created.
- * When a reserve is re-used (rare!) the current_amount can
- * be higher than the requested_amount
- */
- amountInitiallyRequested: AmountJson;
-
- /**
* Wire information (as payto URI) for the bank account that
* transfered funds for this reserve.
*/
@@ -305,7 +297,7 @@ export interface ReserveRecord {
*/
lastError: OperationError | undefined;
- reserveTransactions: ReserveTransaction[];
+ reserveTransactions: WalletReserveHistoryItem[];
}
/**
@@ -627,6 +619,7 @@ export interface PlanchetRecord {
blindingKey: string;
withdrawSig: string;
coinEv: string;
+ coinEvHash: string;
coinValue: AmountJson;
isFromTip: boolean;
}
@@ -675,7 +668,7 @@ export const enum CoinSourceType {
export interface WithdrawCoinSource {
type: CoinSourceType.Withdraw;
- withdrawSessionId: string;
+ withdrawalGroupId: string;
/**
* Index of the coin in the withdrawal session.
@@ -1362,20 +1355,25 @@ export interface CoinsReturnRecord {
wire: any;
}
+export const enum WithdrawalSourceType {
+ Tip = "tip",
+ Reserve = "reserve",
+}
+
export interface WithdrawalSourceTip {
- type: "tip";
+ type: WithdrawalSourceType.Tip;
tipId: string;
}
export interface WithdrawalSourceReserve {
- type: "reserve";
+ type: WithdrawalSourceType.Reserve;
reservePub: string;
}
export type WithdrawalSource = WithdrawalSourceTip | WithdrawalSourceReserve;
-export interface WithdrawalSessionRecord {
- withdrawSessionId: string;
+export interface WithdrawalGroupRecord {
+ withdrawalGroupId: string;
/**
* Withdrawal source. Fields that don't apply to the respective
@@ -1636,9 +1634,9 @@ export namespace Stores {
}
}
- class WithdrawalSessionsStore extends Store<WithdrawalSessionRecord> {
+ class WithdrawalGroupsStore extends Store<WithdrawalGroupRecord> {
constructor() {
- super("withdrawals", { keyPath: "withdrawSessionId" });
+ super("withdrawals", { keyPath: "withdrawalGroupId" });
}
}
@@ -1697,7 +1695,7 @@ export namespace Stores {
export const purchases = new PurchasesStore();
export const tips = new TipsStore();
export const senderWires = new SenderWiresStore();
- export const withdrawalSession = new WithdrawalSessionsStore();
+ export const withdrawalGroups = new WithdrawalGroupsStore();
export const bankWithdrawUris = new BankWithdrawUrisStore();
export const refundEvents = new RefundEventsStore();
export const payEvents = new PayEventsStore();
diff --git a/src/types/history.ts b/src/types/history.ts
index f4f3872ca..8179f6261 100644
--- a/src/types/history.ts
+++ b/src/types/history.ts
@@ -119,8 +119,6 @@ export interface HistoryReserveBalanceUpdatedEvent {
*/
timestamp: Timestamp;
- newHistoryTransactions: ReserveTransaction[];
-
/**
* Condensed information about the reserve.
*/
@@ -129,13 +127,17 @@ export interface HistoryReserveBalanceUpdatedEvent {
/**
* Amount currently left in the reserve.
*/
- amountReserveBalance: string;
+ reserveBalance: string;
+
+ /**
+ * Amount we still expect to be added to the reserve.
+ */
+ reserveAwaitedAmount: string;
/**
- * Amount we expected to be in the reserve at that time,
- * considering ongoing withdrawals from that reserve.
+ * Amount that hasn't been withdrawn yet.
*/
- amountExpected: string;
+ reserveUnclaimedAmount: string;
}
/**
@@ -612,7 +614,7 @@ export interface HistoryWithdrawnEvent {
* Unique identifier for the withdrawal session, can be used to
* query more detailed information from the wallet.
*/
- withdrawSessionId: string;
+ withdrawalGroupId: string;
withdrawalSource: WithdrawalSource;
diff --git a/src/types/notifications.ts b/src/types/notifications.ts
index 39930dcca..05d3c273a 100644
--- a/src/types/notifications.ts
+++ b/src/types/notifications.ts
@@ -1,4 +1,5 @@
import { OperationError } from "./walletTypes";
+import { WithdrawCoinSource, WithdrawalSource } from "./dbTypes";
/*
This file is part of GNU Taler
@@ -34,10 +35,9 @@ export const enum NotificationType {
RefreshUnwarranted = "refresh-unwarranted",
ReserveUpdated = "reserve-updated",
ReserveConfirmed = "reserve-confirmed",
- ReserveDepleted = "reserve-depleted",
ReserveCreated = "reserve-created",
- WithdrawSessionCreated = "withdraw-session-created",
- WithdrawSessionFinished = "withdraw-session-finished",
+ WithdrawGroupCreated = "withdraw-group-created",
+ WithdrawGroupFinished = "withdraw-group-finished",
WaitingForRetry = "waiting-for-retry",
RefundStarted = "refund-started",
RefundQueried = "refund-queried",
@@ -114,19 +114,14 @@ export interface ReserveConfirmedNotification {
type: NotificationType.ReserveConfirmed;
}
-export interface WithdrawSessionCreatedNotification {
- type: NotificationType.WithdrawSessionCreated;
- withdrawSessionId: string;
+export interface WithdrawalGroupCreatedNotification {
+ type: NotificationType.WithdrawGroupCreated;
+ withdrawalGroupId: string;
}
-export interface WithdrawSessionFinishedNotification {
- type: NotificationType.WithdrawSessionFinished;
- withdrawSessionId: string;
-}
-
-export interface ReserveDepletedNotification {
- type: NotificationType.ReserveDepleted;
- reservePub: string;
+export interface WithdrawalGroupFinishedNotification {
+ type: NotificationType.WithdrawGroupFinished;
+ withdrawalSource: WithdrawalSource;
}
export interface WaitingForRetryNotification {
@@ -210,13 +205,12 @@ export type WalletNotification =
| ReserveUpdatedNotification
| ReserveCreatedNotification
| ReserveConfirmedNotification
- | WithdrawSessionFinishedNotification
- | ReserveDepletedNotification
+ | WithdrawalGroupFinishedNotification
| WaitingForRetryNotification
| RefundStartedNotification
| RefundFinishedNotification
| RefundQueriedNotification
- | WithdrawSessionCreatedNotification
+ | WithdrawalGroupCreatedNotification
| CoinWithdrawnNotification
| WildcardNotification
| RecoupOperationErrorNotification;
diff --git a/src/types/pending.ts b/src/types/pending.ts
index d9d17a3b9..1471fa19a 100644
--- a/src/types/pending.ts
+++ b/src/types/pending.ts
@@ -214,7 +214,8 @@ export interface PendingRecoupOperation {
export interface PendingWithdrawOperation {
type: PendingOperationType.Withdraw;
source: WithdrawalSource;
- withdrawSessionId: string;
+ lastError: OperationError | undefined;
+ withdrawalGroupId: string;
numCoinsWithdrawn: number;
numCoinsTotal: number;
}
diff --git a/src/types/types-test.ts b/src/types/types-test.ts
index 885371a1a..ce3092497 100644
--- a/src/types/types-test.ts
+++ b/src/types/types-test.ts
@@ -15,14 +15,14 @@
*/
import test from "ava";
-import * as Amounts from "../util/amounts";
-import { ContractTerms, codecForContractTerms } from "./talerTypes";
+import { Amounts, AmountJson } from "../util/amounts";
+import { codecForContractTerms } from "./talerTypes";
const amt = (
value: number,
fraction: number,
currency: string,
-): Amounts.AmountJson => ({ value, fraction, currency });
+): AmountJson => ({ value, fraction, currency });
test("amount addition (simple)", (t) => {
const a1 = amt(1, 0, "EUR");
@@ -118,13 +118,13 @@ test("amount parsing", (t) => {
});
test("amount stringification", (t) => {
- t.is(Amounts.toString(amt(0, 0, "TESTKUDOS")), "TESTKUDOS:0");
- t.is(Amounts.toString(amt(4, 94000000, "TESTKUDOS")), "TESTKUDOS:4.94");
- t.is(Amounts.toString(amt(0, 10000000, "TESTKUDOS")), "TESTKUDOS:0.1");
- t.is(Amounts.toString(amt(0, 1, "TESTKUDOS")), "TESTKUDOS:0.00000001");
- t.is(Amounts.toString(amt(5, 0, "TESTKUDOS")), "TESTKUDOS:5");
+ t.is(Amounts.stringify(amt(0, 0, "TESTKUDOS")), "TESTKUDOS:0");
+ t.is(Amounts.stringify(amt(4, 94000000, "TESTKUDOS")), "TESTKUDOS:4.94");
+ t.is(Amounts.stringify(amt(0, 10000000, "TESTKUDOS")), "TESTKUDOS:0.1");
+ t.is(Amounts.stringify(amt(0, 1, "TESTKUDOS")), "TESTKUDOS:0.00000001");
+ t.is(Amounts.stringify(amt(5, 0, "TESTKUDOS")), "TESTKUDOS:5");
// denormalized
- t.is(Amounts.toString(amt(1, 100000000, "TESTKUDOS")), "TESTKUDOS:2");
+ t.is(Amounts.stringify(amt(1, 100000000, "TESTKUDOS")), "TESTKUDOS:2");
t.pass();
});
diff --git a/src/types/walletTypes.ts b/src/types/walletTypes.ts
index 7b58ba500..5d28c5ae7 100644
--- a/src/types/walletTypes.ts
+++ b/src/types/walletTypes.ts
@@ -427,6 +427,7 @@ export interface PlanchetCreationResult {
withdrawSig: string;
coinEv: string;
coinValue: AmountJson;
+ coinEvHash: string;
}
export interface PlanchetCreationRequest {
diff --git a/src/util/amounts.ts b/src/util/amounts.ts
index 8deeaeccc..aee7b12b5 100644
--- a/src/util/amounts.ts
+++ b/src/util/amounts.ts
@@ -299,7 +299,7 @@ export function fromFloat(floatVal: number, currency: string) {
* Convert to standard human-readable string representation that's
* also used in JSON formats.
*/
-export function toString(a: AmountJson): string {
+export function stringify(a: AmountJson): string {
const av = a.value + Math.floor(a.fraction / fractionalBase);
const af = a.fraction % fractionalBase;
let s = av.toString();
@@ -322,7 +322,7 @@ export function toString(a: AmountJson): string {
/**
* Check if the argument is a valid amount in string form.
*/
-export function check(a: any): boolean {
+function check(a: any): boolean {
if (typeof a !== "string") {
return false;
}
@@ -333,3 +333,19 @@ export function check(a: any): boolean {
return false;
}
}
+
+// Export all amount-related functions here for better IDE experience.
+export const Amounts = {
+ stringify: stringify,
+ parse: parse,
+ parseOrThrow: parseOrThrow,
+ cmp: cmp,
+ add: add,
+ sum: sum,
+ sub: sub,
+ check: check,
+ getZero: getZero,
+ isZero: isZero,
+ maxAmountValue: maxAmountValue,
+ fromFloat: fromFloat,
+}; \ No newline at end of file
diff --git a/src/util/reserveHistoryUtil-test.ts b/src/util/reserveHistoryUtil-test.ts
new file mode 100644
index 000000000..910d6a01a
--- /dev/null
+++ b/src/util/reserveHistoryUtil-test.ts
@@ -0,0 +1,286 @@
+/*
+ This file is part of GNU Taler
+ (C) 2020 Taler Systems S.A.
+
+ 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
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ * Imports.
+ */
+import test from "ava";
+import {
+ reconcileReserveHistory,
+ summarizeReserveHistory,
+} from "./reserveHistoryUtil";
+import {
+ WalletReserveHistoryItem,
+ WalletReserveHistoryItemType,
+} from "../types/dbTypes";
+import {
+ ReserveTransaction,
+ ReserveTransactionType,
+} from "../types/ReserveTransaction";
+import { Amounts } from "./amounts";
+
+test("basics", (t) => {
+ const r = reconcileReserveHistory([], []);
+ t.deepEqual(r.updatedLocalHistory, []);
+});
+
+test("unmatched credit", (t) => {
+ const localHistory: WalletReserveHistoryItem[] = [];
+ const remoteHistory: ReserveTransaction[] = [
+ {
+ type: ReserveTransactionType.Credit,
+ amount: "TESTKUDOS:100",
+ sender_account_url: "payto://void/",
+ timestamp: { t_ms: 42 },
+ wire_reference: "ABC01",
+ },
+ ];
+ const r = reconcileReserveHistory(localHistory, remoteHistory);
+ const s = summarizeReserveHistory(r.updatedLocalHistory, "TESTKUDOS");
+ t.deepEqual(r.updatedLocalHistory.length, 1);
+ t.deepEqual(Amounts.stringify(s.computedReserveBalance), "TESTKUDOS:100");
+ t.deepEqual(Amounts.stringify(s.awaitedReserveAmount), "TESTKUDOS:0");
+ t.deepEqual(Amounts.stringify(s.unclaimedReserveAmount), "TESTKUDOS:100");
+});
+
+test("unmatched credit #2", (t) => {
+ const localHistory: WalletReserveHistoryItem[] = [];
+ const remoteHistory: ReserveTransaction[] = [
+ {
+ type: ReserveTransactionType.Credit,
+ amount: "TESTKUDOS:100",
+ sender_account_url: "payto://void/",
+ timestamp: { t_ms: 42 },
+ wire_reference: "ABC01",
+ },
+ {
+ type: ReserveTransactionType.Credit,
+ amount: "TESTKUDOS:50",
+ sender_account_url: "payto://void/",
+ timestamp: { t_ms: 42 },
+ wire_reference: "ABC02",
+ },
+ ];
+ const r = reconcileReserveHistory(localHistory, remoteHistory);
+ const s = summarizeReserveHistory(r.updatedLocalHistory, "TESTKUDOS");
+ t.deepEqual(r.updatedLocalHistory.length, 2);
+ t.deepEqual(Amounts.stringify(s.computedReserveBalance), "TESTKUDOS:150");
+ t.deepEqual(Amounts.stringify(s.awaitedReserveAmount), "TESTKUDOS:0");
+ t.deepEqual(Amounts.stringify(s.unclaimedReserveAmount), "TESTKUDOS:150");
+});
+
+test("matched credit", (t) => {
+ const localHistory: WalletReserveHistoryItem[] = [
+ {
+ type: WalletReserveHistoryItemType.Credit,
+ expectedAmount: Amounts.parseOrThrow("TESTKUDOS:100"),
+ matchedExchangeTransaction: {
+ type: ReserveTransactionType.Credit,
+ amount: "TESTKUDOS:100",
+ sender_account_url: "payto://void/",
+ timestamp: { t_ms: 42 },
+ wire_reference: "ABC01",
+ },
+ },
+ ];
+ const remoteHistory: ReserveTransaction[] = [
+ {
+ type: ReserveTransactionType.Credit,
+ amount: "TESTKUDOS:100",
+ sender_account_url: "payto://void/",
+ timestamp: { t_ms: 42 },
+ wire_reference: "ABC01",
+ },
+ {
+ type: ReserveTransactionType.Credit,
+ amount: "TESTKUDOS:50",
+ sender_account_url: "payto://void/",
+ timestamp: { t_ms: 42 },
+ wire_reference: "ABC02",
+ },
+ ];
+ const r = reconcileReserveHistory(localHistory, remoteHistory);
+ const s = summarizeReserveHistory(r.updatedLocalHistory, "TESTKUDOS");
+ t.deepEqual(r.updatedLocalHistory.length, 2);
+ t.deepEqual(Amounts.stringify(s.computedReserveBalance), "TESTKUDOS:150");
+ t.deepEqual(Amounts.stringify(s.awaitedReserveAmount), "TESTKUDOS:0");
+ t.deepEqual(Amounts.stringify(s.unclaimedReserveAmount), "TESTKUDOS:150");
+});
+
+test("fulfilling credit", (t) => {
+ const localHistory: WalletReserveHistoryItem[] = [
+ {
+ type: WalletReserveHistoryItemType.Credit,
+ expectedAmount: Amounts.parseOrThrow("TESTKUDOS:100"),
+ },
+ ];
+ const remoteHistory: ReserveTransaction[] = [
+ {
+ type: ReserveTransactionType.Credit,
+ amount: "TESTKUDOS:100",
+ sender_account_url: "payto://void/",
+ timestamp: { t_ms: 42 },
+ wire_reference: "ABC01",
+ },
+ {
+ type: ReserveTransactionType.Credit,
+ amount: "TESTKUDOS:50",
+ sender_account_url: "payto://void/",
+ timestamp: { t_ms: 42 },
+ wire_reference: "ABC02",
+ },
+ ];
+ const r = reconcileReserveHistory(localHistory, remoteHistory);
+ const s = summarizeReserveHistory(r.updatedLocalHistory, "TESTKUDOS");
+ t.deepEqual(r.updatedLocalHistory.length, 2);
+ t.deepEqual(Amounts.stringify(s.computedReserveBalance), "TESTKUDOS:150");
+});
+
+test("unfulfilled credit", (t) => {
+ const localHistory: WalletReserveHistoryItem[] = [
+ {
+ type: WalletReserveHistoryItemType.Credit,
+ expectedAmount: Amounts.parseOrThrow("TESTKUDOS:100"),
+ },
+ ];
+ const remoteHistory: ReserveTransaction[] = [
+ {
+ type: ReserveTransactionType.Credit,
+ amount: "TESTKUDOS:100",
+ sender_account_url: "payto://void/",
+ timestamp: { t_ms: 42 },
+ wire_reference: "ABC01",
+ },
+ {
+ type: ReserveTransactionType.Credit,
+ amount: "TESTKUDOS:50",
+ sender_account_url: "payto://void/",
+ timestamp: { t_ms: 42 },
+ wire_reference: "ABC02",
+ },
+ ];
+ const r = reconcileReserveHistory(localHistory, remoteHistory);
+ const s = summarizeReserveHistory(r.updatedLocalHistory, "TESTKUDOS");
+ t.deepEqual(r.updatedLocalHistory.length, 2);
+ t.deepEqual(Amounts.stringify(s.computedReserveBalance), "TESTKUDOS:150");
+});
+
+test("awaited credit", (t) => {
+ const localHistory: WalletReserveHistoryItem[] = [
+ {
+ type: WalletReserveHistoryItemType.Credit,
+ expectedAmount: Amounts.parseOrThrow("TESTKUDOS:50"),
+ },
+ {
+ type: WalletReserveHistoryItemType.Credit,
+ expectedAmount: Amounts.parseOrThrow("TESTKUDOS:100"),
+ },
+ ];
+ const remoteHistory: ReserveTransaction[] = [
+ {
+ type: ReserveTransactionType.Credit,
+ amount: "TESTKUDOS:100",
+ sender_account_url: "payto://void/",
+ timestamp: { t_ms: 42 },
+ wire_reference: "ABC01",
+ },
+ ];
+ const r = reconcileReserveHistory(localHistory, remoteHistory);
+ const s = summarizeReserveHistory(r.updatedLocalHistory, "TESTKUDOS");
+ t.deepEqual(r.updatedLocalHistory.length, 2);
+ t.deepEqual(Amounts.stringify(s.computedReserveBalance), "TESTKUDOS:100");
+ t.deepEqual(Amounts.stringify(s.awaitedReserveAmount), "TESTKUDOS:50");
+ t.deepEqual(Amounts.stringify(s.unclaimedReserveAmount), "TESTKUDOS:100");
+});
+
+test("withdrawal new match", (t) => {
+ const localHistory: WalletReserveHistoryItem[] = [
+ {
+ type: WalletReserveHistoryItemType.Credit,
+ expectedAmount: Amounts.parseOrThrow("TESTKUDOS:100"),
+ matchedExchangeTransaction: {
+ type: ReserveTransactionType.Credit,
+ amount: "TESTKUDOS:100",
+ sender_account_url: "payto://void/",
+ timestamp: { t_ms: 42 },
+ wire_reference: "ABC01",
+ },
+ },
+ {
+ type: WalletReserveHistoryItemType.Withdraw,
+ expectedAmount: Amounts.parseOrThrow("TESTKUDOS:5"),
+ },
+ ];
+ const remoteHistory: ReserveTransaction[] = [
+ {
+ type: ReserveTransactionType.Credit,
+ amount: "TESTKUDOS:100",
+ sender_account_url: "payto://void/",
+ timestamp: { t_ms: 42 },
+ wire_reference: "ABC01",
+ },
+ {
+ type: ReserveTransactionType.Withdraw,
+ amount: "TESTKUDOS:5",
+ h_coin_envelope: "foobar",
+ h_denom_pub: "foobar",
+ reserve_sig: "foobar",
+ withdraw_fee: "TESTKUDOS:0.1",
+ },
+ ];
+ const r = reconcileReserveHistory(localHistory, remoteHistory);
+ const s = summarizeReserveHistory(r.updatedLocalHistory, "TESTKUDOS");
+ console.log(r);
+ t.deepEqual(r.updatedLocalHistory.length, 2);
+ t.deepEqual(Amounts.stringify(s.computedReserveBalance), "TESTKUDOS:95");
+ t.deepEqual(Amounts.stringify(s.awaitedReserveAmount), "TESTKUDOS:0");
+ t.deepEqual(Amounts.stringify(s.unclaimedReserveAmount), "TESTKUDOS:95");
+});
+
+test("claimed but now arrived", (t) => {
+ const localHistory: WalletReserveHistoryItem[] = [
+ {
+ type: WalletReserveHistoryItemType.Credit,
+ expectedAmount: Amounts.parseOrThrow("TESTKUDOS:100"),
+ matchedExchangeTransaction: {
+ type: ReserveTransactionType.Credit,
+ amount: "TESTKUDOS:100",
+ sender_account_url: "payto://void/",
+ timestamp: { t_ms: 42 },
+ wire_reference: "ABC01",
+ },
+ },
+ {
+ type: WalletReserveHistoryItemType.Withdraw,
+ expectedAmount: Amounts.parseOrThrow("TESTKUDOS:5"),
+ },
+ ];
+ const remoteHistory: ReserveTransaction[] = [
+ {
+ type: ReserveTransactionType.Credit,
+ amount: "TESTKUDOS:100",
+ sender_account_url: "payto://void/",
+ timestamp: { t_ms: 42 },
+ wire_reference: "ABC01",
+ },
+ ];
+ const r = reconcileReserveHistory(localHistory, remoteHistory);
+ const s = summarizeReserveHistory(r.updatedLocalHistory, "TESTKUDOS");
+ t.deepEqual(r.updatedLocalHistory.length, 2);
+ t.deepEqual(Amounts.stringify(s.computedReserveBalance), "TESTKUDOS:100");
+ t.deepEqual(Amounts.stringify(s.awaitedReserveAmount), "TESTKUDOS:0");
+ t.deepEqual(Amounts.stringify(s.unclaimedReserveAmount), "TESTKUDOS:95");
+});
diff --git a/src/util/reserveHistoryUtil.ts b/src/util/reserveHistoryUtil.ts
new file mode 100644
index 000000000..95f58449e
--- /dev/null
+++ b/src/util/reserveHistoryUtil.ts
@@ -0,0 +1,384 @@
+/*
+ This file is part of GNU Taler
+ (C) 2020 Taler Systems S.A.
+
+ 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
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ * Imports.
+ */
+import {
+ WalletReserveHistoryItem,
+ WalletReserveHistoryItemType,
+} from "../types/dbTypes";
+import {
+ ReserveTransaction,
+ ReserveTransactionType,
+} from "../types/ReserveTransaction";
+import * as Amounts from "../util/amounts";
+import { timestampCmp } from "./time";
+import { deepCopy } from "./helpers";
+import { AmountString } from "../types/talerTypes";
+import { AmountJson } from "../util/amounts";
+
+/**
+ * Helpers for dealing with reserve histories.
+ *
+ * @author Florian Dold <dold@taler.net>
+ */
+
+export interface ReserveReconciliationResult {
+ /**
+ * The wallet's local history reconciled with the exchange's reserve history.
+ */
+ updatedLocalHistory: WalletReserveHistoryItem[];
+
+ /**
+ * History items that were newly created, subset of the
+ * updatedLocalHistory items.
+ */
+ newAddedItems: WalletReserveHistoryItem[];
+
+ /**
+ * History items that were newly matched, subset of the
+ * updatedLocalHistory items.
+ */
+ newMatchedItems: WalletReserveHistoryItem[];
+}
+
+export interface ReserveHistorySummary {
+ /**
+ * Balance computed by the wallet, should match the balance
+ * computed by the reserve.
+ */
+ computedReserveBalance: Amounts.AmountJson;
+
+ /**
+ * Reserve balance that is still available for withdrawal.
+ */
+ unclaimedReserveAmount: Amounts.AmountJson;
+
+ /**
+ * Amount that we're still expecting to come into the reserve.
+ */
+ awaitedReserveAmount: Amounts.AmountJson;
+
+ /**
+ * Amount withdrawn from the reserve so far. Only counts
+ * finished withdrawals, not withdrawals in progress.
+ */
+ withdrawnAmount: Amounts.AmountJson;
+}
+
+export function isRemoteHistoryMatch(
+ t1: ReserveTransaction,
+ t2: ReserveTransaction,
+): boolean {
+ switch (t1.type) {
+ case ReserveTransactionType.Closing: {
+ return t1.type === t2.type && t1.wtid == t2.wtid;
+ }
+ case ReserveTransactionType.Credit: {
+ return t1.type === t2.type && t1.wire_reference === t2.wire_reference;
+ }
+ case ReserveTransactionType.Recoup: {
+ return (
+ t1.type === t2.type &&
+ t1.coin_pub === t2.coin_pub &&
+ timestampCmp(t1.timestamp, t2.timestamp) === 0
+ );
+ }
+ case ReserveTransactionType.Withdraw: {
+ return t1.type === t2.type && t1.h_coin_envelope === t2.h_coin_envelope;
+ }
+ }
+}
+
+export function isLocalRemoteHistoryPreferredMatch(
+ t1: WalletReserveHistoryItem,
+ t2: ReserveTransaction,
+): boolean {
+ switch (t1.type) {
+ case WalletReserveHistoryItemType.Credit: {
+ return (
+ t2.type === ReserveTransactionType.Credit &&
+ !!t1.expectedAmount &&
+ Amounts.cmp(t1.expectedAmount, Amounts.parseOrThrow(t2.amount)) === 0
+ );
+ }
+ case WalletReserveHistoryItemType.Withdraw:
+ return (
+ t2.type === ReserveTransactionType.Withdraw &&
+ !!t1.expectedAmount &&
+ Amounts.cmp(t1.expectedAmount, Amounts.parseOrThrow(t2.amount)) === 0
+ )
+ case WalletReserveHistoryItemType.Recoup: {
+ return (
+ t2.type === ReserveTransactionType.Recoup &&
+ !!t1.expectedAmount &&
+ Amounts.cmp(t1.expectedAmount, Amounts.parseOrThrow(t2.amount)) === 0
+ );
+ }
+ }
+ return false;
+}
+
+export function isLocalRemoteHistoryAcceptableMatch(
+ t1: WalletReserveHistoryItem,
+ t2: ReserveTransaction,
+): boolean {
+ switch (t1.type) {
+ case WalletReserveHistoryItemType.Closing:
+ throw Error("invariant violated");
+ case WalletReserveHistoryItemType.Credit:
+ return !t1.expectedAmount && t2.type == ReserveTransactionType.Credit;
+ case WalletReserveHistoryItemType.Recoup:
+ return !t1.expectedAmount && t2.type == ReserveTransactionType.Recoup;
+ case WalletReserveHistoryItemType.Withdraw:
+ return !t1.expectedAmount && t2.type == ReserveTransactionType.Withdraw;
+ }
+}
+
+export function summarizeReserveHistory(
+ localHistory: WalletReserveHistoryItem[],
+ currency: string,
+): ReserveHistorySummary {
+ const posAmounts: AmountJson[] = [];
+ const negAmounts: AmountJson[] = [];
+ const expectedPosAmounts: AmountJson[] = [];
+ const expectedNegAmounts: AmountJson[] = [];
+ const withdrawnAmounts: AmountJson[] = [];
+
+ for (const item of localHistory) {
+ switch (item.type) {
+ case WalletReserveHistoryItemType.Credit:
+ if (item.matchedExchangeTransaction) {
+ posAmounts.push(
+ Amounts.parseOrThrow(item.matchedExchangeTransaction.amount),
+ );
+ } else if (item.expectedAmount) {
+ expectedPosAmounts.push(item.expectedAmount);
+ }
+ break;
+ case WalletReserveHistoryItemType.Recoup:
+ if (item.matchedExchangeTransaction) {
+ if (item.matchedExchangeTransaction) {
+ posAmounts.push(
+ Amounts.parseOrThrow(item.matchedExchangeTransaction.amount),
+ );
+ } else if (item.expectedAmount) {
+ expectedPosAmounts.push(item.expectedAmount);
+ } else {
+ throw Error("invariant failed");
+ }
+ }
+ break;
+ case WalletReserveHistoryItemType.Closing:
+ if (item.matchedExchangeTransaction) {
+ negAmounts.push(
+ Amounts.parseOrThrow(item.matchedExchangeTransaction.amount),
+ );
+ } else {
+ throw Error("invariant failed");
+ }
+ break;
+ case WalletReserveHistoryItemType.Withdraw:
+ if (item.matchedExchangeTransaction) {
+ negAmounts.push(
+ Amounts.parseOrThrow(item.matchedExchangeTransaction.amount),
+ );
+ withdrawnAmounts.push(Amounts.parseOrThrow(item.matchedExchangeTransaction.amount));
+ } else if (item.expectedAmount) {
+ expectedNegAmounts.push(item.expectedAmount);
+ } else {
+ throw Error("invariant failed");
+ }
+ break;
+ }
+ }
+
+ const z = Amounts.getZero(currency);
+
+ const computedBalance = Amounts.sub(
+ Amounts.add(z, ...posAmounts).amount,
+ ...negAmounts,
+ ).amount;
+
+ const unclaimedReserveAmount = Amounts.sub(
+ Amounts.add(z, ...posAmounts).amount,
+ ...negAmounts,
+ ...expectedNegAmounts,
+ ).amount;
+
+ const awaitedReserveAmount = Amounts.sub(
+ Amounts.add(z, ...expectedPosAmounts).amount,
+ ...expectedNegAmounts,
+ ).amount;
+
+ const withdrawnAmount = Amounts.add(z, ...withdrawnAmounts).amount;
+
+ return {
+ computedReserveBalance: computedBalance,
+ unclaimedReserveAmount: unclaimedReserveAmount,
+ awaitedReserveAmount: awaitedReserveAmount,
+ withdrawnAmount,
+ };
+}
+
+export function reconcileReserveHistory(
+ localHistory: WalletReserveHistoryItem[],
+ remoteHistory: ReserveTransaction[],
+): ReserveReconciliationResult {
+ const updatedLocalHistory: WalletReserveHistoryItem[] = deepCopy(
+ localHistory,
+ );
+ const newMatchedItems: WalletReserveHistoryItem[] = [];
+ const newAddedItems: WalletReserveHistoryItem[] = [];
+
+ const remoteMatched = remoteHistory.map(() => false);
+ const localMatched = localHistory.map(() => false);
+
+ // Take care of deposits
+
+ // First, see which pairs are already a definite match.
+ for (let remoteIndex = 0; remoteIndex < remoteHistory.length; remoteIndex++) {
+ const rhi = remoteHistory[remoteIndex];
+ for (let localIndex = 0; localIndex < localHistory.length; localIndex++) {
+ if (localMatched[localIndex]) {
+ continue;
+ }
+ const lhi = localHistory[localIndex];
+ if (!lhi.matchedExchangeTransaction) {
+ continue;
+ }
+ if (isRemoteHistoryMatch(rhi, lhi.matchedExchangeTransaction)) {
+ localMatched[localIndex] = true;
+ remoteMatched[remoteIndex] = true;
+ break;
+ }
+ }
+ }
+
+ // Check that all previously matched items are still matched
+ for (let localIndex = 0; localIndex < localHistory.length; localIndex++) {
+ if (localMatched[localIndex]) {
+ continue;
+ }
+ const lhi = localHistory[localIndex];
+ if (lhi.matchedExchangeTransaction) {
+ // Don't use for further matching
+ localMatched[localIndex] = true;
+ // FIXME: emit some error here!
+ throw Error("previously matched reserve history item now unmatched");
+ }
+ }
+
+ // Next, find out if there are any exact new matches between local and remote
+ // history items
+ for (let localIndex = 0; localIndex < localHistory.length; localIndex++) {
+ if (localMatched[localIndex]) {
+ continue;
+ }
+ const lhi = localHistory[localIndex];
+ for (
+ let remoteIndex = 0;
+ remoteIndex < remoteHistory.length;
+ remoteIndex++
+ ) {
+ const rhi = remoteHistory[remoteIndex];
+ if (remoteMatched[remoteIndex]) {
+ continue;
+ }
+ if (isLocalRemoteHistoryPreferredMatch(lhi, rhi)) {
+ localMatched[localIndex] = true;
+ remoteMatched[remoteIndex] = true;
+ updatedLocalHistory[localIndex].matchedExchangeTransaction = rhi as any;
+ newMatchedItems.push(lhi);
+ break;
+ }
+ }
+ }
+
+ // Next, find out if there are any acceptable new matches between local and remote
+ // history items
+ for (let localIndex = 0; localIndex < localHistory.length; localIndex++) {
+ if (localMatched[localIndex]) {
+ continue;
+ }
+ const lhi = localHistory[localIndex];
+ for (
+ let remoteIndex = 0;
+ remoteIndex < remoteHistory.length;
+ remoteIndex++
+ ) {
+ const rhi = remoteHistory[remoteIndex];
+ if (remoteMatched[remoteIndex]) {
+ continue;
+ }
+ if (isLocalRemoteHistoryAcceptableMatch(lhi, rhi)) {
+ localMatched[localIndex] = true;
+ remoteMatched[remoteIndex] = true;
+ updatedLocalHistory[localIndex].matchedExchangeTransaction = rhi as any;
+ newMatchedItems.push(lhi);
+ break;
+ }
+ }
+ }
+
+ // Finally we add new history items
+ for (let remoteIndex = 0; remoteIndex < remoteHistory.length; remoteIndex++) {
+ if (remoteMatched[remoteIndex]) {
+ continue;
+ }
+ const rhi = remoteHistory[remoteIndex];
+ let newItem: WalletReserveHistoryItem;
+ switch (rhi.type) {
+ case ReserveTransactionType.Closing: {
+ newItem = {
+ type: WalletReserveHistoryItemType.Closing,
+ matchedExchangeTransaction: rhi,
+ };
+ break;
+ }
+ case ReserveTransactionType.Credit: {
+ newItem = {
+ type: WalletReserveHistoryItemType.Credit,
+ matchedExchangeTransaction: rhi,
+ };
+ break;
+ }
+ case ReserveTransactionType.Recoup: {
+ newItem = {
+ type: WalletReserveHistoryItemType.Recoup,
+ matchedExchangeTransaction: rhi,
+ };
+ break;
+ }
+ case ReserveTransactionType.Withdraw: {
+ newItem = {
+ type: WalletReserveHistoryItemType.Withdraw,
+ matchedExchangeTransaction: rhi,
+ };
+ break;
+ }
+ }
+ updatedLocalHistory.push(newItem);
+ newAddedItems.push(newItem);
+ }
+
+ return {
+ updatedLocalHistory,
+ newAddedItems,
+ newMatchedItems,
+ };
+}
diff --git a/src/wallet.ts b/src/wallet.ts
index 2560b0dc3..3171d0cea 100644
--- a/src/wallet.ts
+++ b/src/wallet.ts
@@ -26,8 +26,7 @@ import { CryptoWorkerFactory } from "./crypto/workers/cryptoApi";
import { HttpRequestLibrary } from "./util/http";
import { Database } from "./util/query";
-import { AmountJson } from "./util/amounts";
-import * as Amounts from "./util/amounts";
+import { Amounts, AmountJson } from "./util/amounts";
import {
getWithdrawDetailsForUri,
@@ -92,7 +91,7 @@ import {
import { InternalWalletState } from "./operations/state";
import { createReserve, confirmReserve } from "./operations/reserves";
import { processRefreshGroup, createRefreshGroup } from "./operations/refresh";
-import { processWithdrawSession } from "./operations/withdraw";
+import { processWithdrawGroup } from "./operations/withdraw";
import { getHistory } from "./operations/history";
import { getPendingOperations } from "./operations/pending";
import { getBalances } from "./operations/balance";
@@ -193,9 +192,9 @@ export class Wallet {
await processReserve(this.ws, pending.reservePub, forceNow);
break;
case PendingOperationType.Withdraw:
- await processWithdrawSession(
+ await processWithdrawGroup(
this.ws,
- pending.withdrawSessionId,
+ pending.withdrawalGroupId,
forceNow,
);
break;
@@ -574,10 +573,14 @@ export class Wallet {
await this.db.put(Stores.currencies, currencyRecord);
}
- async getReserves(exchangeBaseUrl: string): Promise<ReserveRecord[]> {
- return await this.db
- .iter(Stores.reserves)
- .filter((r) => r.exchangeBaseUrl === exchangeBaseUrl);
+ async getReserves(exchangeBaseUrl?: string): Promise<ReserveRecord[]> {
+ if (exchangeBaseUrl) {
+ return await this.db
+ .iter(Stores.reserves)
+ .filter((r) => r.exchangeBaseUrl === exchangeBaseUrl);
+ } else {
+ return await this.db.iter(Stores.reserves).toArray();
+ }
}
async getCoinsForExchange(exchangeBaseUrl: string): Promise<CoinRecord[]> {
@@ -807,8 +810,8 @@ export class Wallet {
let withdrawalReservePub: string | undefined;
if (cs.type == CoinSourceType.Withdraw) {
const ws = await this.db.get(
- Stores.withdrawalSession,
- cs.withdrawSessionId,
+ Stores.withdrawalGroups,
+ cs.withdrawalGroupId,
);
if (!ws) {
console.error("no withdrawal session found for coin");
@@ -822,10 +825,10 @@ export class Wallet {
coin_pub: c.coinPub,
denom_pub: c.denomPub,
denom_pub_hash: c.denomPubHash,
- denom_value: Amounts.toString(denom.value),
+ denom_value: Amounts.stringify(denom.value),
exchange_base_url: c.exchangeBaseUrl,
refresh_parent_coin_pub: refreshParentCoinPub,
- remaining_value: Amounts.toString(c.currentAmount),
+ remaining_value: Amounts.stringify(c.currentAmount),
withdrawal_reserve_pub: withdrawalReservePub,
coin_suspended: c.suspended,
});
diff --git a/src/webex/pages/popup.tsx b/src/webex/pages/popup.tsx
index 7b20f2227..17880db58 100644
--- a/src/webex/pages/popup.tsx
+++ b/src/webex/pages/popup.tsx
@@ -565,10 +565,6 @@ function formatHistoryItem(historyItem: HistoryEvent) {
<HistoryItem
timestamp={historyItem.timestamp}
small={i18n.str`Reserve balance updated`}
- fees={amountDiff(
- historyItem.amountExpected,
- historyItem.amountReserveBalance,
- )}
/>
);
}
diff --git a/src/webex/pages/return-coins.tsx b/src/webex/pages/return-coins.tsx
index fd9238ee2..3786697c6 100644
--- a/src/webex/pages/return-coins.tsx
+++ b/src/webex/pages/return-coins.tsx
@@ -25,7 +25,7 @@
*/
import { AmountJson } from "../../util/amounts";
-import * as Amounts from "../../util/amounts";
+import { Amounts } from "../../util/amounts";
import { SenderWireInfos, WalletBalance } from "../../types/walletTypes";
@@ -70,7 +70,7 @@ class ReturnSelectionItem extends React.Component<
);
this.state = {
currency: props.balance.byExchange[props.exchangeUrl].available.currency,
- selectedValue: Amounts.toString(
+ selectedValue: Amounts.stringify(
props.balance.byExchange[props.exchangeUrl].available,
),
selectedWire: "",