wallet-core: make changes to available amount atomic
W.r.t. transactions
This commit is contained in:
parent
2779086a32
commit
a844136489
@ -119,7 +119,7 @@ export const CURRENT_DB_CONFIG_KEY = "currentMainDbName";
|
|||||||
* backwards-compatible way or object stores and indices
|
* backwards-compatible way or object stores and indices
|
||||||
* are added.
|
* are added.
|
||||||
*/
|
*/
|
||||||
export const WALLET_DB_MINOR_VERSION = 8;
|
export const WALLET_DB_MINOR_VERSION = 9;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Ranges for operation status fields.
|
* Ranges for operation status fields.
|
||||||
@ -723,6 +723,14 @@ export interface CoinRecord {
|
|||||||
*/
|
*/
|
||||||
coinSource: CoinSource;
|
coinSource: CoinSource;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Source transaction ID of the coin.
|
||||||
|
*
|
||||||
|
* Used to make the coin visible after the transaction
|
||||||
|
* has entered a final state.
|
||||||
|
*/
|
||||||
|
sourceTransactionId?: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Public key of the coin.
|
* Public key of the coin.
|
||||||
*/
|
*/
|
||||||
@ -768,6 +776,14 @@ export interface CoinRecord {
|
|||||||
*/
|
*/
|
||||||
status: CoinStatus;
|
status: CoinStatus;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Non-zero for visible.
|
||||||
|
*
|
||||||
|
* A coin is visible when it is fresh and the
|
||||||
|
* source transaction is in a final state.
|
||||||
|
*/
|
||||||
|
visible?: number;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Information about what the coin has been allocated for.
|
* Information about what the coin has been allocated for.
|
||||||
*
|
*
|
||||||
@ -894,7 +910,7 @@ export enum RefreshCoinStatus {
|
|||||||
* The refresh for this coin has been frozen, because of a permanent error.
|
* The refresh for this coin has been frozen, because of a permanent error.
|
||||||
* More info in lastErrorPerCoin.
|
* More info in lastErrorPerCoin.
|
||||||
*/
|
*/
|
||||||
Frozen = OperationStatusRange.DORMANT_START + 1,
|
Failed = OperationStatusRange.DORMANT_START + 1,
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum OperationStatus {
|
export enum OperationStatus {
|
||||||
@ -1748,7 +1764,6 @@ export interface DepositKycInfo {
|
|||||||
exchangeBaseUrl: string;
|
exchangeBaseUrl: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Record for a deposits that the wallet observed
|
* Record for a deposits that the wallet observed
|
||||||
* as a result of double spending, but which is not
|
* as a result of double spending, but which is not
|
||||||
@ -2132,6 +2147,15 @@ export interface CoinAvailabilityRecord {
|
|||||||
* Number of fresh coins of this denomination that are available.
|
* Number of fresh coins of this denomination that are available.
|
||||||
*/
|
*/
|
||||||
freshCoinCount: number;
|
freshCoinCount: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Number of fresh coins that are available
|
||||||
|
* and visible, i.e. the source transaction is in
|
||||||
|
* a final state.
|
||||||
|
*
|
||||||
|
* (Optional for backwards compatibility, defaults to 0.)
|
||||||
|
*/
|
||||||
|
visibleCoinCount?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ContractTermsRecord {
|
export interface ContractTermsRecord {
|
||||||
@ -2318,6 +2342,13 @@ export const WalletStoresV1 = {
|
|||||||
["exchangeBaseUrl", "denomPubHash", "maxAge", "status"],
|
["exchangeBaseUrl", "denomPubHash", "maxAge", "status"],
|
||||||
),
|
),
|
||||||
byCoinEvHash: describeIndex("byCoinEvHash", "coinEvHash"),
|
byCoinEvHash: describeIndex("byCoinEvHash", "coinEvHash"),
|
||||||
|
bySourceTransactionId: describeIndex(
|
||||||
|
"bySourceTransactionId",
|
||||||
|
"sourceTransactionId",
|
||||||
|
{
|
||||||
|
versionAdded: 9,
|
||||||
|
},
|
||||||
|
),
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
reserves: describeStore(
|
reserves: describeStore(
|
||||||
|
@ -20,14 +20,17 @@
|
|||||||
* There are multiple definition of the wallet's balance.
|
* There are multiple definition of the wallet's balance.
|
||||||
* We use the following terminology:
|
* We use the following terminology:
|
||||||
*
|
*
|
||||||
* - "available": Balance that the wallet believes will certainly be available
|
* - "available": Balance that is available
|
||||||
* for spending, modulo any failures of the exchange or double spending issues.
|
* for spending from transactions in their final state and
|
||||||
* This includes available coins *not* allocated to any
|
* expected to be available from pending refreshes.
|
||||||
* spending/refresh/... operation. Pending withdrawals are *not* counted
|
*
|
||||||
* towards this balance, because they are not certain to succeed.
|
* - "pending-incoming": Expected (positive!) delta
|
||||||
* Pending refreshes *are* counted towards this balance.
|
* to the available balance that we expect to have
|
||||||
* This balance type is nice to show to the user, because it does not
|
* after pending operations reach the "done" state.
|
||||||
* temporarily decrease after payment when we are waiting for refreshes
|
*
|
||||||
|
* - "pending-outgoing": Amount that is currently allocated
|
||||||
|
* to be spent, but the spend operation could still be aborted
|
||||||
|
* and part of the pending-outgoing amount could be recovered.
|
||||||
*
|
*
|
||||||
* - "material": Balance that the wallet believes it could spend *right now*,
|
* - "material": Balance that the wallet believes it could spend *right now*,
|
||||||
* without waiting for any operations to complete.
|
* without waiting for any operations to complete.
|
||||||
@ -61,11 +64,13 @@ import {
|
|||||||
AllowedExchangeInfo,
|
AllowedExchangeInfo,
|
||||||
RefreshGroupRecord,
|
RefreshGroupRecord,
|
||||||
WalletStoresV1,
|
WalletStoresV1,
|
||||||
|
WithdrawalGroupStatus,
|
||||||
} from "../db.js";
|
} from "../db.js";
|
||||||
import { InternalWalletState } from "../internal-wallet-state.js";
|
import { InternalWalletState } from "../internal-wallet-state.js";
|
||||||
import { checkLogicInvariant } from "../util/invariants.js";
|
import { checkLogicInvariant } from "../util/invariants.js";
|
||||||
import { GetReadOnlyAccess } from "../util/query.js";
|
import { GetReadOnlyAccess } from "../util/query.js";
|
||||||
import { getExchangeDetails } from "./exchanges.js";
|
import { getExchangeDetails } from "./exchanges.js";
|
||||||
|
import { assertUnreachable } from "../util/assertUnreachable.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Logger.
|
* Logger.
|
||||||
@ -133,7 +138,8 @@ export async function getBalancesInsideTransaction(
|
|||||||
|
|
||||||
await tx.coinAvailability.iter().forEach((ca) => {
|
await tx.coinAvailability.iter().forEach((ca) => {
|
||||||
const b = initBalance(ca.currency);
|
const b = initBalance(ca.currency);
|
||||||
for (let i = 0; i < ca.freshCoinCount; i++) {
|
const count = ca.visibleCoinCount ?? 0;
|
||||||
|
for (let i = 0; i < count; i++) {
|
||||||
b.available = Amounts.add(b.available, {
|
b.available = Amounts.add(b.available, {
|
||||||
currency: ca.currency,
|
currency: ca.currency,
|
||||||
fraction: ca.amountFrac,
|
fraction: ca.amountFrac,
|
||||||
@ -150,14 +156,40 @@ export async function getBalancesInsideTransaction(
|
|||||||
).amount;
|
).amount;
|
||||||
});
|
});
|
||||||
|
|
||||||
await tx.withdrawalGroups.iter().forEach((wds) => {
|
// FIXME: Use indexing to filter out final transactions.
|
||||||
if (wds.timestampFinish) {
|
await tx.withdrawalGroups.iter().forEach((wgRecord) => {
|
||||||
return;
|
switch (wgRecord.status) {
|
||||||
|
case WithdrawalGroupStatus.AbortedBank:
|
||||||
|
case WithdrawalGroupStatus.AbortedExchange:
|
||||||
|
case WithdrawalGroupStatus.FailedAbortingBank:
|
||||||
|
case WithdrawalGroupStatus.FailedBankAborted:
|
||||||
|
case WithdrawalGroupStatus.Finished:
|
||||||
|
// Does not count as pendingIncoming
|
||||||
|
return;
|
||||||
|
case WithdrawalGroupStatus.PendingReady:
|
||||||
|
case WithdrawalGroupStatus.AbortingBank:
|
||||||
|
case WithdrawalGroupStatus.PendingAml:
|
||||||
|
case WithdrawalGroupStatus.PendingKyc:
|
||||||
|
case WithdrawalGroupStatus.PendingQueryingStatus:
|
||||||
|
case WithdrawalGroupStatus.SuspendedWaitConfirmBank:
|
||||||
|
case WithdrawalGroupStatus.SuspendedReady:
|
||||||
|
case WithdrawalGroupStatus.SuspendedRegisteringBank:
|
||||||
|
case WithdrawalGroupStatus.SuspendedKyc:
|
||||||
|
case WithdrawalGroupStatus.SuspendedAbortingBank:
|
||||||
|
case WithdrawalGroupStatus.SuspendedAml:
|
||||||
|
case WithdrawalGroupStatus.PendingRegisteringBank:
|
||||||
|
case WithdrawalGroupStatus.PendingWaitConfirmBank:
|
||||||
|
case WithdrawalGroupStatus.SuspendedQueryingStatus:
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
assertUnreachable(wgRecord.status);
|
||||||
}
|
}
|
||||||
const b = initBalance(Amounts.currencyOf(wds.denomsSel.totalWithdrawCost));
|
const b = initBalance(
|
||||||
|
Amounts.currencyOf(wgRecord.denomsSel.totalWithdrawCost),
|
||||||
|
);
|
||||||
b.pendingIncoming = Amounts.add(
|
b.pendingIncoming = Amounts.add(
|
||||||
b.pendingIncoming,
|
b.pendingIncoming,
|
||||||
wds.denomsSel.totalCoinValue,
|
wgRecord.denomsSel.totalCoinValue,
|
||||||
).amount;
|
).amount;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -81,6 +81,38 @@ export interface CoinsSpendInfo {
|
|||||||
allocationId: TransactionIdStr;
|
allocationId: TransactionIdStr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function makeCoinsVisible(
|
||||||
|
ws: InternalWalletState,
|
||||||
|
tx: GetReadWriteAccess<{
|
||||||
|
coins: typeof WalletStoresV1.coins;
|
||||||
|
coinAvailability: typeof WalletStoresV1.coinAvailability;
|
||||||
|
}>,
|
||||||
|
transactionId: string,
|
||||||
|
): Promise<void> {
|
||||||
|
const coins = await tx.coins.indexes.bySourceTransactionId.getAll(
|
||||||
|
transactionId,
|
||||||
|
);
|
||||||
|
for (const coinRecord of coins) {
|
||||||
|
if (!coinRecord.visible) {
|
||||||
|
coinRecord.visible = 1;
|
||||||
|
await tx.coins.put(coinRecord);
|
||||||
|
const ageRestriction = coinRecord.maxAge;
|
||||||
|
const car = await tx.coinAvailability.get([
|
||||||
|
coinRecord.exchangeBaseUrl,
|
||||||
|
coinRecord.denomPubHash,
|
||||||
|
ageRestriction,
|
||||||
|
]);
|
||||||
|
if (!car) {
|
||||||
|
logger.error("missing coin availability record");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const visCount = car.visibleCoinCount ?? 0;
|
||||||
|
car.visibleCoinCount = visCount + 1;
|
||||||
|
await tx.coinAvailability.put(car);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export async function makeCoinAvailable(
|
export async function makeCoinAvailable(
|
||||||
ws: InternalWalletState,
|
ws: InternalWalletState,
|
||||||
tx: GetReadWriteAccess<{
|
tx: GetReadWriteAccess<{
|
||||||
@ -195,6 +227,13 @@ export async function spendCoins(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
coinAvailability.freshCoinCount--;
|
coinAvailability.freshCoinCount--;
|
||||||
|
if (coin.visible) {
|
||||||
|
if (!coinAvailability.visibleCoinCount) {
|
||||||
|
logger.error("coin availability inconsistent");
|
||||||
|
} else {
|
||||||
|
coinAvailability.visibleCoinCount--;
|
||||||
|
}
|
||||||
|
}
|
||||||
await tx.coins.put(coin);
|
await tx.coins.put(coin);
|
||||||
await tx.coinAvailability.put(coinAvailability);
|
await tx.coinAvailability.put(coinAvailability);
|
||||||
}
|
}
|
||||||
|
@ -46,16 +46,20 @@ import {
|
|||||||
NotificationType,
|
NotificationType,
|
||||||
RefreshGroupId,
|
RefreshGroupId,
|
||||||
RefreshReason,
|
RefreshReason,
|
||||||
|
TalerError,
|
||||||
TalerErrorCode,
|
TalerErrorCode,
|
||||||
TalerErrorDetail,
|
TalerErrorDetail,
|
||||||
TalerPreciseTimestamp,
|
TalerPreciseTimestamp,
|
||||||
TalerProtocolTimestamp,
|
|
||||||
TransactionAction,
|
TransactionAction,
|
||||||
TransactionMajorState,
|
TransactionMajorState,
|
||||||
TransactionState,
|
TransactionState,
|
||||||
TransactionType,
|
TransactionType,
|
||||||
URL,
|
URL,
|
||||||
} from "@gnu-taler/taler-util";
|
} from "@gnu-taler/taler-util";
|
||||||
|
import {
|
||||||
|
readSuccessResponseJsonOrThrow,
|
||||||
|
readUnexpectedResponseDetails,
|
||||||
|
} from "@gnu-taler/taler-util/http";
|
||||||
import { TalerCryptoInterface } from "../crypto/cryptoImplementation.js";
|
import { TalerCryptoInterface } from "../crypto/cryptoImplementation.js";
|
||||||
import {
|
import {
|
||||||
DerivedRefreshSession,
|
DerivedRefreshSession,
|
||||||
@ -72,25 +76,23 @@ import {
|
|||||||
RefreshReasonDetails,
|
RefreshReasonDetails,
|
||||||
WalletStoresV1,
|
WalletStoresV1,
|
||||||
} from "../db.js";
|
} from "../db.js";
|
||||||
import { TalerError } from "@gnu-taler/taler-util";
|
import { isWithdrawableDenom, PendingTaskType } from "../index.js";
|
||||||
import {
|
import {
|
||||||
EXCHANGE_COINS_LOCK,
|
EXCHANGE_COINS_LOCK,
|
||||||
InternalWalletState,
|
InternalWalletState,
|
||||||
} from "../internal-wallet-state.js";
|
} from "../internal-wallet-state.js";
|
||||||
import { assertUnreachable } from "../util/assertUnreachable.js";
|
import { assertUnreachable } from "../util/assertUnreachable.js";
|
||||||
import {
|
import { selectWithdrawalDenominations } from "../util/coinSelection.js";
|
||||||
readSuccessResponseJsonOrThrow,
|
|
||||||
readUnexpectedResponseDetails,
|
|
||||||
} from "@gnu-taler/taler-util/http";
|
|
||||||
import { checkDbInvariant } from "../util/invariants.js";
|
import { checkDbInvariant } from "../util/invariants.js";
|
||||||
import { GetReadOnlyAccess, GetReadWriteAccess } from "../util/query.js";
|
import { GetReadOnlyAccess, GetReadWriteAccess } from "../util/query.js";
|
||||||
import { constructTaskIdentifier, makeCoinAvailable, OperationAttemptResult, OperationAttemptResultType } from "./common.js";
|
|
||||||
import { updateExchangeFromUrl } from "./exchanges.js";
|
|
||||||
import { selectWithdrawalDenominations } from "../util/coinSelection.js";
|
|
||||||
import {
|
import {
|
||||||
isWithdrawableDenom,
|
constructTaskIdentifier,
|
||||||
PendingTaskType,
|
makeCoinAvailable,
|
||||||
} from "../index.js";
|
makeCoinsVisible,
|
||||||
|
OperationAttemptResult,
|
||||||
|
OperationAttemptResultType,
|
||||||
|
} from "./common.js";
|
||||||
|
import { updateExchangeFromUrl } from "./exchanges.js";
|
||||||
import {
|
import {
|
||||||
constructTransactionIdentifier,
|
constructTransactionIdentifier,
|
||||||
notifyTransition,
|
notifyTransition,
|
||||||
@ -144,24 +146,26 @@ export function getTotalRefreshCost(
|
|||||||
return totalCost;
|
return totalCost;
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateGroupStatus(rg: RefreshGroupRecord): void {
|
function updateGroupStatus(rg: RefreshGroupRecord): { final: boolean } {
|
||||||
const allDone = fnutil.all(
|
const allFinal = fnutil.all(
|
||||||
rg.statusPerCoin,
|
rg.statusPerCoin,
|
||||||
(x) => x === RefreshCoinStatus.Finished || x === RefreshCoinStatus.Frozen,
|
(x) => x === RefreshCoinStatus.Finished || x === RefreshCoinStatus.Failed,
|
||||||
);
|
);
|
||||||
const anyFrozen = fnutil.any(
|
const anyFailed = fnutil.any(
|
||||||
rg.statusPerCoin,
|
rg.statusPerCoin,
|
||||||
(x) => x === RefreshCoinStatus.Frozen,
|
(x) => x === RefreshCoinStatus.Failed,
|
||||||
);
|
);
|
||||||
if (allDone) {
|
if (allFinal) {
|
||||||
if (anyFrozen) {
|
if (anyFailed) {
|
||||||
rg.timestampFinished = TalerPreciseTimestamp.now();
|
rg.timestampFinished = TalerPreciseTimestamp.now();
|
||||||
rg.operationStatus = RefreshOperationStatus.Failed;
|
rg.operationStatus = RefreshOperationStatus.Failed;
|
||||||
} else {
|
} else {
|
||||||
rg.timestampFinished = TalerPreciseTimestamp.now();
|
rg.timestampFinished = TalerPreciseTimestamp.now();
|
||||||
rg.operationStatus = RefreshOperationStatus.Finished;
|
rg.operationStatus = RefreshOperationStatus.Finished;
|
||||||
}
|
}
|
||||||
|
return { final: true };
|
||||||
}
|
}
|
||||||
|
return { final: false };
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -248,22 +252,30 @@ async function refreshCreateSession(
|
|||||||
ws.config.testing.denomselAllowLate,
|
ws.config.testing.denomselAllowLate,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const transactionId = constructTransactionIdentifier({
|
||||||
|
tag: TransactionType.Refresh,
|
||||||
|
refreshGroupId,
|
||||||
|
});
|
||||||
|
|
||||||
if (newCoinDenoms.selectedDenoms.length === 0) {
|
if (newCoinDenoms.selectedDenoms.length === 0) {
|
||||||
logger.trace(
|
logger.trace(
|
||||||
`not refreshing, available amount ${amountToPretty(
|
`not refreshing, available amount ${amountToPretty(
|
||||||
availableAmount,
|
availableAmount,
|
||||||
)} too small`,
|
)} too small`,
|
||||||
);
|
);
|
||||||
|
// FIXME: State transition notification missing.
|
||||||
await ws.db
|
await ws.db
|
||||||
.mktx((x) => [x.coins, x.refreshGroups])
|
.mktx((x) => [x.coins, x.coinAvailability, x.refreshGroups])
|
||||||
.runReadWrite(async (tx) => {
|
.runReadWrite(async (tx) => {
|
||||||
const rg = await tx.refreshGroups.get(refreshGroupId);
|
const rg = await tx.refreshGroups.get(refreshGroupId);
|
||||||
if (!rg) {
|
if (!rg) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
rg.statusPerCoin[coinIndex] = RefreshCoinStatus.Finished;
|
rg.statusPerCoin[coinIndex] = RefreshCoinStatus.Finished;
|
||||||
updateGroupStatus(rg);
|
const updateRes = updateGroupStatus(rg);
|
||||||
|
if (updateRes.final) {
|
||||||
|
await makeCoinsVisible(ws, tx, transactionId);
|
||||||
|
}
|
||||||
await tx.refreshGroups.put(rg);
|
await tx.refreshGroups.put(rg);
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
@ -418,10 +430,15 @@ async function refreshMelt(
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const transactionId = constructTransactionIdentifier({
|
||||||
|
tag: TransactionType.Refresh,
|
||||||
|
refreshGroupId,
|
||||||
|
});
|
||||||
|
|
||||||
if (resp.status === HttpStatusCode.NotFound) {
|
if (resp.status === HttpStatusCode.NotFound) {
|
||||||
const errDetails = await readUnexpectedResponseDetails(resp);
|
const errDetails = await readUnexpectedResponseDetails(resp);
|
||||||
await ws.db
|
await ws.db
|
||||||
.mktx((x) => [x.refreshGroups])
|
.mktx((x) => [x.refreshGroups, x.coins, x.coinAvailability])
|
||||||
.runReadWrite(async (tx) => {
|
.runReadWrite(async (tx) => {
|
||||||
const rg = await tx.refreshGroups.get(refreshGroupId);
|
const rg = await tx.refreshGroups.get(refreshGroupId);
|
||||||
if (!rg) {
|
if (!rg) {
|
||||||
@ -433,9 +450,12 @@ async function refreshMelt(
|
|||||||
if (rg.statusPerCoin[coinIndex] !== RefreshCoinStatus.Pending) {
|
if (rg.statusPerCoin[coinIndex] !== RefreshCoinStatus.Pending) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
rg.statusPerCoin[coinIndex] = RefreshCoinStatus.Frozen;
|
rg.statusPerCoin[coinIndex] = RefreshCoinStatus.Failed;
|
||||||
rg.lastErrorPerCoin[coinIndex] = errDetails;
|
rg.lastErrorPerCoin[coinIndex] = errDetails;
|
||||||
updateGroupStatus(rg);
|
const updateRes = updateGroupStatus(rg);
|
||||||
|
if (updateRes.final) {
|
||||||
|
await makeCoinsVisible(ws, tx, transactionId);
|
||||||
|
}
|
||||||
await tx.refreshGroups.put(rg);
|
await tx.refreshGroups.put(rg);
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
@ -672,6 +692,11 @@ async function refreshReveal(
|
|||||||
|
|
||||||
const coins: CoinRecord[] = [];
|
const coins: CoinRecord[] = [];
|
||||||
|
|
||||||
|
const transactionId = constructTransactionIdentifier({
|
||||||
|
tag: TransactionType.Refresh,
|
||||||
|
refreshGroupId,
|
||||||
|
});
|
||||||
|
|
||||||
for (let i = 0; i < refreshSession.newDenoms.length; i++) {
|
for (let i = 0; i < refreshSession.newDenoms.length; i++) {
|
||||||
const ncd = newCoinDenoms[i];
|
const ncd = newCoinDenoms[i];
|
||||||
for (let j = 0; j < refreshSession.newDenoms[i].count; j++) {
|
for (let j = 0; j < refreshSession.newDenoms[i].count; j++) {
|
||||||
@ -701,6 +726,7 @@ async function refreshReveal(
|
|||||||
refreshGroupId,
|
refreshGroupId,
|
||||||
oldCoinPub: refreshGroup.oldCoinPubs[coinIndex],
|
oldCoinPub: refreshGroup.oldCoinPubs[coinIndex],
|
||||||
},
|
},
|
||||||
|
sourceTransactionId: transactionId,
|
||||||
coinEvHash: pc.coinEvHash,
|
coinEvHash: pc.coinEvHash,
|
||||||
maxAge: pc.maxAge,
|
maxAge: pc.maxAge,
|
||||||
ageCommitmentProof: pc.ageCommitmentProof,
|
ageCommitmentProof: pc.ageCommitmentProof,
|
||||||
|
@ -57,7 +57,7 @@ import {
|
|||||||
readSuccessResponseJsonOrThrow,
|
readSuccessResponseJsonOrThrow,
|
||||||
} from "@gnu-taler/taler-util/http";
|
} from "@gnu-taler/taler-util/http";
|
||||||
import { checkDbInvariant, checkLogicInvariant } from "../util/invariants.js";
|
import { checkDbInvariant, checkLogicInvariant } from "../util/invariants.js";
|
||||||
import { constructTaskIdentifier, makeCoinAvailable, OperationAttemptResult, OperationAttemptResultType } from "./common.js";
|
import { constructTaskIdentifier, makeCoinAvailable, makeCoinsVisible, OperationAttemptResult, OperationAttemptResultType } from "./common.js";
|
||||||
import { updateExchangeFromUrl } from "./exchanges.js";
|
import { updateExchangeFromUrl } from "./exchanges.js";
|
||||||
import {
|
import {
|
||||||
getCandidateWithdrawalDenoms,
|
getCandidateWithdrawalDenoms,
|
||||||
@ -387,6 +387,7 @@ export async function processTip(
|
|||||||
coinIndex: i,
|
coinIndex: i,
|
||||||
walletTipId: walletTipId,
|
walletTipId: walletTipId,
|
||||||
},
|
},
|
||||||
|
sourceTransactionId: transactionId,
|
||||||
denomPubHash: denom.denomPubHash,
|
denomPubHash: denom.denomPubHash,
|
||||||
denomSig: { cipher: DenomKeyType.Rsa, rsa_signature: denomSigRsa.sig },
|
denomSig: { cipher: DenomKeyType.Rsa, rsa_signature: denomSigRsa.sig },
|
||||||
exchangeBaseUrl: tipRecord.exchangeBaseUrl,
|
exchangeBaseUrl: tipRecord.exchangeBaseUrl,
|
||||||
@ -416,6 +417,7 @@ export async function processTip(
|
|||||||
for (const cr of newCoinRecords) {
|
for (const cr of newCoinRecords) {
|
||||||
await makeCoinAvailable(ws, tx, cr);
|
await makeCoinAvailable(ws, tx, cr);
|
||||||
}
|
}
|
||||||
|
await makeCoinsVisible(ws, tx, transactionId);
|
||||||
return { oldTxState, newTxState };
|
return { oldTxState, newTxState };
|
||||||
});
|
});
|
||||||
notifyTransition(ws, transactionId, transitionInfo);
|
notifyTransition(ws, transactionId, transitionInfo);
|
||||||
|
@ -97,6 +97,7 @@ import {
|
|||||||
TaskIdentifiers,
|
TaskIdentifiers,
|
||||||
constructTaskIdentifier,
|
constructTaskIdentifier,
|
||||||
makeCoinAvailable,
|
makeCoinAvailable,
|
||||||
|
makeCoinsVisible,
|
||||||
makeExchangeListItem,
|
makeExchangeListItem,
|
||||||
runLongpollAsync,
|
runLongpollAsync,
|
||||||
} from "../operations/common.js";
|
} from "../operations/common.js";
|
||||||
@ -1029,6 +1030,11 @@ async function processPlanchetVerifyAndStoreCoin(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const transactionId = constructTransactionIdentifier({
|
||||||
|
tag: TransactionType.Withdrawal,
|
||||||
|
withdrawalGroupId: wgContext.wgRecord.withdrawalGroupId,
|
||||||
|
});
|
||||||
|
|
||||||
const { planchet, denomInfo } = d;
|
const { planchet, denomInfo } = d;
|
||||||
|
|
||||||
const planchetDenomPub = denomInfo.denomPub;
|
const planchetDenomPub = denomInfo.denomPub;
|
||||||
@ -1099,6 +1105,7 @@ async function processPlanchetVerifyAndStoreCoin(
|
|||||||
reservePub: withdrawalGroup.reservePub,
|
reservePub: withdrawalGroup.reservePub,
|
||||||
withdrawalGroupId: withdrawalGroup.withdrawalGroupId,
|
withdrawalGroupId: withdrawalGroup.withdrawalGroupId,
|
||||||
},
|
},
|
||||||
|
sourceTransactionId: transactionId,
|
||||||
maxAge: withdrawalGroup.restrictAge ?? AgeRestriction.AGE_UNRESTRICTED,
|
maxAge: withdrawalGroup.restrictAge ?? AgeRestriction.AGE_UNRESTRICTED,
|
||||||
ageCommitmentProof: planchet.ageCommitmentProof,
|
ageCommitmentProof: planchet.ageCommitmentProof,
|
||||||
spendAllocation: undefined,
|
spendAllocation: undefined,
|
||||||
@ -1111,7 +1118,7 @@ async function processPlanchetVerifyAndStoreCoin(
|
|||||||
// Check if this is the first time that the whole
|
// Check if this is the first time that the whole
|
||||||
// withdrawal succeeded. If so, mark the withdrawal
|
// withdrawal succeeded. If so, mark the withdrawal
|
||||||
// group as finished.
|
// group as finished.
|
||||||
const firstSuccess = await ws.db
|
const success = await ws.db
|
||||||
.mktx((x) => [
|
.mktx((x) => [
|
||||||
x.coins,
|
x.coins,
|
||||||
x.denominations,
|
x.denominations,
|
||||||
@ -1130,7 +1137,9 @@ async function processPlanchetVerifyAndStoreCoin(
|
|||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
|
|
||||||
ws.notify({ type: NotificationType.BalanceChange });
|
if (success) {
|
||||||
|
ws.notify({ type: NotificationType.BalanceChange });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -1495,10 +1504,7 @@ async function processWithdrawalGroupPendingReady(
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
notifyTransition(ws, transactionId, transitionInfo);
|
notifyTransition(ws, transactionId, transitionInfo);
|
||||||
return {
|
return OperationAttemptResult.finishedEmpty();
|
||||||
type: OperationAttemptResultType.Finished,
|
|
||||||
result: undefined,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const numTotalCoins = withdrawalGroup.denomsSel.selectedDenoms
|
const numTotalCoins = withdrawalGroup.denomsSel.selectedDenoms
|
||||||
@ -1563,7 +1569,7 @@ async function processWithdrawalGroupPendingReady(
|
|||||||
const maxReportedErrors = 5;
|
const maxReportedErrors = 5;
|
||||||
|
|
||||||
const res = await ws.db
|
const res = await ws.db
|
||||||
.mktx((x) => [x.coins, x.withdrawalGroups, x.planchets])
|
.mktx((x) => [x.coins, x.coinAvailability, x.withdrawalGroups, x.planchets])
|
||||||
.runReadWrite(async (tx) => {
|
.runReadWrite(async (tx) => {
|
||||||
const wg = await tx.withdrawalGroups.get(withdrawalGroupId);
|
const wg = await tx.withdrawalGroups.get(withdrawalGroupId);
|
||||||
if (!wg) {
|
if (!wg) {
|
||||||
@ -1588,6 +1594,7 @@ async function processWithdrawalGroupPendingReady(
|
|||||||
if (wg.timestampFinish === undefined && numFinished === numTotalCoins) {
|
if (wg.timestampFinish === undefined && numFinished === numTotalCoins) {
|
||||||
wg.timestampFinish = TalerPreciseTimestamp.now();
|
wg.timestampFinish = TalerPreciseTimestamp.now();
|
||||||
wg.status = WithdrawalGroupStatus.Finished;
|
wg.status = WithdrawalGroupStatus.Finished;
|
||||||
|
await makeCoinsVisible(ws, tx, transactionId);
|
||||||
}
|
}
|
||||||
|
|
||||||
const newTxState = computeWithdrawalTransactionStatus(wg);
|
const newTxState = computeWithdrawalTransactionStatus(wg);
|
||||||
|
@ -31,7 +31,6 @@ import {
|
|||||||
AmountJson,
|
AmountJson,
|
||||||
AmountResponse,
|
AmountResponse,
|
||||||
Amounts,
|
Amounts,
|
||||||
AmountString,
|
|
||||||
CoinStatus,
|
CoinStatus,
|
||||||
ConvertAmountRequest,
|
ConvertAmountRequest,
|
||||||
DenominationInfo,
|
DenominationInfo,
|
||||||
@ -42,7 +41,6 @@ import {
|
|||||||
ForcedDenomSel,
|
ForcedDenomSel,
|
||||||
GetAmountRequest,
|
GetAmountRequest,
|
||||||
GetPlanForOperationRequest,
|
GetPlanForOperationRequest,
|
||||||
GetPlanForOperationResponse,
|
|
||||||
j2s,
|
j2s,
|
||||||
Logger,
|
Logger,
|
||||||
parsePaytoUri,
|
parsePaytoUri,
|
||||||
|
Loading…
Reference in New Issue
Block a user