aboutsummaryrefslogtreecommitdiff
path: root/packages/taler-wallet-core/src
diff options
context:
space:
mode:
authorChristian Blättler <blatc2@bfh.ch>2024-06-13 11:35:52 +0200
committerChristian Blättler <blatc2@bfh.ch>2024-06-13 11:35:52 +0200
commiteb964dfae0a12f9a90eb066d610f627538f8997c (patch)
tree26a6cd74c9a29edce05b2dcd51cf497374bf8e30 /packages/taler-wallet-core/src
parent9d0fc80a905e02a0a0b63dd547daac6e7b17fb52 (diff)
parentf9d4ff5b43e48a07ac81d7e7ef800ddb12f5f90a (diff)
downloadwallet-core-eb964dfae0a12f9a90eb066d610f627538f8997c.tar.xz
Merge branch 'master' into feature/tokens
Diffstat (limited to 'packages/taler-wallet-core/src')
-rw-r--r--packages/taler-wallet-core/src/backup/index.ts16
-rw-r--r--packages/taler-wallet-core/src/balance.ts20
-rw-r--r--packages/taler-wallet-core/src/coinSelection.ts5
-rw-r--r--packages/taler-wallet-core/src/common.ts92
-rw-r--r--packages/taler-wallet-core/src/db.ts126
-rw-r--r--packages/taler-wallet-core/src/dbless.ts2
-rw-r--r--packages/taler-wallet-core/src/deposits.ts26
-rw-r--r--packages/taler-wallet-core/src/exchanges.ts348
-rw-r--r--packages/taler-wallet-core/src/instructedAmountConversion.ts22
-rw-r--r--packages/taler-wallet-core/src/pay-merchant.ts110
-rw-r--r--packages/taler-wallet-core/src/pay-peer-common.ts6
-rw-r--r--packages/taler-wallet-core/src/pay-peer-pull-credit.ts49
-rw-r--r--packages/taler-wallet-core/src/pay-peer-pull-debit.ts2
-rw-r--r--packages/taler-wallet-core/src/pay-peer-push-credit.ts35
-rw-r--r--packages/taler-wallet-core/src/pay-peer-push-debit.ts35
-rw-r--r--packages/taler-wallet-core/src/recoup.ts4
-rw-r--r--packages/taler-wallet-core/src/refresh.ts143
-rw-r--r--packages/taler-wallet-core/src/shepherd.ts70
-rw-r--r--packages/taler-wallet-core/src/transactions.ts117
-rw-r--r--packages/taler-wallet-core/src/versions.ts9
-rw-r--r--packages/taler-wallet-core/src/wallet-api-types.ts13
-rw-r--r--packages/taler-wallet-core/src/wallet.ts84
-rw-r--r--packages/taler-wallet-core/src/withdraw.ts590
23 files changed, 1148 insertions, 776 deletions
diff --git a/packages/taler-wallet-core/src/backup/index.ts b/packages/taler-wallet-core/src/backup/index.ts
index 15904b470..09d5ae75d 100644
--- a/packages/taler-wallet-core/src/backup/index.ts
+++ b/packages/taler-wallet-core/src/backup/index.ts
@@ -805,9 +805,10 @@ async function backupRecoveryTheirs(
let backupStateEntry: ConfigRecord | undefined = await tx.config.get(
ConfigRecordKey.WalletBackupState,
);
- checkDbInvariant(!!backupStateEntry);
+ checkDbInvariant(!!backupStateEntry, `no backup entry`);
checkDbInvariant(
backupStateEntry.key === ConfigRecordKey.WalletBackupState,
+ `backup entry inconsistent`,
);
backupStateEntry.value.lastBackupNonce = undefined;
backupStateEntry.value.lastBackupTimestamp = undefined;
@@ -913,7 +914,10 @@ export async function provideBackupState(
},
);
if (bs) {
- checkDbInvariant(bs.key === ConfigRecordKey.WalletBackupState);
+ checkDbInvariant(
+ bs.key === ConfigRecordKey.WalletBackupState,
+ `backup entry inconsistent`,
+ );
return bs.value;
}
// We need to generate the key outside of the transaction
@@ -941,6 +945,7 @@ export async function provideBackupState(
}
checkDbInvariant(
backupStateEntry.key === ConfigRecordKey.WalletBackupState,
+ `backup entry inconsistent`,
);
return backupStateEntry.value;
});
@@ -952,7 +957,10 @@ export async function getWalletBackupState(
): Promise<WalletBackupConfState> {
const bs = await tx.config.get(ConfigRecordKey.WalletBackupState);
checkDbInvariant(!!bs, "wallet backup state should be in DB");
- checkDbInvariant(bs.key === ConfigRecordKey.WalletBackupState);
+ checkDbInvariant(
+ bs.key === ConfigRecordKey.WalletBackupState,
+ `backup entry inconsistent`,
+ );
return bs.value;
}
@@ -962,7 +970,7 @@ export async function setWalletDeviceId(
): Promise<void> {
await provideBackupState(wex);
await wex.db.runReadWriteTx({ storeNames: ["config"] }, async (tx) => {
- let backupStateEntry: ConfigRecord | undefined = await tx.config.get(
+ const backupStateEntry: ConfigRecord | undefined = await tx.config.get(
ConfigRecordKey.WalletBackupState,
);
if (
diff --git a/packages/taler-wallet-core/src/balance.ts b/packages/taler-wallet-core/src/balance.ts
index 76e604324..381028906 100644
--- a/packages/taler-wallet-core/src/balance.ts
+++ b/packages/taler-wallet-core/src/balance.ts
@@ -69,8 +69,8 @@ import { ExchangeRestrictionSpec, findMatchingWire } from "./coinSelection.js";
import {
DepositOperationStatus,
ExchangeEntryDbRecordStatus,
- OPERATION_STATUS_ACTIVE_FIRST,
- OPERATION_STATUS_ACTIVE_LAST,
+ OPERATION_STATUS_NONFINAL_FIRST,
+ OPERATION_STATUS_NONFINAL_LAST,
PeerPushDebitStatus,
RefreshGroupRecord,
RefreshOperationStatus,
@@ -304,8 +304,8 @@ export async function getBalancesInsideTransaction(
const balanceStore: BalancesStore = new BalancesStore(wex, tx);
const keyRangeActive = GlobalIDB.KeyRange.bound(
- OPERATION_STATUS_ACTIVE_FIRST,
- OPERATION_STATUS_ACTIVE_LAST,
+ OPERATION_STATUS_NONFINAL_FIRST,
+ OPERATION_STATUS_NONFINAL_LAST,
);
await tx.exchanges.iter().forEachAsync(async (ex) => {
@@ -379,6 +379,10 @@ export async function getBalancesInsideTransaction(
wg.denomsSel !== undefined,
"wg in kyc state should have been initialized",
);
+ checkDbInvariant(
+ wg.exchangeBaseUrl !== undefined,
+ "wg in kyc state should have been initialized",
+ );
const currency = Amounts.currencyOf(wg.denomsSel.totalCoinValue);
await balanceStore.setFlagIncomingKyc(currency, wg.exchangeBaseUrl);
break;
@@ -389,6 +393,10 @@ export async function getBalancesInsideTransaction(
wg.denomsSel !== undefined,
"wg in aml state should have been initialized",
);
+ checkDbInvariant(
+ wg.exchangeBaseUrl !== undefined,
+ "wg in kyc state should have been initialized",
+ );
const currency = Amounts.currencyOf(wg.denomsSel.totalCoinValue);
await balanceStore.setFlagIncomingAml(currency, wg.exchangeBaseUrl);
break;
@@ -408,6 +416,10 @@ export async function getBalancesInsideTransaction(
wg.denomsSel !== undefined,
"wg in confirmed state should have been initialized",
);
+ checkDbInvariant(
+ wg.exchangeBaseUrl !== undefined,
+ "wg in kyc state should have been initialized",
+ );
const currency = Amounts.currencyOf(wg.denomsSel.totalCoinValue);
await balanceStore.setFlagIncomingConfirmation(
currency,
diff --git a/packages/taler-wallet-core/src/coinSelection.ts b/packages/taler-wallet-core/src/coinSelection.ts
index a60e41ecd..db6384c93 100644
--- a/packages/taler-wallet-core/src/coinSelection.ts
+++ b/packages/taler-wallet-core/src/coinSelection.ts
@@ -691,7 +691,7 @@ export function checkAccountRestriction(
switch (myRestriction.type) {
case "deny":
return { ok: false };
- case "regex":
+ case "regex": {
const regex = new RegExp(myRestriction.payto_regex);
if (!regex.test(paytoUri)) {
return {
@@ -700,6 +700,7 @@ export function checkAccountRestriction(
hintI18n: myRestriction.human_hint_i18n,
};
}
+ }
}
}
return {
@@ -909,7 +910,7 @@ async function selectPayCandidates(
coinAvail.exchangeBaseUrl,
coinAvail.denomPubHash,
]);
- checkDbInvariant(!!denom);
+ checkDbInvariant(!!denom, `denomination of a coin is missing hash: ${coinAvail.denomPubHash}`);
if (denom.isRevoked) {
logger.trace("denom is revoked");
continue;
diff --git a/packages/taler-wallet-core/src/common.ts b/packages/taler-wallet-core/src/common.ts
index edaba5ba4..13c875575 100644
--- a/packages/taler-wallet-core/src/common.ts
+++ b/packages/taler-wallet-core/src/common.ts
@@ -31,6 +31,8 @@ import {
ExchangeUpdateStatus,
Logger,
RefreshReason,
+ TalerError,
+ TalerErrorCode,
TalerErrorDetail,
TalerPreciseTimestamp,
TalerProtocolTimestamp,
@@ -57,11 +59,11 @@ import {
PurchaseRecord,
RecoupGroupRecord,
RefreshGroupRecord,
- RewardRecord,
WalletDbReadWriteTransaction,
WithdrawalGroupRecord,
timestampPreciseToDb,
} from "./db.js";
+import { ReadyExchangeSummary } from "./exchanges.js";
import { createRefreshGroup } from "./refresh.js";
import { WalletExecutionContext, getDenomInfo } from "./wallet.js";
@@ -121,7 +123,10 @@ export async function makeCoinAvailable(
coinRecord.exchangeBaseUrl,
coinRecord.denomPubHash,
]);
- checkDbInvariant(!!denom);
+ checkDbInvariant(
+ !!denom,
+ `denomination of a coin is missing hash: ${coinRecord.denomPubHash}`,
+ );
const ageRestriction = coinRecord.maxAge;
let car = await tx.coinAvailability.get([
coinRecord.exchangeBaseUrl,
@@ -175,13 +180,19 @@ export async function spendCoins(
coin.exchangeBaseUrl,
coin.denomPubHash,
);
- checkDbInvariant(!!denom);
+ checkDbInvariant(
+ !!denom,
+ `denomination of a coin is missing hash: ${coin.denomPubHash}`,
+ );
const coinAvailability = await tx.coinAvailability.get([
coin.exchangeBaseUrl,
coin.denomPubHash,
coin.maxAge,
]);
- checkDbInvariant(!!coinAvailability);
+ checkDbInvariant(
+ !!coinAvailability,
+ `age denom info is missing for ${coin.maxAge}`,
+ );
const contrib = csi.contributions[i];
if (coin.status !== CoinStatus.Fresh) {
const alloc = coin.spendAllocation;
@@ -213,7 +224,6 @@ export async function spendCoins(
amount: Amounts.stringify(remaining.amount),
coinPub: coin.coinPub,
});
- checkDbInvariant(!!coinAvailability);
if (coinAvailability.freshCoinCount === 0) {
throw Error(
`invalid coin count ${coinAvailability.freshCoinCount} in DB`,
@@ -258,6 +268,9 @@ export enum TombstoneTag {
export function getExchangeTosStatusFromRecord(
exchange: ExchangeEntryRecord,
): ExchangeTosStatus {
+ if (exchange.tosCurrentEtag == null) {
+ return ExchangeTosStatus.MissingTos;
+ }
if (!exchange.tosAcceptedEtag) {
return ExchangeTosStatus.Proposed;
}
@@ -558,6 +571,28 @@ export function getAutoRefreshExecuteThreshold(d: {
}
/**
+ * Type and schema definitions for pending tasks in the wallet.
+ *
+ * These are only used internally, and are not part of the stable public
+ * interface to the wallet.
+ */
+
+export enum PendingTaskType {
+ ExchangeUpdate = "exchange-update",
+ Purchase = "purchase",
+ Refresh = "refresh",
+ Recoup = "recoup",
+ RewardPickup = "reward-pickup",
+ Withdraw = "withdraw",
+ Deposit = "deposit",
+ Backup = "backup",
+ PeerPushDebit = "peer-push-debit",
+ PeerPullCredit = "peer-pull-credit",
+ PeerPushCredit = "peer-push-credit",
+ PeerPullDebit = "peer-pull-debit",
+}
+
+/**
* Parsed representation of task identifiers.
*/
export type ParsedTaskIdentifier =
@@ -660,9 +695,6 @@ export namespace TaskIdentifiers {
exchBaseUrl,
)}` as TaskIdStr;
}
- export function forTipPickup(tipRecord: RewardRecord): TaskIdStr {
- return `${PendingTaskType.RewardPickup}:${tipRecord.walletRewardId}` as TaskIdStr;
- }
export function forRefresh(
refreshGroupRecord: RefreshGroupRecord,
): TaskIdStr {
@@ -747,28 +779,6 @@ export interface TransactionContext {
deleteTransaction(): Promise<void>;
}
-/**
- * Type and schema definitions for pending tasks in the wallet.
- *
- * These are only used internally, and are not part of the stable public
- * interface to the wallet.
- */
-
-export enum PendingTaskType {
- ExchangeUpdate = "exchange-update",
- Purchase = "purchase",
- Refresh = "refresh",
- Recoup = "recoup",
- RewardPickup = "reward-pickup",
- Withdraw = "withdraw",
- Deposit = "deposit",
- Backup = "backup",
- PeerPushDebit = "peer-push-debit",
- PeerPullCredit = "peer-pull-credit",
- PeerPushCredit = "peer-push-credit",
- PeerPullDebit = "peer-pull-debit",
-}
-
declare const __taskIdStr: unique symbol;
export type TaskIdStr = string & { [__taskIdStr]: true };
@@ -799,7 +809,7 @@ export async function genericWaitForState(
flag.raise();
}
});
- const unregisterOnCancelled = wex.cancellationToken.onCancelled(() => {
+ const unregisterOnCancelled = wex.cancellationToken.onCancelled((reason) => {
cancelNotif();
flag.raise();
});
@@ -819,5 +829,25 @@ export async function genericWaitForState(
} catch (e) {
unregisterOnCancelled();
cancelNotif();
+ throw e;
+ }
+}
+
+export function requireExchangeTosAcceptedOrThrow(
+ exchange: ReadyExchangeSummary,
+): void {
+ switch (exchange.tosStatus) {
+ case ExchangeTosStatus.Accepted:
+ case ExchangeTosStatus.MissingTos:
+ break;
+ default:
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_EXCHANGE_TOS_NOT_ACCEPTED,
+ {
+ exchangeBaseUrl: exchange.exchangeBaseUrl,
+ currentEtag: exchange.tosCurrentEtag,
+ tosStatus: exchange.tosStatus,
+ },
+ );
}
}
diff --git a/packages/taler-wallet-core/src/db.ts b/packages/taler-wallet-core/src/db.ts
index 44c241aed..5c381eea7 100644
--- a/packages/taler-wallet-core/src/db.ts
+++ b/packages/taler-wallet-core/src/db.ts
@@ -248,6 +248,9 @@ export function timestampOptionalAbsoluteFromDb(
* 0x0103_nnnn: aborting
* 0x0110_nnnn: suspended
* 0x0113_nnnn: suspended-aborting
+ * a=2: finalizing
+ * 0x0200_nnnn: finalizing
+ * 0x0210_nnnn: suspended-finalizing
* a=5: final
* 0x0500_nnnn: done
* 0x0501_nnnn: failed
@@ -260,12 +263,12 @@ export function timestampOptionalAbsoluteFromDb(
/**
* First possible operation status in the active range (inclusive).
*/
-export const OPERATION_STATUS_ACTIVE_FIRST = 0x0100_0000;
+export const OPERATION_STATUS_NONFINAL_FIRST = 0x0100_0000;
/**
* LAST possible operation status in the active range (inclusive).
*/
-export const OPERATION_STATUS_ACTIVE_LAST = 0x0113_ffff;
+export const OPERATION_STATUS_NONFINAL_LAST = 0x0210_ffff;
/**
* Status of a withdrawal.
@@ -395,6 +398,8 @@ export interface ReserveBankInfo {
timestampBankConfirmed: DbPreciseTimestamp | undefined;
wireTypes: string[] | undefined;
+
+ currency: string | undefined;
}
/**
@@ -918,92 +923,6 @@ export interface CoinAllocation {
amount: AmountString;
}
-/**
- * Status of a reward we got from a merchant.
- */
-export interface RewardRecord {
- /**
- * Has the user accepted the tip? Only after the tip has been accepted coins
- * withdrawn from the tip may be used.
- */
- acceptedTimestamp: DbPreciseTimestamp | undefined;
-
- /**
- * The tipped amount.
- */
- rewardAmountRaw: AmountString;
-
- /**
- * Effect on the balance (including fees etc).
- */
- rewardAmountEffective: AmountString;
-
- /**
- * Timestamp, the tip can't be picked up anymore after this deadline.
- */
- rewardExpiration: DbProtocolTimestamp;
-
- /**
- * The exchange that will sign our coins, chosen by the merchant.
- */
- exchangeBaseUrl: string;
-
- /**
- * Base URL of the merchant that is giving us the tip.
- */
- merchantBaseUrl: string;
-
- /**
- * Denomination selection made by the wallet for picking up
- * this tip.
- *
- * FIXME: Put this into some DenomSelectionCacheRecord instead of
- * storing it here!
- */
- denomsSel: DenomSelectionState;
-
- denomSelUid: string;
-
- /**
- * Tip ID chosen by the wallet.
- */
- walletRewardId: string;
-
- /**
- * Secret seed used to derive planchets for this tip.
- */
- secretSeed: string;
-
- /**
- * The merchant's identifier for this reward.
- */
- merchantRewardId: string;
-
- createdTimestamp: DbPreciseTimestamp;
-
- /**
- * The url to be redirected after the tip is accepted.
- */
- next_url: string | undefined;
-
- /**
- * Timestamp for when the wallet finished picking up the tip
- * from the merchant.
- */
- pickedUpTimestamp: DbPreciseTimestamp | undefined;
-
- status: RewardRecordStatus;
-}
-
-export enum RewardRecordStatus {
- PendingPickup = 0x0100_0000,
- SuspendedPickup = 0x0110_0000,
- DialogAccept = 0x0101_0000,
- Done = 0x0500_0000,
- Aborted = 0x0500_0000,
- Failed = 0x0501_000,
-}
-
export enum RefreshCoinStatus {
Pending = 0x0100_0000,
Finished = 0x0500_0000,
@@ -1178,10 +1097,15 @@ export enum PurchaseStatus {
/**
* Query for refund (until auto-refund deadline is reached).
+ *
+ * Legacy state for compatibility.
*/
PendingQueryingAutoRefund = 0x0100_0004,
SuspendedQueryingAutoRefund = 0x0110_0004,
+ FinalizingQueryingAutoRefund = 0x0200_0001,
+ SuspendedFinalizingQueryingAutoRefund = 0x0210_0001,
+
PendingAcceptRefund = 0x0100_0005,
SuspendedPendingAcceptRefund = 0x0110_0005,
@@ -1197,11 +1121,6 @@ export enum PurchaseStatus {
DialogShared = 0x0101_0001,
/**
- * The user has rejected the proposal.
- */
- AbortedProposalRefused = 0x0503_0000,
-
- /**
* Downloading or processing the proposal has failed permanently.
*/
FailedClaim = 0x0501_0000,
@@ -1224,13 +1143,18 @@ export enum PurchaseStatus {
DoneRepurchaseDetected = 0x0500_0001,
/**
- * The payment has been aborted.
+ * The user has rejected the proposal.
*/
- AbortedIncompletePayment = 0x0503_0000,
+ AbortedProposalRefused = 0x0503_0000,
AbortedRefunded = 0x0503_0001,
AbortedOrderDeleted = 0x0503_0002,
+
+ /**
+ * The payment has been aborted.
+ */
+ AbortedIncompletePayment = 0x0503_0003,
}
/**
@@ -1439,6 +1363,7 @@ export interface WgInfoBankIntegrated {
* a Taler-integrated bank.
*/
bankInfo: ReserveBankInfo;
+
/**
* Info about withdrawal accounts, possibly including currency conversion.
*/
@@ -1530,7 +1455,7 @@ export interface WithdrawalGroupRecord {
* The exchange base URL that we're withdrawing from.
* (Redundantly stored, as the reserve record also has this info.)
*/
- exchangeBaseUrl: string;
+ exchangeBaseUrl?: string;
/**
* When was the withdrawal operation started started?
@@ -1976,7 +1901,7 @@ export enum PeerPullPaymentCreditStatus {
SuspendedCreatePurse = 0x0110_0000,
SuspendedReady = 0x0110_0001,
SuspendedMergeKycRequired = 0x0110_0002,
- SuspendedWithdrawing = 0x0110_0000,
+ SuspendedWithdrawing = 0x0110_0003,
SuspendedAbortingDeletePurse = 0x0113_0000,
@@ -2630,9 +2555,10 @@ export const WalletStoresV1 = {
]),
},
),
+ // Just a tombstone at this point.
rewards: describeStore(
"rewards",
- describeContents<RewardRecord>({ keyPath: "walletRewardId" }),
+ describeContents<any>({ keyPath: "walletRewardId" }),
{
byMerchantTipIdAndBaseUrl: describeIndex("byMerchantRewardIdAndBaseUrl", [
"merchantRewardId",
@@ -2940,6 +2866,8 @@ export interface DbDump {
};
}
+const logger = new Logger("db.ts");
+
export async function exportSingleDb(
idb: IDBFactory,
dbName: string,
@@ -3081,8 +3009,6 @@ export interface FixupDescription {
*/
export const walletDbFixups: FixupDescription[] = [];
-const logger = new Logger("db.ts");
-
export async function applyFixups(
db: DbAccess<typeof WalletStoresV1>,
): Promise<void> {
diff --git a/packages/taler-wallet-core/src/dbless.ts b/packages/taler-wallet-core/src/dbless.ts
index d3085ecb4..ec9655e6f 100644
--- a/packages/taler-wallet-core/src/dbless.ts
+++ b/packages/taler-wallet-core/src/dbless.ts
@@ -123,7 +123,7 @@ export async function topupReserveWithBank(args: TopupReserveWithBankArgs) {
);
const bankInfo = await getBankWithdrawalInfo(http, wopi.taler_withdraw_uri);
const bankStatusUrl = getBankStatusUrl(wopi.taler_withdraw_uri);
- if (!bankInfo.suggestedExchange) {
+ if (!bankInfo.exchange) {
throw Error("no suggested exchange");
}
const plainPaytoUris =
diff --git a/packages/taler-wallet-core/src/deposits.ts b/packages/taler-wallet-core/src/deposits.ts
index c4cd98d73..2004c12cb 100644
--- a/packages/taler-wallet-core/src/deposits.ts
+++ b/packages/taler-wallet-core/src/deposits.ts
@@ -387,11 +387,19 @@ export function computeDepositTransactionActions(
case DepositOperationStatus.Finished:
return [TransactionAction.Delete];
case DepositOperationStatus.PendingDeposit:
- return [TransactionAction.Suspend, TransactionAction.Abort];
+ return [
+ TransactionAction.Retry,
+ TransactionAction.Suspend,
+ TransactionAction.Abort,
+ ];
case DepositOperationStatus.SuspendedDeposit:
return [TransactionAction.Resume];
case DepositOperationStatus.Aborting:
- return [TransactionAction.Fail, TransactionAction.Suspend];
+ return [
+ TransactionAction.Retry,
+ TransactionAction.Fail,
+ TransactionAction.Suspend,
+ ];
case DepositOperationStatus.Aborted:
return [TransactionAction.Delete];
case DepositOperationStatus.Failed:
@@ -399,9 +407,17 @@ export function computeDepositTransactionActions(
case DepositOperationStatus.SuspendedAborting:
return [TransactionAction.Resume, TransactionAction.Fail];
case DepositOperationStatus.PendingKyc:
- return [TransactionAction.Suspend, TransactionAction.Fail];
+ return [
+ TransactionAction.Retry,
+ TransactionAction.Suspend,
+ TransactionAction.Fail,
+ ];
case DepositOperationStatus.PendingTrack:
- return [TransactionAction.Suspend, TransactionAction.Abort];
+ return [
+ TransactionAction.Retry,
+ TransactionAction.Suspend,
+ TransactionAction.Abort,
+ ];
case DepositOperationStatus.SuspendedKyc:
return [TransactionAction.Resume, TransactionAction.Fail];
case DepositOperationStatus.SuspendedTrack:
@@ -441,7 +457,7 @@ async function refundDepositGroup(
{ storeNames: ["coins"] },
async (tx) => {
const coinRecord = await tx.coins.get(coinPub);
- checkDbInvariant(!!coinRecord);
+ checkDbInvariant(!!coinRecord, `coin ${coinPub} not found in DB`);
return coinRecord.exchangeBaseUrl;
},
);
diff --git a/packages/taler-wallet-core/src/exchanges.ts b/packages/taler-wallet-core/src/exchanges.ts
index d8063d561..dd88fa836 100644
--- a/packages/taler-wallet-core/src/exchanges.ts
+++ b/packages/taler-wallet-core/src/exchanges.ts
@@ -28,7 +28,6 @@ import {
AgeRestriction,
Amount,
Amounts,
- AsyncFlag,
CancellationToken,
CoinRefreshRequest,
CoinStatus,
@@ -53,6 +52,7 @@ import {
GetExchangeResourcesResponse,
GetExchangeTosResult,
GlobalFees,
+ HttpStatusCode,
LibtoolVersion,
Logger,
NotificationType,
@@ -79,6 +79,7 @@ import {
WireInfo,
assertUnreachable,
checkDbInvariant,
+ checkLogicInvariant,
codecForExchangeKeysJson,
durationMul,
encodeCrock,
@@ -93,6 +94,8 @@ import {
getExpiry,
readSuccessResponseJsonOrThrow,
readSuccessResponseTextOrThrow,
+ readTalerErrorResponse,
+ throwUnexpectedRequestError,
} from "@gnu-taler/taler-util/http";
import {
PendingTaskType,
@@ -103,6 +106,7 @@ import {
TransactionContext,
computeDbBackoff,
constructTaskIdentifier,
+ genericWaitForState,
getAutoRefreshExecuteThreshold,
getExchangeEntryStatusFromRecord,
getExchangeState,
@@ -861,6 +865,41 @@ async function downloadExchangeKeysInfo(
};
}
+type TosMetaResult = { type: "not-found" } | { type: "ok"; etag: string };
+
+/**
+ * Download metadata about an exchange's terms of service.
+ */
+async function downloadTosMeta(
+ wex: WalletExecutionContext,
+ exchangeBaseUrl: string,
+): Promise<TosMetaResult> {
+ logger.trace(`downloading exchange tos metadata for ${exchangeBaseUrl}`);
+ const reqUrl = new URL("terms", exchangeBaseUrl);
+
+ // FIXME: We can/should make a HEAD request here.
+ // Not sure if qtart supports it at the moment.
+ const resp = await wex.http.fetch(reqUrl.href, {
+ cancellationToken: wex.cancellationToken,
+ });
+
+ switch (resp.status) {
+ case HttpStatusCode.NotFound:
+ case HttpStatusCode.NotImplemented:
+ return { type: "not-found" };
+ case HttpStatusCode.Ok:
+ break;
+ default:
+ throwUnexpectedRequestError(resp, await readTalerErrorResponse(resp));
+ }
+
+ const etag = resp.headers.get("etag") || "unknown";
+ return {
+ type: "ok",
+ etag,
+ };
+}
+
async function downloadTosFromAcceptedFormat(
wex: WalletExecutionContext,
baseUrl: string,
@@ -977,9 +1016,7 @@ async function startUpdateExchangeEntry(
wex.ws.exchangeCache.clear();
await tx.exchanges.put(r);
const newExchangeState = getExchangeState(r);
- // Reset retries for updating the exchange entry.
const taskId = TaskIdentifiers.forExchangeUpdate(r);
- await tx.operationRetries.delete(taskId);
return { oldExchangeState, newExchangeState, taskId };
},
);
@@ -989,6 +1026,8 @@ async function startUpdateExchangeEntry(
newExchangeState: newExchangeState,
oldExchangeState: oldExchangeState,
});
+ logger.info(`start update ${exchangeBaseUrl} task ${taskId}`);
+
await wex.taskScheduler.resetTaskRetries(taskId);
}
@@ -1008,132 +1047,6 @@ export interface ReadyExchangeSummary {
scopeInfo: ScopeInfo;
}
-async function internalWaitReadyExchange(
- wex: WalletExecutionContext,
- canonUrl: string,
- exchangeNotifFlag: AsyncFlag,
- options: {
- cancellationToken?: CancellationToken;
- forceUpdate?: boolean;
- expectedMasterPub?: string;
- } = {},
-): Promise<ReadyExchangeSummary> {
- const operationId = constructTaskIdentifier({
- tag: PendingTaskType.ExchangeUpdate,
- exchangeBaseUrl: canonUrl,
- });
- while (true) {
- if (wex.cancellationToken.isCancelled) {
- throw Error("cancelled");
- }
- logger.info(`waiting for ready exchange ${canonUrl}`);
- const { exchange, exchangeDetails, retryInfo, scopeInfo } =
- await wex.db.runReadOnlyTx(
- {
- storeNames: [
- "exchanges",
- "exchangeDetails",
- "operationRetries",
- "globalCurrencyAuditors",
- "globalCurrencyExchanges",
- ],
- },
- async (tx) => {
- const exchange = await tx.exchanges.get(canonUrl);
- const exchangeDetails = await getExchangeRecordsInternal(
- tx,
- canonUrl,
- );
- const retryInfo = await tx.operationRetries.get(operationId);
- let scopeInfo: ScopeInfo | undefined = undefined;
- if (exchange && exchangeDetails) {
- scopeInfo = await internalGetExchangeScopeInfo(tx, exchangeDetails);
- }
- return { exchange, exchangeDetails, retryInfo, scopeInfo };
- },
- );
-
- if (!exchange) {
- throw Error("exchange entry does not exist anymore");
- }
-
- let ready = false;
-
- switch (exchange.updateStatus) {
- case ExchangeEntryDbUpdateStatus.Ready:
- ready = true;
- break;
- case ExchangeEntryDbUpdateStatus.ReadyUpdate:
- // If the update is forced,
- // we wait until we're in a full "ready" state,
- // as we're not happy with the stale information.
- if (!options.forceUpdate) {
- ready = true;
- }
- break;
- case ExchangeEntryDbUpdateStatus.UnavailableUpdate:
- throw TalerError.fromDetail(
- TalerErrorCode.WALLET_EXCHANGE_UNAVAILABLE,
- {
- exchangeBaseUrl: canonUrl,
- innerError: retryInfo?.lastError,
- },
- );
- default: {
- if (retryInfo) {
- throw TalerError.fromDetail(
- TalerErrorCode.WALLET_EXCHANGE_UNAVAILABLE,
- {
- exchangeBaseUrl: canonUrl,
- innerError: retryInfo?.lastError,
- },
- );
- }
- }
- }
-
- if (!ready) {
- logger.info("waiting for exchange update notification");
- await exchangeNotifFlag.wait();
- logger.info("done waiting for exchange update notification");
- exchangeNotifFlag.reset();
- continue;
- }
-
- if (!exchangeDetails) {
- throw Error("invariant failed");
- }
-
- if (!scopeInfo) {
- throw Error("invariant failed");
- }
-
- const res: ReadyExchangeSummary = {
- currency: exchangeDetails.currency,
- exchangeBaseUrl: canonUrl,
- masterPub: exchangeDetails.masterPublicKey,
- tosStatus: getExchangeTosStatusFromRecord(exchange),
- tosAcceptedEtag: exchange.tosAcceptedEtag,
- wireInfo: exchangeDetails.wireInfo,
- protocolVersionRange: exchangeDetails.protocolVersionRange,
- tosCurrentEtag: exchange.tosCurrentEtag,
- tosAcceptedTimestamp: timestampOptionalPreciseFromDb(
- exchange.tosAcceptedTimestamp,
- ),
- scopeInfo,
- };
-
- if (options.expectedMasterPub) {
- if (res.masterPub !== options.expectedMasterPub) {
- throw Error(
- "public key of the exchange does not match expected public key",
- );
- }
- }
- return res;
- }
-}
-
/**
* Ensure that a fresh exchange entry exists for the given
* exchange base URL.
@@ -1155,6 +1068,8 @@ export async function fetchFreshExchange(
forceUpdate?: boolean;
} = {},
): Promise<ReadyExchangeSummary> {
+ logger.info(`fetch fresh ${baseUrl} forced ${options.forceUpdate}`);
+
if (!options.forceUpdate) {
const cachedResp = wex.ws.exchangeCache.get(baseUrl);
if (cachedResp) {
@@ -1184,39 +1099,131 @@ async function waitReadyExchange(
} = {},
): Promise<ReadyExchangeSummary> {
logger.trace(`waiting for exchange ${canonUrl} to become ready`);
- // FIXME: We should use Symbol.dispose magic here for cleanup!
- const exchangeNotifFlag = new AsyncFlag();
- // Raise exchangeNotifFlag whenever we get a notification
- // about our exchange.
- const cancelNotif = wex.ws.addNotificationListener((notif) => {
- if (
- notif.type === NotificationType.ExchangeStateTransition &&
- notif.exchangeBaseUrl === canonUrl
- ) {
- logger.info(`raising update notification: ${j2s(notif)}`);
- exchangeNotifFlag.raise();
- }
+ const operationId = constructTaskIdentifier({
+ tag: PendingTaskType.ExchangeUpdate,
+ exchangeBaseUrl: canonUrl,
});
- const unregisterOnCancelled = wex.cancellationToken.onCancelled(() => {
- cancelNotif();
- exchangeNotifFlag.raise();
+ let res: ReadyExchangeSummary | undefined = undefined;
+
+ await genericWaitForState(wex, {
+ filterNotification(notif): boolean {
+ return (
+ notif.type === NotificationType.ExchangeStateTransition &&
+ notif.exchangeBaseUrl === canonUrl
+ );
+ },
+ async checkState(): Promise<boolean> {
+ const { exchange, exchangeDetails, retryInfo, scopeInfo } =
+ await wex.db.runReadOnlyTx(
+ {
+ storeNames: [
+ "exchanges",
+ "exchangeDetails",
+ "operationRetries",
+ "globalCurrencyAuditors",
+ "globalCurrencyExchanges",
+ ],
+ },
+ async (tx) => {
+ const exchange = await tx.exchanges.get(canonUrl);
+ const exchangeDetails = await getExchangeRecordsInternal(
+ tx,
+ canonUrl,
+ );
+ const retryInfo = await tx.operationRetries.get(operationId);
+ let scopeInfo: ScopeInfo | undefined = undefined;
+ if (exchange && exchangeDetails) {
+ scopeInfo = await internalGetExchangeScopeInfo(
+ tx,
+ exchangeDetails,
+ );
+ }
+ return { exchange, exchangeDetails, retryInfo, scopeInfo };
+ },
+ );
+
+ if (!exchange) {
+ throw Error("exchange entry does not exist anymore");
+ }
+
+ let ready = false;
+
+ switch (exchange.updateStatus) {
+ case ExchangeEntryDbUpdateStatus.Ready:
+ ready = true;
+ break;
+ case ExchangeEntryDbUpdateStatus.ReadyUpdate:
+ // If the update is forced,
+ // we wait until we're in a full "ready" state,
+ // as we're not happy with the stale information.
+ if (!options.forceUpdate) {
+ ready = true;
+ }
+ break;
+ case ExchangeEntryDbUpdateStatus.UnavailableUpdate:
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_EXCHANGE_UNAVAILABLE,
+ {
+ exchangeBaseUrl: canonUrl,
+ innerError: retryInfo?.lastError,
+ },
+ );
+ default: {
+ if (retryInfo) {
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_EXCHANGE_UNAVAILABLE,
+ {
+ exchangeBaseUrl: canonUrl,
+ innerError: retryInfo?.lastError,
+ },
+ );
+ }
+ }
+ }
+
+ if (!ready) {
+ return false;
+ }
+
+ if (!exchangeDetails) {
+ throw Error("invariant failed");
+ }
+
+ if (!scopeInfo) {
+ throw Error("invariant failed");
+ }
+
+ const mySummary: ReadyExchangeSummary = {
+ currency: exchangeDetails.currency,
+ exchangeBaseUrl: canonUrl,
+ masterPub: exchangeDetails.masterPublicKey,
+ tosStatus: getExchangeTosStatusFromRecord(exchange),
+ tosAcceptedEtag: exchange.tosAcceptedEtag,
+ wireInfo: exchangeDetails.wireInfo,
+ protocolVersionRange: exchangeDetails.protocolVersionRange,
+ tosCurrentEtag: exchange.tosCurrentEtag,
+ tosAcceptedTimestamp: timestampOptionalPreciseFromDb(
+ exchange.tosAcceptedTimestamp,
+ ),
+ scopeInfo,
+ };
+
+ if (options.expectedMasterPub) {
+ if (mySummary.masterPub !== options.expectedMasterPub) {
+ throw Error(
+ "public key of the exchange does not match expected public key",
+ );
+ }
+ }
+ res = mySummary;
+ return true;
+ },
});
- try {
- const res = await internalWaitReadyExchange(
- wex,
- canonUrl,
- exchangeNotifFlag,
- options,
- );
- logger.info("done waiting for ready exchange");
- return res;
- } finally {
- unregisterOnCancelled();
- cancelNotif();
- }
+ checkLogicInvariant(!!res);
+ return res;
}
function checkPeerPaymentsDisabled(
@@ -1359,7 +1366,6 @@ export async function updateExchangeFromUrlHandler(
);
refreshCheckNecessary = false;
}
-
if (!(updateNecessary || refreshCheckNecessary)) {
logger.trace("update not necessary, running again later");
return TaskRunResult.runAgainAt(
@@ -1421,15 +1427,7 @@ export async function updateExchangeFromUrlHandler(
logger.trace("finished validating exchange /wire info");
- // We download the text/plain version here,
- // because that one needs to exist, and we
- // will get the current etag from the response.
- const tosDownload = await downloadTosFromAcceptedFormat(
- wex,
- exchangeBaseUrl,
- timeout,
- ["text/plain"],
- );
+ const tosMeta = await downloadTosMeta(wex, exchangeBaseUrl);
logger.trace("updating exchange info in database");
@@ -1522,7 +1520,14 @@ export async function updateExchangeFromUrlHandler(
};
r.noFees = noFees;
r.peerPaymentsDisabled = peerPaymentsDisabled;
- r.tosCurrentEtag = tosDownload.tosEtag;
+ switch (tosMeta.type) {
+ case "not-found":
+ r.tosCurrentEtag = undefined;
+ break;
+ case "ok":
+ r.tosCurrentEtag = tosMeta.etag;
+ break;
+ }
if (existingDetails?.rowId) {
newDetails.rowId = existingDetails.rowId;
}
@@ -1548,7 +1553,10 @@ export async function updateExchangeFromUrlHandler(
r.cachebreakNextUpdate = false;
await tx.exchanges.put(r);
const drRowId = await tx.exchangeDetails.put(newDetails);
- checkDbInvariant(typeof drRowId.key === "number");
+ checkDbInvariant(
+ typeof drRowId.key === "number",
+ "exchange details key is not a number",
+ );
for (const sk of keysInfo.signingKeys) {
// FIXME: validate signing keys before inserting them
@@ -2227,10 +2235,12 @@ export async function markExchangeUsed(
logger.info(`marking exchange ${exchangeBaseUrl} as used`);
const exch = await tx.exchanges.get(exchangeBaseUrl);
if (!exch) {
+ logger.info(`exchange ${exchangeBaseUrl} NOT found`);
return {
notif: undefined,
};
}
+
const oldExchangeState = getExchangeState(exch);
switch (exch.entryStatus) {
case ExchangeEntryDbRecordStatus.Ephemeral:
diff --git a/packages/taler-wallet-core/src/instructedAmountConversion.ts b/packages/taler-wallet-core/src/instructedAmountConversion.ts
index 1f7d95959..5b399a0a7 100644
--- a/packages/taler-wallet-core/src/instructedAmountConversion.ts
+++ b/packages/taler-wallet-core/src/instructedAmountConversion.ts
@@ -283,7 +283,7 @@ async function getAvailableDenoms(
coinAvail.exchangeBaseUrl,
coinAvail.denomPubHash,
]);
- checkDbInvariant(!!denom);
+ checkDbInvariant(!!denom, `denomination of a coin is missing hash: ${coinAvail.denomPubHash}`);
if (denom.isRevoked || !denom.isOffered) {
continue;
}
@@ -472,7 +472,7 @@ export async function getMaxDepositAmount(
export function getMaxDepositAmountForAvailableCoins(
denoms: AvailableCoins,
currency: string,
-) {
+): AmountWithFee {
const zero = Amounts.zeroOfCurrency(currency);
if (!denoms.list.length) {
// no coins in the database
@@ -663,8 +663,13 @@ function rankDenominationForWithdrawals(
//different exchanges may have different wireFee
//ranking should take the relative contribution in the exchange
//which is (value - denomFee / fixedFee)
- const rate1 = Amounts.divmod(d1.value, d1.denomWithdraw).quotient;
- const rate2 = Amounts.divmod(d2.value, d2.denomWithdraw).quotient;
+
+ const rate1 = Amounts.isZero(d1.denomWithdraw)
+ ? Number.MIN_SAFE_INTEGER
+ : Amounts.divmod(d1.value, d1.denomWithdraw).quotient;
+ const rate2 = Amounts.isZero(d2.denomWithdraw)
+ ? Number.MIN_SAFE_INTEGER
+ : Amounts.divmod(d2.value, d2.denomWithdraw).quotient;
const contribCmp = rate1 === rate2 ? 0 : rate1 < rate2 ? 1 : -1;
return (
contribCmp ||
@@ -719,8 +724,13 @@ function rankDenominationForDeposit(
//different exchanges may have different wireFee
//ranking should take the relative contribution in the exchange
//which is (value - denomFee / fixedFee)
- const rate1 = Amounts.divmod(d1.value, d1.denomDeposit).quotient;
- const rate2 = Amounts.divmod(d2.value, d2.denomDeposit).quotient;
+ const rate1 = Amounts.isZero(d1.denomDeposit)
+ ? Number.MIN_SAFE_INTEGER
+ : Amounts.divmod(d1.value, d1.denomDeposit).quotient;
+ const rate2 = Amounts.isZero(d2.denomDeposit)
+ ? Number.MIN_SAFE_INTEGER
+ : Amounts.divmod(d2.value, d2.denomDeposit).quotient;
+
const contribCmp = rate1 === rate2 ? 0 : rate1 < rate2 ? 1 : -1;
return (
contribCmp ||
diff --git a/packages/taler-wallet-core/src/pay-merchant.ts b/packages/taler-wallet-core/src/pay-merchant.ts
index 090a11cf0..ee154252f 100644
--- a/packages/taler-wallet-core/src/pay-merchant.ts
+++ b/packages/taler-wallet-core/src/pay-merchant.ts
@@ -34,7 +34,6 @@ import {
assertUnreachable,
AsyncFlag,
checkDbInvariant,
- CheckPaymentResponse,
CheckPayTemplateReponse,
CheckPayTemplateRequest,
codecForAbortResponse,
@@ -342,7 +341,6 @@ export class PayMerchantTransactionContext implements TransactionContext {
return;
}
await tx.purchases.put(purchase);
- await tx.operationRetries.delete(this.taskId);
const newTxState = computePayMerchantTransactionState(purchase);
return { oldTxState, newTxState };
},
@@ -1028,11 +1026,17 @@ async function storeFirstPaySuccess(
purchase.merchantPaySig = payResponse.sig;
purchase.posConfirmation = payResponse.pos_confirmation;
const dl = purchase.download;
- checkDbInvariant(!!dl);
+ checkDbInvariant(
+ !!dl,
+ `purchase ${purchase.orderId} without ct downloaded`,
+ );
const contractTermsRecord = await tx.contractTerms.get(
dl.contractTermsHash,
);
- checkDbInvariant(!!contractTermsRecord);
+ checkDbInvariant(
+ !!contractTermsRecord,
+ `no contract terms found for purchase ${purchase.orderId}`,
+ );
const contractData = extractContractData(
contractTermsRecord.contractTermsRaw,
dl.contractTermsHash,
@@ -1625,6 +1629,9 @@ export async function checkPayForTemplate(
throw TalerError.fromUncheckedDetail(cfg.detail);
}
+ // FIXME: Put body.currencies *and* body.currency in the set of
+ // supported currencies.
+
return {
templateDetails,
supportedCurrencies: Object.keys(cfg.body.currencies),
@@ -2086,6 +2093,7 @@ export async function processPurchase(
case PurchaseStatus.PendingPayingReplay:
return processPurchasePay(wex, proposalId);
case PurchaseStatus.PendingQueryingRefund:
+ case PurchaseStatus.FinalizingQueryingAutoRefund:
return processPurchaseQueryRefund(wex, purchase);
case PurchaseStatus.PendingQueryingAutoRefund:
return processPurchaseAutoRefund(wex, purchase);
@@ -2110,6 +2118,7 @@ export async function processPurchase(
case PurchaseStatus.SuspendedPendingAcceptRefund:
case PurchaseStatus.SuspendedQueryingAutoRefund:
case PurchaseStatus.SuspendedQueryingRefund:
+ case PurchaseStatus.SuspendedFinalizingQueryingAutoRefund:
case PurchaseStatus.FailedAbort:
case PurchaseStatus.FailedPaidByOther:
return TaskRunResult.finished();
@@ -2155,7 +2164,7 @@ async function processPurchasePay(
logger.trace(`paying with session ID ${sessionId}`);
const payInfo = purchase.payInfo;
- checkDbInvariant(!!payInfo, "payInfo");
+ checkDbInvariant(!!payInfo, `purchase ${purchase.orderId} without payInfo`);
const download = await expectProposalDownload(wex, purchase);
@@ -2487,6 +2496,9 @@ const transitionSuspend: {
[PurchaseStatus.PendingQueryingAutoRefund]: {
next: PurchaseStatus.SuspendedQueryingAutoRefund,
},
+ [PurchaseStatus.FinalizingQueryingAutoRefund]: {
+ next: PurchaseStatus.SuspendedFinalizingQueryingAutoRefund,
+ },
};
const transitionResume: {
@@ -2509,6 +2521,9 @@ const transitionResume: {
[PurchaseStatus.SuspendedQueryingAutoRefund]: {
next: PurchaseStatus.PendingQueryingAutoRefund,
},
+ [PurchaseStatus.SuspendedFinalizingQueryingAutoRefund]: {
+ next: PurchaseStatus.FinalizingQueryingAutoRefund,
+ },
};
export function computePayMerchantTransactionState(
@@ -2637,6 +2652,16 @@ export function computePayMerchantTransactionState(
major: TransactionMajorState.Failed,
minor: TransactionMinorState.PaidByOther,
};
+ case PurchaseStatus.FinalizingQueryingAutoRefund:
+ return {
+ major: TransactionMajorState.Finalizing,
+ minor: TransactionMinorState.AutoRefund,
+ };
+ case PurchaseStatus.SuspendedFinalizingQueryingAutoRefund:
+ return {
+ major: TransactionMajorState.SuspendedFinalizing,
+ minor: TransactionMinorState.AutoRefund,
+ };
default:
assertUnreachable(purchaseRecord.purchaseStatus);
}
@@ -2648,21 +2673,45 @@ export function computePayMerchantTransactionActions(
switch (purchaseRecord.purchaseStatus) {
// Pending States
case PurchaseStatus.PendingDownloadingProposal:
- return [TransactionAction.Suspend, TransactionAction.Abort];
+ return [
+ TransactionAction.Retry,
+ TransactionAction.Suspend,
+ TransactionAction.Abort,
+ ];
case PurchaseStatus.PendingPaying:
- return [TransactionAction.Suspend, TransactionAction.Abort];
+ return [
+ TransactionAction.Retry,
+ TransactionAction.Suspend,
+ TransactionAction.Abort,
+ ];
case PurchaseStatus.PendingPayingReplay:
// Special "abort" since it goes back to "done".
- return [TransactionAction.Suspend, TransactionAction.Abort];
+ return [
+ TransactionAction.Retry,
+ TransactionAction.Suspend,
+ TransactionAction.Abort,
+ ];
case PurchaseStatus.PendingQueryingAutoRefund:
// Special "abort" since it goes back to "done".
- return [TransactionAction.Suspend, TransactionAction.Abort];
+ return [
+ TransactionAction.Retry,
+ TransactionAction.Suspend,
+ TransactionAction.Abort,
+ ];
case PurchaseStatus.PendingQueryingRefund:
// Special "abort" since it goes back to "done".
- return [TransactionAction.Suspend, TransactionAction.Abort];
+ return [
+ TransactionAction.Retry,
+ TransactionAction.Suspend,
+ TransactionAction.Abort,
+ ];
case PurchaseStatus.PendingAcceptRefund:
// Special "abort" since it goes back to "done".
- return [TransactionAction.Suspend, TransactionAction.Abort];
+ return [
+ TransactionAction.Retry,
+ TransactionAction.Suspend,
+ TransactionAction.Abort,
+ ];
// Suspended Pending States
case PurchaseStatus.SuspendedDownloadingProposal:
return [TransactionAction.Resume, TransactionAction.Abort];
@@ -2682,14 +2731,18 @@ export function computePayMerchantTransactionActions(
return [TransactionAction.Resume, TransactionAction.Abort];
// Aborting States
case PurchaseStatus.AbortingWithRefund:
- return [TransactionAction.Fail, TransactionAction.Suspend];
+ return [
+ TransactionAction.Retry,
+ TransactionAction.Fail,
+ TransactionAction.Suspend,
+ ];
case PurchaseStatus.SuspendedAbortingWithRefund:
return [TransactionAction.Fail, TransactionAction.Resume];
// Dialog States
case PurchaseStatus.DialogProposed:
- return [];
+ return [TransactionAction.Retry];
case PurchaseStatus.DialogShared:
- return [];
+ return [TransactionAction.Retry];
// Final States
case PurchaseStatus.AbortedProposalRefused:
case PurchaseStatus.AbortedOrderDeleted:
@@ -2707,6 +2760,14 @@ export function computePayMerchantTransactionActions(
return [TransactionAction.Delete];
case PurchaseStatus.FailedPaidByOther:
return [TransactionAction.Delete];
+ case PurchaseStatus.FinalizingQueryingAutoRefund:
+ return [
+ TransactionAction.Suspend,
+ TransactionAction.Retry,
+ TransactionAction.Delete,
+ ];
+ case PurchaseStatus.SuspendedFinalizingQueryingAutoRefund:
+ return [TransactionAction.Resume, TransactionAction.Delete];
default:
assertUnreachable(purchaseRecord.purchaseStatus);
}
@@ -2909,8 +2970,12 @@ async function processPurchaseAutoRefund(
logger.warn("purchase does not exist anymore");
return;
}
- if (p.purchaseStatus !== PurchaseStatus.PendingQueryingAutoRefund) {
- return;
+ switch (p.purchaseStatus) {
+ case PurchaseStatus.PendingQueryingAutoRefund:
+ case PurchaseStatus.FinalizingQueryingAutoRefund:
+ break;
+ default:
+ return;
}
const oldTxState = computePayMerchantTransactionState(p);
p.purchaseStatus = PurchaseStatus.Done;
@@ -2956,8 +3021,12 @@ async function processPurchaseAutoRefund(
logger.warn("purchase does not exist anymore");
return;
}
- if (p.purchaseStatus !== PurchaseStatus.PendingQueryingAutoRefund) {
- return;
+ switch (p.purchaseStatus) {
+ case PurchaseStatus.PendingQueryingAutoRefund:
+ case PurchaseStatus.FinalizingQueryingAutoRefund:
+ break;
+ default:
+ return;
}
const oldTxState = computePayMerchantTransactionState(p);
p.purchaseStatus = PurchaseStatus.PendingAcceptRefund;
@@ -2997,7 +3066,7 @@ async function processPurchaseAbortingRefund(
for (let i = 0; i < payCoinSelection.coinPubs.length; i++) {
const coinPub = payCoinSelection.coinPubs[i];
const coin = await tx.coins.get(coinPub);
- checkDbInvariant(!!coin, "expected coin to be present");
+ checkDbInvariant(!!coin, `coin not found for ${coinPub}`);
abortingCoins.push({
coin_pub: coinPub,
contribution: Amounts.stringify(payCoinSelection.coinContributions[i]),
@@ -3501,7 +3570,8 @@ async function storeRefunds(
if (isAborting) {
myPurchase.purchaseStatus = PurchaseStatus.AbortedRefunded;
} else if (shouldCheckAutoRefund) {
- myPurchase.purchaseStatus = PurchaseStatus.PendingQueryingAutoRefund;
+ myPurchase.purchaseStatus =
+ PurchaseStatus.FinalizingQueryingAutoRefund;
} else {
myPurchase.purchaseStatus = PurchaseStatus.Done;
}
diff --git a/packages/taler-wallet-core/src/pay-peer-common.ts b/packages/taler-wallet-core/src/pay-peer-common.ts
index bfd39b657..a1729ced7 100644
--- a/packages/taler-wallet-core/src/pay-peer-common.ts
+++ b/packages/taler-wallet-core/src/pay-peer-common.ts
@@ -140,10 +140,10 @@ export async function getMergeReserveInfo(
{ storeNames: ["exchanges", "reserves"] },
async (tx) => {
const ex = await tx.exchanges.get(req.exchangeBaseUrl);
- checkDbInvariant(!!ex);
+ checkDbInvariant(!!ex, `no exchange record for ${req.exchangeBaseUrl}`);
if (ex.currentMergeReserveRowId != null) {
const reserve = await tx.reserves.get(ex.currentMergeReserveRowId);
- checkDbInvariant(!!reserve);
+ checkDbInvariant(!!reserve, `reserver ${ex.currentMergeReserveRowId} missing in db`);
return reserve;
}
const reserve: ReserveRecord = {
@@ -151,7 +151,7 @@ export async function getMergeReserveInfo(
reservePub: newReservePair.pub,
};
const insertResp = await tx.reserves.put(reserve);
- checkDbInvariant(typeof insertResp.key === "number");
+ checkDbInvariant(typeof insertResp.key === "number", `reserve key is not a number`);
reserve.rowId = insertResp.key;
ex.currentMergeReserveRowId = reserve.rowId;
await tx.exchanges.put(ex);
diff --git a/packages/taler-wallet-core/src/pay-peer-pull-credit.ts b/packages/taler-wallet-core/src/pay-peer-pull-credit.ts
index 840c244d0..3e7fdd36b 100644
--- a/packages/taler-wallet-core/src/pay-peer-pull-credit.ts
+++ b/packages/taler-wallet-core/src/pay-peer-pull-credit.ts
@@ -59,6 +59,7 @@ import {
TombstoneTag,
TransactionContext,
constructTaskIdentifier,
+ requireExchangeTosAcceptedOrThrow,
} from "./common.js";
import {
KycPendingInfo,
@@ -933,6 +934,11 @@ export async function checkPeerPullPaymentInitiation(
Amounts.parseOrThrow(req.amount),
undefined,
);
+ if (wi.selectedDenoms.selectedDenoms.length === 0) {
+ throw Error(
+ `unable to check pull payment from ${exchangeUrl}, can't select denominations for instructed amount (${req.amount}`,
+ );
+ }
logger.trace(`got withdrawal info`);
@@ -1021,7 +1027,8 @@ export async function initiatePeerPullPayment(
const exchangeBaseUrl = maybeExchangeBaseUrl;
- await fetchFreshExchange(wex, exchangeBaseUrl);
+ const exchange = await fetchFreshExchange(wex, exchangeBaseUrl);
+ requireExchangeTosAcceptedOrThrow(exchange);
const mergeReserveInfo = await getMergeReserveInfo(wex, {
exchangeBaseUrl: exchangeBaseUrl,
@@ -1039,7 +1046,10 @@ export async function initiatePeerPullPayment(
const withdrawalGroupId = encodeCrock(getRandomBytes(32));
const mergeReserveRowId = mergeReserveInfo.rowId;
- checkDbInvariant(!!mergeReserveRowId);
+ checkDbInvariant(
+ !!mergeReserveRowId,
+ `merge reserve for ${exchangeBaseUrl} without rowid`,
+ );
const contractEncNonce = encodeCrock(getRandomBytes(24));
@@ -1049,6 +1059,11 @@ export async function initiatePeerPullPayment(
Amounts.parseOrThrow(req.partialContractTerms.amount),
undefined,
);
+ if (wi.selectedDenoms.selectedDenoms.length === 0) {
+ throw Error(
+ `unable to initiate pull payment from ${exchangeBaseUrl}, can't select denominations for instructed amount (${req.partialContractTerms.amount}`,
+ );
+ }
const mergeTimestamp = TalerPreciseTimestamp.now();
@@ -1184,15 +1199,31 @@ export function computePeerPullCreditTransactionActions(
): TransactionAction[] {
switch (pullCreditRecord.status) {
case PeerPullPaymentCreditStatus.PendingCreatePurse:
- return [TransactionAction.Abort, TransactionAction.Suspend];
+ return [
+ TransactionAction.Retry,
+ TransactionAction.Abort,
+ TransactionAction.Suspend,
+ ];
case PeerPullPaymentCreditStatus.PendingMergeKycRequired:
- return [TransactionAction.Abort, TransactionAction.Suspend];
+ return [
+ TransactionAction.Retry,
+ TransactionAction.Abort,
+ TransactionAction.Suspend,
+ ];
case PeerPullPaymentCreditStatus.PendingReady:
- return [TransactionAction.Abort, TransactionAction.Suspend];
+ return [
+ TransactionAction.Retry,
+ TransactionAction.Abort,
+ TransactionAction.Suspend,
+ ];
case PeerPullPaymentCreditStatus.Done:
return [TransactionAction.Delete];
case PeerPullPaymentCreditStatus.PendingWithdrawing:
- return [TransactionAction.Abort, TransactionAction.Suspend];
+ return [
+ TransactionAction.Retry,
+ TransactionAction.Abort,
+ TransactionAction.Suspend,
+ ];
case PeerPullPaymentCreditStatus.SuspendedCreatePurse:
return [TransactionAction.Resume, TransactionAction.Abort];
case PeerPullPaymentCreditStatus.SuspendedReady:
@@ -1204,7 +1235,11 @@ export function computePeerPullCreditTransactionActions(
case PeerPullPaymentCreditStatus.Aborted:
return [TransactionAction.Delete];
case PeerPullPaymentCreditStatus.AbortingDeletePurse:
- return [TransactionAction.Suspend, TransactionAction.Fail];
+ return [
+ TransactionAction.Retry,
+ TransactionAction.Suspend,
+ TransactionAction.Fail,
+ ];
case PeerPullPaymentCreditStatus.Failed:
return [TransactionAction.Delete];
case PeerPullPaymentCreditStatus.Expired:
diff --git a/packages/taler-wallet-core/src/pay-peer-pull-debit.ts b/packages/taler-wallet-core/src/pay-peer-pull-debit.ts
index 0355b58ad..e9be15026 100644
--- a/packages/taler-wallet-core/src/pay-peer-pull-debit.ts
+++ b/packages/taler-wallet-core/src/pay-peer-pull-debit.ts
@@ -1000,7 +1000,7 @@ export function computePeerPullDebitTransactionActions(
): TransactionAction[] {
switch (pullDebitRecord.status) {
case PeerPullDebitRecordStatus.DialogProposed:
- return [];
+ return [TransactionAction.Retry, TransactionAction.Delete];
case PeerPullDebitRecordStatus.PendingDeposit:
return [TransactionAction.Abort, TransactionAction.Suspend];
case PeerPullDebitRecordStatus.Done:
diff --git a/packages/taler-wallet-core/src/pay-peer-push-credit.ts b/packages/taler-wallet-core/src/pay-peer-push-credit.ts
index 93f1a63a7..5a1bfbdbd 100644
--- a/packages/taler-wallet-core/src/pay-peer-push-credit.ts
+++ b/packages/taler-wallet-core/src/pay-peer-push-credit.ts
@@ -61,6 +61,7 @@ import {
TombstoneTag,
TransactionContext,
constructTaskIdentifier,
+ requireExchangeTosAcceptedOrThrow,
} from "./common.js";
import {
KycPendingInfo,
@@ -407,7 +408,8 @@ export async function preparePeerPushCredit(
const exchangeBaseUrl = uri.exchangeBaseUrl;
- await fetchFreshExchange(wex, exchangeBaseUrl);
+ const exchange = await fetchFreshExchange(wex, exchangeBaseUrl);
+ requireExchangeTosAcceptedOrThrow(exchange);
const contractPriv = uri.contractPriv;
const contractPub = encodeCrock(eddsaGetPublic(decodeCrock(contractPriv)));
@@ -459,6 +461,12 @@ export async function preparePeerPushCredit(
undefined,
);
+ if (wi.selectedDenoms.selectedDenoms.length === 0) {
+ throw Error(
+ `unable to prepare push credit from ${exchangeBaseUrl}, can't select denominations for instructed amount (${purseStatus.balance}`,
+ );
+ }
+
const transitionInfo = await wex.db.runReadWriteTx(
{ storeNames: ["contractTerms", "peerPushCredit"] },
async (tx) => {
@@ -872,7 +880,10 @@ export async function processPeerPushCredit(
`processing peerPushCredit in state ${peerInc.status.toString(16)}`,
);
- checkDbInvariant(!!contractTerms);
+ checkDbInvariant(
+ !!contractTerms,
+ `not contract terms for peer push ${peerPushCreditId}`,
+ );
switch (peerInc.status) {
case PeerPushCreditStatus.PendingMergeKycRequired: {
@@ -1011,15 +1022,27 @@ export function computePeerPushCreditTransactionActions(
): TransactionAction[] {
switch (pushCreditRecord.status) {
case PeerPushCreditStatus.DialogProposed:
- return [TransactionAction.Delete];
+ return [TransactionAction.Retry, TransactionAction.Delete];
case PeerPushCreditStatus.PendingMerge:
- return [TransactionAction.Abort, TransactionAction.Suspend];
+ return [
+ TransactionAction.Retry,
+ TransactionAction.Abort,
+ TransactionAction.Suspend,
+ ];
case PeerPushCreditStatus.Done:
return [TransactionAction.Delete];
case PeerPushCreditStatus.PendingMergeKycRequired:
- return [TransactionAction.Abort, TransactionAction.Suspend];
+ return [
+ TransactionAction.Retry,
+ TransactionAction.Abort,
+ TransactionAction.Suspend,
+ ];
case PeerPushCreditStatus.PendingWithdrawing:
- return [TransactionAction.Suspend, TransactionAction.Fail];
+ return [
+ TransactionAction.Retry,
+ TransactionAction.Suspend,
+ TransactionAction.Fail,
+ ];
case PeerPushCreditStatus.SuspendedMerge:
return [TransactionAction.Resume, TransactionAction.Abort];
case PeerPushCreditStatus.SuspendedMergeKycRequired:
diff --git a/packages/taler-wallet-core/src/pay-peer-push-debit.ts b/packages/taler-wallet-core/src/pay-peer-push-debit.ts
index 6452407ff..f8e6adb3c 100644
--- a/packages/taler-wallet-core/src/pay-peer-push-debit.ts
+++ b/packages/taler-wallet-core/src/pay-peer-push-debit.ts
@@ -406,7 +406,10 @@ async function handlePurseCreationConflict(
const instructedAmount = Amounts.parseOrThrow(peerPushInitiation.amount);
const sel = peerPushInitiation.coinSel;
- checkDbInvariant(!!sel);
+ checkDbInvariant(
+ !!sel,
+ `no coin selected for peer push initiation ${peerPushInitiation.pursePub}`,
+ );
const repair: PreviousPayCoins = [];
@@ -1218,17 +1221,37 @@ export function computePeerPushDebitTransactionActions(
): TransactionAction[] {
switch (ppiRecord.status) {
case PeerPushDebitStatus.PendingCreatePurse:
- return [TransactionAction.Abort, TransactionAction.Suspend];
+ return [
+ TransactionAction.Retry,
+ TransactionAction.Abort,
+ TransactionAction.Suspend,
+ ];
case PeerPushDebitStatus.PendingReady:
- return [TransactionAction.Abort, TransactionAction.Suspend];
+ return [
+ TransactionAction.Retry,
+ TransactionAction.Abort,
+ TransactionAction.Suspend,
+ ];
case PeerPushDebitStatus.Aborted:
return [TransactionAction.Delete];
case PeerPushDebitStatus.AbortingDeletePurse:
- return [TransactionAction.Suspend, TransactionAction.Fail];
+ return [
+ TransactionAction.Retry,
+ TransactionAction.Suspend,
+ TransactionAction.Fail,
+ ];
case PeerPushDebitStatus.AbortingRefreshDeleted:
- return [TransactionAction.Suspend, TransactionAction.Fail];
+ return [
+ TransactionAction.Retry,
+ TransactionAction.Suspend,
+ TransactionAction.Fail,
+ ];
case PeerPushDebitStatus.AbortingRefreshExpired:
- return [TransactionAction.Suspend, TransactionAction.Fail];
+ return [
+ TransactionAction.Retry,
+ TransactionAction.Suspend,
+ TransactionAction.Fail,
+ ];
case PeerPushDebitStatus.SuspendedAbortingRefreshExpired:
return [TransactionAction.Resume, TransactionAction.Fail];
case PeerPushDebitStatus.SuspendedAbortingDeletePurse:
diff --git a/packages/taler-wallet-core/src/recoup.ts b/packages/taler-wallet-core/src/recoup.ts
index 6a09f9a0e..be5731b0b 100644
--- a/packages/taler-wallet-core/src/recoup.ts
+++ b/packages/taler-wallet-core/src/recoup.ts
@@ -199,8 +199,8 @@ async function recoupRefreshCoin(
revokedCoin.exchangeBaseUrl,
revokedCoin.denomPubHash,
);
- checkDbInvariant(!!oldCoinDenom);
- checkDbInvariant(!!revokedCoinDenom);
+ checkDbInvariant(!!oldCoinDenom, `no denom for coin, hash ${oldCoin.denomPubHash}`);
+ checkDbInvariant(!!revokedCoinDenom, `no revoked denom for coin, hash ${revokedCoin.denomPubHash}`);
revokedCoin.status = CoinStatus.Dormant;
if (!revokedCoin.spendAllocation) {
// We don't know what happened to this coin
diff --git a/packages/taler-wallet-core/src/refresh.ts b/packages/taler-wallet-core/src/refresh.ts
index 7800967e6..05c65f6b6 100644
--- a/packages/taler-wallet-core/src/refresh.ts
+++ b/packages/taler-wallet-core/src/refresh.ts
@@ -29,7 +29,6 @@ import {
Amounts,
amountToPretty,
assertUnreachable,
- AsyncFlag,
checkDbInvariant,
codecForCoinHistoryResponse,
codecForExchangeMeltResponse,
@@ -68,12 +67,14 @@ import {
WalletNotification,
} from "@gnu-taler/taler-util";
import {
+ HttpResponse,
readSuccessResponseJsonOrThrow,
readTalerErrorResponse,
throwUnexpectedRequestError,
} from "@gnu-taler/taler-util/http";
import {
constructTaskIdentifier,
+ genericWaitForState,
makeCoinsVisible,
PendingTaskType,
TaskIdStr,
@@ -386,7 +387,6 @@ async function getCoinAvailabilityForDenom(
denom: DenominationInfo,
ageRestriction: number,
): Promise<CoinAvailabilityRecord> {
- checkDbInvariant(!!denom);
let car = await tx.coinAvailability.get([
denom.exchangeBaseUrl,
denom.denomPubHash,
@@ -537,7 +537,10 @@ async function destroyRefreshSession(
denom,
oldCoin.maxAge,
);
- checkDbInvariant(car.pendingRefreshOutputCount != null);
+ checkDbInvariant(
+ car.pendingRefreshOutputCount != null,
+ `no pendingRefreshOutputCount for denom ${dph}`,
+ );
car.pendingRefreshOutputCount =
car.pendingRefreshOutputCount - refreshSession.newDenoms[i].count;
await tx.coinAvailability.put(car);
@@ -693,7 +696,7 @@ async function refreshMelt(
switch (resp.status) {
case HttpStatusCode.NotFound: {
const errDetail = await readTalerErrorResponse(resp);
- await handleRefreshMeltNotFound(ctx, coinIndex, errDetail);
+ await handleRefreshMeltNotFound(ctx, coinIndex, resp, errDetail);
return;
}
case HttpStatusCode.Gone: {
@@ -898,9 +901,18 @@ async function handleRefreshMeltConflict(
async function handleRefreshMeltNotFound(
ctx: RefreshTransactionContext,
coinIndex: number,
+ resp: HttpResponse,
errDetails: TalerErrorDetail,
): Promise<void> {
- // FIXME: Validate the exchange's error response
+ // Make sure that we only act on a 404 that indicates a final problem
+ // with the coin.
+ switch (errDetails.code) {
+ case TalerErrorCode.EXCHANGE_GENERIC_COIN_UNKNOWN:
+ case TalerErrorCode.EXCHANGE_GENERIC_DENOMINATION_KEY_UNKNOWN:
+ break;
+ default:
+ throwUnexpectedRequestError(resp, errDetails);
+ }
await ctx.wex.db.runReadWriteTx(
{
storeNames: [
@@ -1242,7 +1254,10 @@ async function refreshReveal(
coin.exchangeBaseUrl,
coin.denomPubHash,
);
- checkDbInvariant(!!denomInfo);
+ checkDbInvariant(
+ !!denomInfo,
+ `no denom with hash ${coin.denomPubHash}`,
+ );
const car = await getCoinAvailabilityForDenom(
wex,
tx,
@@ -1252,6 +1267,7 @@ async function refreshReveal(
checkDbInvariant(
car.pendingRefreshOutputCount != null &&
car.pendingRefreshOutputCount > 0,
+ `no pendingRefreshOutputCount for denom ${coin.denomPubHash} age ${coin.maxAge}`,
);
car.pendingRefreshOutputCount--;
car.freshCoinCount++;
@@ -1559,9 +1575,22 @@ async function applyRefreshToOldCoins(
coin.denomPubHash,
coin.maxAge,
]);
- checkDbInvariant(!!coinAv);
- checkDbInvariant(coinAv.freshCoinCount > 0);
+ checkDbInvariant(
+ !!coinAv,
+ `no denom info for ${coin.denomPubHash} age ${coin.maxAge}`,
+ );
+ checkDbInvariant(
+ coinAv.freshCoinCount > 0,
+ `no fresh coins for ${coin.denomPubHash}`,
+ );
coinAv.freshCoinCount--;
+ if (coin.visible) {
+ if (!coinAv.visibleCoinCount) {
+ logger.error("coin availability inconsistent");
+ } else {
+ coinAv.visibleCoinCount--;
+ }
+ }
await tx.coinAvailability.put(coinAv);
break;
}
@@ -1770,7 +1799,7 @@ export async function forceRefresh(
],
},
async (tx) => {
- let coinPubs: CoinRefreshRequest[] = [];
+ const coinPubs: CoinRefreshRequest[] = [];
for (const c of req.refreshCoinSpecs) {
const coin = await tx.coins.get(c.coinPub);
if (!coin) {
@@ -1782,7 +1811,7 @@ export async function forceRefresh(
coin.exchangeBaseUrl,
coin.denomPubHash,
);
- checkDbInvariant(!!denom);
+ checkDbInvariant(!!denom, `no denom hash: ${coin.denomPubHash}`);
coinPubs.push({
coinPub: c.coinPub,
amount: c.amount ?? denom.value,
@@ -1818,66 +1847,38 @@ export async function waitRefreshFinal(
const ctx = new RefreshTransactionContext(wex, refreshGroupId);
wex.taskScheduler.startShepherdTask(ctx.taskId);
- // FIXME: Clean up using the new JS "using" / Symbol.dispose syntax.
- const refreshNotifFlag = new AsyncFlag();
- // Raise purchaseNotifFlag whenever we get a notification
- // about our refresh.
- const cancelNotif = wex.ws.addNotificationListener((notif) => {
- if (
- notif.type === NotificationType.TransactionStateTransition &&
- notif.transactionId === ctx.transactionId
- ) {
- refreshNotifFlag.raise();
- }
- });
- const unregisterOnCancelled = wex.cancellationToken.onCancelled(() => {
- cancelNotif();
- refreshNotifFlag.raise();
+ await genericWaitForState(wex, {
+ async checkState(): Promise<boolean> {
+ // Check if refresh is final
+ const res = await ctx.wex.db.runReadOnlyTx(
+ { storeNames: ["refreshGroups"] },
+ async (tx) => {
+ return {
+ rg: await tx.refreshGroups.get(ctx.refreshGroupId),
+ };
+ },
+ );
+ const { rg } = res;
+ if (!rg) {
+ // Must've been deleted, we consider that final.
+ return true;
+ }
+ switch (rg.operationStatus) {
+ case RefreshOperationStatus.Failed:
+ case RefreshOperationStatus.Finished:
+ // Transaction is final
+ return true;
+ case RefreshOperationStatus.Pending:
+ case RefreshOperationStatus.Suspended:
+ break;
+ }
+ return false;
+ },
+ filterNotification(notif): boolean {
+ return (
+ notif.type === NotificationType.TransactionStateTransition &&
+ notif.transactionId === ctx.transactionId
+ );
+ },
});
-
- try {
- await internalWaitRefreshFinal(ctx, refreshNotifFlag);
- } catch (e) {
- unregisterOnCancelled();
- cancelNotif();
- }
-}
-
-async function internalWaitRefreshFinal(
- ctx: RefreshTransactionContext,
- flag: AsyncFlag,
-): Promise<void> {
- while (true) {
- if (ctx.wex.cancellationToken.isCancelled) {
- throw Error("cancelled");
- }
-
- // Check if refresh is final
- const res = await ctx.wex.db.runReadOnlyTx(
- { storeNames: ["refreshGroups", "operationRetries"] },
- async (tx) => {
- return {
- rg: await tx.refreshGroups.get(ctx.refreshGroupId),
- };
- },
- );
- const { rg } = res;
- if (!rg) {
- // Must've been deleted, we consider that final.
- return;
- }
- switch (rg.operationStatus) {
- case RefreshOperationStatus.Failed:
- case RefreshOperationStatus.Finished:
- // Transaction is final
- return;
- case RefreshOperationStatus.Pending:
- case RefreshOperationStatus.Suspended:
- break;
- }
-
- // Wait for the next transition
- await flag.wait();
- flag.reset();
- }
}
diff --git a/packages/taler-wallet-core/src/shepherd.ts b/packages/taler-wallet-core/src/shepherd.ts
index 3b160d97f..470f45aff 100644
--- a/packages/taler-wallet-core/src/shepherd.ts
+++ b/packages/taler-wallet-core/src/shepherd.ts
@@ -50,12 +50,13 @@ import {
parseTaskIdentifier,
} from "./common.js";
import {
- OPERATION_STATUS_ACTIVE_FIRST,
- OPERATION_STATUS_ACTIVE_LAST,
+ OPERATION_STATUS_NONFINAL_FIRST,
+ OPERATION_STATUS_NONFINAL_LAST,
OperationRetryRecord,
WalletDbAllStoresReadOnlyTransaction,
WalletDbReadOnlyTransaction,
timestampAbsoluteFromDb,
+ timestampPreciseToDb,
} from "./db.js";
import {
computeDepositTransactionStatus,
@@ -113,6 +114,8 @@ const logger = new Logger("shepherd.ts");
*/
interface ShepherdInfo {
cts: CancellationToken.Source;
+ latch?: Promise<void>;
+ stopped: boolean;
}
/**
@@ -256,29 +259,36 @@ export class TaskSchedulerImpl implements TaskScheduler {
async reload(): Promise<void> {
await this.ensureRunning();
const tasksIds = [...this.sheps.keys()];
- logger.info(`reloading sheperd with ${tasksIds.length} tasks`);
+ logger.info(`reloading shepherd with ${tasksIds.length} tasks`);
for (const taskId of tasksIds) {
- this.stopShepherdTask(taskId);
+ await this.stopShepherdTask(taskId);
}
for (const taskId of tasksIds) {
this.startShepherdTask(taskId);
}
}
-
private async internalStartShepherdTask(taskId: TaskIdStr): Promise<void> {
logger.trace(`Starting to shepherd task ${taskId}`);
const oldShep = this.sheps.get(taskId);
if (oldShep) {
- logger.trace(`Already have a shepherd for ${taskId}`);
- return;
+ if (!oldShep.stopped) {
+ logger.trace(`Already have a shepherd for ${taskId}`);
+ return;
+ }
+ logger.trace(
+ `Waiting old task to complete the loop in cancel mode ${taskId}`,
+ );
+ await oldShep.latch;
}
logger.trace(`Creating new shepherd for ${taskId}`);
const newShep: ShepherdInfo = {
cts: CancellationToken.create(),
+ stopped: false,
};
this.sheps.set(taskId, newShep);
try {
- await this.internalShepherdTask(taskId, newShep);
+ newShep.latch = this.internalShepherdTask(taskId, newShep);
+ await newShep.latch;
} finally {
logger.trace(`Done shepherding ${taskId}`);
this.sheps.delete(taskId);
@@ -291,8 +301,8 @@ export class TaskSchedulerImpl implements TaskScheduler {
const oldShep = this.sheps.get(taskId);
if (oldShep) {
logger.trace(`Cancelling old shepherd for ${taskId}`);
- oldShep.cts.cancel();
- this.sheps.delete(taskId);
+ oldShep.cts.cancel(`stopping task ${taskId}`);
+ oldShep.stopped = true;
this.iterCond.trigger();
}
}
@@ -306,6 +316,7 @@ export class TaskSchedulerImpl implements TaskScheduler {
const maybeNotification = await this.ws.db.runAllStoresReadWriteTx(
{},
async (tx) => {
+ logger.trace(`storing task [reset] for ${taskId}`);
await tx.operationRetries.delete(taskId);
return taskToRetryNotification(this.ws, tx, taskId, undefined);
},
@@ -325,7 +336,13 @@ export class TaskSchedulerImpl implements TaskScheduler {
try {
await info.cts.token.racePromise(this.ws.timerGroup.resolveAfter(delay));
} catch (e) {
- logger.info(`waiting for ${taskId} interrupted`);
+ if (e instanceof CancellationToken.CancellationError) {
+ logger.info(
+ `waiting for ${taskId} interrupted: ${e.message} ${j2s(e.reason)}`,
+ );
+ } else {
+ logger.info(`waiting for ${taskId} interrupted: ${e}`);
+ }
}
}
@@ -363,13 +380,14 @@ export class TaskSchedulerImpl implements TaskScheduler {
try {
res = await callOperationHandlerForTaskId(wex, taskId);
} catch (e) {
+ logger.trace(`Shepherd error ${taskId} saving response ${e}`);
res = {
type: TaskRunResultType.Error,
errorDetail: getErrorDetailFromException(e),
};
}
if (info.cts.token.isCancelled) {
- logger.trace("task cancelled, not processing result");
+ logger.trace(`task ${taskId} cancelled, not processing result`);
return;
}
if (this.ws.stopped) {
@@ -382,7 +400,9 @@ export class TaskSchedulerImpl implements TaskScheduler {
});
switch (res.type) {
case TaskRunResultType.Error: {
- logger.trace(`Shepherd for ${taskId} got error result.`);
+ logger.trace(
+ `Shepherd for ${taskId} got error result: ${j2s(res.errorDetail)}`,
+ );
const retryRecord = await storePendingTaskError(
this.ws,
taskId,
@@ -412,8 +432,13 @@ export class TaskSchedulerImpl implements TaskScheduler {
}
case TaskRunResultType.ScheduleLater: {
logger.trace(`Shepherd for ${taskId} got schedule-later result.`);
- await storeTaskProgress(this.ws, taskId);
- const delay = AbsoluteTime.remaining(res.runAt);
+ const retryRecord = await storePendingTaskPending(
+ this.ws,
+ taskId,
+ res.runAt,
+ );
+ const t = timestampAbsoluteFromDb(retryRecord.retryInfo.nextRetry);
+ const delay = AbsoluteTime.remaining(t);
logger.trace(`Waiting for ${delay.d_ms} ms`);
await this.wait(taskId, info, delay);
break;
@@ -451,7 +476,7 @@ async function storePendingTaskError(
pendingTaskId: string,
e: TalerErrorDetail,
): Promise<OperationRetryRecord> {
- logger.info(`storing pending task error for ${pendingTaskId}`);
+ logger.trace(`storing task [pending] with ERROR for ${pendingTaskId}`);
const res = await ws.db.runAllStoresReadWriteTx({}, async (tx) => {
let retryRecord = await tx.operationRetries.get(pendingTaskId);
if (!retryRecord) {
@@ -483,6 +508,7 @@ async function storeTaskProgress(
ws: InternalWalletState,
pendingTaskId: string,
): Promise<void> {
+ logger.trace(`storing task [progress] for ${pendingTaskId}`);
await ws.db.runReadWriteTx(
{ storeNames: ["operationRetries"] },
async (tx) => {
@@ -494,7 +520,9 @@ async function storeTaskProgress(
async function storePendingTaskPending(
ws: InternalWalletState,
pendingTaskId: string,
+ schedTime?: AbsoluteTime,
): Promise<OperationRetryRecord> {
+ logger.trace(`storing task [pending] for ${pendingTaskId}`);
const res = await ws.db.runAllStoresReadWriteTx({}, async (tx) => {
let retryRecord = await tx.operationRetries.get(pendingTaskId);
let hadError = false;
@@ -510,6 +538,11 @@ async function storePendingTaskPending(
delete retryRecord.lastError;
retryRecord.retryInfo = DbRetryInfo.increment(retryRecord.retryInfo);
}
+ if (schedTime) {
+ retryRecord.retryInfo.nextRetry = timestampPreciseToDb(
+ AbsoluteTime.toPreciseTimestamp(schedTime),
+ );
+ }
await tx.operationRetries.put(retryRecord);
let notification: WalletNotification | undefined = undefined;
if (hadError) {
@@ -535,6 +568,7 @@ async function storePendingTaskFinished(
ws: InternalWalletState,
pendingTaskId: string,
): Promise<void> {
+ logger.trace(`storing task [finished] for ${pendingTaskId}`);
await ws.db.runReadWriteTx(
{ storeNames: ["operationRetries"] },
async (tx) => {
@@ -978,8 +1012,8 @@ export async function getActiveTaskIds(
},
async (tx) => {
const active = GlobalIDB.KeyRange.bound(
- OPERATION_STATUS_ACTIVE_FIRST,
- OPERATION_STATUS_ACTIVE_LAST,
+ OPERATION_STATUS_NONFINAL_FIRST,
+ OPERATION_STATUS_NONFINAL_LAST,
);
// Withdrawals
diff --git a/packages/taler-wallet-core/src/transactions.ts b/packages/taler-wallet-core/src/transactions.ts
index 9a9fb524f..7782d09ba 100644
--- a/packages/taler-wallet-core/src/transactions.ts
+++ b/packages/taler-wallet-core/src/transactions.ts
@@ -62,8 +62,8 @@ import {
DenomLossEventRecord,
DepositElementStatus,
DepositGroupRecord,
- OPERATION_STATUS_ACTIVE_FIRST,
- OPERATION_STATUS_ACTIVE_LAST,
+ OPERATION_STATUS_NONFINAL_FIRST,
+ OPERATION_STATUS_NONFINAL_LAST,
OperationRetryRecord,
PeerPullCreditRecord,
PeerPullDebitRecordStatus,
@@ -93,7 +93,6 @@ import {
computeDenomLossTransactionStatus,
DenomLossTransactionContext,
ExchangeWireDetails,
- fetchFreshExchange,
getExchangeWireDetailsInTx,
} from "./exchanges.js";
import {
@@ -244,11 +243,14 @@ export async function getTransactionById(
const opId = TaskIdentifiers.forWithdrawal(withdrawalGroupRecord);
const ort = await tx.operationRetries.get(opId);
- const exchangeDetails = await getExchangeWireDetailsInTx(
- tx,
- withdrawalGroupRecord.exchangeBaseUrl,
- );
- if (!exchangeDetails) throw Error("not exchange details");
+ const exchangeDetails =
+ withdrawalGroupRecord.exchangeBaseUrl === undefined
+ ? undefined
+ : await getExchangeWireDetailsInTx(
+ tx,
+ withdrawalGroupRecord.exchangeBaseUrl,
+ );
+ // if (!exchangeDetails) throw Error("not exchange details");
if (
withdrawalGroupRecord.wgInfo.withdrawalType ===
@@ -260,7 +262,10 @@ export async function getTransactionById(
ort,
);
}
-
+ checkDbInvariant(
+ exchangeDetails !== undefined,
+ "manual withdrawal without exchange",
+ );
return buildTransactionForManualWithdraw(
withdrawalGroupRecord,
exchangeDetails,
@@ -405,7 +410,10 @@ export async function getTransactionById(
const debit = await tx.peerPushDebit.get(parsedTx.pursePub);
if (!debit) throw Error("not found");
const ct = await tx.contractTerms.get(debit.contractTermsHash);
- checkDbInvariant(!!ct);
+ checkDbInvariant(
+ !!ct,
+ `no contract terms for p2p push ${parsedTx.pursePub}`,
+ );
return buildTransactionForPushPaymentDebit(
debit,
ct.contractTermsRaw,
@@ -429,7 +437,10 @@ export async function getTransactionById(
const pushInc = await tx.peerPushCredit.get(peerPushCreditId);
if (!pushInc) throw Error("not found");
const ct = await tx.contractTerms.get(pushInc.contractTermsHash);
- checkDbInvariant(!!ct);
+ checkDbInvariant(
+ !!ct,
+ `no contract terms for p2p push ${peerPushCreditId}`,
+ );
let wg: WithdrawalGroupRecord | undefined = undefined;
let wgOrt: OperationRetryRecord | undefined = undefined;
@@ -441,7 +452,7 @@ export async function getTransactionById(
}
}
const pushIncOpId = TaskIdentifiers.forPeerPushCredit(pushInc);
- let pushIncOrt = await tx.operationRetries.get(pushIncOpId);
+ const pushIncOrt = await tx.operationRetries.get(pushIncOpId);
return buildTransactionForPeerPushCredit(
pushInc,
@@ -469,7 +480,7 @@ export async function getTransactionById(
const pushInc = await tx.peerPullCredit.get(pursePub);
if (!pushInc) throw Error("not found");
const ct = await tx.contractTerms.get(pushInc.contractTermsHash);
- checkDbInvariant(!!ct);
+ checkDbInvariant(!!ct, `no contract terms for p2p push ${pursePub}`);
let wg: WithdrawalGroupRecord | undefined = undefined;
let wgOrt: OperationRetryRecord | undefined = undefined;
@@ -594,6 +605,7 @@ function buildTransactionForPeerPullCredit(
const txState = computePeerPullCreditTransactionState(pullCredit);
checkDbInvariant(wsr.instructedAmount !== undefined, "wg uninitialized");
checkDbInvariant(wsr.denomsSel !== undefined, "wg uninitialized");
+ checkDbInvariant(wsr.exchangeBaseUrl !== undefined, "wg uninitialized");
return {
type: TransactionType.PeerPullCredit,
txState,
@@ -668,6 +680,7 @@ function buildTransactionForPeerPushCredit(
}
checkDbInvariant(wg.instructedAmount !== undefined, "wg uninitialized");
checkDbInvariant(wg.denomsSel !== undefined, "wg uninitialized");
+ checkDbInvariant(wg.exchangeBaseUrl !== undefined, "wg uninitialized");
const txState = computePeerPushCreditTransactionState(pushInc);
return {
@@ -720,16 +733,21 @@ function buildTransactionForPeerPushCredit(
function buildTransactionForBankIntegratedWithdraw(
wg: WithdrawalGroupRecord,
- exchangeDetails: ExchangeWireDetails,
+ exchangeDetails: ExchangeWireDetails | undefined,
ort?: OperationRetryRecord,
): TransactionWithdrawal {
if (wg.wgInfo.withdrawalType !== WithdrawalRecordType.BankIntegrated) {
throw Error("");
}
+ const instructedCurrency =
+ wg.instructedAmount === undefined
+ ? undefined
+ : Amounts.currencyOf(wg.instructedAmount);
+ const currency = wg.wgInfo.bankInfo.currency ?? instructedCurrency;
+ checkDbInvariant(currency !== undefined, "wg uninitialized (missing currency)");
const txState = computeWithdrawalTransactionStatus(wg);
- const zero = Amounts.stringify(
- Amounts.zeroOfCurrency(exchangeDetails.currency),
- );
+
+ const zero = Amounts.stringify(Amounts.zeroOfCurrency(currency));
return {
type: TransactionType.Withdrawal,
txState,
@@ -785,6 +803,7 @@ function buildTransactionForManualWithdraw(
checkDbInvariant(wg.instructedAmount !== undefined, "wg uninitialized");
checkDbInvariant(wg.denomsSel !== undefined, "wg uninitialized");
+ checkDbInvariant(wg.exchangeBaseUrl !== undefined, "wg uninitialized");
const exchangePaytoUris = augmentPaytoUrisForWithdrawal(
plainPaytoUris,
wg.reservePub,
@@ -1035,8 +1054,14 @@ function buildTransactionForPurchase(
}));
const timestamp = purchaseRecord.timestampAccept;
- checkDbInvariant(!!timestamp);
- checkDbInvariant(!!purchaseRecord.payInfo);
+ checkDbInvariant(
+ !!timestamp,
+ `purchase ${purchaseRecord.orderId} without accepted time`,
+ );
+ checkDbInvariant(
+ !!purchaseRecord.payInfo,
+ `purchase ${purchaseRecord.orderId} without payinfo`,
+ );
const txState = computePayMerchantTransactionState(purchaseRecord);
return {
@@ -1090,6 +1115,10 @@ export async function getWithdrawalTransactionByUri(
if (!withdrawalGroupRecord) {
return undefined;
}
+ if (withdrawalGroupRecord.exchangeBaseUrl === undefined) {
+ // prepared and unconfirmed withdrawals are hidden
+ return undefined;
+ }
const opId = TaskIdentifiers.forWithdrawal(withdrawalGroupRecord);
const ort = await tx.operationRetries.get(opId);
@@ -1176,7 +1205,7 @@ export async function getTransactions(
return;
}
const ct = await tx.contractTerms.get(pi.contractTermsHash);
- checkDbInvariant(!!ct);
+ checkDbInvariant(!!ct, `no contract terms for p2p push ${pi.pursePub}`);
transactions.push(
buildTransactionForPushPaymentDebit(pi, ct.contractTermsRaw),
);
@@ -1250,9 +1279,9 @@ export async function getTransactions(
}
}
const pushIncOpId = TaskIdentifiers.forPeerPushCredit(pi);
- let pushIncOrt = await tx.operationRetries.get(pushIncOpId);
+ const pushIncOrt = await tx.operationRetries.get(pushIncOpId);
- checkDbInvariant(!!ct);
+ checkDbInvariant(!!ct, `no contract terms for p2p push ${pi.pursePub}`);
transactions.push(
buildTransactionForPeerPushCredit(
pi,
@@ -1284,9 +1313,9 @@ export async function getTransactions(
}
}
const pushIncOpId = TaskIdentifiers.forPeerPullPaymentInitiation(pi);
- let pushIncOrt = await tx.operationRetries.get(pushIncOpId);
+ const pushIncOrt = await tx.operationRetries.get(pushIncOpId);
- checkDbInvariant(!!ct);
+ checkDbInvariant(!!ct, `no contract terms for p2p push ${pi.pursePub}`);
transactions.push(
buildTransactionForPeerPullCredit(
pi,
@@ -1772,7 +1801,11 @@ export async function retryTransaction(
}
}
+/**
+ * Reset the task retry counter for all tasks.
+ */
export async function retryAll(wex: WalletExecutionContext): Promise<void> {
+ await wex.taskScheduler.ensureRunning();
const tasks = wex.taskScheduler.getActiveTasks();
for (const task of tasks) {
await wex.taskScheduler.resetTaskRetries(task);
@@ -1935,8 +1968,8 @@ async function iterRecordsForWithdrawal(
let withdrawalGroupRecords: WithdrawalGroupRecord[];
if (filter.onlyState === "nonfinal") {
const keyRange = GlobalIDB.KeyRange.bound(
- OPERATION_STATUS_ACTIVE_FIRST,
- OPERATION_STATUS_ACTIVE_LAST,
+ OPERATION_STATUS_NONFINAL_FIRST,
+ OPERATION_STATUS_NONFINAL_LAST,
);
withdrawalGroupRecords =
await tx.withdrawalGroups.indexes.byStatus.getAll(keyRange);
@@ -1957,8 +1990,8 @@ async function iterRecordsForDeposit(
let dgs: DepositGroupRecord[];
if (filter.onlyState === "nonfinal") {
const keyRange = GlobalIDB.KeyRange.bound(
- OPERATION_STATUS_ACTIVE_FIRST,
- OPERATION_STATUS_ACTIVE_LAST,
+ OPERATION_STATUS_NONFINAL_FIRST,
+ OPERATION_STATUS_NONFINAL_LAST,
);
dgs = await tx.depositGroups.indexes.byStatus.getAll(keyRange);
} else {
@@ -1978,8 +2011,8 @@ async function iterRecordsForDenomLoss(
let dgs: DenomLossEventRecord[];
if (filter.onlyState === "nonfinal") {
const keyRange = GlobalIDB.KeyRange.bound(
- OPERATION_STATUS_ACTIVE_FIRST,
- OPERATION_STATUS_ACTIVE_LAST,
+ OPERATION_STATUS_NONFINAL_FIRST,
+ OPERATION_STATUS_NONFINAL_LAST,
);
dgs = await tx.denomLossEvents.indexes.byStatus.getAll(keyRange);
} else {
@@ -1998,8 +2031,8 @@ async function iterRecordsForRefund(
): Promise<void> {
if (filter.onlyState === "nonfinal") {
const keyRange = GlobalIDB.KeyRange.bound(
- OPERATION_STATUS_ACTIVE_FIRST,
- OPERATION_STATUS_ACTIVE_LAST,
+ OPERATION_STATUS_NONFINAL_FIRST,
+ OPERATION_STATUS_NONFINAL_LAST,
);
await tx.refundGroups.indexes.byStatus.iter(keyRange).forEachAsync(f);
} else {
@@ -2014,8 +2047,8 @@ async function iterRecordsForPurchase(
): Promise<void> {
if (filter.onlyState === "nonfinal") {
const keyRange = GlobalIDB.KeyRange.bound(
- OPERATION_STATUS_ACTIVE_FIRST,
- OPERATION_STATUS_ACTIVE_LAST,
+ OPERATION_STATUS_NONFINAL_FIRST,
+ OPERATION_STATUS_NONFINAL_LAST,
);
await tx.purchases.indexes.byStatus.iter(keyRange).forEachAsync(f);
} else {
@@ -2030,8 +2063,8 @@ async function iterRecordsForPeerPullCredit(
): Promise<void> {
if (filter.onlyState === "nonfinal") {
const keyRange = GlobalIDB.KeyRange.bound(
- OPERATION_STATUS_ACTIVE_FIRST,
- OPERATION_STATUS_ACTIVE_LAST,
+ OPERATION_STATUS_NONFINAL_FIRST,
+ OPERATION_STATUS_NONFINAL_LAST,
);
await tx.peerPullCredit.indexes.byStatus.iter(keyRange).forEachAsync(f);
} else {
@@ -2046,8 +2079,8 @@ async function iterRecordsForPeerPullDebit(
): Promise<void> {
if (filter.onlyState === "nonfinal") {
const keyRange = GlobalIDB.KeyRange.bound(
- OPERATION_STATUS_ACTIVE_FIRST,
- OPERATION_STATUS_ACTIVE_LAST,
+ OPERATION_STATUS_NONFINAL_FIRST,
+ OPERATION_STATUS_NONFINAL_LAST,
);
await tx.peerPullDebit.indexes.byStatus.iter(keyRange).forEachAsync(f);
} else {
@@ -2062,8 +2095,8 @@ async function iterRecordsForPeerPushDebit(
): Promise<void> {
if (filter.onlyState === "nonfinal") {
const keyRange = GlobalIDB.KeyRange.bound(
- OPERATION_STATUS_ACTIVE_FIRST,
- OPERATION_STATUS_ACTIVE_LAST,
+ OPERATION_STATUS_NONFINAL_FIRST,
+ OPERATION_STATUS_NONFINAL_LAST,
);
await tx.peerPushDebit.indexes.byStatus.iter(keyRange).forEachAsync(f);
} else {
@@ -2078,8 +2111,8 @@ async function iterRecordsForPeerPushCredit(
): Promise<void> {
if (filter.onlyState === "nonfinal") {
const keyRange = GlobalIDB.KeyRange.bound(
- OPERATION_STATUS_ACTIVE_FIRST,
- OPERATION_STATUS_ACTIVE_LAST,
+ OPERATION_STATUS_NONFINAL_FIRST,
+ OPERATION_STATUS_NONFINAL_LAST,
);
await tx.peerPushCredit.indexes.byStatus.iter(keyRange).forEachAsync(f);
} else {
diff --git a/packages/taler-wallet-core/src/versions.ts b/packages/taler-wallet-core/src/versions.ts
index d33a23cdd..8b4b24351 100644
--- a/packages/taler-wallet-core/src/versions.ts
+++ b/packages/taler-wallet-core/src/versions.ts
@@ -29,13 +29,6 @@ export const WALLET_EXCHANGE_PROTOCOL_VERSION = "17:0:0";
export const WALLET_MERCHANT_PROTOCOL_VERSION = "5:0:1";
/**
- * Protocol version spoken with the bank (bank integration API).
- *
- * Uses libtool's current:revision:age versioning.
- */
-export const WALLET_BANK_INTEGRATION_PROTOCOL_VERSION = "1:0:0";
-
-/**
* Protocol version spoken with the bank (corebank API).
*
* Uses libtool's current:revision:age versioning.
@@ -52,7 +45,7 @@ export const WALLET_BANK_CONVERSION_API_PROTOCOL_VERSION = "2:0:0";
/**
* Libtool version of the wallet-core API.
*/
-export const WALLET_CORE_API_PROTOCOL_VERSION = "5:0:0";
+export const WALLET_CORE_API_PROTOCOL_VERSION = "7:0:0";
/**
* Libtool rules:
diff --git a/packages/taler-wallet-core/src/wallet-api-types.ts b/packages/taler-wallet-core/src/wallet-api-types.ts
index 1bcab801c..aa88331ea 100644
--- a/packages/taler-wallet-core/src/wallet-api-types.ts
+++ b/packages/taler-wallet-core/src/wallet-api-types.ts
@@ -123,7 +123,6 @@ import {
StartRefundQueryForUriResponse,
StartRefundQueryRequest,
StoredBackupList,
- TalerMerchantApi,
TestPayArgs,
TestPayResult,
TestingGetDenomStatsRequest,
@@ -277,6 +276,7 @@ export enum WalletApiOperation {
TestingGetDenomStats = "testingGetDenomStats",
TestingPing = "testingPing",
TestingGetReserveHistory = "testingGetReserveHistory",
+ TestingResetAllRetries = "testingResetAllRetries",
}
// group: Initialization
@@ -1213,6 +1213,16 @@ export type TestingGetReserveHistoryOp = {
};
/**
+ * Reset all task/transaction retries,
+ * resulting in immediate re-try of all operations.
+ */
+export type TestingResetAllRetriesOp = {
+ op: WalletApiOperation.TestingResetAllRetries;
+ request: EmptyObject;
+ response: EmptyObject;
+};
+
+/**
* Get stats about an exchange denomination.
*/
export type TestingGetDenomStatsOp = {
@@ -1356,6 +1366,7 @@ export type WalletOperations = {
[WalletApiOperation.ConfirmWithdrawal]: ConfirmWithdrawalOp;
[WalletApiOperation.CanonicalizeBaseUrl]: CanonicalizeBaseUrlOp;
[WalletApiOperation.TestingGetReserveHistory]: TestingGetReserveHistoryOp;
+ [WalletApiOperation.TestingResetAllRetries]: TestingResetAllRetriesOp;
[WalletApiOperation.HintNetworkAvailability]: HintNetworkAvailabilityOp;
};
diff --git a/packages/taler-wallet-core/src/wallet.ts b/packages/taler-wallet-core/src/wallet.ts
index 68da15410..7a69fcb21 100644
--- a/packages/taler-wallet-core/src/wallet.ts
+++ b/packages/taler-wallet-core/src/wallet.ts
@@ -56,6 +56,7 @@ import {
PrepareWithdrawExchangeResponse,
RecoverStoredBackupRequest,
StoredBackupList,
+ TalerBankIntegrationHttpClient,
TalerError,
TalerErrorCode,
TalerProtocolTimestamp,
@@ -107,6 +108,7 @@ import {
codecForGetExchangeTosRequest,
codecForGetWithdrawalDetailsForAmountRequest,
codecForGetWithdrawalDetailsForUri,
+ codecForHintNetworkAvailabilityRequest,
codecForImportDbRequest,
codecForInitRequest,
codecForInitiatePeerPullPaymentRequest,
@@ -265,6 +267,7 @@ import {
TaskScheduler,
TaskSchedulerImpl,
convertTaskToTransactionId,
+ getActiveTaskIds,
listTaskForTransactionId,
} from "./shepherd.js";
import {
@@ -287,12 +290,12 @@ import {
getWithdrawalTransactionByUri,
parseTransactionIdentifier,
resumeTransaction,
+ retryAll,
retryTransaction,
suspendTransaction,
} from "./transactions.js";
import {
WALLET_BANK_CONVERSION_API_PROTOCOL_VERSION,
- WALLET_BANK_INTEGRATION_PROTOCOL_VERSION,
WALLET_COREBANK_API_PROTOCOL_VERSION,
WALLET_CORE_API_PROTOCOL_VERSION,
WALLET_EXCHANGE_PROTOCOL_VERSION,
@@ -476,7 +479,10 @@ async function setCoinSuspended(
c.denomPubHash,
c.maxAge,
]);
- checkDbInvariant(!!coinAvailability);
+ checkDbInvariant(
+ !!coinAvailability,
+ `no denom info for ${c.denomPubHash} age ${c.maxAge}`,
+ );
if (suspended) {
if (c.status !== CoinStatus.Fresh) {
return;
@@ -721,12 +727,11 @@ async function dispatchRequestInternal(
case WalletApiOperation.InitWallet: {
const req = codecForInitRequest().decode(payload);
- logger.info(`init request: ${j2s(req)}`);
-
- if (wex.ws.initCalled) {
- logger.info("initializing wallet (repeat initialization)");
- } else {
- logger.info("initializing wallet (first initialization)");
+ if (logger.shouldLogTrace()) {
+ const initType = wex.ws.initCalled
+ ? "repeat initialization"
+ : "first initialization";
+ logger.trace(`init request (${initType}): ${j2s(req)}`);
}
// Write to the DB to make sure that we're failing early in
@@ -744,7 +749,6 @@ async function dispatchRequestInternal(
innerError: getErrorDetailFromException(e),
});
}
-
wex.ws.initWithConfig(applyRunConfigDefaults(req.config));
if (wex.ws.config.testing.skipDefaults) {
@@ -757,8 +761,11 @@ async function dispatchRequestInternal(
versionInfo: getVersion(wex),
};
- // After initialization, task loop should run.
- await wex.taskScheduler.ensureRunning();
+ if (req.config?.lazyTaskLoop) {
+ logger.trace("lazily starting task loop");
+ } else {
+ await wex.taskScheduler.ensureRunning();
+ }
wex.ws.initCalled = true;
return resp;
@@ -996,6 +1003,7 @@ async function dispatchRequestInternal(
talerWithdrawUri: req.talerWithdrawUri,
forcedDenomSel: req.forcedDenomSel,
restrictAge: req.restrictAge,
+ amount: req.amount,
});
}
case WalletApiOperation.ConfirmWithdrawal: {
@@ -1005,10 +1013,7 @@ async function dispatchRequestInternal(
case WalletApiOperation.PrepareBankIntegratedWithdrawal: {
const req =
codecForPrepareBankIntegratedWithdrawalRequest().decode(payload);
- return prepareBankIntegratedWithdrawal(wex, {
- talerWithdrawUri: req.talerWithdrawUri,
- selectedExchange: req.selectedExchange,
- });
+ return prepareBankIntegratedWithdrawal(wex, req);
}
case WalletApiOperation.GetExchangeTos: {
const req = codecForGetExchangeTosRequest().decode(payload);
@@ -1046,6 +1051,10 @@ async function dispatchRequestInternal(
const req = codecForPrepareWithdrawExchangeRequest().decode(payload);
return handlePrepareWithdrawExchange(wex, req);
}
+ case WalletApiOperation.CheckPayForTemplate: {
+ const req = codecForCheckPayTemplateRequest().decode(payload);
+ return await checkPayForTemplate(wex, req);
+ }
case WalletApiOperation.PreparePayForUri: {
const req = codecForPreparePayRequest().decode(payload);
return await preparePayForUri(wex, req.talerPayUri);
@@ -1054,10 +1063,6 @@ async function dispatchRequestInternal(
const req = codecForPreparePayTemplateRequest().decode(payload);
return preparePayForTemplate(wex, req);
}
- case WalletApiOperation.CheckPayForTemplate: {
- const req = codecForCheckPayTemplateRequest().decode(payload);
- return checkPayForTemplate(wex, req);
- }
case WalletApiOperation.ConfirmPay: {
const req = codecForConfirmPayRequest().decode(payload);
let transactionId;
@@ -1085,7 +1090,7 @@ async function dispatchRequestInternal(
return {};
}
case WalletApiOperation.GetActiveTasks: {
- const allTasksId = wex.taskScheduler.getActiveTasks();
+ const allTasksId = (await getActiveTaskIds(wex.ws)).taskIds;
const tasksInfo = await Promise.all(
allTasksId.map(async (id) => {
@@ -1234,10 +1239,16 @@ async function dispatchRequestInternal(
await loadBackupRecovery(wex, req);
return {};
}
- // case WalletApiOperation.GetPlanForOperation: {
- // const req = codecForGetPlanForOperationRequest().decode(payload);
- // return await getPlanForOperation(ws, req);
- // }
+ case WalletApiOperation.HintNetworkAvailability: {
+ const req = codecForHintNetworkAvailabilityRequest().decode(payload);
+ if (req.isNetworkAvailable) {
+ await retryAll(wex);
+ } else {
+ // We're not doing anything right now, but we could stop showing
+ // certain errors!
+ }
+ return {};
+ }
case WalletApiOperation.ConvertDepositAmount: {
const req = codecForConvertAmountRequest.decode(payload);
return await convertDepositAmount(wex, req);
@@ -1388,7 +1399,10 @@ async function dispatchRequestInternal(
return;
}
wex.ws.exchangeCache.clear();
- checkDbInvariant(!!existingRec.id);
+ checkDbInvariant(
+ !!existingRec.id,
+ `no global exchange for ${j2s(key)}`,
+ );
await tx.globalCurrencyExchanges.delete(existingRec.id);
},
);
@@ -1421,6 +1435,9 @@ async function dispatchRequestInternal(
await waitTasksDone(wex);
return {};
}
+ case WalletApiOperation.TestingResetAllRetries:
+ await retryAll(wex);
+ return {};
case WalletApiOperation.RemoveGlobalCurrencyAuditor: {
const req = codecForRemoveGlobalCurrencyAuditorRequest().decode(payload);
await wex.db.runReadWriteTx(
@@ -1434,7 +1451,10 @@ async function dispatchRequestInternal(
if (!existingRec) {
return;
}
- checkDbInvariant(!!existingRec.id);
+ checkDbInvariant(
+ !!existingRec.id,
+ `no global currency for ${j2s(key)}`,
+ );
await tx.globalCurrencyAuditors.delete(existingRec.id);
wex.ws.exchangeCache.clear();
},
@@ -1569,9 +1589,9 @@ export function getVersion(wex: WalletExecutionContext): WalletCoreVersion {
exchange: WALLET_EXCHANGE_PROTOCOL_VERSION,
merchant: WALLET_MERCHANT_PROTOCOL_VERSION,
bankConversionApiRange: WALLET_BANK_CONVERSION_API_PROTOCOL_VERSION,
- bankIntegrationApiRange: WALLET_BANK_INTEGRATION_PROTOCOL_VERSION,
+ bankIntegrationApiRange: TalerBankIntegrationHttpClient.PROTOCOL_VERSION,
corebankApiRange: WALLET_COREBANK_API_PROTOCOL_VERSION,
- bank: WALLET_BANK_INTEGRATION_PROTOCOL_VERSION,
+ bank: TalerBankIntegrationHttpClient.PROTOCOL_VERSION,
devMode: wex.ws.config.testing.devModeActive,
};
return result;
@@ -1629,10 +1649,10 @@ async function handleCoreApiRequest(
if (!ws.initCalled) {
throw Error("init must be called first");
}
- // Might be lazily initialized!
- await ws.taskScheduler.ensureRunning();
}
+ await ws.ensureWalletDbOpen();
+
let wex: WalletExecutionContext;
let oc: ObservabilityContext;
@@ -1832,7 +1852,7 @@ class WalletDbTriggerSpec implements TriggerSpec {
if (info.mode !== "readwrite") {
return;
}
- logger.info(
+ logger.trace(
`in after commit callback for readwrite, modified ${j2s([
...info.modifiedStores,
])}`,
@@ -1924,8 +1944,6 @@ export class InternalWalletState {
initWithConfig(newConfig: WalletRunConfig): void {
this._config = newConfig;
- logger.info(`setting new config to ${j2s(newConfig)}`);
-
this._http = this.httpFactory(newConfig);
if (this.config.testing.devModeActive) {
diff --git a/packages/taler-wallet-core/src/withdraw.ts b/packages/taler-wallet-core/src/withdraw.ts
index 4a7c7873c..8bc4aafd1 100644
--- a/packages/taler-wallet-core/src/withdraw.ts
+++ b/packages/taler-wallet-core/src/withdraw.ts
@@ -44,6 +44,8 @@ import {
Duration,
EddsaPrivateKeyString,
ExchangeBatchWithdrawRequest,
+ ExchangeListItem,
+ ExchangeTosStatus,
ExchangeUpdateStatus,
ExchangeWireAccount,
ExchangeWithdrawBatchResponse,
@@ -114,8 +116,10 @@ import {
TransitionResult,
TransitionResultType,
constructTaskIdentifier,
+ genericWaitForState,
makeCoinAvailable,
makeCoinsVisible,
+ requireExchangeTosAcceptedOrThrow,
} from "./common.js";
import { EddsaKeypair } from "./crypto/cryptoImplementation.js";
import {
@@ -149,6 +153,7 @@ import {
getExchangePaytoUri,
getExchangeWireDetailsInTx,
listExchanges,
+ lookupExchangeByUri,
markExchangeUsed,
} from "./exchanges.js";
import { DbAccess } from "./query.js";
@@ -159,10 +164,7 @@ import {
notifyTransition,
parseTransactionIdentifier,
} from "./transactions.js";
-import {
- WALLET_BANK_INTEGRATION_PROTOCOL_VERSION,
- WALLET_EXCHANGE_PROTOCOL_VERSION,
-} from "./versions.js";
+import { WALLET_EXCHANGE_PROTOCOL_VERSION } from "./versions.js";
import { WalletExecutionContext, getDenomInfo } from "./wallet.js";
/**
@@ -343,9 +345,11 @@ export class WithdrawTransactionContext implements TransactionContext {
"exchanges" as const,
"exchangeDetails" as const,
];
- let stores = opts.extraStores
+ const stores = opts.extraStores
? [...baseStores, ...opts.extraStores]
: baseStores;
+
+ let errorThrown: Error | undefined;
const transitionInfo = await this.wex.db.runReadWriteTx(
{ storeNames: stores },
async (tx) => {
@@ -358,7 +362,17 @@ export class WithdrawTransactionContext implements TransactionContext {
major: TransactionMajorState.None,
};
}
- const res = await f(wgRec, tx);
+ let res: TransitionResult<WithdrawalGroupRecord> | undefined;
+ try {
+ res = await f(wgRec, tx);
+ } catch (error) {
+ if (error instanceof Error) {
+ errorThrown = error;
+ }
+ return undefined;
+ }
+
+ // const res = await f(wgRec, tx);
switch (res.type) {
case TransitionResultType.Transition: {
await tx.withdrawalGroups.put(res.rec);
@@ -383,6 +397,9 @@ export class WithdrawTransactionContext implements TransactionContext {
}
},
);
+ if (errorThrown) {
+ throw errorThrown;
+ }
notifyTransition(this.wex, this.transactionId, transitionInfo);
return transitionInfo;
}
@@ -715,15 +732,35 @@ export function computeWithdrawalTransactionActions(
case WithdrawalGroupStatus.Done:
return [TransactionAction.Delete];
case WithdrawalGroupStatus.PendingRegisteringBank:
- return [TransactionAction.Suspend, TransactionAction.Abort];
+ return [
+ TransactionAction.Retry,
+ TransactionAction.Suspend,
+ TransactionAction.Abort,
+ ];
case WithdrawalGroupStatus.PendingReady:
- return [TransactionAction.Suspend, TransactionAction.Abort];
+ return [
+ TransactionAction.Retry,
+ TransactionAction.Suspend,
+ TransactionAction.Abort,
+ ];
case WithdrawalGroupStatus.PendingQueryingStatus:
- return [TransactionAction.Suspend, TransactionAction.Abort];
+ return [
+ TransactionAction.Retry,
+ TransactionAction.Suspend,
+ TransactionAction.Abort,
+ ];
case WithdrawalGroupStatus.PendingWaitConfirmBank:
- return [TransactionAction.Suspend, TransactionAction.Abort];
+ return [
+ TransactionAction.Retry,
+ TransactionAction.Suspend,
+ TransactionAction.Abort,
+ ];
case WithdrawalGroupStatus.AbortingBank:
- return [TransactionAction.Suspend, TransactionAction.Fail];
+ return [
+ TransactionAction.Retry,
+ TransactionAction.Suspend,
+ TransactionAction.Fail,
+ ];
case WithdrawalGroupStatus.SuspendedAbortingBank:
return [TransactionAction.Resume, TransactionAction.Fail];
case WithdrawalGroupStatus.SuspendedQueryingStatus:
@@ -735,9 +772,17 @@ export function computeWithdrawalTransactionActions(
case WithdrawalGroupStatus.SuspendedReady:
return [TransactionAction.Resume, TransactionAction.Abort];
case WithdrawalGroupStatus.PendingAml:
- return [TransactionAction.Resume, TransactionAction.Abort];
+ return [
+ TransactionAction.Retry,
+ TransactionAction.Resume,
+ TransactionAction.Abort,
+ ];
case WithdrawalGroupStatus.PendingKyc:
- return [TransactionAction.Resume, TransactionAction.Abort];
+ return [
+ TransactionAction.Retry,
+ TransactionAction.Resume,
+ TransactionAction.Abort,
+ ];
case WithdrawalGroupStatus.SuspendedAml:
return [TransactionAction.Resume, TransactionAction.Abort];
case WithdrawalGroupStatus.SuspendedKyc:
@@ -842,7 +887,7 @@ export async function getBankWithdrawalInfo(
TalerErrorCode.WALLET_BANK_INTEGRATION_PROTOCOL_VERSION_INCOMPATIBLE,
{
bankProtocolVersion: config.version,
- walletProtocolVersion: WALLET_BANK_INTEGRATION_PROTOCOL_VERSION,
+ walletProtocolVersion: bankApi.PROTOCOL_VERSION,
},
"bank integration protocol version not compatible with wallet",
);
@@ -857,13 +902,48 @@ export async function getBankWithdrawalInfo(
}
const { body: status } = resp;
+ const maxAmount =
+ status.max_amount === undefined
+ ? undefined
+ : Amounts.parseOrThrow(status.max_amount);
+
+ let amount: AmountJson | undefined;
+ let editableAmount = false;
+ if (status.amount !== undefined) {
+ amount = Amounts.parseOrThrow(status.amount);
+ } else {
+ amount =
+ status.suggested_amount === undefined
+ ? undefined
+ : Amounts.parseOrThrow(status.suggested_amount);
+ editableAmount = true;
+ }
+
+ let wireFee: AmountJson | undefined;
+ if (status.card_fees) {
+ wireFee = Amounts.parseOrThrow(status.card_fees);
+ }
+
+ let exchange: string | undefined = undefined;
+ let editableExchange = false;
+ if (status.required_exchange !== undefined) {
+ exchange = status.required_exchange;
+ } else {
+ exchange = status.suggested_exchange;
+ editableExchange = true;
+ }
return {
operationId: uriResult.withdrawalOperationId,
apiBaseUrl: uriResult.bankIntegrationApiBaseUrl,
- amount: Amounts.parseOrThrow(status.amount),
+ currency: config.currency,
+ amount,
+ wireFee,
confirmTransferUrl: status.confirm_transfer_url,
senderWire: status.sender_wire,
- suggestedExchange: status.suggested_exchange,
+ exchange,
+ editableAmount,
+ editableExchange,
+ maxAmount,
wireTypes: status.wire_types,
status: status.status,
};
@@ -917,6 +997,10 @@ async function processPlanchetGenerate(
withdrawalGroup.denomsSel !== undefined,
"can't process uninitialized exchange",
);
+ checkDbInvariant(
+ withdrawalGroup.exchangeBaseUrl !== undefined,
+ "can't get funding uri from uninitialized wg",
+ );
const exchangeBaseUrl = withdrawalGroup.exchangeBaseUrl;
let planchet = await wex.db.runReadOnlyTx(
{ storeNames: ["planchets"] },
@@ -958,7 +1042,7 @@ async function processPlanchetGenerate(
return getDenomInfo(wex, tx, exchangeBaseUrl, denomPubHash);
},
);
- checkDbInvariant(!!denom);
+ checkDbInvariant(!!denom, `no denom info for ${denomPubHash}`);
const r = await wex.cryptoApi.createPlanchet({
denomPub: denom.denomPub,
feeWithdraw: Amounts.parseOrThrow(denom.feeWithdraw),
@@ -1121,6 +1205,10 @@ async function processPlanchetExchangeBatchRequest(
logger.info(
`processing planchet exchange batch request ${withdrawalGroup.withdrawalGroupId}, start=${args.coinStartIndex}, len=${args.batchSize}`,
);
+ checkDbInvariant(
+ withdrawalGroup.exchangeBaseUrl !== undefined,
+ "can't get funding uri from uninitialized wg",
+ );
const exchangeBaseUrl = withdrawalGroup.exchangeBaseUrl;
const batchReq: ExchangeBatchWithdrawRequest = { planchets: [] };
@@ -1256,6 +1344,10 @@ async function processPlanchetVerifyAndStoreCoin(
resp: ExchangeWithdrawResponse,
): Promise<void> {
const withdrawalGroup = wgContext.wgRecord;
+ checkDbInvariant(
+ withdrawalGroup.exchangeBaseUrl !== undefined,
+ "can't get funding uri from uninitialized wg",
+ );
const exchangeBaseUrl = withdrawalGroup.exchangeBaseUrl;
logger.trace(`checking and storing planchet idx=${coinIdx}`);
@@ -1505,6 +1597,10 @@ async function processQueryReserve(
return TaskRunResult.backoff();
}
checkDbInvariant(
+ withdrawalGroup.exchangeBaseUrl !== undefined,
+ "can't get funding uri from uninitialized wg",
+ );
+ checkDbInvariant(
withdrawalGroup.denomsSel !== undefined,
"can't process uninitialized exchange",
);
@@ -1740,6 +1836,10 @@ async function redenominateWithdrawal(
return;
}
checkDbInvariant(
+ wg.exchangeBaseUrl !== undefined,
+ "can't get funding uri from uninitialized wg",
+ );
+ checkDbInvariant(
wg.denomsSel !== undefined,
"can't process uninitialized exchange",
);
@@ -1882,7 +1982,12 @@ async function processWithdrawalGroupPendingReady(
withdrawalGroup.denomsSel !== undefined,
"can't process uninitialized exchange",
);
+ checkDbInvariant(
+ withdrawalGroup.exchangeBaseUrl !== undefined,
+ "can't get funding uri from uninitialized wg",
+ );
const exchangeBaseUrl = withdrawalGroup.exchangeBaseUrl;
+ logger.trace(`updating exchange beofre processing wg`);
await fetchFreshExchange(wex, withdrawalGroup.exchangeBaseUrl);
if (withdrawalGroup.denomsSel.selectedDenoms.length === 0) {
@@ -2162,14 +2267,6 @@ export async function getExchangeWithdrawalInfo(
logger.trace("selection done");
- if (selectedDenoms.selectedDenoms.length === 0) {
- throw Error(
- `unable to withdraw from ${exchangeBaseUrl}, can't select denominations for instructed amount (${Amounts.stringify(
- instructedAmount,
- )}`,
- );
- }
-
const exchangeWireAccounts: string[] = [];
for (const account of exchange.wireInfo.accounts) {
@@ -2248,38 +2345,52 @@ export async function getWithdrawalDetailsForUri(
logger.trace(`getting withdrawal details for URI ${talerWithdrawUri}`);
const info = await getBankWithdrawalInfo(wex.http, talerWithdrawUri);
logger.trace(`got bank info`);
- if (info.suggestedExchange) {
+ if (info.exchange) {
try {
// If the exchange entry doesn't exist yet,
// it'll be created as an ephemeral entry.
- await fetchFreshExchange(wex, info.suggestedExchange);
+ await fetchFreshExchange(wex, info.exchange);
} catch (e) {
// We still continued if it failed, as other exchanges might be available.
// We don't want to fail if the bank-suggested exchange is broken/offline.
logger.trace(
- `querying bank-suggested exchange (${info.suggestedExchange}) failed`,
+ `querying bank-suggested exchange (${info.exchange}) failed`,
);
}
}
- const currency = Amounts.currencyOf(info.amount);
+ const currency = info.currency;
- const listExchangesResp = await listExchanges(wex);
- const possibleExchanges = listExchangesResp.exchanges.filter((x) => {
- return (
- x.currency === currency &&
- (x.exchangeUpdateStatus === ExchangeUpdateStatus.Ready ||
- x.exchangeUpdateStatus === ExchangeUpdateStatus.ReadyUpdate)
- );
- });
+ let possibleExchanges: ExchangeListItem[];
+ if (!info.editableExchange && info.exchange !== undefined) {
+ const ex: ExchangeListItem = await lookupExchangeByUri(wex, {
+ exchangeBaseUrl: info.exchange,
+ });
+ possibleExchanges = [ex];
+ } else {
+ const listExchangesResp = await listExchanges(wex);
+
+ possibleExchanges = listExchangesResp.exchanges.filter((x) => {
+ return (
+ x.currency === currency &&
+ (x.exchangeUpdateStatus === ExchangeUpdateStatus.Ready ||
+ x.exchangeUpdateStatus === ExchangeUpdateStatus.ReadyUpdate)
+ );
+ });
+ }
return {
operationId: info.operationId,
confirmTransferUrl: info.confirmTransferUrl,
status: info.status,
- amount: Amounts.stringify(info.amount),
- defaultExchangeBaseUrl: info.suggestedExchange,
+ currency,
+ editableAmount: info.editableAmount,
+ editableExchange: info.editableExchange,
+ maxAmount: info.maxAmount ? Amounts.stringify(info.maxAmount) : undefined,
+ amount: info.amount ? Amounts.stringify(info.amount) : undefined,
+ defaultExchangeBaseUrl: info.exchange,
possibleExchanges,
+ wireFee: info.wireFee ? Amounts.stringify(info.wireFee) : undefined,
};
}
@@ -2306,7 +2417,11 @@ export async function getFundingPaytoUris(
withdrawalGroupId: string,
): Promise<string[]> {
const withdrawalGroup = await tx.withdrawalGroups.get(withdrawalGroupId);
- checkDbInvariant(!!withdrawalGroup);
+ checkDbInvariant(!!withdrawalGroup, `no withdrawal for ${withdrawalGroupId}`);
+ checkDbInvariant(
+ withdrawalGroup.exchangeBaseUrl !== undefined,
+ "can't get funding uri from uninitialized wg",
+ );
checkDbInvariant(
withdrawalGroup.instructedAmount !== undefined,
"can't get funding uri from uninitialized wg",
@@ -2379,6 +2494,7 @@ export function getBankAbortUrl(talerWithdrawUri: string): string {
async function registerReserveWithBank(
wex: WalletExecutionContext,
withdrawalGroupId: string,
+ isFlexibleAmount: boolean,
): Promise<void> {
const withdrawalGroup = await wex.db.runReadOnlyTx(
{ storeNames: ["withdrawalGroups"] },
@@ -2407,7 +2523,11 @@ async function registerReserveWithBank(
const reqBody = {
reserve_pub: withdrawalGroup.reservePub,
selected_exchange: bankInfo.exchangePaytoUri,
- };
+ } as any;
+ if (isFlexibleAmount) {
+ reqBody.amount = withdrawalGroup.instructedAmount;
+ }
+ logger.trace(`isFlexibleAmount: ${isFlexibleAmount}`);
logger.info(`registering reserve with bank: ${j2s(reqBody)}`);
const httpResp = await wex.http.fetch(bankStatusUrl, {
method: "POST",
@@ -2516,7 +2636,9 @@ async function processBankRegisterReserve(
// FIXME: Put confirm transfer URL in the DB!
- await registerReserveWithBank(wex, withdrawalGroupId);
+ const isFlexibleAmount = status.amount == null;
+
+ await registerReserveWithBank(wex, withdrawalGroupId, isFlexibleAmount);
return TaskRunResult.progress();
}
@@ -2553,6 +2675,7 @@ async function processReserveBankStatus(
uriResult.bankIntegrationApiBaseUrl,
);
bankStatusUrl.searchParams.set("long_poll_ms", "30000");
+ bankStatusUrl.searchParams.set("old_state", "selected");
logger.info(`long-polling for withdrawal operation at ${bankStatusUrl.href}`);
const statusResp = await wex.http.fetch(bankStatusUrl.href, {
@@ -2655,7 +2778,7 @@ export async function internalPrepareCreateWithdrawalGroup(
args: {
reserveStatus: WithdrawalGroupStatus;
amount?: AmountJson;
- exchangeBaseUrl: string;
+ exchangeBaseUrl: string | undefined;
forcedWithdrawalGroupId?: string;
forcedDenomSel?: ForcedDenomSel;
reserveKeyPair?: EddsaKeypair;
@@ -2696,7 +2819,7 @@ export async function internalPrepareCreateWithdrawalGroup(
let initialDenomSel: DenomSelectionState | undefined;
const denomSelUid = encodeCrock(getRandomBytes(16));
- if (amount !== undefined) {
+ if (amount !== undefined && exchangeBaseUrl !== undefined) {
initialDenomSel = await getInitialDenomsSelection(
wex,
exchangeBaseUrl,
@@ -2727,7 +2850,9 @@ export async function internalPrepareCreateWithdrawalGroup(
wgInfo: args.wgInfo,
};
- await fetchFreshExchange(wex, exchangeBaseUrl);
+ if (exchangeBaseUrl !== undefined) {
+ await fetchFreshExchange(wex, exchangeBaseUrl);
+ }
const transactionId = constructTransactionIdentifier({
tag: TransactionType.Withdrawal,
@@ -2737,12 +2862,13 @@ export async function internalPrepareCreateWithdrawalGroup(
return {
withdrawalGroup,
transactionId,
- creationInfo: !amount
- ? undefined
- : {
- amount,
- canonExchange: exchangeBaseUrl,
- },
+ creationInfo:
+ !amount || !exchangeBaseUrl
+ ? undefined
+ : {
+ amount,
+ canonExchange: exchangeBaseUrl,
+ },
};
}
@@ -2772,8 +2898,8 @@ export async function internalPerformCreateWithdrawalGroup(
if (existingWg) {
return {
withdrawalGroup: existingWg,
- exchangeNotif: undefined,
transitionInfo: undefined,
+ exchangeNotif: undefined,
};
}
await tx.withdrawalGroups.add(withdrawalGroup);
@@ -2789,7 +2915,21 @@ export async function internalPerformCreateWithdrawalGroup(
exchangeNotif: undefined,
};
}
- const exchange = await tx.exchanges.get(prep.creationInfo.canonExchange);
+ return internalPerformExchangeWasUsed(
+ wex,
+ tx,
+ prep.creationInfo.canonExchange,
+ withdrawalGroup,
+ );
+}
+
+export async function internalPerformExchangeWasUsed(
+ wex: WalletExecutionContext,
+ tx: WalletDbReadWriteTransaction<["exchanges"]>,
+ canonExchange: string,
+ withdrawalGroup: WithdrawalGroupRecord,
+): Promise<PerformCreateWithdrawalGroupResult> {
+ const exchange = await tx.exchanges.get(canonExchange);
if (exchange) {
exchange.lastWithdrawal = timestampPreciseToDb(TalerPreciseTimestamp.now());
await tx.exchanges.put(exchange);
@@ -2805,11 +2945,7 @@ export async function internalPerformCreateWithdrawalGroup(
newTxState,
};
- const exchangeUsedRes = await markExchangeUsed(
- wex,
- tx,
- prep.creationInfo.canonExchange,
- );
+ const exchangeUsedRes = await markExchangeUsed(wex, tx, canonExchange);
const ctx = new WithdrawTransactionContext(
wex,
@@ -2837,7 +2973,7 @@ export async function internalCreateWithdrawalGroup(
wex: WalletExecutionContext,
args: {
reserveStatus: WithdrawalGroupStatus;
- exchangeBaseUrl: string;
+ exchangeBaseUrl: string | undefined;
amount?: AmountJson;
forcedWithdrawalGroupId?: string;
forcedDenomSel?: ForcedDenomSel;
@@ -2883,7 +3019,6 @@ export async function prepareBankIntegratedWithdrawal(
wex: WalletExecutionContext,
req: {
talerWithdrawUri: string;
- selectedExchange?: string;
},
): Promise<PrepareBankIntegratedWithdrawalResponse> {
const existingWithdrawalGroup = await wex.db.runReadOnlyTx(
@@ -2912,12 +3047,6 @@ export async function prepareBankIntegratedWithdrawal(
const info = await getWithdrawalDetailsForUri(wex, req.talerWithdrawUri);
- const exchangeBaseUrl =
- req.selectedExchange ?? withdrawInfo.suggestedExchange;
- if (!exchangeBaseUrl) {
- return { info };
- }
-
/**
* Withdrawal group without exchange and amount
* this is an special case when the user haven't yet
@@ -2926,7 +3055,7 @@ export async function prepareBankIntegratedWithdrawal(
* same URI
*/
const withdrawalGroup = await internalCreateWithdrawalGroup(wex, {
- exchangeBaseUrl,
+ exchangeBaseUrl: undefined,
wgInfo: {
withdrawalType: WithdrawalRecordType.BankIntegrated,
bankInfo: {
@@ -2935,6 +3064,7 @@ export async function prepareBankIntegratedWithdrawal(
timestampBankConfirmed: undefined,
timestampReserveInfoPosted: undefined,
wireTypes: withdrawInfo.wireTypes,
+ currency: withdrawInfo.currency,
},
},
reserveStatus: WithdrawalGroupStatus.DialogProposed,
@@ -2957,6 +3087,9 @@ export async function confirmWithdrawal(
req: ConfirmWithdrawalRequest,
): Promise<void> {
const parsedTx = parseTransactionIdentifier(req.transactionId);
+ const selectedExchange = req.exchangeBaseUrl;
+ const instructedAmount = Amounts.parseOrThrow(req.amount);
+
if (parsedTx?.tag !== TransactionType.Withdrawal) {
throw Error("invalid withdrawal transaction ID");
}
@@ -2978,38 +3111,44 @@ export async function confirmWithdrawal(
throw Error("not a bank integrated withdrawal");
}
- const selectedExchange = req.exchangeBaseUrl;
const exchange = await fetchFreshExchange(wex, selectedExchange);
+ requireExchangeTosAcceptedOrThrow(exchange);
const talerWithdrawUri = withdrawalGroup.wgInfo.bankInfo.talerWithdrawUri;
const confirmUrl = withdrawalGroup.wgInfo.bankInfo.confirmUrl;
/**
- * The only reasong this to be undefined is because it is an old wallet
- * database before adding the wireType field was added
+ * The only reason this could be undefined is because it is an old wallet
+ * database before adding the prepareWithdrawal feature
*/
- let wtypes: string[];
- if (withdrawalGroup.wgInfo.bankInfo.wireTypes === undefined) {
+ let bankWireTypes: string[];
+ let bankCurrency: string;
+ if (
+ withdrawalGroup.wgInfo.bankInfo.wireTypes === undefined ||
+ withdrawalGroup.wgInfo.bankInfo.currency === undefined
+ ) {
const withdrawInfo = await getBankWithdrawalInfo(
wex.http,
talerWithdrawUri,
);
- wtypes = withdrawInfo.wireTypes;
+ bankWireTypes = withdrawInfo.wireTypes;
+ bankCurrency = withdrawInfo.currency;
} else {
- wtypes = withdrawalGroup.wgInfo.bankInfo.wireTypes;
+ bankWireTypes = withdrawalGroup.wgInfo.bankInfo.wireTypes;
+ bankCurrency = withdrawalGroup.wgInfo.bankInfo.currency;
}
const exchangePaytoUri = await getExchangePaytoUri(
wex,
selectedExchange,
- wtypes,
+ bankWireTypes,
);
const withdrawalAccountList = await fetchWithdrawalAccountInfo(
wex,
{
exchange,
- instructedAmount: Amounts.parseOrThrow(req.amount),
+ instructedAmount,
},
wex.cancellationToken,
);
@@ -3020,23 +3159,34 @@ export async function confirmWithdrawal(
);
const initalDenoms = await getInitialDenomsSelection(
wex,
- req.exchangeBaseUrl,
- Amounts.parseOrThrow(req.amount),
+ exchange.exchangeBaseUrl,
+ instructedAmount,
req.forcedDenomSel,
);
- ctx.transition({}, async (rec) => {
+ let pending = false;
+ await ctx.transition({}, async (rec) => {
if (!rec) {
return TransitionResult.stay();
}
switch (rec.status) {
+ case WithdrawalGroupStatus.PendingWaitConfirmBank: {
+ pending = true;
+ return TransitionResult.stay();
+ }
+ case WithdrawalGroupStatus.AbortedOtherWallet: {
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_WITHDRAWAL_OPERATION_ABORTED_BY_BANK,
+ {},
+ );
+ }
case WithdrawalGroupStatus.DialogProposed: {
- rec.exchangeBaseUrl = req.exchangeBaseUrl;
+ rec.exchangeBaseUrl = exchange.exchangeBaseUrl;
rec.instructedAmount = req.amount;
+ rec.restrictAge = req.restrictAge;
rec.denomsSel = initalDenoms;
rec.rawWithdrawalAmount = initalDenoms.totalWithdrawCost;
rec.effectiveWithdrawalAmount = initalDenoms.totalCoinValue;
- rec.restrictAge = req.restrictAge;
rec.wgInfo = {
withdrawalType: WithdrawalRecordType.BankIntegrated,
@@ -3047,20 +3197,50 @@ export async function confirmWithdrawal(
confirmUrl: confirmUrl,
timestampBankConfirmed: undefined,
timestampReserveInfoPosted: undefined,
- wireTypes: wtypes,
+ wireTypes: bankWireTypes,
+ currency: bankCurrency,
},
};
-
+ pending = true;
rec.status = WithdrawalGroupStatus.PendingRegisteringBank;
return TransitionResult.transition(rec);
}
- default:
- throw Error("unable to confirm withdrawal in current state");
+ default: {
+ throw Error(
+ `unable to confirm withdrawal in current state: ${rec.status}`,
+ );
+ }
}
});
await wex.taskScheduler.resetTaskRetries(ctx.taskId);
- wex.taskScheduler.startShepherdTask(ctx.taskId);
+
+ wex.ws.notify({
+ type: NotificationType.BalanceChange,
+ hintTransactionId: ctx.transactionId,
+ });
+
+ const res = await wex.db.runReadWriteTx(
+ {
+ storeNames: ["exchanges"],
+ },
+ async (tx) => {
+ const r = await internalPerformExchangeWasUsed(
+ wex,
+ tx,
+ exchange.exchangeBaseUrl,
+ withdrawalGroup,
+ );
+ return r;
+ },
+ );
+ if (res.exchangeNotif) {
+ wex.ws.notify(res.exchangeNotif);
+ }
+
+ if (pending) {
+ await waitWithdrawalRegistered(wex, ctx);
+ }
}
/**
@@ -3080,181 +3260,119 @@ export async function acceptWithdrawalFromUri(
selectedExchange: string;
forcedDenomSel?: ForcedDenomSel;
restrictAge?: number;
+ amount?: AmountLike;
},
): Promise<AcceptWithdrawalResponse> {
const selectedExchange = req.selectedExchange;
logger.info(
- `accepting withdrawal via ${req.talerWithdrawUri}, canonicalized selected exchange ${selectedExchange}`,
- );
- const existingWithdrawalGroup = await wex.db.runReadOnlyTx(
- { storeNames: ["withdrawalGroups"] },
- async (tx) => {
- return await tx.withdrawalGroups.indexes.byTalerWithdrawUri.get(
- req.talerWithdrawUri,
- );
- },
+ `preparing withdrawal via ${req.talerWithdrawUri}, canonicalized selected exchange ${selectedExchange}`,
);
- if (existingWithdrawalGroup) {
- let url: string | undefined;
- if (
- existingWithdrawalGroup.wgInfo.withdrawalType ===
- WithdrawalRecordType.BankIntegrated
- ) {
- url = existingWithdrawalGroup.wgInfo.bankInfo.confirmUrl;
+ const p = await prepareBankIntegratedWithdrawal(wex, {
+ talerWithdrawUri: req.talerWithdrawUri,
+ });
+
+ let amount: AmountString;
+ if (p.info.amount == null) {
+ if (req.amount == null) {
+ throw Error(
+ "amount required, as withdrawal operation has flexible amount",
+ );
}
- return {
- reservePub: existingWithdrawalGroup.reservePub,
- confirmTransferUrl: url,
- transactionId: constructTransactionIdentifier({
- tag: TransactionType.Withdrawal,
- withdrawalGroupId: existingWithdrawalGroup.withdrawalGroupId,
- }),
- };
+ amount = req.amount as AmountString;
+ } else {
+ if (req.amount != null && Amounts.cmp(req.amount, p.info.amount) != 0) {
+ throw Error(
+ "mismatched amount, amount is fixed by bank but client provided different amount",
+ );
+ }
+ amount = p.info.amount;
}
- const exchange = await fetchFreshExchange(wex, selectedExchange);
- const withdrawInfo = await getBankWithdrawalInfo(
- wex.http,
- req.talerWithdrawUri,
- );
- const exchangePaytoUri = await getExchangePaytoUri(
- wex,
- selectedExchange,
- withdrawInfo.wireTypes,
- );
-
- const withdrawalAccountList = await fetchWithdrawalAccountInfo(
- wex,
- {
- exchange,
- instructedAmount: withdrawInfo.amount,
- },
- CancellationToken.CONTINUE,
- );
-
- const withdrawalGroup = await internalCreateWithdrawalGroup(wex, {
- amount: withdrawInfo.amount,
- exchangeBaseUrl: req.selectedExchange,
- wgInfo: {
- withdrawalType: WithdrawalRecordType.BankIntegrated,
- exchangeCreditAccounts: withdrawalAccountList,
- bankInfo: {
- exchangePaytoUri,
- talerWithdrawUri: req.talerWithdrawUri,
- confirmUrl: withdrawInfo.confirmTransferUrl,
- timestampBankConfirmed: undefined,
- timestampReserveInfoPosted: undefined,
- wireTypes: withdrawInfo.wireTypes,
- },
- },
+ logger.info(`confirming withdrawal with tx ${p.transactionId}`);
+ await confirmWithdrawal(wex, {
+ amount: Amounts.stringify(amount),
+ exchangeBaseUrl: selectedExchange,
+ transactionId: p.transactionId,
restrictAge: req.restrictAge,
forcedDenomSel: req.forcedDenomSel,
- reserveStatus: WithdrawalGroupStatus.PendingRegisteringBank,
});
- const withdrawalGroupId = withdrawalGroup.withdrawalGroupId;
-
- const ctx = new WithdrawTransactionContext(wex, withdrawalGroupId);
-
- wex.ws.notify({
- type: NotificationType.BalanceChange,
- hintTransactionId: ctx.transactionId,
- });
-
- await waitWithdrawalRegistered(wex, ctx);
+ const newWithdrawralGroup = await wex.db.runReadOnlyTx(
+ { storeNames: ["withdrawalGroups"] },
+ async (tx) => {
+ return await tx.withdrawalGroups.indexes.byTalerWithdrawUri.get(
+ req.talerWithdrawUri,
+ );
+ },
+ );
- wex.taskScheduler.startShepherdTask(ctx.taskId);
+ checkDbInvariant(
+ newWithdrawralGroup !== undefined,
+ "withdrawal don't exist after confirm",
+ );
return {
- reservePub: withdrawalGroup.reservePub,
- confirmTransferUrl: withdrawInfo.confirmTransferUrl,
- transactionId: ctx.transactionId,
+ reservePub: newWithdrawralGroup.reservePub,
+ confirmTransferUrl: p.info.confirmTransferUrl,
+ transactionId: p.transactionId,
};
}
-async function internalWaitWithdrawalRegistered(
+async function waitWithdrawalRegistered(
wex: WalletExecutionContext,
ctx: WithdrawTransactionContext,
- withdrawalNotifFlag: AsyncFlag,
): Promise<void> {
- while (true) {
- const { withdrawalRec, retryRec } = await wex.db.runReadOnlyTx(
- { storeNames: ["withdrawalGroups", "operationRetries"] },
- async (tx) => {
- return {
- withdrawalRec: await tx.withdrawalGroups.get(ctx.withdrawalGroupId),
- retryRec: await tx.operationRetries.get(ctx.taskId),
- };
- },
- );
+ await genericWaitForState(wex, {
+ async checkState(): Promise<boolean> {
+ const { withdrawalRec, retryRec } = await wex.db.runReadOnlyTx(
+ { storeNames: ["withdrawalGroups", "operationRetries"] },
+ async (tx) => {
+ return {
+ withdrawalRec: await tx.withdrawalGroups.get(ctx.withdrawalGroupId),
+ retryRec: await tx.operationRetries.get(ctx.taskId),
+ };
+ },
+ );
- if (!withdrawalRec) {
- throw Error("withdrawal not found anymore");
- }
+ if (!withdrawalRec) {
+ throw Error("withdrawal not found anymore");
+ }
- switch (withdrawalRec.status) {
- case WithdrawalGroupStatus.FailedBankAborted:
- throw TalerError.fromDetail(
- TalerErrorCode.WALLET_WITHDRAWAL_OPERATION_ABORTED_BY_BANK,
- {},
- );
- case WithdrawalGroupStatus.PendingKyc:
- case WithdrawalGroupStatus.PendingAml:
- case WithdrawalGroupStatus.PendingQueryingStatus:
- case WithdrawalGroupStatus.PendingReady:
- case WithdrawalGroupStatus.Done:
- case WithdrawalGroupStatus.PendingWaitConfirmBank:
- return;
- case WithdrawalGroupStatus.PendingRegisteringBank:
- break;
- default: {
- if (retryRec) {
- if (retryRec.lastError) {
- throw TalerError.fromUncheckedDetail(retryRec.lastError);
- } else {
- throw Error("withdrawal unexpectedly pending");
+ switch (withdrawalRec.status) {
+ case WithdrawalGroupStatus.FailedBankAborted:
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_WITHDRAWAL_OPERATION_ABORTED_BY_BANK,
+ {},
+ );
+ case WithdrawalGroupStatus.PendingKyc:
+ case WithdrawalGroupStatus.PendingAml:
+ case WithdrawalGroupStatus.PendingQueryingStatus:
+ case WithdrawalGroupStatus.PendingReady:
+ case WithdrawalGroupStatus.Done:
+ case WithdrawalGroupStatus.PendingWaitConfirmBank:
+ return true;
+ case WithdrawalGroupStatus.PendingRegisteringBank:
+ break;
+ default: {
+ if (retryRec) {
+ if (retryRec.lastError) {
+ throw TalerError.fromUncheckedDetail(retryRec.lastError);
+ } else {
+ throw Error("withdrawal unexpectedly pending");
+ }
}
}
}
- }
-
- await withdrawalNotifFlag.wait();
- withdrawalNotifFlag.reset();
- }
-}
-
-async function waitWithdrawalRegistered(
- wex: WalletExecutionContext,
- ctx: WithdrawTransactionContext,
-): Promise<void> {
- // FIXME: Doesn't support cancellation yet
- // FIXME: We should use Symbol.dispose magic here for cleanup!
-
- const withdrawalNotifFlag = new AsyncFlag();
- // Raise exchangeNotifFlag whenever we get a notification
- // about our exchange.
- const cancelNotif = wex.ws.addNotificationListener((notif) => {
- if (
- notif.type === NotificationType.TransactionStateTransition &&
- notif.transactionId === ctx.transactionId
- ) {
- logger.info(`raising update notification: ${j2s(notif)}`);
- withdrawalNotifFlag.raise();
- }
+ return false;
+ },
+ filterNotification(notif) {
+ return (
+ notif.type === NotificationType.TransactionStateTransition &&
+ notif.transactionId === ctx.transactionId
+ );
+ },
});
-
- try {
- const res = await internalWaitWithdrawalRegistered(
- wex,
- ctx,
- withdrawalNotifFlag,
- );
- logger.info("done waiting for ready exchange");
- return res;
- } finally {
- cancelNotif();
- }
}
async function fetchAccount(
@@ -3422,7 +3540,7 @@ export async function createManualWithdrawal(
);
const withdrawalGroup = await internalCreateWithdrawalGroup(wex, {
- amount: Amounts.jsonifyAmount(req.amount),
+ amount: amount,
wgInfo: {
withdrawalType: WithdrawalRecordType.BankManual,
exchangeCreditAccounts: withdrawalAccountsList,
@@ -3507,7 +3625,7 @@ async function internalWaitWithdrawalFinal(
// Check if refresh is final
const res = await ctx.wex.db.runReadOnlyTx(
- { storeNames: ["withdrawalGroups", "operationRetries"] },
+ { storeNames: ["withdrawalGroups"] },
async (tx) => {
return {
wg: await tx.withdrawalGroups.get(ctx.withdrawalGroupId),
@@ -3550,7 +3668,7 @@ export async function getWithdrawalDetailsForAmount(
type: ObservabilityEventType.Message,
contents: `Cancelling previous key ${clientCancelKey}`,
});
- prevCts.cancel();
+ prevCts.cancel(`getting details amount`);
} else {
wex.oc.observe({
type: ObservabilityEventType.Message,