summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFlorian Dold <florian.dold@gmail.com>2020-09-01 23:01:44 +0530
committerFlorian Dold <florian.dold@gmail.com>2020-09-01 23:01:44 +0530
commit38e6d519461cff32107b5eebfc217fd9276960db (patch)
treeacae414baba420cfbb859af63c00370cc0baf3d1
parent5f3d9835fa1d6174da87da3882221d186f5df1b6 (diff)
downloadwallet-core-38e6d519461cff32107b5eebfc217fd9276960db.tar.gz
wallet-core-38e6d519461cff32107b5eebfc217fd9276960db.tar.bz2
wallet-core-38e6d519461cff32107b5eebfc217fd9276960db.zip
estimate refresh output, show correct(er) balance
-rw-r--r--packages/taler-integrationtests/src/harness.ts44
-rw-r--r--packages/taler-integrationtests/src/test-refund-incremental.ts4
-rw-r--r--packages/taler-wallet-core/src/operations/balance.ts5
-rw-r--r--packages/taler-wallet-core/src/operations/pay.ts8
-rw-r--r--packages/taler-wallet-core/src/operations/recoup.ts2
-rw-r--r--packages/taler-wallet-core/src/operations/refresh.ts78
-rw-r--r--packages/taler-wallet-core/src/operations/withdraw.ts6
-rw-r--r--packages/taler-wallet-core/src/types/dbTypes.ts4
-rw-r--r--packages/taler-wallet-core/src/util/amounts.ts7
-rw-r--r--packages/taler-wallet-core/src/wallet.ts2
10 files changed, 99 insertions, 61 deletions
diff --git a/packages/taler-integrationtests/src/harness.ts b/packages/taler-integrationtests/src/harness.ts
index 0b41923a4..0beed3976 100644
--- a/packages/taler-integrationtests/src/harness.ts
+++ b/packages/taler-integrationtests/src/harness.ts
@@ -361,53 +361,27 @@ export class GlobalTestState {
}
assertAmountEquals(
- amtExpected: string | AmountJson,
amtActual: string | AmountJson,
+ amtExpected: string | AmountJson,
): void {
- let ja1: AmountJson;
- let ja2: AmountJson;
- if (typeof amtExpected === "string") {
- ja1 = Amounts.parseOrThrow(amtExpected);
- } else {
- ja1 = amtExpected;
- }
- if (typeof amtActual === "string") {
- ja2 = Amounts.parseOrThrow(amtActual);
- } else {
- ja2 = amtActual;
- }
-
- if (Amounts.cmp(ja1, ja2) != 0) {
+ if (Amounts.cmp(amtActual, amtExpected) != 0) {
throw Error(
`test assertion failed: expected ${Amounts.stringify(
- ja1,
- )} but got ${Amounts.stringify(ja2)}`,
+ amtExpected,
+ )} but got ${Amounts.stringify(amtActual)}`,
);
}
}
assertAmountLeq(
- amtExpected: string | AmountJson,
- amtActual: string | AmountJson,
+ a: string | AmountJson,
+ b: string | AmountJson,
): void {
- let ja1: AmountJson;
- let ja2: AmountJson;
- if (typeof amtExpected === "string") {
- ja1 = Amounts.parseOrThrow(amtExpected);
- } else {
- ja1 = amtExpected;
- }
- if (typeof amtActual === "string") {
- ja2 = Amounts.parseOrThrow(amtActual);
- } else {
- ja2 = amtActual;
- }
-
- if (Amounts.cmp(ja1, ja2) > 0) {
+ if (Amounts.cmp(a, b) > 0) {
throw Error(
`test assertion failed: expected ${Amounts.stringify(
- ja1,
- )} to be less or equal (leq) than ${Amounts.stringify(ja2)}`,
+ a,
+ )} to be less or equal (leq) than ${Amounts.stringify(b)}`,
);
}
}
diff --git a/packages/taler-integrationtests/src/test-refund-incremental.ts b/packages/taler-integrationtests/src/test-refund-incremental.ts
index 2e0f03f29..661378dc8 100644
--- a/packages/taler-integrationtests/src/test-refund-incremental.ts
+++ b/packages/taler-integrationtests/src/test-refund-incremental.ts
@@ -175,7 +175,7 @@ runTest(async (t: GlobalTestState) => {
.map((x) => x.amountRaw),
).amount;
- t.assertAmountEquals(raw, "TESTKUDOS:10");
+ t.assertAmountEquals("TESTKUDOS:10", raw);
const effective = Amounts.sum(
txs.transactions
@@ -183,7 +183,7 @@ runTest(async (t: GlobalTestState) => {
.map((x) => x.amountEffective),
).amount;
- t.assertAmountEquals(effective, "TESTKUDOS:8.17");
+ t.assertAmountEquals("TESTKUDOS:8.33", effective);
}
await t.shutdown();
diff --git a/packages/taler-wallet-core/src/operations/balance.ts b/packages/taler-wallet-core/src/operations/balance.ts
index 26f0aaeee..47ce5f482 100644
--- a/packages/taler-wallet-core/src/operations/balance.ts
+++ b/packages/taler-wallet-core/src/operations/balance.ts
@@ -90,7 +90,10 @@ export async function getBalancesInsideTransaction(
const b = initBalance(session.amountRefreshOutput.currency);
// We are always assuming the refresh will succeed, thus we
// report the output as available balance.
- b.available = Amounts.add(session.amountRefreshOutput).amount;
+ b.available = Amounts.add(b.available, session.amountRefreshOutput).amount;
+ } else {
+ const b = initBalance(r.inputPerCoin[i].currency);
+ b.available = Amounts.add(b.available, r.estimatedOutputPerCoin[i]).amount;
}
}
});
diff --git a/packages/taler-wallet-core/src/operations/pay.ts b/packages/taler-wallet-core/src/operations/pay.ts
index 6981b44fc..ce71737d1 100644
--- a/packages/taler-wallet-core/src/operations/pay.ts
+++ b/packages/taler-wallet-core/src/operations/pay.ts
@@ -473,7 +473,13 @@ async function recordConfirmPay(
};
await ws.db.runWithWriteTransaction(
- [Stores.coins, Stores.purchases, Stores.proposals, Stores.refreshGroups],
+ [
+ Stores.coins,
+ Stores.purchases,
+ Stores.proposals,
+ Stores.refreshGroups,
+ Stores.denominations,
+ ],
async (tx) => {
const p = await tx.get(Stores.proposals, proposal.proposalId);
if (p) {
diff --git a/packages/taler-wallet-core/src/operations/recoup.ts b/packages/taler-wallet-core/src/operations/recoup.ts
index 7896fc275..0e4ce18d3 100644
--- a/packages/taler-wallet-core/src/operations/recoup.ts
+++ b/packages/taler-wallet-core/src/operations/recoup.ts
@@ -180,7 +180,7 @@ async function recoupWithdrawCoin(
// FIXME: verify that our expectations about the amount match
await ws.db.runWithWriteTransaction(
- [Stores.coins, Stores.reserves, Stores.recoupGroups],
+ [Stores.coins, Stores.denominations, Stores.reserves, Stores.recoupGroups],
async (tx) => {
const recoupGroup = await tx.get(Stores.recoupGroups, recoupGroupId);
if (!recoupGroup) {
diff --git a/packages/taler-wallet-core/src/operations/refresh.ts b/packages/taler-wallet-core/src/operations/refresh.ts
index d951cfeb8..770c67da6 100644
--- a/packages/taler-wallet-core/src/operations/refresh.ts
+++ b/packages/taler-wallet-core/src/operations/refresh.ts
@@ -31,7 +31,7 @@ import { amountToPretty } from "../util/helpers";
import { TransactionHandle } from "../util/query";
import { InternalWalletState, EXCHANGE_COINS_LOCK } from "./state";
import { Logger } from "../util/logging";
-import { getWithdrawDenomList } from "./withdraw";
+import { getWithdrawDenomList, isWithdrawableDenom } from "./withdraw";
import { updateExchangeFromUrl } from "./exchanges";
import {
TalerErrorDetails,
@@ -49,6 +49,7 @@ import {
codecForExchangeRevealResponse,
} from "../types/talerTypes";
import { URL } from "../util/url";
+import { checkDbInvariant } from "../util/invariants";
const logger = new Logger("refresh.ts");
@@ -132,8 +133,10 @@ async function refreshCreateSession(
.iterIndex(Stores.denominations.exchangeBaseUrlIndex, exchange.baseUrl)
.toArray();
- const availableAmount = Amounts.sub(coin.currentAmount, oldDenom.feeRefresh)
- .amount;
+ const availableAmount = Amounts.sub(
+ refreshGroup.inputPerCoin[coinIndex],
+ oldDenom.feeRefresh,
+ ).amount;
const newCoinDenoms = getWithdrawDenomList(availableAmount, availableDenoms);
@@ -177,22 +180,10 @@ async function refreshCreateSession(
oldDenom.feeRefresh,
);
- // Store refresh session and subtract refreshed amount from
- // coin in the same transaction.
+ // Store refresh session for this coin in the database.
await ws.db.runWithWriteTransaction(
[Stores.refreshGroups, Stores.coins],
async (tx) => {
- const c = await tx.get(Stores.coins, coin.coinPub);
- if (!c) {
- throw Error("coin not found, but marked for refresh");
- }
- const r = Amounts.sub(c.currentAmount, refreshSession.amountRefreshInput);
- if (r.saturated) {
- logger.warn("can't refresh coin, no amount left");
- return;
- }
- c.currentAmount = r.amount;
- c.status = CoinStatus.Dormant;
const rg = await tx.get(Stores.refreshGroups, refreshGroupId);
if (!rg) {
return;
@@ -202,7 +193,6 @@ async function refreshCreateSession(
}
rg.refreshSessionPerCoin[coinIndex] = refreshSession;
await tx.put(Stores.refreshGroups, rg);
- await tx.put(Stores.coins, c);
},
);
logger.info(
@@ -552,6 +542,16 @@ async function processRefreshSession(
/**
* Create a refresh group for a list of coins.
+ *
+ * Refreshes the remaining amount on the coin, effectively capturing the remaining
+ * value in the refresh group.
+ *
+ * The caller must ensure that
+ * the remaining amount was updated correctly before the coin was deposited or
+ * credited.
+ *
+ * The caller must also ensure that the coins that should be refreshed exist
+ * in the current database transaction.
*/
export async function createRefreshGroup(
ws: InternalWalletState,
@@ -561,6 +561,48 @@ export async function createRefreshGroup(
): Promise<RefreshGroupId> {
const refreshGroupId = encodeCrock(getRandomBytes(32));
+ const inputPerCoin: AmountJson[] = [];
+ const estimatedOutputPerCoin: AmountJson[] = [];
+
+ const denomsPerExchange: Record<string, DenominationRecord[]> = {};
+
+ const getDenoms = async (
+ exchangeBaseUrl: string,
+ ): Promise<DenominationRecord[]> => {
+ if (denomsPerExchange[exchangeBaseUrl]) {
+ return denomsPerExchange[exchangeBaseUrl];
+ }
+ const allDenoms = await tx
+ .iterIndexed(Stores.denominations.exchangeBaseUrlIndex, exchangeBaseUrl)
+ .filter((x) => {
+ return isWithdrawableDenom(x);
+ });
+ denomsPerExchange[exchangeBaseUrl] = allDenoms;
+ return allDenoms;
+ };
+
+ for (const ocp of oldCoinPubs) {
+ const coin = await tx.get(Stores.coins, ocp.coinPub);
+ checkDbInvariant(!!coin, "coin must be in database");
+ const denom = await tx.get(Stores.denominations, [
+ coin.exchangeBaseUrl,
+ coin.denomPub,
+ ]);
+ checkDbInvariant(
+ !!denom,
+ "denomination for existing coin must be in database",
+ );
+ const refreshAmount = coin.currentAmount;
+ inputPerCoin.push(refreshAmount);
+ coin.currentAmount = Amounts.getZero(refreshAmount.currency);
+ coin.status = CoinStatus.Dormant;
+ await tx.put(Stores.coins, coin);
+ const denoms = await getDenoms(coin.exchangeBaseUrl);
+ const cost = getTotalRefreshCost(denoms, denom, refreshAmount);
+ const output = Amounts.sub(refreshAmount, cost).amount;
+ estimatedOutputPerCoin.push(output);
+ }
+
const refreshGroup: RefreshGroupRecord = {
timestampFinished: undefined,
finishedPerCoin: oldCoinPubs.map((x) => false),
@@ -571,6 +613,8 @@ export async function createRefreshGroup(
refreshGroupId,
refreshSessionPerCoin: oldCoinPubs.map((x) => undefined),
retryInfo: initRetryInfo(),
+ inputPerCoin,
+ estimatedOutputPerCoin,
};
if (oldCoinPubs.length == 0) {
diff --git a/packages/taler-wallet-core/src/operations/withdraw.ts b/packages/taler-wallet-core/src/operations/withdraw.ts
index c68f1521f..c719f7ab8 100644
--- a/packages/taler-wallet-core/src/operations/withdraw.ts
+++ b/packages/taler-wallet-core/src/operations/withdraw.ts
@@ -67,7 +67,11 @@ import { encodeCrock } from "../crypto/talerCrypto";
const logger = new Logger("withdraw.ts");
-function isWithdrawableDenom(d: DenominationRecord): boolean {
+/**
+ * Check if a denom is withdrawable based on the expiration time
+ * and revocation state.
+ */
+export function isWithdrawableDenom(d: DenominationRecord): boolean {
const now = getTimestampNow();
const started = timestampCmp(now, d.stampStart) >= 0;
const lastPossibleWithdraw = timestampSubtractDuraction(
diff --git a/packages/taler-wallet-core/src/types/dbTypes.ts b/packages/taler-wallet-core/src/types/dbTypes.ts
index 148ec8c35..9ba07a0fd 100644
--- a/packages/taler-wallet-core/src/types/dbTypes.ts
+++ b/packages/taler-wallet-core/src/types/dbTypes.ts
@@ -1014,6 +1014,10 @@ export interface RefreshGroupRecord {
refreshSessionPerCoin: (RefreshSessionRecord | undefined)[];
+ inputPerCoin: AmountJson[];
+
+ estimatedOutputPerCoin: AmountJson[];
+
/**
* Flag for each coin whether refreshing finished.
* If a coin can't be refreshed (remaining value too small),
diff --git a/packages/taler-wallet-core/src/util/amounts.ts b/packages/taler-wallet-core/src/util/amounts.ts
index 2f912cff4..533005b18 100644
--- a/packages/taler-wallet-core/src/util/amounts.ts
+++ b/packages/taler-wallet-core/src/util/amounts.ts
@@ -195,7 +195,9 @@ export function sub(a: AmountJson, ...rest: AmountJson[]): Result {
* Compare two amounts. Returns 0 when equal, -1 when a < b
* and +1 when a > b. Throws when currencies don't match.
*/
-export function cmp(a: AmountJson, b: AmountJson): -1 | 0 | 1 {
+export function cmp(a: AmountLike, b: AmountLike): -1 | 0 | 1 {
+ a = jsonifyAmount(a);
+ b = jsonifyAmount(b);
if (a.currency !== b.currency) {
throw Error(`Mismatched currency: ${a.currency} and ${b.currency}`);
}
@@ -310,7 +312,8 @@ export function fromFloat(floatVal: number, currency: string): AmountJson {
* Convert to standard human-readable string representation that's
* also used in JSON formats.
*/
-export function stringify(a: AmountJson): string {
+export function stringify(a: AmountLike): string {
+ a = jsonifyAmount(a);
const av = a.value + Math.floor(a.fraction / fractionalBase);
const af = a.fraction % fractionalBase;
let s = av.toString();
diff --git a/packages/taler-wallet-core/src/wallet.ts b/packages/taler-wallet-core/src/wallet.ts
index ccc4e9d2c..b448df648 100644
--- a/packages/taler-wallet-core/src/wallet.ts
+++ b/packages/taler-wallet-core/src/wallet.ts
@@ -571,7 +571,7 @@ export class Wallet {
async refresh(oldCoinPub: string): Promise<void> {
try {
const refreshGroupId = await this.db.runWithWriteTransaction(
- [Stores.refreshGroups],
+ [Stores.refreshGroups, Stores.denominations, Stores.coins],
async (tx) => {
return await createRefreshGroup(
this.ws,