don't store reserve history anymore, adjust withdrawal implementation accordingly
This commit is contained in:
parent
c09c5bbe62
commit
bafb52edff
@ -206,6 +206,7 @@ export class CryptoImplementation {
|
|||||||
const tipPlanchet: DerivedTipPlanchet = {
|
const tipPlanchet: DerivedTipPlanchet = {
|
||||||
blindingKey: encodeCrock(blindingFactor),
|
blindingKey: encodeCrock(blindingFactor),
|
||||||
coinEv: encodeCrock(ev),
|
coinEv: encodeCrock(ev),
|
||||||
|
coinEvHash: encodeCrock(hash(ev)),
|
||||||
coinPriv: encodeCrock(fc.coinPriv),
|
coinPriv: encodeCrock(fc.coinPriv),
|
||||||
coinPub: encodeCrock(fc.coinPub),
|
coinPub: encodeCrock(fc.coinPub),
|
||||||
};
|
};
|
||||||
@ -463,6 +464,7 @@ export class CryptoImplementation {
|
|||||||
coinEv: encodeCrock(ev),
|
coinEv: encodeCrock(ev),
|
||||||
privateKey: encodeCrock(coinPriv),
|
privateKey: encodeCrock(coinPriv),
|
||||||
publicKey: encodeCrock(coinPub),
|
publicKey: encodeCrock(coinPub),
|
||||||
|
coinEvHash: encodeCrock(hash(ev)),
|
||||||
};
|
};
|
||||||
planchets.push(planchet);
|
planchets.push(planchet);
|
||||||
|
|
||||||
|
@ -63,5 +63,5 @@ export * from "./util/time";
|
|||||||
export * from "./types/talerTypes";
|
export * from "./types/talerTypes";
|
||||||
export * from "./types/walletTypes";
|
export * from "./types/walletTypes";
|
||||||
export * from "./types/notifications";
|
export * from "./types/notifications";
|
||||||
export * from "./types/transactions";
|
export * from "./types/transactionsTypes";
|
||||||
export * from "./types/pending";
|
export * from "./types/pendingTypes";
|
||||||
|
@ -44,6 +44,7 @@ import {
|
|||||||
BackupRefundState,
|
BackupRefundState,
|
||||||
BackupReserve,
|
BackupReserve,
|
||||||
BackupTip,
|
BackupTip,
|
||||||
|
BackupWithdrawalGroup,
|
||||||
WalletBackupContentV1,
|
WalletBackupContentV1,
|
||||||
} from "../types/backupTypes";
|
} from "../types/backupTypes";
|
||||||
import { TransactionHandle } from "../util/query";
|
import { TransactionHandle } from "../util/query";
|
||||||
@ -172,6 +173,7 @@ export async function exportBackup(
|
|||||||
Stores.tips,
|
Stores.tips,
|
||||||
Stores.recoupGroups,
|
Stores.recoupGroups,
|
||||||
Stores.reserves,
|
Stores.reserves,
|
||||||
|
Stores.withdrawalGroups,
|
||||||
],
|
],
|
||||||
async (tx) => {
|
async (tx) => {
|
||||||
const bs = await getWalletBackupState(ws, tx);
|
const bs = await getWalletBackupState(ws, tx);
|
||||||
@ -188,9 +190,46 @@ export async function exportBackup(
|
|||||||
const backupBackupProviders: BackupBackupProvider[] = [];
|
const backupBackupProviders: BackupBackupProvider[] = [];
|
||||||
const backupTips: BackupTip[] = [];
|
const backupTips: BackupTip[] = [];
|
||||||
const backupRecoupGroups: BackupRecoupGroup[] = [];
|
const backupRecoupGroups: BackupRecoupGroup[] = [];
|
||||||
|
const withdrawalGroupsByReserve: {
|
||||||
|
[reservePub: string]: BackupWithdrawalGroup[];
|
||||||
|
} = {};
|
||||||
|
|
||||||
|
await tx.iter(Stores.withdrawalGroups).forEachAsync(async (wg) => {
|
||||||
|
const withdrawalGroups = (withdrawalGroupsByReserve[
|
||||||
|
wg.reservePub
|
||||||
|
] ??= []);
|
||||||
|
// FIXME: finish!
|
||||||
|
// withdrawalGroups.push({
|
||||||
|
// raw_withdrawal_amount: Amounts.stringify(wg.rawWithdrawalAmount),
|
||||||
|
// selected_denoms: wg.denomsSel.selectedDenoms.map((x) => ({
|
||||||
|
// count: x.count,
|
||||||
|
// denom_pub_hash: x.denomPubHash,
|
||||||
|
// })),
|
||||||
|
// timestamp_start: wg.timestampStart,
|
||||||
|
// timestamp_finish: wg.timestampFinish,
|
||||||
|
// withdrawal_group_id: wg.withdrawalGroupId,
|
||||||
|
// });
|
||||||
|
});
|
||||||
|
|
||||||
await tx.iter(Stores.reserves).forEach((reserve) => {
|
await tx.iter(Stores.reserves).forEach((reserve) => {
|
||||||
// FIXME: implement
|
const backupReserve: BackupReserve = {
|
||||||
|
initial_selected_denoms: reserve.initialDenomSel.selectedDenoms.map(
|
||||||
|
(x) => ({
|
||||||
|
count: x.count,
|
||||||
|
denom_pub_hash: x.denomPubHash,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
initial_withdrawal_group_id: reserve.initialWithdrawalGroupId,
|
||||||
|
instructed_amount: Amounts.stringify(reserve.instructedAmount),
|
||||||
|
reserve_priv: reserve.reservePriv,
|
||||||
|
timestamp_created: reserve.timestampCreated,
|
||||||
|
withdrawal_groups:
|
||||||
|
withdrawalGroupsByReserve[reserve.reservePub] ?? [],
|
||||||
|
};
|
||||||
|
const backupReserves = (backupReservesByExchange[
|
||||||
|
reserve.exchangeBaseUrl
|
||||||
|
] ??= []);
|
||||||
|
backupReserves.push(backupReserve);
|
||||||
});
|
});
|
||||||
|
|
||||||
await tx.iter(Stores.tips).forEach((tip) => {
|
await tx.iter(Stores.tips).forEach((tip) => {
|
||||||
|
@ -58,7 +58,6 @@ import {
|
|||||||
} from "../util/http";
|
} from "../util/http";
|
||||||
import { Logger } from "../util/logging";
|
import { Logger } from "../util/logging";
|
||||||
import { URL } from "../util/url";
|
import { URL } from "../util/url";
|
||||||
import { reconcileReserveHistory } from "../util/reserveHistoryUtil";
|
|
||||||
import { checkDbInvariant } from "../util/invariants";
|
import { checkDbInvariant } from "../util/invariants";
|
||||||
import { NotificationType } from "../types/notifications";
|
import { NotificationType } from "../types/notifications";
|
||||||
import { updateRetryInfoTimeout, initRetryInfo } from "../util/retries";
|
import { updateRetryInfoTimeout, initRetryInfo } from "../util/retries";
|
||||||
|
@ -460,6 +460,8 @@ async function recordConfirmPay(
|
|||||||
paymentSubmitPending: true,
|
paymentSubmitPending: true,
|
||||||
refunds: {},
|
refunds: {},
|
||||||
merchantPaySig: undefined,
|
merchantPaySig: undefined,
|
||||||
|
noncePriv: proposal.noncePriv,
|
||||||
|
noncePub: proposal.noncePub,
|
||||||
};
|
};
|
||||||
|
|
||||||
await ws.db.runWithWriteTransaction(
|
await ws.db.runWithWriteTransaction(
|
||||||
|
@ -29,7 +29,7 @@ import {
|
|||||||
PendingOperationType,
|
PendingOperationType,
|
||||||
ExchangeUpdateOperationStage,
|
ExchangeUpdateOperationStage,
|
||||||
ReserveType,
|
ReserveType,
|
||||||
} from "../types/pending";
|
} from "../types/pendingTypes";
|
||||||
import {
|
import {
|
||||||
Duration,
|
Duration,
|
||||||
getTimestampNow,
|
getTimestampNow,
|
||||||
@ -189,7 +189,6 @@ async function gatherReservePending(
|
|||||||
// nothing to report as pending
|
// nothing to report as pending
|
||||||
break;
|
break;
|
||||||
case ReserveRecordStatus.WAIT_CONFIRM_BANK:
|
case ReserveRecordStatus.WAIT_CONFIRM_BANK:
|
||||||
case ReserveRecordStatus.WITHDRAWING:
|
|
||||||
case ReserveRecordStatus.QUERYING_STATUS:
|
case ReserveRecordStatus.QUERYING_STATUS:
|
||||||
case ReserveRecordStatus.REGISTERING_BANK:
|
case ReserveRecordStatus.REGISTERING_BANK:
|
||||||
resp.nextRetryDelay = updateRetryDelay(
|
resp.nextRetryDelay = updateRetryDelay(
|
||||||
|
@ -29,7 +29,7 @@ import { amountToPretty } from "../util/helpers";
|
|||||||
import { TransactionHandle } from "../util/query";
|
import { TransactionHandle } from "../util/query";
|
||||||
import { InternalWalletState, EXCHANGE_COINS_LOCK } from "./state";
|
import { InternalWalletState, EXCHANGE_COINS_LOCK } from "./state";
|
||||||
import { Logger } from "../util/logging";
|
import { Logger } from "../util/logging";
|
||||||
import { getWithdrawDenomList, isWithdrawableDenom } from "./withdraw";
|
import { selectWithdrawalDenominations, isWithdrawableDenom } from "./withdraw";
|
||||||
import { updateExchangeFromUrl } from "./exchanges";
|
import { updateExchangeFromUrl } from "./exchanges";
|
||||||
import {
|
import {
|
||||||
TalerErrorDetails,
|
TalerErrorDetails,
|
||||||
@ -83,7 +83,7 @@ export function getTotalRefreshCost(
|
|||||||
): AmountJson {
|
): AmountJson {
|
||||||
const withdrawAmount = Amounts.sub(amountLeft, refreshedDenom.feeRefresh)
|
const withdrawAmount = Amounts.sub(amountLeft, refreshedDenom.feeRefresh)
|
||||||
.amount;
|
.amount;
|
||||||
const withdrawDenoms = getWithdrawDenomList(withdrawAmount, denoms);
|
const withdrawDenoms = selectWithdrawalDenominations(withdrawAmount, denoms);
|
||||||
const resultingAmount = Amounts.add(
|
const resultingAmount = Amounts.add(
|
||||||
Amounts.getZero(withdrawAmount.currency),
|
Amounts.getZero(withdrawAmount.currency),
|
||||||
...withdrawDenoms.selectedDenoms.map(
|
...withdrawDenoms.selectedDenoms.map(
|
||||||
@ -150,7 +150,7 @@ async function refreshCreateSession(
|
|||||||
oldDenom.feeRefresh,
|
oldDenom.feeRefresh,
|
||||||
).amount;
|
).amount;
|
||||||
|
|
||||||
const newCoinDenoms = getWithdrawDenomList(availableAmount, availableDenoms);
|
const newCoinDenoms = selectWithdrawalDenominations(availableAmount, availableDenoms);
|
||||||
|
|
||||||
if (newCoinDenoms.selectedDenoms.length === 0) {
|
if (newCoinDenoms.selectedDenoms.length === 0) {
|
||||||
logger.trace(
|
logger.trace(
|
||||||
@ -478,6 +478,7 @@ async function refreshReveal(
|
|||||||
oldCoinPub: refreshGroup.oldCoinPubs[coinIndex],
|
oldCoinPub: refreshGroup.oldCoinPubs[coinIndex],
|
||||||
},
|
},
|
||||||
suspended: false,
|
suspended: false,
|
||||||
|
coinEvHash: pc.coinEv,
|
||||||
};
|
};
|
||||||
|
|
||||||
coins.push(coin);
|
coins.push(coin);
|
||||||
|
@ -28,8 +28,6 @@ import {
|
|||||||
CurrencyRecord,
|
CurrencyRecord,
|
||||||
Stores,
|
Stores,
|
||||||
WithdrawalGroupRecord,
|
WithdrawalGroupRecord,
|
||||||
WalletReserveHistoryItemType,
|
|
||||||
ReserveHistoryRecord,
|
|
||||||
ReserveBankInfo,
|
ReserveBankInfo,
|
||||||
} from "../types/dbTypes";
|
} from "../types/dbTypes";
|
||||||
import { Logger } from "../util/logging";
|
import { Logger } from "../util/logging";
|
||||||
@ -47,10 +45,12 @@ import { assertUnreachable } from "../util/assertUnreachable";
|
|||||||
import { encodeCrock, getRandomBytes } from "../crypto/talerCrypto";
|
import { encodeCrock, getRandomBytes } from "../crypto/talerCrypto";
|
||||||
import { randomBytes } from "../crypto/primitives/nacl-fast";
|
import { randomBytes } from "../crypto/primitives/nacl-fast";
|
||||||
import {
|
import {
|
||||||
selectWithdrawalDenoms,
|
|
||||||
processWithdrawGroup,
|
processWithdrawGroup,
|
||||||
getBankWithdrawalInfo,
|
getBankWithdrawalInfo,
|
||||||
denomSelectionInfoToState,
|
denomSelectionInfoToState,
|
||||||
|
updateWithdrawalDenoms,
|
||||||
|
selectWithdrawalDenominations,
|
||||||
|
getPossibleWithdrawalDenoms,
|
||||||
} from "./withdraw";
|
} from "./withdraw";
|
||||||
import {
|
import {
|
||||||
guardOperationException,
|
guardOperationException,
|
||||||
@ -66,11 +66,6 @@ import {
|
|||||||
durationMin,
|
durationMin,
|
||||||
durationMax,
|
durationMax,
|
||||||
} from "../util/time";
|
} from "../util/time";
|
||||||
import {
|
|
||||||
reconcileReserveHistory,
|
|
||||||
summarizeReserveHistory,
|
|
||||||
ReserveHistorySummary,
|
|
||||||
} from "../util/reserveHistoryUtil";
|
|
||||||
import { TransactionHandle } from "../util/query";
|
import { TransactionHandle } from "../util/query";
|
||||||
import { addPaytoQueryParams } from "../util/payto";
|
import { addPaytoQueryParams } from "../util/payto";
|
||||||
import { TalerErrorCode } from "../TalerErrorCode";
|
import { TalerErrorCode } from "../TalerErrorCode";
|
||||||
@ -86,6 +81,7 @@ import {
|
|||||||
getRetryDuration,
|
getRetryDuration,
|
||||||
updateRetryInfoTimeout,
|
updateRetryInfoTimeout,
|
||||||
} from "../util/retries";
|
} from "../util/retries";
|
||||||
|
import { ReserveTransactionType } from "../types/ReserveTransaction";
|
||||||
|
|
||||||
const logger = new Logger("reserves.ts");
|
const logger = new Logger("reserves.ts");
|
||||||
|
|
||||||
@ -138,11 +134,9 @@ export async function createReserve(
|
|||||||
|
|
||||||
const initialWithdrawalGroupId = encodeCrock(getRandomBytes(32));
|
const initialWithdrawalGroupId = encodeCrock(getRandomBytes(32));
|
||||||
|
|
||||||
const denomSelInfo = await selectWithdrawalDenoms(
|
await updateWithdrawalDenoms(ws, canonExchange);
|
||||||
ws,
|
const denoms = await getPossibleWithdrawalDenoms(ws, canonExchange);
|
||||||
canonExchange,
|
const denomSelInfo = selectWithdrawalDenominations(req.amount, denoms);
|
||||||
req.amount,
|
|
||||||
);
|
|
||||||
const initialDenomSel = denomSelectionInfoToState(denomSelInfo);
|
const initialDenomSel = denomSelectionInfoToState(denomSelInfo);
|
||||||
|
|
||||||
const reserveRecord: ReserveRecord = {
|
const reserveRecord: ReserveRecord = {
|
||||||
@ -166,16 +160,6 @@ export async function createReserve(
|
|||||||
requestedQuery: false,
|
requestedQuery: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
const reserveHistoryRecord: ReserveHistoryRecord = {
|
|
||||||
reservePub: keypair.pub,
|
|
||||||
reserveTransactions: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
reserveHistoryRecord.reserveTransactions.push({
|
|
||||||
type: WalletReserveHistoryItemType.Credit,
|
|
||||||
expectedAmount: req.amount,
|
|
||||||
});
|
|
||||||
|
|
||||||
const exchangeInfo = await updateExchangeFromUrl(ws, req.exchange);
|
const exchangeInfo = await updateExchangeFromUrl(ws, req.exchange);
|
||||||
const exchangeDetails = exchangeInfo.details;
|
const exchangeDetails = exchangeInfo.details;
|
||||||
if (!exchangeDetails) {
|
if (!exchangeDetails) {
|
||||||
@ -206,12 +190,7 @@ export async function createReserve(
|
|||||||
const cr: CurrencyRecord = currencyRecord;
|
const cr: CurrencyRecord = currencyRecord;
|
||||||
|
|
||||||
const resp = await ws.db.runWithWriteTransaction(
|
const resp = await ws.db.runWithWriteTransaction(
|
||||||
[
|
[Stores.currencies, Stores.reserves, Stores.bankWithdrawUris],
|
||||||
Stores.currencies,
|
|
||||||
Stores.reserves,
|
|
||||||
Stores.reserveHistory,
|
|
||||||
Stores.bankWithdrawUris,
|
|
||||||
],
|
|
||||||
async (tx) => {
|
async (tx) => {
|
||||||
// Check if we have already created a reserve for that bankWithdrawStatusUrl
|
// Check if we have already created a reserve for that bankWithdrawStatusUrl
|
||||||
if (reserveRecord.bankInfo?.statusUrl) {
|
if (reserveRecord.bankInfo?.statusUrl) {
|
||||||
@ -238,7 +217,6 @@ export async function createReserve(
|
|||||||
}
|
}
|
||||||
await tx.put(Stores.currencies, cr);
|
await tx.put(Stores.currencies, cr);
|
||||||
await tx.put(Stores.reserves, reserveRecord);
|
await tx.put(Stores.reserves, reserveRecord);
|
||||||
await tx.put(Stores.reserveHistory, reserveHistoryRecord);
|
|
||||||
const r: CreateReserveResponse = {
|
const r: CreateReserveResponse = {
|
||||||
exchange: canonExchange,
|
exchange: canonExchange,
|
||||||
reservePub: keypair.pub,
|
reservePub: keypair.pub,
|
||||||
@ -499,6 +477,10 @@ async function incrementReserveRetry(
|
|||||||
/**
|
/**
|
||||||
* Update the information about a reserve that is stored in the wallet
|
* Update the information about a reserve that is stored in the wallet
|
||||||
* by quering the reserve's exchange.
|
* by quering the reserve's exchange.
|
||||||
|
*
|
||||||
|
* If the reserve have funds that are not allocated in a withdrawal group yet
|
||||||
|
* and are big enough to withdraw with available denominations,
|
||||||
|
* create a new withdrawal group for the remaining amount.
|
||||||
*/
|
*/
|
||||||
async function updateReserve(
|
async function updateReserve(
|
||||||
ws: InternalWalletState,
|
ws: InternalWalletState,
|
||||||
@ -542,78 +524,130 @@ async function updateReserve(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const reserveInfo = result.response;
|
const reserveInfo = result.response;
|
||||||
|
|
||||||
const balance = Amounts.parseOrThrow(reserveInfo.balance);
|
const balance = Amounts.parseOrThrow(reserveInfo.balance);
|
||||||
const currency = balance.currency;
|
const currency = balance.currency;
|
||||||
let updateSummary: ReserveHistorySummary | undefined;
|
|
||||||
await ws.db.runWithWriteTransaction(
|
await updateWithdrawalDenoms(ws, reserve.exchangeBaseUrl);
|
||||||
[Stores.reserves, Stores.reserveHistory],
|
const denoms = await getPossibleWithdrawalDenoms(ws, reserve.exchangeBaseUrl);
|
||||||
|
|
||||||
|
const newWithdrawalGroup = await ws.db.runWithWriteTransaction(
|
||||||
|
[Stores.coins, Stores.planchets, Stores.withdrawalGroups, Stores.reserves],
|
||||||
async (tx) => {
|
async (tx) => {
|
||||||
const r = await tx.get(Stores.reserves, reservePub);
|
const newReserve = await tx.get(Stores.reserves, reserve.reservePub);
|
||||||
if (!r) {
|
if (!newReserve) {
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (r.reserveStatus !== ReserveRecordStatus.QUERYING_STATUS) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
let amountReservePlus = Amounts.getZero(currency);
|
||||||
|
let amountReserveMinus = Amounts.getZero(currency);
|
||||||
|
|
||||||
const hist = await tx.get(Stores.reserveHistory, reservePub);
|
// Subtract withdrawal groups for this reserve from the available amount.
|
||||||
if (!hist) {
|
await tx
|
||||||
throw Error("inconsistent database");
|
.iterIndexed(Stores.withdrawalGroups.byReservePub, reservePub)
|
||||||
}
|
.forEach((wg) => {
|
||||||
|
const cost = wg.denomsSel.totalWithdrawCost;
|
||||||
|
amountReserveMinus = Amounts.add(amountReserveMinus, cost).amount;
|
||||||
|
});
|
||||||
|
|
||||||
const newHistoryTransactions = reserveInfo.history.slice(
|
for (const entry of reserveInfo.history) {
|
||||||
hist.reserveTransactions.length,
|
switch (entry.type) {
|
||||||
);
|
case ReserveTransactionType.Credit:
|
||||||
|
amountReservePlus = Amounts.add(
|
||||||
const reserveUpdateId = encodeCrock(getRandomBytes(32));
|
amountReservePlus,
|
||||||
|
Amounts.parseOrThrow(entry.amount),
|
||||||
const reconciled = reconcileReserveHistory(
|
).amount;
|
||||||
hist.reserveTransactions,
|
break;
|
||||||
reserveInfo.history,
|
case ReserveTransactionType.Recoup:
|
||||||
);
|
amountReservePlus = Amounts.add(
|
||||||
|
amountReservePlus,
|
||||||
updateSummary = summarizeReserveHistory(
|
Amounts.parseOrThrow(entry.amount),
|
||||||
reconciled.updatedLocalHistory,
|
).amount;
|
||||||
currency,
|
break;
|
||||||
);
|
case ReserveTransactionType.Closing:
|
||||||
|
amountReserveMinus = Amounts.add(
|
||||||
if (
|
amountReserveMinus,
|
||||||
reconciled.newAddedItems.length + reconciled.newMatchedItems.length !=
|
Amounts.parseOrThrow(entry.amount),
|
||||||
0
|
).amount;
|
||||||
) {
|
break;
|
||||||
logger.trace("setting reserve status to 'withdrawing' after query");
|
case ReserveTransactionType.Withdraw: {
|
||||||
r.reserveStatus = ReserveRecordStatus.WITHDRAWING;
|
// Now we check if the withdrawal transaction
|
||||||
r.retryInfo = initRetryInfo();
|
// is part of any withdrawal known to this wallet.
|
||||||
r.requestedQuery = false;
|
const planchet = await tx.getIndexed(
|
||||||
} else {
|
Stores.planchets.coinEvHashIndex,
|
||||||
if (r.requestedQuery) {
|
entry.h_coin_envelope,
|
||||||
logger.trace(
|
);
|
||||||
"setting reserve status to 'querying-status' (requested query) after query",
|
if (planchet) {
|
||||||
);
|
// Amount is already accounted in some withdrawal session
|
||||||
r.reserveStatus = ReserveRecordStatus.QUERYING_STATUS;
|
break;
|
||||||
r.requestedQuery = false;
|
}
|
||||||
r.retryInfo = initRetryInfo();
|
const coin = await tx.getIndexed(
|
||||||
} else {
|
Stores.coins.coinEvHashIndex,
|
||||||
logger.trace("setting reserve status to 'dormant' after query");
|
entry.h_coin_envelope,
|
||||||
r.reserveStatus = ReserveRecordStatus.DORMANT;
|
);
|
||||||
r.retryInfo = initRetryInfo(false);
|
if (coin) {
|
||||||
|
// Amount is already accounted in some withdrawal session
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// Amount has been claimed by some withdrawal we don't know about
|
||||||
|
amountReserveMinus = Amounts.add(
|
||||||
|
amountReserveMinus,
|
||||||
|
Amounts.parseOrThrow(entry.amount),
|
||||||
|
).amount;
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
r.lastSuccessfulStatusQuery = getTimestampNow();
|
|
||||||
hist.reserveTransactions = reconciled.updatedLocalHistory;
|
const remainingAmount = Amounts.sub(amountReservePlus, amountReserveMinus)
|
||||||
r.lastError = undefined;
|
.amount;
|
||||||
await tx.put(Stores.reserves, r);
|
const denomSelInfo = selectWithdrawalDenominations(
|
||||||
await tx.put(Stores.reserveHistory, hist);
|
remainingAmount,
|
||||||
|
denoms,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (denomSelInfo.selectedDenoms.length > 0) {
|
||||||
|
let withdrawalGroupId: string;
|
||||||
|
|
||||||
|
if (!newReserve.initialWithdrawalStarted) {
|
||||||
|
withdrawalGroupId = newReserve.initialWithdrawalGroupId;
|
||||||
|
newReserve.initialWithdrawalStarted = true;
|
||||||
|
} else {
|
||||||
|
withdrawalGroupId = encodeCrock(randomBytes(32));
|
||||||
|
}
|
||||||
|
|
||||||
|
const withdrawalRecord: WithdrawalGroupRecord = {
|
||||||
|
withdrawalGroupId: withdrawalGroupId,
|
||||||
|
exchangeBaseUrl: reserve.exchangeBaseUrl,
|
||||||
|
reservePub: reserve.reservePub,
|
||||||
|
rawWithdrawalAmount: remainingAmount,
|
||||||
|
timestampStart: getTimestampNow(),
|
||||||
|
retryInfo: initRetryInfo(),
|
||||||
|
lastError: undefined,
|
||||||
|
denomsSel: denomSelectionInfoToState(denomSelInfo),
|
||||||
|
};
|
||||||
|
|
||||||
|
newReserve.lastError = undefined;
|
||||||
|
newReserve.retryInfo = initRetryInfo(false);
|
||||||
|
newReserve.reserveStatus = ReserveRecordStatus.DORMANT;
|
||||||
|
|
||||||
|
await tx.put(Stores.reserves, newReserve);
|
||||||
|
await tx.put(Stores.withdrawalGroups, withdrawalRecord);
|
||||||
|
return withdrawalRecord;
|
||||||
|
}
|
||||||
|
return;
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
ws.notify({ type: NotificationType.ReserveUpdated, updateSummary });
|
|
||||||
const reserve2 = await ws.db.get(Stores.reserves, reservePub);
|
if (newWithdrawalGroup) {
|
||||||
if (reserve2) {
|
logger.trace("processing new withdraw group");
|
||||||
logger.trace(
|
ws.notify({
|
||||||
`after db transaction, reserve status is ${reserve2.reserveStatus}`,
|
type: NotificationType.WithdrawGroupCreated,
|
||||||
);
|
withdrawalGroupId: newWithdrawalGroup.withdrawalGroupId,
|
||||||
|
});
|
||||||
|
await processWithdrawGroup(ws, newWithdrawalGroup.withdrawalGroupId);
|
||||||
|
} else {
|
||||||
|
console.trace("withdraw session already existed");
|
||||||
}
|
}
|
||||||
|
|
||||||
return { ready: true };
|
return { ready: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -651,9 +685,6 @@ async function processReserveImpl(
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case ReserveRecordStatus.WITHDRAWING:
|
|
||||||
await depleteReserve(ws, reservePub);
|
|
||||||
break;
|
|
||||||
case ReserveRecordStatus.DORMANT:
|
case ReserveRecordStatus.DORMANT:
|
||||||
// nothing to do
|
// nothing to do
|
||||||
break;
|
break;
|
||||||
@ -669,166 +700,6 @@ async function processReserveImpl(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Withdraw coins from a reserve until it is empty.
|
|
||||||
*
|
|
||||||
* When finished, marks the reserve as depleted by setting
|
|
||||||
* the depleted timestamp.
|
|
||||||
*/
|
|
||||||
async function depleteReserve(
|
|
||||||
ws: InternalWalletState,
|
|
||||||
reservePub: string,
|
|
||||||
): Promise<void> {
|
|
||||||
let reserve: ReserveRecord | undefined;
|
|
||||||
let hist: ReserveHistoryRecord | undefined;
|
|
||||||
await ws.db.runWithReadTransaction(
|
|
||||||
[Stores.reserves, Stores.reserveHistory],
|
|
||||||
async (tx) => {
|
|
||||||
reserve = await tx.get(Stores.reserves, reservePub);
|
|
||||||
hist = await tx.get(Stores.reserveHistory, reservePub);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!reserve) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!hist) {
|
|
||||||
throw Error("inconsistent database");
|
|
||||||
}
|
|
||||||
if (reserve.reserveStatus !== ReserveRecordStatus.WITHDRAWING) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
logger.trace(`depleting reserve ${reservePub}`);
|
|
||||||
|
|
||||||
const summary = summarizeReserveHistory(
|
|
||||||
hist.reserveTransactions,
|
|
||||||
reserve.currency,
|
|
||||||
);
|
|
||||||
|
|
||||||
const withdrawAmount = summary.unclaimedReserveAmount;
|
|
||||||
|
|
||||||
const denomsForWithdraw = await selectWithdrawalDenoms(
|
|
||||||
ws,
|
|
||||||
reserve.exchangeBaseUrl,
|
|
||||||
withdrawAmount,
|
|
||||||
);
|
|
||||||
if (!denomsForWithdraw) {
|
|
||||||
// Only complain about inability to withdraw if we
|
|
||||||
// didn't withdraw before.
|
|
||||||
if (Amounts.isZero(summary.withdrawnAmount)) {
|
|
||||||
const opErr = makeErrorDetails(
|
|
||||||
TalerErrorCode.WALLET_EXCHANGE_DENOMINATIONS_INSUFFICIENT,
|
|
||||||
`Unable to withdraw from reserve, no denominations are available to withdraw.`,
|
|
||||||
{},
|
|
||||||
);
|
|
||||||
await incrementReserveRetry(ws, reserve.reservePub, opErr);
|
|
||||||
throw new OperationFailedAndReportedError(opErr);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.trace(
|
|
||||||
`Selected coins total cost ${Amounts.stringify(
|
|
||||||
denomsForWithdraw.totalWithdrawCost,
|
|
||||||
)} for withdrawal of ${Amounts.stringify(withdrawAmount)}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
logger.trace("selected denominations");
|
|
||||||
|
|
||||||
const newWithdrawalGroup = await ws.db.runWithWriteTransaction(
|
|
||||||
[
|
|
||||||
Stores.withdrawalGroups,
|
|
||||||
Stores.reserves,
|
|
||||||
Stores.reserveHistory,
|
|
||||||
Stores.planchets,
|
|
||||||
],
|
|
||||||
async (tx) => {
|
|
||||||
const newReserve = await tx.get(Stores.reserves, reservePub);
|
|
||||||
if (!newReserve) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (newReserve.reserveStatus !== ReserveRecordStatus.WITHDRAWING) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const newHist = await tx.get(Stores.reserveHistory, reservePub);
|
|
||||||
if (!newHist) {
|
|
||||||
throw Error("inconsistent database");
|
|
||||||
}
|
|
||||||
const newSummary = summarizeReserveHistory(
|
|
||||||
newHist.reserveTransactions,
|
|
||||||
newReserve.currency,
|
|
||||||
);
|
|
||||||
if (
|
|
||||||
Amounts.cmp(
|
|
||||||
newSummary.unclaimedReserveAmount,
|
|
||||||
denomsForWithdraw.totalWithdrawCost,
|
|
||||||
) < 0
|
|
||||||
) {
|
|
||||||
// Something must have happened concurrently!
|
|
||||||
logger.error(
|
|
||||||
"aborting withdrawal session, likely concurrent withdrawal happened",
|
|
||||||
);
|
|
||||||
logger.error(
|
|
||||||
`unclaimed reserve amount is ${newSummary.unclaimedReserveAmount}`,
|
|
||||||
);
|
|
||||||
logger.error(
|
|
||||||
`withdrawal cost is ${denomsForWithdraw.totalWithdrawCost}`,
|
|
||||||
);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
for (let i = 0; i < denomsForWithdraw.selectedDenoms.length; i++) {
|
|
||||||
const sd = denomsForWithdraw.selectedDenoms[i];
|
|
||||||
for (let j = 0; j < sd.count; j++) {
|
|
||||||
const amt = Amounts.add(sd.denom.value, sd.denom.feeWithdraw).amount;
|
|
||||||
newHist.reserveTransactions.push({
|
|
||||||
type: WalletReserveHistoryItemType.Withdraw,
|
|
||||||
expectedAmount: amt,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
logger.trace("setting reserve status to dormant after depletion");
|
|
||||||
newReserve.reserveStatus = ReserveRecordStatus.DORMANT;
|
|
||||||
newReserve.retryInfo = initRetryInfo(false);
|
|
||||||
|
|
||||||
let withdrawalGroupId: string;
|
|
||||||
|
|
||||||
if (!newReserve.initialWithdrawalStarted) {
|
|
||||||
withdrawalGroupId = newReserve.initialWithdrawalGroupId;
|
|
||||||
newReserve.initialWithdrawalStarted = true;
|
|
||||||
} else {
|
|
||||||
withdrawalGroupId = encodeCrock(randomBytes(32));
|
|
||||||
}
|
|
||||||
|
|
||||||
const withdrawalRecord: WithdrawalGroupRecord = {
|
|
||||||
withdrawalGroupId: withdrawalGroupId,
|
|
||||||
exchangeBaseUrl: newReserve.exchangeBaseUrl,
|
|
||||||
reservePub: newReserve.reservePub,
|
|
||||||
rawWithdrawalAmount: withdrawAmount,
|
|
||||||
timestampStart: getTimestampNow(),
|
|
||||||
retryInfo: initRetryInfo(),
|
|
||||||
lastError: undefined,
|
|
||||||
denomsSel: denomSelectionInfoToState(denomsForWithdraw),
|
|
||||||
};
|
|
||||||
|
|
||||||
await tx.put(Stores.reserves, newReserve);
|
|
||||||
await tx.put(Stores.reserveHistory, newHist);
|
|
||||||
await tx.put(Stores.withdrawalGroups, withdrawalRecord);
|
|
||||||
return withdrawalRecord;
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
if (newWithdrawalGroup) {
|
|
||||||
logger.trace("processing new withdraw group");
|
|
||||||
ws.notify({
|
|
||||||
type: NotificationType.WithdrawGroupCreated,
|
|
||||||
withdrawalGroupId: newWithdrawalGroup.withdrawalGroupId,
|
|
||||||
});
|
|
||||||
await processWithdrawGroup(ws, newWithdrawalGroup.withdrawalGroupId);
|
|
||||||
} else {
|
|
||||||
console.trace("withdraw session already existed");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function createTalerWithdrawReserve(
|
export async function createTalerWithdrawReserve(
|
||||||
ws: InternalWalletState,
|
ws: InternalWalletState,
|
||||||
talerWithdrawUri: string,
|
talerWithdrawUri: string,
|
||||||
|
@ -19,7 +19,7 @@ import { BalancesResponse } from "../types/walletTypes";
|
|||||||
import { CryptoApi, CryptoWorkerFactory } from "../crypto/workers/cryptoApi";
|
import { CryptoApi, CryptoWorkerFactory } from "../crypto/workers/cryptoApi";
|
||||||
import { AsyncOpMemoMap, AsyncOpMemoSingle } from "../util/asyncMemo";
|
import { AsyncOpMemoMap, AsyncOpMemoSingle } from "../util/asyncMemo";
|
||||||
import { Logger } from "../util/logging";
|
import { Logger } from "../util/logging";
|
||||||
import { PendingOperationsResponse } from "../types/pending";
|
import { PendingOperationsResponse } from "../types/pendingTypes";
|
||||||
import { WalletNotification } from "../types/notifications";
|
import { WalletNotification } from "../types/notifications";
|
||||||
import { Database } from "../util/query";
|
import { Database } from "../util/query";
|
||||||
import { openPromise, OpenedPromise } from "../util/promiseUtils";
|
import { openPromise, OpenedPromise } from "../util/promiseUtils";
|
||||||
|
@ -32,8 +32,10 @@ import {
|
|||||||
} from "../types/dbTypes";
|
} from "../types/dbTypes";
|
||||||
import {
|
import {
|
||||||
getExchangeWithdrawalInfo,
|
getExchangeWithdrawalInfo,
|
||||||
selectWithdrawalDenoms,
|
|
||||||
denomSelectionInfoToState,
|
denomSelectionInfoToState,
|
||||||
|
updateWithdrawalDenoms,
|
||||||
|
getPossibleWithdrawalDenoms,
|
||||||
|
selectWithdrawalDenominations,
|
||||||
} from "./withdraw";
|
} from "./withdraw";
|
||||||
import { updateExchangeFromUrl } from "./exchanges";
|
import { updateExchangeFromUrl } from "./exchanges";
|
||||||
import { getRandomBytes, encodeCrock } from "../crypto/talerCrypto";
|
import { getRandomBytes, encodeCrock } from "../crypto/talerCrypto";
|
||||||
@ -92,12 +94,15 @@ export async function prepareTip(
|
|||||||
);
|
);
|
||||||
|
|
||||||
const walletTipId = encodeCrock(getRandomBytes(32));
|
const walletTipId = encodeCrock(getRandomBytes(32));
|
||||||
const selectedDenoms = await selectWithdrawalDenoms(
|
await updateWithdrawalDenoms(ws, tipPickupStatus.exchange_url);
|
||||||
ws,
|
const denoms = await getPossibleWithdrawalDenoms(ws, tipPickupStatus.exchange_url);
|
||||||
tipPickupStatus.exchange_url,
|
const selectedDenoms = await selectWithdrawalDenominations(
|
||||||
amount,
|
amount,
|
||||||
|
denoms
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const secretSeed = encodeCrock(getRandomBytes(64));
|
||||||
|
|
||||||
tipRecord = {
|
tipRecord = {
|
||||||
walletTipId: walletTipId,
|
walletTipId: walletTipId,
|
||||||
acceptedTimestamp: undefined,
|
acceptedTimestamp: undefined,
|
||||||
@ -105,7 +110,6 @@ export async function prepareTip(
|
|||||||
tipExpiration: tipPickupStatus.expiration,
|
tipExpiration: tipPickupStatus.expiration,
|
||||||
exchangeBaseUrl: tipPickupStatus.exchange_url,
|
exchangeBaseUrl: tipPickupStatus.exchange_url,
|
||||||
merchantBaseUrl: res.merchantBaseUrl,
|
merchantBaseUrl: res.merchantBaseUrl,
|
||||||
planchets: undefined,
|
|
||||||
createdTimestamp: getTimestampNow(),
|
createdTimestamp: getTimestampNow(),
|
||||||
merchantTipId: res.merchantTipId,
|
merchantTipId: res.merchantTipId,
|
||||||
tipAmountEffective: Amounts.sub(
|
tipAmountEffective: Amounts.sub(
|
||||||
@ -117,6 +121,7 @@ export async function prepareTip(
|
|||||||
lastError: undefined,
|
lastError: undefined,
|
||||||
denomsSel: denomSelectionInfoToState(selectedDenoms),
|
denomsSel: denomSelectionInfoToState(selectedDenoms),
|
||||||
pickedUpTimestamp: undefined,
|
pickedUpTimestamp: undefined,
|
||||||
|
secretSeed,
|
||||||
};
|
};
|
||||||
await ws.db.put(Stores.tips, tipRecord);
|
await ws.db.put(Stores.tips, tipRecord);
|
||||||
}
|
}
|
||||||
@ -316,6 +321,7 @@ async function processTipImpl(
|
|||||||
exchangeBaseUrl: tipRecord.exchangeBaseUrl,
|
exchangeBaseUrl: tipRecord.exchangeBaseUrl,
|
||||||
status: CoinStatus.Fresh,
|
status: CoinStatus.Fresh,
|
||||||
suspended: false,
|
suspended: false,
|
||||||
|
coinEvHash: planchet.coinEvHash,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -36,7 +36,7 @@ import {
|
|||||||
WithdrawalType,
|
WithdrawalType,
|
||||||
WithdrawalDetails,
|
WithdrawalDetails,
|
||||||
OrderShortInfo,
|
OrderShortInfo,
|
||||||
} from "../types/transactions";
|
} from "../types/transactionsTypes";
|
||||||
import { getFundingPaytoUris } from "./reserves";
|
import { getFundingPaytoUris } from "./reserves";
|
||||||
import { TipResponse } from "../types/talerTypes";
|
import { TipResponse } from "../types/talerTypes";
|
||||||
|
|
||||||
|
@ -15,7 +15,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import test from "ava";
|
import test from "ava";
|
||||||
import { getWithdrawDenomList } from "./withdraw";
|
import { selectWithdrawalDenominations } from "./withdraw";
|
||||||
import { Amounts } from "../util/amounts";
|
import { Amounts } from "../util/amounts";
|
||||||
|
|
||||||
test("withdrawal selection bug repro", (t) => {
|
test("withdrawal selection bug repro", (t) => {
|
||||||
@ -322,7 +322,7 @@ test("withdrawal selection bug repro", (t) => {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const res = getWithdrawDenomList(amount, denoms);
|
const res = selectWithdrawalDenominations(amount, denoms);
|
||||||
|
|
||||||
console.error("cost", Amounts.stringify(res.totalWithdrawCost));
|
console.error("cost", Amounts.stringify(res.totalWithdrawCost));
|
||||||
console.error("withdraw amount", Amounts.stringify(amount));
|
console.error("withdraw amount", Amounts.stringify(amount));
|
||||||
|
@ -86,12 +86,13 @@ export function isWithdrawableDenom(d: DenominationRecord): boolean {
|
|||||||
return started && stillOkay && !d.isRevoked;
|
return started && stillOkay && !d.isRevoked;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get a list of denominations (with repetitions possible)
|
* Get a list of denominations (with repetitions possible)
|
||||||
* whose total value is as close as possible to the available
|
* whose total value is as close as possible to the available
|
||||||
* amount, but never larger.
|
* amount, but never larger.
|
||||||
*/
|
*/
|
||||||
export function getWithdrawDenomList(
|
export function selectWithdrawalDenominations(
|
||||||
amountAvailable: AmountJson,
|
amountAvailable: AmountJson,
|
||||||
denoms: DenominationRecord[],
|
denoms: DenominationRecord[],
|
||||||
): DenominationSelectionInfo {
|
): DenominationSelectionInfo {
|
||||||
@ -207,7 +208,7 @@ export async function getBankWithdrawalInfo(
|
|||||||
/**
|
/**
|
||||||
* Return denominations that can potentially used for a withdrawal.
|
* Return denominations that can potentially used for a withdrawal.
|
||||||
*/
|
*/
|
||||||
async function getPossibleDenoms(
|
export async function getPossibleWithdrawalDenoms(
|
||||||
ws: InternalWalletState,
|
ws: InternalWalletState,
|
||||||
exchangeBaseUrl: string,
|
exchangeBaseUrl: string,
|
||||||
): Promise<DenominationRecord[]> {
|
): Promise<DenominationRecord[]> {
|
||||||
@ -470,6 +471,7 @@ async function processPlanchetVerifyAndStoreCoin(
|
|||||||
denomPub: planchet.denomPub,
|
denomPub: planchet.denomPub,
|
||||||
denomPubHash: planchet.denomPubHash,
|
denomPubHash: planchet.denomPubHash,
|
||||||
denomSig,
|
denomSig,
|
||||||
|
coinEvHash: planchet.coinEvHash,
|
||||||
exchangeBaseUrl: withdrawalGroup.exchangeBaseUrl,
|
exchangeBaseUrl: withdrawalGroup.exchangeBaseUrl,
|
||||||
status: CoinStatus.Fresh,
|
status: CoinStatus.Fresh,
|
||||||
coinSource: {
|
coinSource: {
|
||||||
@ -524,17 +526,13 @@ export function denomSelectionInfoToState(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get a list of denominations to withdraw from the given exchange for the
|
* Make sure that denominations that currently can be used for withdrawal
|
||||||
* given amount, making sure that all denominations' signatures are verified.
|
* are validated, and the result of validation is stored in the database.
|
||||||
*
|
|
||||||
* Writes to the DB in order to record the result from verifying
|
|
||||||
* denominations.
|
|
||||||
*/
|
*/
|
||||||
export async function selectWithdrawalDenoms(
|
export async function updateWithdrawalDenoms(
|
||||||
ws: InternalWalletState,
|
ws: InternalWalletState,
|
||||||
exchangeBaseUrl: string,
|
exchangeBaseUrl: string,
|
||||||
amount: AmountJson,
|
): Promise<void> {
|
||||||
): Promise<DenominationSelectionInfo> {
|
|
||||||
const exchange = await ws.db.get(Stores.exchanges, exchangeBaseUrl);
|
const exchange = await ws.db.get(Stores.exchanges, exchangeBaseUrl);
|
||||||
if (!exchange) {
|
if (!exchange) {
|
||||||
logger.error("exchange not found");
|
logger.error("exchange not found");
|
||||||
@ -545,43 +543,24 @@ export async function selectWithdrawalDenoms(
|
|||||||
logger.error("exchange details not available");
|
logger.error("exchange details not available");
|
||||||
throw Error(`exchange ${exchangeBaseUrl} details not available`);
|
throw Error(`exchange ${exchangeBaseUrl} details not available`);
|
||||||
}
|
}
|
||||||
|
const denominations = await getPossibleWithdrawalDenoms(ws, exchangeBaseUrl);
|
||||||
let allValid = false;
|
for (const denom of denominations) {
|
||||||
let selectedDenoms: DenominationSelectionInfo;
|
if (denom.status === DenominationStatus.Unverified) {
|
||||||
|
const valid = await ws.cryptoApi.isValidDenom(
|
||||||
// Find a denomination selection for the requested amount.
|
denom,
|
||||||
// If a selected denomination has not been validated yet
|
exchangeDetails.masterPublicKey,
|
||||||
// and turns our to be invalid, we try again with the
|
);
|
||||||
// reduced set of denominations.
|
if (!valid) {
|
||||||
do {
|
denom.status = DenominationStatus.VerifiedBad;
|
||||||
allValid = true;
|
} else {
|
||||||
const nextPossibleDenoms = await getPossibleDenoms(ws, exchange.baseUrl);
|
denom.status = DenominationStatus.VerifiedGood;
|
||||||
selectedDenoms = getWithdrawDenomList(amount, nextPossibleDenoms);
|
|
||||||
for (const denomSel of selectedDenoms.selectedDenoms) {
|
|
||||||
const denom = denomSel.denom;
|
|
||||||
if (denom.status === DenominationStatus.Unverified) {
|
|
||||||
const valid = await ws.cryptoApi.isValidDenom(
|
|
||||||
denom,
|
|
||||||
exchangeDetails.masterPublicKey,
|
|
||||||
);
|
|
||||||
if (!valid) {
|
|
||||||
denom.status = DenominationStatus.VerifiedBad;
|
|
||||||
allValid = false;
|
|
||||||
} else {
|
|
||||||
denom.status = DenominationStatus.VerifiedGood;
|
|
||||||
}
|
|
||||||
await ws.db.put(Stores.denominations, denom);
|
|
||||||
}
|
}
|
||||||
|
await ws.db.put(Stores.denominations, denom);
|
||||||
}
|
}
|
||||||
} while (selectedDenoms.selectedDenoms.length > 0 && !allValid);
|
|
||||||
|
|
||||||
if (Amounts.cmp(selectedDenoms.totalWithdrawCost, amount) > 0) {
|
|
||||||
throw Error("Bug: withdrawal coin selection is wrong");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return selectedDenoms;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
async function incrementWithdrawalRetry(
|
async function incrementWithdrawalRetry(
|
||||||
ws: InternalWalletState,
|
ws: InternalWalletState,
|
||||||
withdrawalGroupId: string,
|
withdrawalGroupId: string,
|
||||||
@ -745,7 +724,9 @@ export async function getExchangeWithdrawalInfo(
|
|||||||
throw Error(`exchange ${exchangeInfo.baseUrl} wire details not available`);
|
throw Error(`exchange ${exchangeInfo.baseUrl} wire details not available`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const selectedDenoms = await selectWithdrawalDenoms(ws, baseUrl, amount);
|
await updateWithdrawalDenoms(ws, baseUrl);
|
||||||
|
const denoms = await getPossibleWithdrawalDenoms(ws, baseUrl);
|
||||||
|
const selectedDenoms = selectWithdrawalDenominations(amount, denoms);
|
||||||
const exchangeWireAccounts: string[] = [];
|
const exchangeWireAccounts: string[] = [];
|
||||||
for (const account of exchangeWireInfo.accounts) {
|
for (const account of exchangeWireInfo.accounts) {
|
||||||
exchangeWireAccounts.push(account.payto_uri);
|
exchangeWireAccounts.push(account.payto_uri);
|
||||||
|
@ -53,12 +53,6 @@
|
|||||||
* Imports.
|
* Imports.
|
||||||
*/
|
*/
|
||||||
import { Timestamp } from "../util/time";
|
import { Timestamp } from "../util/time";
|
||||||
import {
|
|
||||||
ReserveClosingTransaction,
|
|
||||||
ReserveCreditTransaction,
|
|
||||||
ReserveRecoupTransaction,
|
|
||||||
ReserveWithdrawTransaction,
|
|
||||||
} from "./ReserveTransaction";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Type alias for strings that are to be treated like amounts.
|
* Type alias for strings that are to be treated like amounts.
|
||||||
@ -1128,82 +1122,6 @@ export interface BackupExchange {
|
|||||||
tos_etag_accepted: string | undefined;
|
tos_etag_accepted: string | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum WalletReserveHistoryItemType {
|
|
||||||
Credit = "credit",
|
|
||||||
Withdraw = "withdraw",
|
|
||||||
Closing = "closing",
|
|
||||||
Recoup = "recoup",
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface BackupReserveHistoryCreditItem {
|
|
||||||
type: WalletReserveHistoryItemType.Credit;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Amount we expect to see credited.
|
|
||||||
*/
|
|
||||||
expected_amount?: BackupAmountString;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Item from the reserve transaction history that this
|
|
||||||
* wallet reserve history item matches up with.
|
|
||||||
*/
|
|
||||||
matched_exchange_transaction?: ReserveCreditTransaction;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reserve history item for a withdrawal
|
|
||||||
*/
|
|
||||||
export interface BackupReserveHistoryWithdrawItem {
|
|
||||||
type: WalletReserveHistoryItemType.Withdraw;
|
|
||||||
|
|
||||||
expected_amount?: BackupAmountString;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Hash of the blinded coin.
|
|
||||||
*
|
|
||||||
* When this value is set, it indicates that a withdrawal is active
|
|
||||||
* in the wallet for the reserve.
|
|
||||||
*/
|
|
||||||
expected_coin_ev_hash?: string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Item from the reserve transaction history that this
|
|
||||||
* wallet reserve history item matches up with.
|
|
||||||
*/
|
|
||||||
matched_exchange_transaction?: ReserveWithdrawTransaction;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface BackupReserveHistoryClosingItem {
|
|
||||||
type: WalletReserveHistoryItemType.Closing;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Item from the reserve transaction history that this
|
|
||||||
* wallet reserve history item matches up with.
|
|
||||||
*/
|
|
||||||
matched_exchange_transaction?: ReserveClosingTransaction;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface BackupReserveHistoryRecoupItem {
|
|
||||||
type: WalletReserveHistoryItemType.Recoup;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Amount we expect to see recouped.
|
|
||||||
*/
|
|
||||||
expected_amount?: BackupAmountString;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Item from the reserve transaction history that this
|
|
||||||
* wallet reserve history item matches up with.
|
|
||||||
*/
|
|
||||||
matched_exchange_transaction?: ReserveRecoupTransaction;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type BackupReserveHistoryItem =
|
|
||||||
| BackupReserveHistoryCreditItem
|
|
||||||
| BackupReserveHistoryWithdrawItem
|
|
||||||
| BackupReserveHistoryRecoupItem
|
|
||||||
| BackupReserveHistoryClosingItem;
|
|
||||||
|
|
||||||
export enum BackupProposalStatus {
|
export enum BackupProposalStatus {
|
||||||
/**
|
/**
|
||||||
* Proposed (and either downloaded or not,
|
* Proposed (and either downloaded or not,
|
||||||
|
@ -83,6 +83,11 @@ export interface DerivedRefreshSession {
|
|||||||
*/
|
*/
|
||||||
coinEv: string;
|
coinEv: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hash of the blinded public key.
|
||||||
|
*/
|
||||||
|
coinEvHash: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Blinding key used.
|
* Blinding key used.
|
||||||
*/
|
*/
|
||||||
@ -122,6 +127,7 @@ export interface DeriveTipRequest {
|
|||||||
export interface DerivedTipPlanchet {
|
export interface DerivedTipPlanchet {
|
||||||
blindingKey: string;
|
blindingKey: string;
|
||||||
coinEv: string;
|
coinEv: string;
|
||||||
|
coinEvHash: string;
|
||||||
coinPriv: string;
|
coinPriv: string;
|
||||||
coinPub: string;
|
coinPub: string;
|
||||||
}
|
}
|
||||||
|
@ -65,12 +65,6 @@ export enum ReserveRecordStatus {
|
|||||||
*/
|
*/
|
||||||
QUERYING_STATUS = "querying-status",
|
QUERYING_STATUS = "querying-status",
|
||||||
|
|
||||||
/**
|
|
||||||
* Status is queried, the wallet must now select coins
|
|
||||||
* and start withdrawing.
|
|
||||||
*/
|
|
||||||
WITHDRAWING = "withdrawing",
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The corresponding withdraw record has been created.
|
* The corresponding withdraw record has been created.
|
||||||
* No further processing is done, unless explicitly requested
|
* No further processing is done, unless explicitly requested
|
||||||
@ -84,76 +78,6 @@ export enum ReserveRecordStatus {
|
|||||||
BANK_ABORTED = "bank-aborted",
|
BANK_ABORTED = "bank-aborted",
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum WalletReserveHistoryItemType {
|
|
||||||
Credit = "credit",
|
|
||||||
Withdraw = "withdraw",
|
|
||||||
Closing = "closing",
|
|
||||||
Recoup = "recoup",
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface WalletReserveHistoryCreditItem {
|
|
||||||
type: WalletReserveHistoryItemType.Credit;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Amount we expect to see credited.
|
|
||||||
*/
|
|
||||||
expectedAmount?: AmountJson;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Item from the reserve transaction history that this
|
|
||||||
* wallet reserve history item matches up with.
|
|
||||||
*/
|
|
||||||
matchedExchangeTransaction?: ReserveCreditTransaction;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface WalletReserveHistoryWithdrawItem {
|
|
||||||
expectedAmount?: AmountJson;
|
|
||||||
|
|
||||||
type: WalletReserveHistoryItemType.Withdraw;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Item from the reserve transaction history that this
|
|
||||||
* wallet reserve history item matches up with.
|
|
||||||
*/
|
|
||||||
matchedExchangeTransaction?: ReserveWithdrawTransaction;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface WalletReserveHistoryClosingItem {
|
|
||||||
type: WalletReserveHistoryItemType.Closing;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Item from the reserve transaction history that this
|
|
||||||
* wallet reserve history item matches up with.
|
|
||||||
*/
|
|
||||||
matchedExchangeTransaction?: ReserveClosingTransaction;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface WalletReserveHistoryRecoupItem {
|
|
||||||
type: WalletReserveHistoryItemType.Recoup;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Amount we expect to see recouped.
|
|
||||||
*/
|
|
||||||
expectedAmount?: AmountJson;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Item from the reserve transaction history that this
|
|
||||||
* wallet reserve history item matches up with.
|
|
||||||
*/
|
|
||||||
matchedExchangeTransaction?: ReserveRecoupTransaction;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type WalletReserveHistoryItem =
|
|
||||||
| WalletReserveHistoryCreditItem
|
|
||||||
| WalletReserveHistoryWithdrawItem
|
|
||||||
| WalletReserveHistoryRecoupItem
|
|
||||||
| WalletReserveHistoryClosingItem;
|
|
||||||
|
|
||||||
export interface ReserveHistoryRecord {
|
|
||||||
reservePub: string;
|
|
||||||
reserveTransactions: WalletReserveHistoryItem[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ReserveBankInfo {
|
export interface ReserveBankInfo {
|
||||||
/**
|
/**
|
||||||
* Status URL that the wallet will use to query the status
|
* Status URL that the wallet will use to query the status
|
||||||
@ -667,6 +591,8 @@ export interface RefreshPlanchet {
|
|||||||
*/
|
*/
|
||||||
coinEv: string;
|
coinEv: string;
|
||||||
|
|
||||||
|
coinEvHash: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Blinding key used.
|
* Blinding key used.
|
||||||
*/
|
*/
|
||||||
@ -782,6 +708,14 @@ export interface CoinRecord {
|
|||||||
*/
|
*/
|
||||||
blindingKey: string;
|
blindingKey: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hash of the coin envelope.
|
||||||
|
*
|
||||||
|
* Stored here for indexing purposes, so that when looking at a
|
||||||
|
* reserve history, we can quickly find the coin for a withdrawal transaction.
|
||||||
|
*/
|
||||||
|
coinEvHash: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Status of the coin.
|
* Status of the coin.
|
||||||
*/
|
*/
|
||||||
@ -1536,6 +1470,12 @@ class CoinsStore extends Store<"coins", CoinRecord> {
|
|||||||
string,
|
string,
|
||||||
CoinRecord
|
CoinRecord
|
||||||
>(this, "denomPubHashIndex", "denomPubHash");
|
>(this, "denomPubHashIndex", "denomPubHash");
|
||||||
|
|
||||||
|
coinEvHashIndex = new Index<"coins", "coinEvHashIndex", string, CoinRecord>(
|
||||||
|
this,
|
||||||
|
"coinEvHashIndex",
|
||||||
|
"coinEvHash",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
class ProposalsStore extends Store<"proposals", ProposalRecord> {
|
class ProposalsStore extends Store<"proposals", ProposalRecord> {
|
||||||
@ -1602,15 +1542,6 @@ class ReservesStore extends Store<"reserves", ReserveRecord> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class ReserveHistoryStore extends Store<
|
|
||||||
"reserveHistory",
|
|
||||||
ReserveHistoryRecord
|
|
||||||
> {
|
|
||||||
constructor() {
|
|
||||||
super("reserveHistory", { keyPath: "reservePub" });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class TipsStore extends Store<"tips", TipRecord> {
|
class TipsStore extends Store<"tips", TipRecord> {
|
||||||
constructor() {
|
constructor() {
|
||||||
super("tips", { keyPath: "walletTipId" });
|
super("tips", { keyPath: "walletTipId" });
|
||||||
@ -1638,6 +1569,12 @@ class WithdrawalGroupsStore extends Store<
|
|||||||
constructor() {
|
constructor() {
|
||||||
super("withdrawals", { keyPath: "withdrawalGroupId" });
|
super("withdrawals", { keyPath: "withdrawalGroupId" });
|
||||||
}
|
}
|
||||||
|
byReservePub = new Index<
|
||||||
|
"withdrawals",
|
||||||
|
"withdrawalsByReserveIndex",
|
||||||
|
string,
|
||||||
|
WithdrawalGroupRecord
|
||||||
|
>(this, "withdrawalsByReserveIndex", "reservePub");
|
||||||
}
|
}
|
||||||
|
|
||||||
class PlanchetsStore extends Store<"planchets", PlanchetRecord> {
|
class PlanchetsStore extends Store<"planchets", PlanchetRecord> {
|
||||||
@ -1656,6 +1593,12 @@ class PlanchetsStore extends Store<"planchets", PlanchetRecord> {
|
|||||||
string,
|
string,
|
||||||
PlanchetRecord
|
PlanchetRecord
|
||||||
>(this, "withdrawalGroupIndex", "withdrawalGroupId");
|
>(this, "withdrawalGroupIndex", "withdrawalGroupId");
|
||||||
|
|
||||||
|
coinEvHashIndex = new Index<"planchets", "coinEvHashIndex", string, PlanchetRecord>(
|
||||||
|
this,
|
||||||
|
"coinEvHashIndex",
|
||||||
|
"coinEvHash",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -1702,7 +1645,6 @@ export const Stores = {
|
|||||||
keyPath: "recoupGroupId",
|
keyPath: "recoupGroupId",
|
||||||
}),
|
}),
|
||||||
reserves: new ReservesStore(),
|
reserves: new ReservesStore(),
|
||||||
reserveHistory: new ReserveHistoryStore(),
|
|
||||||
purchases: new PurchasesStore(),
|
purchases: new PurchasesStore(),
|
||||||
tips: new TipsStore(),
|
tips: new TipsStore(),
|
||||||
withdrawalGroups: new WithdrawalGroupsStore(),
|
withdrawalGroups: new WithdrawalGroupsStore(),
|
||||||
|
@ -23,7 +23,6 @@
|
|||||||
* Imports.
|
* Imports.
|
||||||
*/
|
*/
|
||||||
import { TalerErrorDetails } from "./walletTypes";
|
import { TalerErrorDetails } from "./walletTypes";
|
||||||
import { ReserveHistorySummary } from "../util/reserveHistoryUtil";
|
|
||||||
|
|
||||||
export enum NotificationType {
|
export enum NotificationType {
|
||||||
CoinWithdrawn = "coin-withdrawn",
|
CoinWithdrawn = "coin-withdrawn",
|
||||||
@ -125,10 +124,6 @@ export interface RefreshRefusedNotification {
|
|||||||
type: NotificationType.RefreshUnwarranted;
|
type: NotificationType.RefreshUnwarranted;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ReserveUpdatedNotification {
|
|
||||||
type: NotificationType.ReserveUpdated;
|
|
||||||
updateSummary?: ReserveHistorySummary;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ReserveConfirmedNotification {
|
export interface ReserveConfirmedNotification {
|
||||||
type: NotificationType.ReserveConfirmed;
|
type: NotificationType.ReserveConfirmed;
|
||||||
@ -252,7 +247,6 @@ export type WalletNotification =
|
|||||||
| RefreshRevealedNotification
|
| RefreshRevealedNotification
|
||||||
| RefreshStartedNotification
|
| RefreshStartedNotification
|
||||||
| RefreshRefusedNotification
|
| RefreshRefusedNotification
|
||||||
| ReserveUpdatedNotification
|
|
||||||
| ReserveCreatedNotification
|
| ReserveCreatedNotification
|
||||||
| ReserveConfirmedNotification
|
| ReserveConfirmedNotification
|
||||||
| WithdrawalGroupFinishedNotification
|
| WithdrawalGroupFinishedNotification
|
||||||
|
@ -1,276 +0,0 @@
|
|||||||
/*
|
|
||||||
This file is part of GNU Taler
|
|
||||||
(C) 2019 GNUnet e.V.
|
|
||||||
|
|
||||||
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/>
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Type and schema definitions for pending operations in the wallet.
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Imports.
|
|
||||||
*/
|
|
||||||
import { TalerErrorDetails, BalancesResponse } from "./walletTypes";
|
|
||||||
import { ReserveRecordStatus } from "./dbTypes";
|
|
||||||
import { Timestamp, Duration } from "../util/time";
|
|
||||||
import { RetryInfo } from "../util/retries";
|
|
||||||
|
|
||||||
export enum PendingOperationType {
|
|
||||||
Bug = "bug",
|
|
||||||
ExchangeUpdate = "exchange-update",
|
|
||||||
ExchangeCheckRefresh = "exchange-check-refresh",
|
|
||||||
Pay = "pay",
|
|
||||||
ProposalChoice = "proposal-choice",
|
|
||||||
ProposalDownload = "proposal-download",
|
|
||||||
Refresh = "refresh",
|
|
||||||
Reserve = "reserve",
|
|
||||||
Recoup = "recoup",
|
|
||||||
RefundQuery = "refund-query",
|
|
||||||
TipChoice = "tip-choice",
|
|
||||||
TipPickup = "tip-pickup",
|
|
||||||
Withdraw = "withdraw",
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Information about a pending operation.
|
|
||||||
*/
|
|
||||||
export type PendingOperationInfo = PendingOperationInfoCommon &
|
|
||||||
(
|
|
||||||
| PendingBugOperation
|
|
||||||
| PendingExchangeUpdateOperation
|
|
||||||
| PendingExchangeCheckRefreshOperation
|
|
||||||
| PendingPayOperation
|
|
||||||
| PendingProposalChoiceOperation
|
|
||||||
| PendingProposalDownloadOperation
|
|
||||||
| PendingRefreshOperation
|
|
||||||
| PendingRefundQueryOperation
|
|
||||||
| PendingReserveOperation
|
|
||||||
| PendingTipChoiceOperation
|
|
||||||
| PendingTipPickupOperation
|
|
||||||
| PendingWithdrawOperation
|
|
||||||
| PendingRecoupOperation
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The wallet is currently updating information about an exchange.
|
|
||||||
*/
|
|
||||||
export interface PendingExchangeUpdateOperation {
|
|
||||||
type: PendingOperationType.ExchangeUpdate;
|
|
||||||
stage: ExchangeUpdateOperationStage;
|
|
||||||
reason: string;
|
|
||||||
exchangeBaseUrl: string;
|
|
||||||
lastError: TalerErrorDetails | undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The wallet should check whether coins from this exchange
|
|
||||||
* need to be auto-refreshed.
|
|
||||||
*/
|
|
||||||
export interface PendingExchangeCheckRefreshOperation {
|
|
||||||
type: PendingOperationType.ExchangeCheckRefresh;
|
|
||||||
exchangeBaseUrl: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Some interal error happened in the wallet. This pending operation
|
|
||||||
* should *only* be reported for problems in the wallet, not when
|
|
||||||
* a problem with a merchant/exchange/etc. occurs.
|
|
||||||
*/
|
|
||||||
export interface PendingBugOperation {
|
|
||||||
type: PendingOperationType.Bug;
|
|
||||||
message: string;
|
|
||||||
details: any;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Current state of an exchange update operation.
|
|
||||||
*/
|
|
||||||
export enum ExchangeUpdateOperationStage {
|
|
||||||
FetchKeys = "fetch-keys",
|
|
||||||
FetchWire = "fetch-wire",
|
|
||||||
FinalizeUpdate = "finalize-update",
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum ReserveType {
|
|
||||||
/**
|
|
||||||
* Manually created.
|
|
||||||
*/
|
|
||||||
Manual = "manual",
|
|
||||||
/**
|
|
||||||
* Withdrawn from a bank that has "tight" Taler integration
|
|
||||||
*/
|
|
||||||
TalerBankWithdraw = "taler-bank-withdraw",
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Status of processing a reserve.
|
|
||||||
*
|
|
||||||
* Does *not* include the withdrawal operation that might result
|
|
||||||
* from this.
|
|
||||||
*/
|
|
||||||
export interface PendingReserveOperation {
|
|
||||||
type: PendingOperationType.Reserve;
|
|
||||||
retryInfo: RetryInfo | undefined;
|
|
||||||
stage: ReserveRecordStatus;
|
|
||||||
timestampCreated: Timestamp;
|
|
||||||
reserveType: ReserveType;
|
|
||||||
reservePub: string;
|
|
||||||
bankWithdrawConfirmUrl?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Status of an ongoing withdrawal operation.
|
|
||||||
*/
|
|
||||||
export interface PendingRefreshOperation {
|
|
||||||
type: PendingOperationType.Refresh;
|
|
||||||
lastError?: TalerErrorDetails;
|
|
||||||
refreshGroupId: string;
|
|
||||||
finishedPerCoin: boolean[];
|
|
||||||
retryInfo: RetryInfo;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Status of downloading signed contract terms from a merchant.
|
|
||||||
*/
|
|
||||||
export interface PendingProposalDownloadOperation {
|
|
||||||
type: PendingOperationType.ProposalDownload;
|
|
||||||
merchantBaseUrl: string;
|
|
||||||
proposalTimestamp: Timestamp;
|
|
||||||
proposalId: string;
|
|
||||||
orderId: string;
|
|
||||||
lastError?: TalerErrorDetails;
|
|
||||||
retryInfo: RetryInfo;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* User must choose whether to accept or reject the merchant's
|
|
||||||
* proposed contract terms.
|
|
||||||
*/
|
|
||||||
export interface PendingProposalChoiceOperation {
|
|
||||||
type: PendingOperationType.ProposalChoice;
|
|
||||||
merchantBaseUrl: string;
|
|
||||||
proposalTimestamp: Timestamp;
|
|
||||||
proposalId: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The wallet is picking up a tip that the user has accepted.
|
|
||||||
*/
|
|
||||||
export interface PendingTipPickupOperation {
|
|
||||||
type: PendingOperationType.TipPickup;
|
|
||||||
tipId: string;
|
|
||||||
merchantBaseUrl: string;
|
|
||||||
merchantTipId: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The wallet has been offered a tip, and the user now needs to
|
|
||||||
* decide whether to accept or reject the tip.
|
|
||||||
*/
|
|
||||||
export interface PendingTipChoiceOperation {
|
|
||||||
type: PendingOperationType.TipChoice;
|
|
||||||
tipId: string;
|
|
||||||
merchantBaseUrl: string;
|
|
||||||
merchantTipId: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The wallet is signing coins and then sending them to
|
|
||||||
* the merchant.
|
|
||||||
*/
|
|
||||||
export interface PendingPayOperation {
|
|
||||||
type: PendingOperationType.Pay;
|
|
||||||
proposalId: string;
|
|
||||||
isReplay: boolean;
|
|
||||||
retryInfo: RetryInfo;
|
|
||||||
lastError: TalerErrorDetails | undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The wallet is querying the merchant about whether any refund
|
|
||||||
* permissions are available for a purchase.
|
|
||||||
*/
|
|
||||||
export interface PendingRefundQueryOperation {
|
|
||||||
type: PendingOperationType.RefundQuery;
|
|
||||||
proposalId: string;
|
|
||||||
retryInfo: RetryInfo;
|
|
||||||
lastError: TalerErrorDetails | undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface PendingRecoupOperation {
|
|
||||||
type: PendingOperationType.Recoup;
|
|
||||||
recoupGroupId: string;
|
|
||||||
retryInfo: RetryInfo;
|
|
||||||
lastError: TalerErrorDetails | undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Status of an ongoing withdrawal operation.
|
|
||||||
*/
|
|
||||||
export interface PendingWithdrawOperation {
|
|
||||||
type: PendingOperationType.Withdraw;
|
|
||||||
lastError: TalerErrorDetails | undefined;
|
|
||||||
retryInfo: RetryInfo;
|
|
||||||
withdrawalGroupId: string;
|
|
||||||
numCoinsWithdrawn: number;
|
|
||||||
numCoinsTotal: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fields that are present in every pending operation.
|
|
||||||
*/
|
|
||||||
export interface PendingOperationInfoCommon {
|
|
||||||
/**
|
|
||||||
* Type of the pending operation.
|
|
||||||
*/
|
|
||||||
type: PendingOperationType;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set to true if the operation indicates that something is really in progress,
|
|
||||||
* as opposed to some regular scheduled operation or a permanent failure.
|
|
||||||
*/
|
|
||||||
givesLifeness: boolean;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retry info, not available on all pending operations.
|
|
||||||
* If it is available, it must have the same name.
|
|
||||||
*/
|
|
||||||
retryInfo?: RetryInfo;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Response returned from the pending operations API.
|
|
||||||
*/
|
|
||||||
export interface PendingOperationsResponse {
|
|
||||||
/**
|
|
||||||
* List of pending operations.
|
|
||||||
*/
|
|
||||||
pendingOperations: PendingOperationInfo[];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Current wallet balance, including pending balances.
|
|
||||||
*/
|
|
||||||
walletBalance: BalancesResponse;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* When is the next pending operation due to be re-tried?
|
|
||||||
*/
|
|
||||||
nextRetryDelay: Duration;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Does this response only include pending operations that
|
|
||||||
* are due to be executed right now?
|
|
||||||
*/
|
|
||||||
onlyDue: boolean;
|
|
||||||
}
|
|
@ -1,337 +0,0 @@
|
|||||||
/*
|
|
||||||
This file is part of GNU Taler
|
|
||||||
(C) 2019 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/>
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Type and schema definitions for the wallet's transaction list.
|
|
||||||
*
|
|
||||||
* @author Florian Dold
|
|
||||||
* @author Torsten Grote
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Imports.
|
|
||||||
*/
|
|
||||||
import { Timestamp } from "../util/time";
|
|
||||||
import {
|
|
||||||
AmountString,
|
|
||||||
Product,
|
|
||||||
InternationalizedString,
|
|
||||||
MerchantInfo,
|
|
||||||
codecForInternationalizedString,
|
|
||||||
codecForMerchantInfo,
|
|
||||||
codecForProduct,
|
|
||||||
} from "./talerTypes";
|
|
||||||
import {
|
|
||||||
Codec,
|
|
||||||
buildCodecForObject,
|
|
||||||
codecOptional,
|
|
||||||
codecForString,
|
|
||||||
codecForList,
|
|
||||||
codecForAny,
|
|
||||||
} from "../util/codec";
|
|
||||||
import { TalerErrorDetails } from "./walletTypes";
|
|
||||||
|
|
||||||
export interface TransactionsRequest {
|
|
||||||
/**
|
|
||||||
* return only transactions in the given currency
|
|
||||||
*/
|
|
||||||
currency?: string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* if present, results will be limited to transactions related to the given search string
|
|
||||||
*/
|
|
||||||
search?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface TransactionsResponse {
|
|
||||||
// a list of past and pending transactions sorted by pending, timestamp and transactionId.
|
|
||||||
// In case two events are both pending and have the same timestamp,
|
|
||||||
// they are sorted by the transactionId
|
|
||||||
// (lexically ascending and locale-independent comparison).
|
|
||||||
transactions: Transaction[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface TransactionCommon {
|
|
||||||
// opaque unique ID for the transaction, used as a starting point for paginating queries
|
|
||||||
// and for invoking actions on the transaction (e.g. deleting/hiding it from the history)
|
|
||||||
transactionId: string;
|
|
||||||
|
|
||||||
// the type of the transaction; different types might provide additional information
|
|
||||||
type: TransactionType;
|
|
||||||
|
|
||||||
// main timestamp of the transaction
|
|
||||||
timestamp: Timestamp;
|
|
||||||
|
|
||||||
// true if the transaction is still pending, false otherwise
|
|
||||||
// If a transaction is not longer pending, its timestamp will be updated,
|
|
||||||
// but its transactionId will remain unchanged
|
|
||||||
pending: boolean;
|
|
||||||
|
|
||||||
// Raw amount of the transaction (exclusive of fees or other extra costs)
|
|
||||||
amountRaw: AmountString;
|
|
||||||
|
|
||||||
// Amount added or removed from the wallet's balance (including all fees and other costs)
|
|
||||||
amountEffective: AmountString;
|
|
||||||
|
|
||||||
error?: TalerErrorDetails;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type Transaction =
|
|
||||||
| TransactionWithdrawal
|
|
||||||
| TransactionPayment
|
|
||||||
| TransactionRefund
|
|
||||||
| TransactionTip
|
|
||||||
| TransactionRefresh;
|
|
||||||
|
|
||||||
export enum TransactionType {
|
|
||||||
Withdrawal = "withdrawal",
|
|
||||||
Payment = "payment",
|
|
||||||
Refund = "refund",
|
|
||||||
Refresh = "refresh",
|
|
||||||
Tip = "tip",
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum WithdrawalType {
|
|
||||||
TalerBankIntegrationApi = "taler-bank-integration-api",
|
|
||||||
ManualTransfer = "manual-transfer",
|
|
||||||
}
|
|
||||||
|
|
||||||
export type WithdrawalDetails =
|
|
||||||
| WithdrawalDetailsForManualTransfer
|
|
||||||
| WithdrawalDetailsForTalerBankIntegrationApi;
|
|
||||||
|
|
||||||
interface WithdrawalDetailsForManualTransfer {
|
|
||||||
type: WithdrawalType.ManualTransfer;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Payto URIs that the exchange supports.
|
|
||||||
*
|
|
||||||
* Already contains the amount and message.
|
|
||||||
*/
|
|
||||||
exchangePaytoUris: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
interface WithdrawalDetailsForTalerBankIntegrationApi {
|
|
||||||
type: WithdrawalType.TalerBankIntegrationApi;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set to true if the bank has confirmed the withdrawal, false if not.
|
|
||||||
* An unconfirmed withdrawal usually requires user-input and should be highlighted in the UI.
|
|
||||||
* See also bankConfirmationUrl below.
|
|
||||||
*/
|
|
||||||
confirmed: boolean;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* If the withdrawal is unconfirmed, this can include a URL for user
|
|
||||||
* initiated confirmation.
|
|
||||||
*/
|
|
||||||
bankConfirmationUrl?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
// This should only be used for actual withdrawals
|
|
||||||
// and not for tips that have their own transactions type.
|
|
||||||
interface TransactionWithdrawal extends TransactionCommon {
|
|
||||||
type: TransactionType.Withdrawal;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Exchange of the withdrawal.
|
|
||||||
*/
|
|
||||||
exchangeBaseUrl: string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Amount that got subtracted from the reserve balance.
|
|
||||||
*/
|
|
||||||
amountRaw: AmountString;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Amount that actually was (or will be) added to the wallet's balance.
|
|
||||||
*/
|
|
||||||
amountEffective: AmountString;
|
|
||||||
|
|
||||||
withdrawalDetails: WithdrawalDetails;
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum PaymentStatus {
|
|
||||||
/**
|
|
||||||
* Explicitly aborted after timeout / failure
|
|
||||||
*/
|
|
||||||
Aborted = "aborted",
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Payment failed, wallet will auto-retry.
|
|
||||||
* User should be given the option to retry now / abort.
|
|
||||||
*/
|
|
||||||
Failed = "failed",
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Paid successfully
|
|
||||||
*/
|
|
||||||
Paid = "paid",
|
|
||||||
|
|
||||||
/**
|
|
||||||
* User accepted, payment is processing.
|
|
||||||
*/
|
|
||||||
Accepted = "accepted",
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface TransactionPayment extends TransactionCommon {
|
|
||||||
type: TransactionType.Payment;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Additional information about the payment.
|
|
||||||
*/
|
|
||||||
info: OrderShortInfo;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Wallet-internal end-to-end identifier for the payment.
|
|
||||||
*/
|
|
||||||
proposalId: string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* How far did the wallet get with processing the payment?
|
|
||||||
*/
|
|
||||||
status: PaymentStatus;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Amount that must be paid for the contract
|
|
||||||
*/
|
|
||||||
amountRaw: AmountString;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Amount that was paid, including deposit, wire and refresh fees.
|
|
||||||
*/
|
|
||||||
amountEffective: AmountString;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface OrderShortInfo {
|
|
||||||
/**
|
|
||||||
* Order ID, uniquely identifies the order within a merchant instance
|
|
||||||
*/
|
|
||||||
orderId: string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Hash of the contract terms.
|
|
||||||
*/
|
|
||||||
contractTermsHash: string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* More information about the merchant
|
|
||||||
*/
|
|
||||||
merchant: MerchantInfo;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Summary of the order, given by the merchant
|
|
||||||
*/
|
|
||||||
summary: string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Map from IETF BCP 47 language tags to localized summaries
|
|
||||||
*/
|
|
||||||
summary_i18n?: InternationalizedString;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* List of products that are part of the order
|
|
||||||
*/
|
|
||||||
products: Product[] | undefined;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* URL of the fulfillment, given by the merchant
|
|
||||||
*/
|
|
||||||
fulfillmentUrl?: string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Plain text message that should be shown to the user
|
|
||||||
* when the payment is complete.
|
|
||||||
*/
|
|
||||||
fulfillmentMessage?: string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Translations of fulfillmentMessage.
|
|
||||||
*/
|
|
||||||
fulfillmentMessage_i18n?: InternationalizedString;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface TransactionRefund extends TransactionCommon {
|
|
||||||
type: TransactionType.Refund;
|
|
||||||
|
|
||||||
// ID for the transaction that is refunded
|
|
||||||
refundedTransactionId: string;
|
|
||||||
|
|
||||||
// Additional information about the refunded payment
|
|
||||||
info: OrderShortInfo;
|
|
||||||
|
|
||||||
// Amount that has been refunded by the merchant
|
|
||||||
amountRaw: AmountString;
|
|
||||||
|
|
||||||
// Amount will be added to the wallet's balance after fees and refreshing
|
|
||||||
amountEffective: AmountString;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface TransactionTip extends TransactionCommon {
|
|
||||||
type: TransactionType.Tip;
|
|
||||||
|
|
||||||
// Raw amount of the tip, without extra fees that apply
|
|
||||||
amountRaw: AmountString;
|
|
||||||
|
|
||||||
// Amount will be (or was) added to the wallet's balance after fees and refreshing
|
|
||||||
amountEffective: AmountString;
|
|
||||||
|
|
||||||
merchantBaseUrl: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
// A transaction shown for refreshes that are not associated to other transactions
|
|
||||||
// such as a refresh necessary before coin expiration.
|
|
||||||
// It should only be returned by the API if the effective amount is different from zero.
|
|
||||||
interface TransactionRefresh extends TransactionCommon {
|
|
||||||
type: TransactionType.Refresh;
|
|
||||||
|
|
||||||
// Exchange that the coins are refreshed with
|
|
||||||
exchangeBaseUrl: string;
|
|
||||||
|
|
||||||
// Raw amount that is refreshed
|
|
||||||
amountRaw: AmountString;
|
|
||||||
|
|
||||||
// Amount that will be paid as fees for the refresh
|
|
||||||
amountEffective: AmountString;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const codecForTransactionsRequest = (): Codec<TransactionsRequest> =>
|
|
||||||
buildCodecForObject<TransactionsRequest>()
|
|
||||||
.property("currency", codecOptional(codecForString()))
|
|
||||||
.property("search", codecOptional(codecForString()))
|
|
||||||
.build("TransactionsRequest");
|
|
||||||
|
|
||||||
// FIXME: do full validation here!
|
|
||||||
export const codecForTransactionsResponse = (): Codec<TransactionsResponse> =>
|
|
||||||
buildCodecForObject<TransactionsResponse>()
|
|
||||||
.property("transactions", codecForList(codecForAny()))
|
|
||||||
.build("TransactionsResponse");
|
|
||||||
|
|
||||||
export const codecForOrderShortInfo = (): Codec<OrderShortInfo> =>
|
|
||||||
buildCodecForObject<OrderShortInfo>()
|
|
||||||
.property("contractTermsHash", codecForString())
|
|
||||||
.property("fulfillmentMessage", codecOptional(codecForString()))
|
|
||||||
.property(
|
|
||||||
"fulfillmentMessage_i18n",
|
|
||||||
codecOptional(codecForInternationalizedString()),
|
|
||||||
)
|
|
||||||
.property("fulfillmentUrl", codecOptional(codecForString()))
|
|
||||||
.property("merchant", codecForMerchantInfo())
|
|
||||||
.property("orderId", codecForString())
|
|
||||||
.property("products", codecOptional(codecForList(codecForProduct())))
|
|
||||||
.property("summary", codecForString())
|
|
||||||
.property("summary_i18n", codecOptional(codecForInternationalizedString()))
|
|
||||||
.build("OrderShortInfo");
|
|
@ -55,7 +55,7 @@ import {
|
|||||||
codecForContractTerms,
|
codecForContractTerms,
|
||||||
ContractTerms,
|
ContractTerms,
|
||||||
} from "./talerTypes";
|
} from "./talerTypes";
|
||||||
import { OrderShortInfo, codecForOrderShortInfo } from "./transactions";
|
import { OrderShortInfo, codecForOrderShortInfo } from "./transactionsTypes";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Response for the create reserve request to the wallet.
|
* Response for the create reserve request to the wallet.
|
||||||
|
@ -583,7 +583,7 @@ export class Database {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getIndexed<Ind extends Index<string, string, any, any>>(
|
async getIndexed<Ind extends Index<string, string, any, any>>(
|
||||||
index: InferIndex<Ind>,
|
index: Ind,
|
||||||
key: IDBValidKey,
|
key: IDBValidKey,
|
||||||
): Promise<IndexRecord<Ind> | undefined> {
|
): Promise<IndexRecord<Ind> | undefined> {
|
||||||
const tx = this.db.transaction([index.storeName], "readonly");
|
const tx = this.db.transaction([index.storeName], "readonly");
|
||||||
|
@ -1,285 +0,0 @@
|
|||||||
/*
|
|
||||||
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");
|
|
||||||
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");
|
|
||||||
});
|
|
@ -1,363 +0,0 @@
|
|||||||
/*
|
|
||||||
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/>
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Helpers for dealing with reserve histories.
|
|
||||||
*
|
|
||||||
* @author Florian Dold <dold@taler.net>
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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 { AmountJson } from "../util/amounts";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Result of a reserve reconciliation.
|
|
||||||
*/
|
|
||||||
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[];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Various totals computed from the wallet's view
|
|
||||||
* on the reserve history.
|
|
||||||
*/
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if two reserve history items (exchange's version) match.
|
|
||||||
*/
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check a local reserve history item and a remote history item are a match.
|
|
||||||
*/
|
|
||||||
export function isLocalRemoteHistoryMatch(
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Compute totals for the wallet's view of the reserve history.
|
|
||||||
*/
|
|
||||||
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,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reconcile the wallet's local model of the reserve history
|
|
||||||
* with the reserve history of the exchange.
|
|
||||||
*/
|
|
||||||
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 (isLocalRemoteHistoryMatch(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,
|
|
||||||
};
|
|
||||||
}
|
|
@ -130,7 +130,7 @@ import {
|
|||||||
PendingOperationInfo,
|
PendingOperationInfo,
|
||||||
PendingOperationsResponse,
|
PendingOperationsResponse,
|
||||||
PendingOperationType,
|
PendingOperationType,
|
||||||
} from "./types/pending";
|
} from "./types/pendingTypes";
|
||||||
import { WalletNotification, NotificationType } from "./types/notifications";
|
import { WalletNotification, NotificationType } from "./types/notifications";
|
||||||
import {
|
import {
|
||||||
processPurchaseQueryRefund,
|
processPurchaseQueryRefund,
|
||||||
@ -148,7 +148,7 @@ import {
|
|||||||
TransactionsRequest,
|
TransactionsRequest,
|
||||||
TransactionsResponse,
|
TransactionsResponse,
|
||||||
codecForTransactionsRequest,
|
codecForTransactionsRequest,
|
||||||
} from "./types/transactions";
|
} from "./types/transactionsTypes";
|
||||||
import { getTransactions } from "./operations/transactions";
|
import { getTransactions } from "./operations/transactions";
|
||||||
import {
|
import {
|
||||||
withdrawTestBalance,
|
withdrawTestBalance,
|
||||||
@ -326,7 +326,7 @@ export class Wallet {
|
|||||||
} = {},
|
} = {},
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
let done = false;
|
let done = false;
|
||||||
const p = new Promise((resolve, reject) => {
|
const p = new Promise<void>((resolve, reject) => {
|
||||||
// Monitor for conditions that means we're done or we
|
// Monitor for conditions that means we're done or we
|
||||||
// should quit with an error (due to exceeded retries).
|
// should quit with an error (due to exceeded retries).
|
||||||
this.addNotificationListener((n) => {
|
this.addNotificationListener((n) => {
|
||||||
|
Loading…
Reference in New Issue
Block a user