estimate refresh output, show correct(er) balance
This commit is contained in:
parent
5f3d9835fa
commit
38e6d51946
@ -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)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -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) {
|
||||
|
@ -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) {
|
||||
|
@ -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) {
|
||||
|
@ -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(
|
||||
|
@ -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),
|
||||
|
@ -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();
|
||||
|
@ -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,
|
||||
|
Loading…
Reference in New Issue
Block a user