diff options
Diffstat (limited to 'packages/taler-wallet-core/src')
30 files changed, 931 insertions, 682 deletions
diff --git a/packages/taler-wallet-core/src/crypto/cryptoImplementation.ts b/packages/taler-wallet-core/src/crypto/cryptoImplementation.ts index 35777e714..56392f090 100644 --- a/packages/taler-wallet-core/src/crypto/cryptoImplementation.ts +++ b/packages/taler-wallet-core/src/crypto/cryptoImplementation.ts @@ -87,7 +87,7 @@ import { WithdrawalPlanchet, } from "@gnu-taler/taler-util"; // FIXME: Crypto should not use DB Types! -import { DenominationRecord } from "../db.js"; +import { DenominationRecord, timestampProtocolFromDb } from "../db.js"; import { CreateRecoupRefreshReqRequest, CreateRecoupReqRequest, @@ -962,10 +962,22 @@ export const nativeCryptoR: TalerCryptoInterfaceR = { const value: AmountJson = Amounts.parseOrThrow(denom.value); const p = buildSigPS(TalerSignaturePurpose.MASTER_DENOMINATION_KEY_VALIDITY) .put(decodeCrock(masterPub)) - .put(timestampRoundedToBuffer(denom.stampStart)) - .put(timestampRoundedToBuffer(denom.stampExpireWithdraw)) - .put(timestampRoundedToBuffer(denom.stampExpireDeposit)) - .put(timestampRoundedToBuffer(denom.stampExpireLegal)) + .put(timestampRoundedToBuffer(timestampProtocolFromDb(denom.stampStart))) + .put( + timestampRoundedToBuffer( + timestampProtocolFromDb(denom.stampExpireWithdraw), + ), + ) + .put( + timestampRoundedToBuffer( + timestampProtocolFromDb(denom.stampExpireDeposit), + ), + ) + .put( + timestampRoundedToBuffer( + timestampProtocolFromDb(denom.stampExpireLegal), + ), + ) .put(amountToBuffer(value)) .put(amountToBuffer(denom.fees.feeWithdraw)) .put(amountToBuffer(denom.fees.feeDeposit)) diff --git a/packages/taler-wallet-core/src/db.ts b/packages/taler-wallet-core/src/db.ts index 239a6d4a4..46a073156 100644 --- a/packages/taler-wallet-core/src/db.ts +++ b/packages/taler-wallet-core/src/db.ts @@ -27,8 +27,8 @@ import { structuredEncapsulate, } from "@gnu-taler/idb-bridge"; import { + AbsoluteTime, AgeCommitmentProof, - AmountJson, AmountString, Amounts, AttentionInfo, @@ -45,23 +45,20 @@ import { ExchangeAuditor, ExchangeGlobalFees, HashCodeString, - InternationalizedString, Logger, - MerchantContractTerms, - MerchantInfo, PayCoinSelection, - PeerContractTerms, RefreshReason, TalerErrorDetail, TalerPreciseTimestamp, TalerProtocolDuration, TalerProtocolTimestamp, + //TalerProtocolTimestamp, TransactionIdStr, UnblindedSignature, WireInfo, codecForAny, } from "@gnu-taler/taler-util"; -import { RetryInfo, TaskIdentifiers } from "./operations/common.js"; +import { DbRetryInfo, TaskIdentifiers } from "./operations/common.js"; import { DbAccess, DbReadOnlyTransaction, @@ -151,6 +148,91 @@ export const CURRENT_DB_CONFIG_KEY = "currentMainDbName"; */ export const WALLET_DB_MINOR_VERSION = 1; +declare const symDbProtocolTimestamp: unique symbol; + +declare const symDbPreciseTimestamp: unique symbol; + +/** + * Timestamp, stored as microseconds. + * + * Always rounded to a full second. + */ +export type DbProtocolTimestamp = number & { [symDbProtocolTimestamp]: true }; + +/** + * Timestamp, stored as microseconds. + */ +export type DbPreciseTimestamp = number & { [symDbPreciseTimestamp]: true }; + +const DB_TIMESTAMP_FOREVER = Number.MAX_SAFE_INTEGER; + +export function timestampPreciseFromDb( + dbTs: DbPreciseTimestamp, +): TalerPreciseTimestamp { + return TalerPreciseTimestamp.fromMilliseconds(Math.floor(dbTs / 1000)); +} + +export function timestampOptionalPreciseFromDb( + dbTs: DbPreciseTimestamp | undefined, +): TalerPreciseTimestamp | undefined { + if (!dbTs) { + return undefined; + } + return TalerPreciseTimestamp.fromMilliseconds(Math.floor(dbTs / 1000)); +} + +export function timestampPreciseToDb( + stamp: TalerPreciseTimestamp, +): DbPreciseTimestamp { + if (stamp.t_s === "never") { + return DB_TIMESTAMP_FOREVER as DbPreciseTimestamp; + } else { + let tUs = stamp.t_s * 1000000; + if (stamp.off_us) { + tUs == stamp.off_us; + } + return tUs as DbPreciseTimestamp; + } +} + +export function timestampProtocolToDb( + stamp: TalerProtocolTimestamp, +): DbProtocolTimestamp { + if (stamp.t_s === "never") { + return DB_TIMESTAMP_FOREVER as DbProtocolTimestamp; + } else { + let tUs = stamp.t_s * 1000000; + return tUs as DbProtocolTimestamp; + } +} + +export function timestampProtocolFromDb( + stamp: DbProtocolTimestamp, +): TalerProtocolTimestamp { + return TalerProtocolTimestamp.fromSeconds(Math.floor(stamp / 1000000)); +} + +export function timestampAbsoluteFromDb( + stamp: DbProtocolTimestamp | DbPreciseTimestamp, +): AbsoluteTime { + if (stamp >= DB_TIMESTAMP_FOREVER) { + return AbsoluteTime.never(); + } + return AbsoluteTime.fromMilliseconds(Math.floor(stamp / 1000)); +} + +export function timestampOptionalAbsoluteFromDb( + stamp: DbProtocolTimestamp | DbPreciseTimestamp | undefined, +): AbsoluteTime | undefined { + if (stamp == null) { + return undefined; + } + if (stamp >= DB_TIMESTAMP_FOREVER) { + return AbsoluteTime.never(); + } + return AbsoluteTime.fromMilliseconds(Math.floor(stamp / 1000)); +} + /** * Format of the operation status code: 0x0abc_nnnn @@ -217,7 +299,7 @@ export enum WithdrawalGroupStatus { * Exchange is doing AML checks. */ PendingAml = 0x0100_0006, - SuspendedAml = 0x0100_0006, + SuspendedAml = 0x0110_0006, /** * The corresponding withdraw record has been created. @@ -268,14 +350,14 @@ export interface ReserveBankInfo { * * Set to undefined if that hasn't happened yet. */ - timestampReserveInfoPosted: TalerPreciseTimestamp | undefined; + timestampReserveInfoPosted: DbPreciseTimestamp | undefined; /** * Time when the reserve was confirmed by the bank. * * Set to undefined if not confirmed yet. */ - timestampBankConfirmed: TalerPreciseTimestamp | undefined; + timestampBankConfirmed: DbPreciseTimestamp | undefined; } /** @@ -349,22 +431,22 @@ export interface DenominationRecord { /** * Validity start date of the denomination. */ - stampStart: TalerProtocolTimestamp; + stampStart: DbProtocolTimestamp; /** * Date after which the currency can't be withdrawn anymore. */ - stampExpireWithdraw: TalerProtocolTimestamp; + stampExpireWithdraw: DbProtocolTimestamp; /** * Date after the denomination officially doesn't exist anymore. */ - stampExpireLegal: TalerProtocolTimestamp; + stampExpireLegal: DbProtocolTimestamp; /** * Data after which coins of this denomination can't be deposited anymore. */ - stampExpireDeposit: TalerProtocolTimestamp; + stampExpireDeposit: DbProtocolTimestamp; /** * Signature by the exchange's master key over the denomination @@ -406,7 +488,7 @@ export interface DenominationRecord { * Latest list issue date of the "/keys" response * that includes this denomination. */ - listIssueDate: TalerProtocolTimestamp; + listIssueDate: DbProtocolTimestamp; } export namespace DenominationRecord { @@ -418,10 +500,10 @@ export namespace DenominationRecord { feeRefresh: Amounts.stringify(d.fees.feeRefresh), feeRefund: Amounts.stringify(d.fees.feeRefund), feeWithdraw: Amounts.stringify(d.fees.feeWithdraw), - stampExpireDeposit: d.stampExpireDeposit, - stampExpireLegal: d.stampExpireLegal, - stampExpireWithdraw: d.stampExpireWithdraw, - stampStart: d.stampStart, + stampExpireDeposit: timestampProtocolFromDb(d.stampExpireDeposit), + stampExpireLegal: timestampProtocolFromDb(d.stampExpireLegal), + stampExpireWithdraw: timestampProtocolFromDb(d.stampExpireWithdraw), + stampStart: timestampProtocolFromDb(d.stampStart), value: Amounts.stringify(d.value), exchangeBaseUrl: d.exchangeBaseUrl, }; @@ -429,9 +511,9 @@ export namespace DenominationRecord { } export interface ExchangeSignkeysRecord { - stampStart: TalerProtocolTimestamp; - stampExpire: TalerProtocolTimestamp; - stampEnd: TalerProtocolTimestamp; + stampStart: DbProtocolTimestamp; + stampExpire: DbProtocolTimestamp; + stampEnd: DbProtocolTimestamp; signkeyPub: EddsaPublicKeyString; masterSig: EddsaSignatureString; @@ -488,7 +570,7 @@ export interface ExchangeDetailsRecord { tosAccepted: | { etag: string; - timestamp: TalerPreciseTimestamp; + timestamp: DbPreciseTimestamp; } | undefined; @@ -500,25 +582,6 @@ export interface ExchangeDetailsRecord { ageMask?: number; } -export interface ExchangeTosRecord { - exchangeBaseUrl: string; - - etag: string; - - /** - * Terms of service text or undefined if not downloaded yet. - * - * This is just used as a cache of the last downloaded ToS. - * - */ - termsOfServiceText: string | undefined; - - /** - * Content-type of the last downloaded termsOfServiceText. - */ - termsOfServiceContentType: string | undefined; -} - export interface ExchangeDetailsPointer { masterPublicKey: string; @@ -528,7 +591,7 @@ export interface ExchangeDetailsPointer { * Timestamp when the (masterPublicKey, currency) pointer * has been updated. */ - updateClock: TalerPreciseTimestamp; + updateClock: DbPreciseTimestamp; } export enum ExchangeEntryDbRecordStatus { @@ -549,11 +612,6 @@ export enum ExchangeEntryDbUpdateStatus { } /** - * Timestamp stored as a IEEE 754 double, in milliseconds. - */ -export type DbIndexableTimestampMs = number; - -/** * Exchange record as stored in the wallet's database. */ export interface ExchangeEntryRecord { @@ -563,11 +621,17 @@ export interface ExchangeEntryRecord { baseUrl: string; /** + * Currency hint for a preset exchange, relevant + * when we didn't contact a preset exchange yet. + */ + presetCurrencyHint?: string; + + /** * When did we confirm the last withdrawal from this exchange? * * Used mostly in the UI to suggest exchanges. */ - lastWithdrawal?: TalerPreciseTimestamp; + lastWithdrawal?: DbPreciseTimestamp; /** * Pointer to the current exchange details. @@ -588,17 +652,12 @@ export interface ExchangeEntryRecord { /** * Last time when the exchange /keys info was updated. */ - lastUpdate: TalerPreciseTimestamp | undefined; + lastUpdate: DbPreciseTimestamp | undefined; /** * Next scheduled update for the exchange. - * - * (This field must always be present, so we can index on the timestamp.) - * - * FIXME: To index on the timestamp, this needs to be a number of - * binary timestamp! */ - nextUpdateStampMs: DbIndexableTimestampMs; + nextUpdateStamp: DbPreciseTimestamp; lastKeysEtag: string | undefined; @@ -608,7 +667,7 @@ export interface ExchangeEntryRecord { * Updated whenever the exchange's denominations are updated or when * the refresh check has been done. */ - nextRefreshCheckStampMs: DbIndexableTimestampMs; + nextRefreshCheckStamp: DbPreciseTimestamp; /** * Public key of the reserve that we're currently using for @@ -823,7 +882,7 @@ 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: TalerPreciseTimestamp | undefined; + acceptedTimestamp: DbPreciseTimestamp | undefined; /** * The tipped amount. @@ -838,7 +897,7 @@ export interface RewardRecord { /** * Timestamp, the tip can't be picked up anymore after this deadline. */ - rewardExpiration: TalerProtocolTimestamp; + rewardExpiration: DbProtocolTimestamp; /** * The exchange that will sign our coins, chosen by the merchant. @@ -876,7 +935,7 @@ export interface RewardRecord { */ merchantRewardId: string; - createdTimestamp: TalerPreciseTimestamp; + createdTimestamp: DbPreciseTimestamp; /** * The url to be redirected after the tip is accepted. @@ -887,7 +946,7 @@ export interface RewardRecord { * Timestamp for when the wallet finished picking up the tip * from the merchant. */ - pickedUpTimestamp: TalerPreciseTimestamp | undefined; + pickedUpTimestamp: DbPreciseTimestamp | undefined; status: RewardRecordStatus; } @@ -985,12 +1044,12 @@ export interface RefreshGroupRecord { */ statusPerCoin: RefreshCoinStatus[]; - timestampCreated: TalerPreciseTimestamp; + timestampCreated: DbPreciseTimestamp; /** * Timestamp when the refresh session finished. */ - timestampFinished: TalerPreciseTimestamp | undefined; + timestampFinished: DbPreciseTimestamp | undefined; } /** @@ -1215,7 +1274,7 @@ export interface PurchaseRecord { * Timestamp of the first time that sending a payment to the merchant * for this purchase was successful. */ - timestampFirstSuccessfulPay: TalerPreciseTimestamp | undefined; + timestampFirstSuccessfulPay: DbPreciseTimestamp | undefined; merchantPaySig: string | undefined; @@ -1230,19 +1289,19 @@ export interface PurchaseRecord { /** * When was the purchase record created? */ - timestamp: TalerPreciseTimestamp; + timestamp: DbPreciseTimestamp; /** * When was the purchase made? * Refers to the time that the user accepted. */ - timestampAccept: TalerPreciseTimestamp | undefined; + timestampAccept: DbPreciseTimestamp | undefined; /** * When was the last refund made? * Set to 0 if no refund was made on the purchase. */ - timestampLastRefundStatus: TalerPreciseTimestamp | undefined; + timestampLastRefundStatus: DbPreciseTimestamp | undefined; /** * Last session signature that we submitted to /pay (if any). @@ -1252,7 +1311,7 @@ export interface PurchaseRecord { /** * Continue querying the refund status until this deadline has expired. */ - autoRefundDeadline: TalerProtocolTimestamp | undefined; + autoRefundDeadline: DbProtocolTimestamp | undefined; /** * How much merchant has refund to be taken but the wallet @@ -1292,12 +1351,12 @@ export interface WalletBackupConfState { /** * Timestamp stored in the last backup. */ - lastBackupTimestamp?: TalerPreciseTimestamp; + lastBackupTimestamp?: DbPreciseTimestamp; /** * Last time we tried to do a backup. */ - lastBackupCheckTimestamp?: TalerPreciseTimestamp; + lastBackupCheckTimestamp?: DbPreciseTimestamp; lastBackupNonce?: string; } @@ -1405,12 +1464,12 @@ export interface WithdrawalGroupRecord { * When was the withdrawal operation started started? * Timestamp in milliseconds. */ - timestampStart: TalerPreciseTimestamp; + timestampStart: DbPreciseTimestamp; /** * When was the withdrawal operation completed? */ - timestampFinish?: TalerPreciseTimestamp; + timestampFinish?: DbPreciseTimestamp; /** * Current status of the reserve. @@ -1501,9 +1560,9 @@ export interface RecoupGroupRecord { exchangeBaseUrl: string; - timestampStarted: TalerPreciseTimestamp; + timestampStarted: DbPreciseTimestamp; - timestampFinished: TalerPreciseTimestamp | undefined; + timestampFinished: DbPreciseTimestamp | undefined; /** * Public keys that identify the coins being recouped @@ -1537,7 +1596,7 @@ export type BackupProviderState = } | { tag: BackupProviderStateTag.Ready; - nextBackupTimestamp: TalerPreciseTimestamp; + nextBackupTimestamp: DbPreciseTimestamp; } | { tag: BackupProviderStateTag.Retrying; @@ -1582,7 +1641,7 @@ export interface BackupProviderRecord { * Does NOT correspond to the timestamp of the backup, * which only changes when the backup content changes. */ - lastBackupCycleTimestamp?: TalerPreciseTimestamp; + lastBackupCycleTimestamp?: DbPreciseTimestamp; /** * Proposal that we're currently trying to pay for. @@ -1633,7 +1692,7 @@ export interface DepositTrackingInfo { // Raw wire transfer identifier of the deposit. wireTransferId: string; // When was the wire transfer given to the bank. - timestampExecuted: TalerProtocolTimestamp; + timestampExecuted: DbProtocolTimestamp; // Total amount transfer for this wtid (including fees) amountRaw: AmountString; // Wire fee amount for this exchange @@ -1655,7 +1714,7 @@ export interface DepositGroupRecord { */ amount: AmountString; - wireTransferDeadline: TalerProtocolTimestamp; + wireTransferDeadline: DbProtocolTimestamp; merchantPub: string; merchantPriv: string; @@ -1685,9 +1744,9 @@ export interface DepositGroupRecord { */ counterpartyEffectiveDepositAmount: AmountString; - timestampCreated: TalerPreciseTimestamp; + timestampCreated: DbPreciseTimestamp; - timestampFinished: TalerPreciseTimestamp | undefined; + timestampFinished: DbPreciseTimestamp | undefined; operationStatus: DepositOperationStatus; @@ -1796,9 +1855,9 @@ export interface PeerPushDebitRecord { */ contractEncNonce: string; - purseExpiration: TalerProtocolTimestamp; + purseExpiration: DbProtocolTimestamp; - timestampCreated: TalerPreciseTimestamp; + timestampCreated: DbPreciseTimestamp; abortRefreshGroupId?: string; @@ -1871,7 +1930,7 @@ export interface PeerPullCreditRecord { contractEncNonce: string; - mergeTimestamp: TalerPreciseTimestamp; + mergeTimestamp: DbPreciseTimestamp; mergeReserveRowId: number; @@ -1923,7 +1982,7 @@ export interface PeerPushPaymentIncomingRecord { contractPriv: string; - timestamp: TalerPreciseTimestamp; + timestamp: DbPreciseTimestamp; estimatedAmountEffective: AmountString; @@ -1995,7 +2054,7 @@ export interface PeerPullPaymentIncomingRecord { contractTermsHash: string; - timestampCreated: TalerPreciseTimestamp; + timestampCreated: DbPreciseTimestamp; /** * Contract priv that we got from the other party. @@ -2042,7 +2101,7 @@ export interface OperationRetryRecord { lastError?: TalerErrorDetail; - retryInfo: RetryInfo; + retryInfo: DbRetryInfo; } /** @@ -2095,14 +2154,13 @@ export interface UserAttentionRecord { /** * When the notification was created. - * FIXME: This should be a TalerPreciseTimestamp */ - createdMs: number; + created: DbPreciseTimestamp; /** * When the user mark this notification as read. */ - read: TalerPreciseTimestamp | undefined; + read: DbPreciseTimestamp | undefined; } export interface DbExchangeHandle { @@ -2146,7 +2204,7 @@ export interface RefundGroupRecord { /** * Timestamp when the refund group was created. */ - timestampCreated: TalerPreciseTimestamp; + timestampCreated: DbPreciseTimestamp; proposalId: string; @@ -2198,12 +2256,12 @@ export interface RefundItemRecord { /** * Execution time as claimed by the merchant */ - executionTime: TalerProtocolTimestamp; + executionTime: DbProtocolTimestamp; /** * Time when the wallet became aware of the refund. */ - obtainedTime: TalerPreciseTimestamp; + obtainedTime: DbPreciseTimestamp; refundAmount: AmountString; @@ -2389,15 +2447,19 @@ export const WalletStoresV1 = { "planchets", describeContents<PlanchetRecord>({ keyPath: "coinPub" }), { - byGroupAgeCoin: describeIndex("byGroupAgeCoin", [ - "withdrawalGroupId", - "ageWithdrawIdx", - "coinIdx", - ]), - byGroupAndIndex: describeIndex("byGroupAndIndex", [ - "withdrawalGroupId", - "coinIdx", - ]), + byGroupAgeCoin: describeIndex( + "byGroupAgeCoin", [ "withdrawalGroupId", "ageWithdrawIdx", "coinIdx" ], + { + unique: true, + }, + ), + byGroupAndIndex: describeIndex( + "byGroupAndIndex", + ["withdrawalGroupId", "coinIdx"], + { + unique: true, + }, + ), byGroup: describeIndex("byGroup", "withdrawalGroupId"), byCoinEvHash: describeIndex("byCoinEv", "coinEvHash"), }, @@ -3049,6 +3111,7 @@ export async function openTalerDatabase( case "taler-wallet-main-v6": case "taler-wallet-main-v7": case "taler-wallet-main-v8": + case "taler-wallet-main-v9": // We consider this a pre-release // development version, no migration is done. await metaDb diff --git a/packages/taler-wallet-core/src/dbless.ts b/packages/taler-wallet-core/src/dbless.ts index d70eab888..4fc890788 100644 --- a/packages/taler-wallet-core/src/dbless.ts +++ b/packages/taler-wallet-core/src/dbless.ts @@ -31,7 +31,7 @@ import { AmountJson, Amounts, AmountString, - BankAccessApiClient, + TalerCorebankApiClient, codecForAny, codecForBankWithdrawalOperationPostResponse, codecForBatchDepositSuccess, @@ -109,7 +109,7 @@ export async function checkReserve( export interface TopupReserveWithDemobankArgs { http: HttpRequestLibrary; reservePub: string; - bankAccessApiBaseUrl: string; + corebankApiBaseUrl: string; exchangeInfo: ExchangeInfo; amount: AmountString; } @@ -117,8 +117,8 @@ export interface TopupReserveWithDemobankArgs { export async function topupReserveWithDemobank( args: TopupReserveWithDemobankArgs, ) { - const { http, bankAccessApiBaseUrl, amount, exchangeInfo, reservePub } = args; - const bankClient = new BankAccessApiClient(bankAccessApiBaseUrl); + const { http, corebankApiBaseUrl, amount, exchangeInfo, reservePub } = args; + const bankClient = new TalerCorebankApiClient(corebankApiBaseUrl); const bankUser = await bankClient.createRandomBankUser(); const wopi = await bankClient.createWithdrawalOperation( bankUser.username, @@ -142,7 +142,9 @@ export async function topupReserveWithDemobank( httpResp, codecForBankWithdrawalOperationPostResponse(), ); - await bankClient.confirmWithdrawalOperation(bankUser.username, wopi); + await bankClient.confirmWithdrawalOperation(bankUser.username, { + withdrawalOperationId: wopi.withdrawal_id, + }); } export async function withdrawCoin(args: { @@ -276,7 +278,10 @@ export async function depositCoin(args: { merchant_pub: merchantPub, }; const url = new URL(`batch-deposit`, dp.exchange_url); - const httpResp = await http.fetch(url.href, { body: requestBody }); + const httpResp = await http.fetch(url.href, { + method: "POST", + body: requestBody, + }); await readSuccessResponseJsonOrThrow(httpResp, codecForBatchDepositSuccess()); } diff --git a/packages/taler-wallet-core/src/host-impl.node.ts b/packages/taler-wallet-core/src/host-impl.node.ts index a6dae58a1..33162ec50 100644 --- a/packages/taler-wallet-core/src/host-impl.node.ts +++ b/packages/taler-wallet-core/src/host-impl.node.ts @@ -108,10 +108,13 @@ async function makeSqliteDb( filename: args.persistentStoragePath ?? ":memory:", }); myBackend.enableTracing = false; + if (process.env.TALER_WALLET_DBSTATS) { + myBackend.trackStats = true; + } const myBridgeIdbFactory = new BridgeIDBFactory(myBackend); return { getStats() { - throw Error("not implemented"); + return myBackend.accessStats; }, idbFactory: myBridgeIdbFactory, }; diff --git a/packages/taler-wallet-core/src/operations/attention.ts b/packages/taler-wallet-core/src/operations/attention.ts index 7d84b43ef..92d69e93e 100644 --- a/packages/taler-wallet-core/src/operations/attention.ts +++ b/packages/taler-wallet-core/src/operations/attention.ts @@ -31,6 +31,7 @@ import { UserAttentionUnreadList, } from "@gnu-taler/taler-util"; import { InternalWalletState } from "../internal-wallet-state.js"; +import { timestampPreciseFromDb, timestampPreciseToDb } from "../index.js"; const logger = new Logger("operations/attention.ts"); @@ -74,7 +75,7 @@ export async function getUserAttentions( return; pending.push({ info: x.info, - when: TalerPreciseTimestamp.fromMilliseconds(x.createdMs), + when: timestampPreciseFromDb(x.created), read: x.read !== undefined, }); }); @@ -94,7 +95,7 @@ export async function markAttentionRequestAsRead( if (!ua) throw Error("attention request not found"); tx.userAttention.put({ ...ua, - read: TalerPreciseTimestamp.now(), + read: timestampPreciseToDb(TalerPreciseTimestamp.now()), }); }); } @@ -117,7 +118,7 @@ export async function addAttentionRequest( await tx.userAttention.put({ info, entityId, - createdMs: AbsoluteTime.now().t_ms as number, + created: timestampPreciseToDb(TalerPreciseTimestamp.now()), read: undefined, }); }); diff --git a/packages/taler-wallet-core/src/operations/backup/index.ts b/packages/taler-wallet-core/src/operations/backup/index.ts index a5e8dbd42..7a2771c57 100644 --- a/packages/taler-wallet-core/src/operations/backup/index.ts +++ b/packages/taler-wallet-core/src/operations/backup/index.ts @@ -84,6 +84,9 @@ import { ConfigRecord, ConfigRecordKey, WalletBackupConfState, + timestampOptionalPreciseFromDb, + timestampPreciseFromDb, + timestampPreciseToDb, } from "../../db.js"; import { InternalWalletState } from "../../internal-wallet-state.js"; import { assertUnreachable } from "../../util/assertUnreachable.js"; @@ -259,10 +262,12 @@ async function runBackupCycleForProvider( if (!prov) { return; } - prov.lastBackupCycleTimestamp = TalerPreciseTimestamp.now(); + prov.lastBackupCycleTimestamp = timestampPreciseToDb( + TalerPreciseTimestamp.now(), + ); prov.state = { tag: BackupProviderStateTag.Ready, - nextBackupTimestamp: getNextBackupTimestamp(), + nextBackupTimestamp: timestampPreciseToDb(getNextBackupTimestamp()), }; await tx.backupProviders.put(prov); }); @@ -361,10 +366,12 @@ async function runBackupCycleForProvider( return; } prov.lastBackupHash = encodeCrock(currentBackupHash); - prov.lastBackupCycleTimestamp = TalerPreciseTimestamp.now(); + prov.lastBackupCycleTimestamp = timestampPreciseToDb( + TalerPreciseTimestamp.now(), + ); prov.state = { tag: BackupProviderStateTag.Ready, - nextBackupTimestamp: getNextBackupTimestamp(), + nextBackupTimestamp: timestampPreciseToDb(getNextBackupTimestamp()), }; await tx.backupProviders.put(prov); }); @@ -594,7 +601,9 @@ export async function addBackupProvider( if (req.activate) { oldProv.state = { tag: BackupProviderStateTag.Ready, - nextBackupTimestamp: TalerPreciseTimestamp.now(), + nextBackupTimestamp: timestampPreciseToDb( + TalerPreciseTimestamp.now(), + ), }; logger.info("setting existing backup provider to active"); await tx.backupProviders.put(oldProv); @@ -616,7 +625,9 @@ export async function addBackupProvider( if (req.activate) { state = { tag: BackupProviderStateTag.Ready, - nextBackupTimestamp: TalerPreciseTimestamp.now(), + nextBackupTimestamp: timestampPreciseToDb( + TalerPreciseTimestamp.now(), + ), }; } else { state = { @@ -840,7 +851,9 @@ export async function getBackupInfo( providers.push({ active: x.provider.state.tag !== BackupProviderStateTag.Provisional, syncProviderBaseUrl: x.provider.baseUrl, - lastSuccessfulBackupTimestamp: x.provider.lastBackupCycleTimestamp, + lastSuccessfulBackupTimestamp: timestampOptionalPreciseFromDb( + x.provider.lastBackupCycleTimestamp, + ), paymentProposalIds: x.provider.paymentProposalIds, lastError: x.provider.state.tag === BackupProviderStateTag.Retrying @@ -917,7 +930,9 @@ async function backupRecoveryTheirs( shouldRetryFreshProposal: false, state: { tag: BackupProviderStateTag.Ready, - nextBackupTimestamp: TalerPreciseTimestamp.now(), + nextBackupTimestamp: timestampPreciseToDb( + TalerPreciseTimestamp.now(), + ), }, uids: [encodeCrock(getRandomBytes(32))], }); diff --git a/packages/taler-wallet-core/src/operations/common.ts b/packages/taler-wallet-core/src/operations/common.ts index 50dd3dc5c..b28a5363d 100644 --- a/packages/taler-wallet-core/src/operations/common.ts +++ b/packages/taler-wallet-core/src/operations/common.ts @@ -40,6 +40,7 @@ import { TalerError, TalerErrorCode, TalerErrorDetail, + TalerPreciseTimestamp, TombstoneIdStr, TransactionIdStr, TransactionType, @@ -49,6 +50,7 @@ import { CryptoApiStoppedError } from "../crypto/workers/crypto-dispatcher.js"; import { BackupProviderRecord, CoinRecord, + DbPreciseTimestamp, DepositGroupRecord, ExchangeDetailsRecord, ExchangeEntryDbRecordStatus, @@ -62,6 +64,7 @@ import { RecoupGroupRecord, RefreshGroupRecord, RewardRecord, + timestampPreciseToDb, WalletStoresV1, WithdrawalGroupRecord, } from "../db.js"; @@ -360,11 +363,11 @@ async function storePendingTaskError( retryRecord = { id: pendingTaskId, lastError: e, - retryInfo: RetryInfo.reset(), + retryInfo: DbRetryInfo.reset(), }; } else { retryRecord.lastError = e; - retryRecord.retryInfo = RetryInfo.increment(retryRecord.retryInfo); + retryRecord.retryInfo = DbRetryInfo.increment(retryRecord.retryInfo); } await tx.operationRetries.put(retryRecord); return taskToTransactionNotification(ws, tx, pendingTaskId, e); @@ -383,7 +386,7 @@ export async function resetPendingTaskTimeout( if (retryRecord) { // Note that we don't reset the lastError, it should still be visible // while the retry runs. - retryRecord.retryInfo = RetryInfo.reset(); + retryRecord.retryInfo = DbRetryInfo.reset(); await tx.operationRetries.put(retryRecord); } return taskToTransactionNotification(ws, tx, pendingTaskId, undefined); @@ -403,14 +406,14 @@ async function storePendingTaskPending( if (!retryRecord) { retryRecord = { id: pendingTaskId, - retryInfo: RetryInfo.reset(), + retryInfo: DbRetryInfo.reset(), }; } else { if (retryRecord.lastError) { hadError = true; } delete retryRecord.lastError; - retryRecord.retryInfo = RetryInfo.increment(retryRecord.retryInfo); + retryRecord.retryInfo = DbRetryInfo.increment(retryRecord.retryInfo); } await tx.operationRetries.put(retryRecord); if (hadError) { @@ -590,7 +593,7 @@ export function makeExchangeListItem( return { exchangeBaseUrl: r.baseUrl, - currency: exchangeDetails?.currency, + currency: exchangeDetails?.currency ?? r.presetCurrencyHint, exchangeUpdateStatus, exchangeEntryStatus, tosStatus: exchangeDetails @@ -736,9 +739,9 @@ export interface TaskRunLongpollResult { type: TaskRunResultType.Longpoll; } -export interface RetryInfo { - firstTry: AbsoluteTime; - nextRetry: AbsoluteTime; +export interface DbRetryInfo { + firstTry: DbPreciseTimestamp; + nextRetry: DbPreciseTimestamp; retryCounter: number; } @@ -755,7 +758,7 @@ const defaultRetryPolicy: RetryPolicy = { }; function updateTimeout( - r: RetryInfo, + r: DbRetryInfo, p: RetryPolicy = defaultRetryPolicy, ): void { const now = AbsoluteTime.now(); @@ -763,7 +766,9 @@ function updateTimeout( throw Error("assertion failed"); } if (p.backoffDelta.d_ms === "forever") { - r.nextRetry = AbsoluteTime.never(); + r.nextRetry = timestampPreciseToDb( + AbsoluteTime.toPreciseTimestamp(AbsoluteTime.never()), + ); return; } @@ -775,12 +780,12 @@ function updateTimeout( (p.maxTimeout.d_ms === "forever" ? nextIncrement : Math.min(p.maxTimeout.d_ms, nextIncrement)); - r.nextRetry = AbsoluteTime.fromMilliseconds(t); + r.nextRetry = timestampPreciseToDb(TalerPreciseTimestamp.fromMilliseconds(t)); } -export namespace RetryInfo { +export namespace DbRetryInfo { export function getDuration( - r: RetryInfo | undefined, + r: DbRetryInfo | undefined, p: RetryPolicy = defaultRetryPolicy, ): Duration { if (!r) { @@ -797,11 +802,11 @@ export namespace RetryInfo { }; } - export function reset(p: RetryPolicy = defaultRetryPolicy): RetryInfo { - const now = AbsoluteTime.now(); - const info = { - firstTry: now, - nextRetry: now, + export function reset(p: RetryPolicy = defaultRetryPolicy): DbRetryInfo { + const now = TalerPreciseTimestamp.now(); + const info: DbRetryInfo = { + firstTry: timestampPreciseToDb(now), + nextRetry: timestampPreciseToDb(now), retryCounter: 0, }; updateTimeout(info, p); @@ -809,9 +814,9 @@ export namespace RetryInfo { } export function increment( - r: RetryInfo | undefined, + r: DbRetryInfo | undefined, p: RetryPolicy = defaultRetryPolicy, - ): RetryInfo { + ): DbRetryInfo { if (!r) { return reset(p); } diff --git a/packages/taler-wallet-core/src/operations/deposits.ts b/packages/taler-wallet-core/src/operations/deposits.ts index 2de8f30a1..111d15989 100644 --- a/packages/taler-wallet-core/src/operations/deposits.ts +++ b/packages/taler-wallet-core/src/operations/deposits.ts @@ -73,6 +73,9 @@ import { RefreshOperationStatus, createRefreshGroup, getTotalRefreshCost, + timestampPreciseToDb, + timestampProtocolFromDb, + timestampProtocolToDb, } from "../index.js"; import { InternalWalletState } from "../internal-wallet-state.js"; import { assertUnreachable } from "../util/assertUnreachable.js"; @@ -799,7 +802,7 @@ async function processDepositGroupPendingTrack( amountRaw: Amounts.stringify(raw), wireFee: Amounts.stringify(wireFee), exchangePub: track.exchange_pub, - timestampExecuted: track.execution_time, + timestampExecuted: timestampProtocolToDb(track.execution_time), wireTransferId: track.wtid, }, id: track.exchange_sig, @@ -857,7 +860,9 @@ async function processDepositGroupPendingTrack( } } if (allWired) { - dg.timestampFinished = TalerPreciseTimestamp.now(); + dg.timestampFinished = timestampPreciseToDb( + TalerPreciseTimestamp.now(), + ); dg.operationStatus = DepositOperationStatus.Finished; await tx.depositGroups.put(dg); } @@ -1375,7 +1380,9 @@ export async function createDepositGroup( amount: contractData.amount, noncePriv: noncePair.priv, noncePub: noncePair.pub, - timestampCreated: AbsoluteTime.toPreciseTimestamp(now), + timestampCreated: timestampPreciseToDb( + AbsoluteTime.toPreciseTimestamp(now), + ), timestampFinished: undefined, statusPerCoin: payCoinSel.coinSel.coinPubs.map( () => DepositElementStatus.DepositPending, @@ -1388,7 +1395,9 @@ export async function createDepositGroup( counterpartyEffectiveDepositAmount: Amounts.stringify( counterpartyEffectiveDepositAmount, ), - wireTransferDeadline: contractTerms.wire_transfer_deadline, + wireTransferDeadline: timestampProtocolToDb( + contractTerms.wire_transfer_deadline, + ), wire: { payto_uri: req.depositPaytoUri, salt: wireSalt, diff --git a/packages/taler-wallet-core/src/operations/exchanges.ts b/packages/taler-wallet-core/src/operations/exchanges.ts index 43a08ed3b..82d7b42bf 100644 --- a/packages/taler-wallet-core/src/operations/exchanges.ts +++ b/packages/taler-wallet-core/src/operations/exchanges.ts @@ -74,6 +74,9 @@ import { ExchangeEntryDbRecordStatus, ExchangeEntryDbUpdateStatus, isWithdrawableDenom, + timestampPreciseFromDb, + timestampPreciseToDb, + timestampProtocolToDb, WalletDbReadWriteTransaction, } from "../index.js"; import { InternalWalletState, TrustInfo } from "../internal-wallet-state.js"; @@ -174,7 +177,7 @@ export async function acceptExchangeTermsOfService( if (d) { d.tosAccepted = { etag: etag || d.tosCurrentEtag, - timestamp: TalerPreciseTimestamp.now(), + timestamp: timestampPreciseToDb(TalerPreciseTimestamp.now()), }; await tx.exchangeDetails.put(d); } @@ -306,6 +309,7 @@ export async function downloadExchangeInfo( export async function addPresetExchangeEntry( tx: WalletDbReadWriteTransaction<"exchanges">, exchangeBaseUrl: string, + currencyHint?: string, ): Promise<void> { let exchange = await tx.exchanges.get(exchangeBaseUrl); if (!exchange) { @@ -313,17 +317,22 @@ export async function addPresetExchangeEntry( entryStatus: ExchangeEntryDbRecordStatus.Preset, updateStatus: ExchangeEntryDbUpdateStatus.Initial, baseUrl: exchangeBaseUrl, + presetCurrencyHint: currencyHint, detailsPointer: undefined, lastUpdate: undefined, lastKeysEtag: undefined, - nextRefreshCheckStampMs: AbsoluteTime.getStampMsNever(), - nextUpdateStampMs: AbsoluteTime.getStampMsNever(), + nextRefreshCheckStamp: timestampPreciseToDb( + AbsoluteTime.toPreciseTimestamp(AbsoluteTime.never()), + ), + nextUpdateStamp: timestampPreciseToDb( + AbsoluteTime.toPreciseTimestamp(AbsoluteTime.never()), + ), }; await tx.exchanges.put(r); } } -export async function provideExchangeRecordInTx( +async function provideExchangeRecordInTx( ws: InternalWalletState, tx: GetReadWriteAccess<{ exchanges: typeof WalletStoresV1.exchanges; @@ -343,8 +352,12 @@ export async function provideExchangeRecordInTx( baseUrl: baseUrl, detailsPointer: undefined, lastUpdate: undefined, - nextUpdateStampMs: AbsoluteTime.getStampMsNever(), - nextRefreshCheckStampMs: AbsoluteTime.getStampMsNever(), + nextUpdateStamp: timestampPreciseToDb( + AbsoluteTime.toPreciseTimestamp(AbsoluteTime.never()), + ), + nextRefreshCheckStamp: timestampPreciseToDb( + AbsoluteTime.toPreciseTimestamp(AbsoluteTime.never()), + ), lastKeysEtag: undefined, }; await tx.exchanges.put(r); @@ -445,13 +458,19 @@ async function downloadExchangeKeysInfo( isRevoked: false, value: Amounts.stringify(value), currency: value.currency, - stampExpireDeposit: denomIn.stamp_expire_deposit, - stampExpireLegal: denomIn.stamp_expire_legal, - stampExpireWithdraw: denomIn.stamp_expire_withdraw, - stampStart: denomIn.stamp_start, + stampExpireDeposit: timestampProtocolToDb( + denomIn.stamp_expire_deposit, + ), + stampExpireLegal: timestampProtocolToDb(denomIn.stamp_expire_legal), + stampExpireWithdraw: timestampProtocolToDb( + denomIn.stamp_expire_withdraw, + ), + stampStart: timestampProtocolToDb(denomIn.stamp_start), verificationStatus: DenominationVerificationStatus.Unverified, masterSig: denomIn.master_sig, - listIssueDate: exchangeKeysJsonUnchecked.list_issue_date, + listIssueDate: timestampProtocolToDb( + exchangeKeysJsonUnchecked.list_issue_date, + ), fees: { feeDeposit: Amounts.stringify(denomGroup.fee_deposit), feeRefresh: Amounts.stringify(denomGroup.fee_refresh), @@ -613,7 +632,9 @@ export async function updateExchangeFromUrlHandler( !forceNow && exchangeDetails !== undefined && !AbsoluteTime.isExpired( - AbsoluteTime.fromStampMs(exchange.nextUpdateStampMs), + AbsoluteTime.fromPreciseTimestamp( + timestampPreciseFromDb(exchange.nextUpdateStamp), + ), ) ) { logger.trace("using existing exchange info"); @@ -753,17 +774,21 @@ export async function updateExchangeFromUrlHandler( if (existingDetails?.rowId) { newDetails.rowId = existingDetails.rowId; } - r.lastUpdate = TalerPreciseTimestamp.now(); - r.nextUpdateStampMs = AbsoluteTime.toStampMs( - AbsoluteTime.fromProtocolTimestamp(keysInfo.expiry), + r.lastUpdate = timestampPreciseToDb(TalerPreciseTimestamp.now()); + r.nextUpdateStamp = timestampPreciseToDb( + AbsoluteTime.toPreciseTimestamp( + AbsoluteTime.fromProtocolTimestamp(keysInfo.expiry), + ), ); // New denominations might be available. - r.nextRefreshCheckStampMs = AbsoluteTime.getStampMsNow(); + r.nextRefreshCheckStamp = timestampPreciseToDb( + TalerPreciseTimestamp.now(), + ); if (detailsPointerChanged) { r.detailsPointer = { currency: newDetails.currency, masterPublicKey: newDetails.masterPublicKey, - updateClock: TalerPreciseTimestamp.now(), + updateClock: timestampPreciseToDb(TalerPreciseTimestamp.now()), }; } await tx.exchanges.put(r); @@ -776,9 +801,9 @@ export async function updateExchangeFromUrlHandler( exchangeDetailsRowId: drRowId.key, masterSig: sk.master_sig, signkeyPub: sk.key, - stampEnd: sk.stamp_end, - stampExpire: sk.stamp_expire, - stampStart: sk.stamp_start, + stampEnd: timestampProtocolToDb(sk.stamp_end), + stampExpire: timestampProtocolToDb(sk.stamp_expire), + stampStart: timestampProtocolToDb(sk.stamp_start), }); } @@ -813,7 +838,7 @@ export async function updateExchangeFromUrlHandler( ); } } else { - x.listIssueDate = keysInfo.listIssueDate; + x.listIssueDate = timestampProtocolToDb(keysInfo.listIssueDate); if (!x.isOffered) { x.isOffered = true; logger.info( diff --git a/packages/taler-wallet-core/src/operations/pay-merchant.ts b/packages/taler-wallet-core/src/operations/pay-merchant.ts index fe0cbeda0..157541ed3 100644 --- a/packages/taler-wallet-core/src/operations/pay-merchant.ts +++ b/packages/taler-wallet-core/src/operations/pay-merchant.ts @@ -103,6 +103,9 @@ import { RefundGroupStatus, RefundItemRecord, RefundItemStatus, + timestampPreciseToDb, + timestampProtocolFromDb, + timestampProtocolToDb, } from "../index.js"; import { EXCHANGE_COINS_LOCK, @@ -114,7 +117,7 @@ import { checkDbInvariant } from "../util/invariants.js"; import { GetReadOnlyAccess } from "../util/query.js"; import { constructTaskIdentifier, - RetryInfo, + DbRetryInfo, runLongpollAsync, runTaskWithErrorReporting, spendCoins, @@ -216,11 +219,13 @@ async function failProposalPermanently( notifyTransition(ws, transactionId, transitionInfo); } -function getProposalRequestTimeout(retryInfo?: RetryInfo): Duration { +function getProposalRequestTimeout(retryInfo?: DbRetryInfo): Duration { return Duration.clamp({ lower: Duration.fromSpec({ seconds: 1 }), upper: Duration.fromSpec({ seconds: 60 }), - value: retryInfo ? RetryInfo.getDuration(retryInfo) : Duration.fromSpec({}), + value: retryInfo + ? DbRetryInfo.getDuration(retryInfo) + : Duration.fromSpec({}), }); } @@ -644,7 +649,7 @@ async function createPurchase( noncePriv: priv, noncePub: pub, claimToken, - timestamp: TalerPreciseTimestamp.now(), + timestamp: timestampPreciseToDb(TalerPreciseTimestamp.now()), merchantBaseUrl, orderId, proposalId: proposalId, @@ -717,7 +722,7 @@ async function storeFirstPaySuccess( if (purchase.purchaseStatus === PurchaseStatus.PendingPaying) { purchase.purchaseStatus = PurchaseStatus.Done; } - purchase.timestampFirstSuccessfulPay = now; + purchase.timestampFirstSuccessfulPay = timestampPreciseToDb(now); purchase.lastSessionId = sessionId; purchase.merchantPaySig = payResponse.sig; purchase.posConfirmation = payResponse.pos_confirmation; @@ -737,8 +742,10 @@ async function storeFirstPaySuccess( const ar = Duration.fromTalerProtocolDuration(protoAr); logger.info("auto_refund present"); purchase.purchaseStatus = PurchaseStatus.PendingQueryingAutoRefund; - purchase.autoRefundDeadline = AbsoluteTime.toProtocolTimestamp( - AbsoluteTime.addDuration(AbsoluteTime.now(), ar), + purchase.autoRefundDeadline = timestampProtocolToDb( + AbsoluteTime.toProtocolTimestamp( + AbsoluteTime.addDuration(AbsoluteTime.now(), ar), + ), ); } await tx.purchases.put(purchase); @@ -941,7 +948,9 @@ async function unblockBackup( .forEachAsync(async (bp) => { bp.state = { tag: BackupProviderStateTag.Ready, - nextBackupTimestamp: TalerPreciseTimestamp.now(), + nextBackupTimestamp: timestampPreciseToDb( + TalerPreciseTimestamp.now(), + ), }; tx.backupProviders.put(bp); }); @@ -1447,7 +1456,7 @@ export async function confirmPay( totalPayCost: Amounts.stringify(payCostInfo), }; p.lastSessionId = sessionId; - p.timestampAccept = TalerPreciseTimestamp.now(); + p.timestampAccept = timestampPreciseToDb(TalerPreciseTimestamp.now()); p.purchaseStatus = PurchaseStatus.PendingPaying; await tx.purchases.put(p); await spendCoins(ws, tx, { @@ -2340,7 +2349,9 @@ async function processPurchaseAutoRefund( if ( !purchase.autoRefundDeadline || AbsoluteTime.isExpired( - AbsoluteTime.fromProtocolTimestamp(purchase.autoRefundDeadline), + AbsoluteTime.fromProtocolTimestamp( + timestampProtocolFromDb(purchase.autoRefundDeadline), + ), ) ) { const transitionInfo = await ws.db @@ -2791,7 +2802,7 @@ async function storeRefunds( proposalId: purchase.proposalId, refundGroupId: newRefundGroupId, status: RefundGroupStatus.Pending, - timestampCreated: now, + timestampCreated: timestampPreciseToDb(now), amountEffective: Amounts.stringify( Amounts.zeroOfCurrency(currency), ), @@ -2801,8 +2812,8 @@ async function storeRefunds( const status: RefundItemStatus = getItemStatus(rf); const newItem: RefundItemRecord = { coinPub: rf.coin_pub, - executionTime: rf.execution_time, - obtainedTime: now, + executionTime: timestampProtocolToDb(rf.execution_time), + obtainedTime: timestampPreciseToDb(now), refundAmount: rf.refund_amount, refundGroupId: newGroup.refundGroupId, rtxid: rf.rtransaction_id, diff --git a/packages/taler-wallet-core/src/operations/pay-peer-pull-credit.ts b/packages/taler-wallet-core/src/operations/pay-peer-pull-credit.ts index 0355eb152..54b78957f 100644 --- a/packages/taler-wallet-core/src/operations/pay-peer-pull-credit.ts +++ b/packages/taler-wallet-core/src/operations/pay-peer-pull-credit.ts @@ -60,6 +60,9 @@ import { PeerPullPaymentCreditStatus, WithdrawalGroupStatus, WithdrawalRecordType, + timestampOptionalPreciseFromDb, + timestampPreciseFromDb, + timestampPreciseToDb, updateExchangeFromUrl, } from "../index.js"; import { InternalWalletState } from "../internal-wallet-state.js"; @@ -389,18 +392,20 @@ async function handlePeerPullCreditCreatePurse( const econtractResp = await ws.cryptoApi.encryptContractForDeposit({ contractPriv: pullIni.contractPriv, contractPub: pullIni.contractPub, - contractTerms: contractTermsRecord, + contractTerms: contractTermsRecord.contractTermsRaw, pursePriv: pullIni.pursePriv, pursePub: pullIni.pursePub, nonce: pullIni.contractEncNonce, }); + const mergeTimestamp = timestampPreciseFromDb(pullIni.mergeTimestamp); + const purseExpiration = contractTerms.purse_expiration; const sigRes = await ws.cryptoApi.signReservePurseCreate({ contractTermsHash: pullIni.contractTermsHash, flags: WalletAccountMergeFlags.CreateWithPurseFee, mergePriv: pullIni.mergePriv, - mergeTimestamp: TalerPreciseTimestamp.round(pullIni.mergeTimestamp), + mergeTimestamp: TalerPreciseTimestamp.round(mergeTimestamp), purseAmount: pullIni.amount, purseExpiration: purseExpiration, purseFee: purseFee, @@ -412,7 +417,7 @@ async function handlePeerPullCreditCreatePurse( const reservePurseReqBody: ExchangeReservePurseRequest = { merge_sig: sigRes.mergeSig, - merge_timestamp: TalerPreciseTimestamp.round(pullIni.mergeTimestamp), + merge_timestamp: TalerPreciseTimestamp.round(mergeTimestamp), h_contract_terms: pullIni.contractTermsHash, merge_pub: pullIni.mergePub, min_age: 0, @@ -695,11 +700,17 @@ async function getPreferredExchangeForCurrency( if (candidate.lastWithdrawal && !e.lastWithdrawal) { continue; } - if (candidate.lastWithdrawal && e.lastWithdrawal) { + const exchangeLastWithdrawal = timestampOptionalPreciseFromDb( + e.lastWithdrawal, + ); + const candidateLastWithdrawal = timestampOptionalPreciseFromDb( + candidate.lastWithdrawal, + ); + if (exchangeLastWithdrawal && candidateLastWithdrawal) { if ( AbsoluteTime.cmp( - AbsoluteTime.fromPreciseTimestamp(e.lastWithdrawal), - AbsoluteTime.fromPreciseTimestamp(candidate.lastWithdrawal), + AbsoluteTime.fromPreciseTimestamp(exchangeLastWithdrawal), + AbsoluteTime.fromPreciseTimestamp(candidateLastWithdrawal), ) > 0 ) { candidate = e; @@ -741,8 +752,6 @@ export async function initiatePeerPullPayment( exchangeBaseUrl: exchangeBaseUrl, }); - const mergeTimestamp = TalerPreciseTimestamp.now(); - const pursePair = await ws.cryptoApi.createEddsaKeypair({}); const mergePair = await ws.cryptoApi.createEddsaKeypair({}); @@ -766,6 +775,8 @@ export async function initiatePeerPullPayment( undefined, ); + const mergeTimestamp = TalerPreciseTimestamp.now(); + const transitionInfo = await ws.db .mktx((x) => [x.peerPullCredit, x.contractTerms]) .runReadWrite(async (tx) => { @@ -778,7 +789,7 @@ export async function initiatePeerPullPayment( mergePriv: mergePair.priv, mergePub: mergePair.pub, status: PeerPullPaymentCreditStatus.PendingCreatePurse, - mergeTimestamp, + mergeTimestamp: timestampPreciseToDb(mergeTimestamp), contractEncNonce, mergeReserveRowId: mergeReserveRowId, contractPriv: contractKeyPair.priv, diff --git a/packages/taler-wallet-core/src/operations/pay-peer-pull-debit.ts b/packages/taler-wallet-core/src/operations/pay-peer-pull-debit.ts index 5bcfa3418..48cbf574f 100644 --- a/packages/taler-wallet-core/src/operations/pay-peer-pull-debit.ts +++ b/packages/taler-wallet-core/src/operations/pay-peer-pull-debit.ts @@ -59,6 +59,7 @@ import { PendingTaskType, RefreshOperationStatus, createRefreshGroup, + timestampPreciseToDb, } from "../index.js"; import { assertUnreachable } from "../util/assertUnreachable.js"; import { checkLogicInvariant } from "../util/invariants.js"; @@ -595,7 +596,7 @@ export async function preparePeerPullDebit( contractPriv: contractPriv, exchangeBaseUrl: exchangeBaseUrl, pursePub: pursePub, - timestampCreated: TalerPreciseTimestamp.now(), + timestampCreated: timestampPreciseToDb(TalerPreciseTimestamp.now()), contractTermsHash, amount: contractTerms.amount, status: PeerPullDebitRecordStatus.DialogProposed, diff --git a/packages/taler-wallet-core/src/operations/pay-peer-push-credit.ts b/packages/taler-wallet-core/src/operations/pay-peer-push-credit.ts index 89d9e3b49..e4698c203 100644 --- a/packages/taler-wallet-core/src/operations/pay-peer-push-credit.ts +++ b/packages/taler-wallet-core/src/operations/pay-peer-push-credit.ts @@ -59,6 +59,7 @@ import { PendingTaskType, WithdrawalGroupStatus, WithdrawalRecordType, + timestampPreciseToDb, } from "../index.js"; import { assertUnreachable } from "../util/assertUnreachable.js"; import { checkDbInvariant } from "../util/invariants.js"; @@ -129,12 +130,10 @@ export async function preparePeerPushCredit( amountEffective: existing.existingPushInc.estimatedAmountEffective, amountRaw: existing.existingContractTerms.amount, contractTerms: existing.existingContractTerms, - peerPushCreditId: - existing.existingPushInc.peerPushCreditId, + peerPushCreditId: existing.existingPushInc.peerPushCreditId, transactionId: constructTransactionIdentifier({ tag: TransactionType.PeerPushCredit, - peerPushCreditId: - existing.existingPushInc.peerPushCreditId, + peerPushCreditId: existing.existingPushInc.peerPushCreditId, }), }; } @@ -196,7 +195,7 @@ export async function preparePeerPushCredit( exchangeBaseUrl: exchangeBaseUrl, mergePriv: dec.mergePriv, pursePub: pursePub, - timestamp: TalerPreciseTimestamp.now(), + timestamp: timestampPreciseToDb(TalerPreciseTimestamp.now()), contractTermsHash, status: PeerPushCreditStatus.DialogProposed, withdrawalGroupId, @@ -263,16 +262,11 @@ async function longpollKycStatus( const transitionInfo = await ws.db .mktx((x) => [x.peerPushCredit]) .runReadWrite(async (tx) => { - const peerInc = await tx.peerPushCredit.get( - peerPushCreditId, - ); + const peerInc = await tx.peerPushCredit.get(peerPushCreditId); if (!peerInc) { return; } - if ( - peerInc.status !== - PeerPushCreditStatus.PendingMergeKycRequired - ) { + if (peerInc.status !== PeerPushCreditStatus.PendingMergeKycRequired) { return; } const oldTxState = computePeerPushCreditTransactionState(peerInc); @@ -333,9 +327,7 @@ async function processPeerPushCreditKycRequired( const { transitionInfo, result } = await ws.db .mktx((x) => [x.peerPushCredit]) .runReadWrite(async (tx) => { - const peerInc = await tx.peerPushCredit.get( - peerPushCreditId, - ); + const peerInc = await tx.peerPushCredit.get(peerPushCreditId); if (!peerInc) { return { transitionInfo: undefined, @@ -466,9 +458,7 @@ async function handlePendingMerge( x.exchangeDetails, ]) .runReadWrite(async (tx) => { - const peerInc = await tx.peerPushCredit.get( - peerPushCreditId, - ); + const peerInc = await tx.peerPushCredit.get(peerPushCreditId); if (!peerInc) { return undefined; } @@ -520,9 +510,7 @@ async function handlePendingWithdrawing( const transitionInfo = await ws.db .mktx((x) => [x.peerPushCredit, x.withdrawalGroups]) .runReadWrite(async (tx) => { - const ppi = await tx.peerPushCredit.get( - peerInc.peerPushCreditId, - ); + const ppi = await tx.peerPushCredit.get(peerInc.peerPushCreditId); if (!ppi) { finished = true; return; @@ -631,9 +619,7 @@ export async function confirmPeerPushCredit( } peerPushCreditId = parsedTx.peerPushCreditId; } else { - throw Error( - "no transaction ID (or deprecated peerPushCreditId) provided", - ); + throw Error("no transaction ID (or deprecated peerPushCreditId) provided"); } await ws.db @@ -683,9 +669,7 @@ export async function suspendPeerPushCreditTransaction( const transitionInfo = await ws.db .mktx((x) => [x.peerPushCredit]) .runReadWrite(async (tx) => { - const pushCreditRec = await tx.peerPushCredit.get( - peerPushCreditId, - ); + const pushCreditRec = await tx.peerPushCredit.get(peerPushCreditId); if (!pushCreditRec) { logger.warn(`peer push credit ${peerPushCreditId} not found`); return; @@ -746,9 +730,7 @@ export async function abortPeerPushCreditTransaction( const transitionInfo = await ws.db .mktx((x) => [x.peerPushCredit]) .runReadWrite(async (tx) => { - const pushCreditRec = await tx.peerPushCredit.get( - peerPushCreditId, - ); + const pushCreditRec = await tx.peerPushCredit.get(peerPushCreditId); if (!pushCreditRec) { logger.warn(`peer push credit ${peerPushCreditId} not found`); return; @@ -820,9 +802,7 @@ export async function resumePeerPushCreditTransaction( const transitionInfo = await ws.db .mktx((x) => [x.peerPushCredit]) .runReadWrite(async (tx) => { - const pushCreditRec = await tx.peerPushCredit.get( - peerPushCreditId, - ); + const pushCreditRec = await tx.peerPushCredit.get(peerPushCreditId); if (!pushCreditRec) { logger.warn(`peer push credit ${peerPushCreditId} not found`); return; diff --git a/packages/taler-wallet-core/src/operations/pay-peer-push-debit.ts b/packages/taler-wallet-core/src/operations/pay-peer-push-debit.ts index e80ffc059..50ae8d41b 100644 --- a/packages/taler-wallet-core/src/operations/pay-peer-push-debit.ts +++ b/packages/taler-wallet-core/src/operations/pay-peer-push-debit.ts @@ -55,10 +55,14 @@ import { PeerPushDebitStatus, RefreshOperationStatus, createRefreshGroup, + timestampPreciseToDb, + timestampProtocolFromDb, + timestampProtocolToDb, } from "../index.js"; import { InternalWalletState } from "../internal-wallet-state.js"; import { PendingTaskType } from "../pending-types.js"; import { assertUnreachable } from "../util/assertUnreachable.js"; +import { PeerCoinRepair, selectPeerCoins } from "../util/coinSelection.js"; import { checkLogicInvariant } from "../util/invariants.js"; import { TaskRunResult, @@ -77,7 +81,6 @@ import { notifyTransition, stopLongpolling, } from "./transactions.js"; -import { PeerCoinRepair, selectPeerCoins } from "../util/coinSelection.js"; const logger = new Logger("pay-peer-push-debit.ts"); @@ -207,7 +210,7 @@ async function processPeerPushDebitCreateReserve( mergePub: peerPushInitiation.mergePub, minAge: 0, purseAmount: peerPushInitiation.amount, - purseExpiration, + purseExpiration: timestampProtocolFromDb(purseExpiration), pursePriv: peerPushInitiation.pursePriv, }); @@ -242,8 +245,6 @@ async function processPeerPushDebitCreateReserve( hash(decodeCrock(econtractResp.econtract.econtract)), ); - logger.info(`econtract hash: ${econtractHash}`); - const createPurseUrl = new URL( `purses/${peerPushInitiation.pursePub}/create`, peerPushInitiation.exchangeBaseUrl, @@ -254,7 +255,7 @@ async function processPeerPushDebitCreateReserve( merge_pub: peerPushInitiation.mergePub, purse_sig: purseSigResp.sig, h_contract_terms: hContractTerms, - purse_expiration: purseExpiration, + purse_expiration: timestampProtocolFromDb(purseExpiration), deposits: depositSigsResp.deposits, min_age: 0, econtract: econtractResp.econtract, @@ -646,7 +647,6 @@ export async function initiatePeerPushDebit( // we might want to mark the coins as used and spend them // after we've been able to create the purse. await spendCoins(ws, tx, { - // allocationId: `txn:peer-push-debit:${pursePair.pub}`, allocationId: constructTransactionIdentifier({ tag: TransactionType.PeerPushDebit, pursePub: pursePair.pub, @@ -666,10 +666,10 @@ export async function initiatePeerPushDebit( exchangeBaseUrl: sel.exchangeBaseUrl, mergePriv: mergePair.priv, mergePub: mergePair.pub, - purseExpiration: purseExpiration, + purseExpiration: timestampProtocolToDb(purseExpiration), pursePriv: pursePair.priv, pursePub: pursePair.pub, - timestampCreated: TalerPreciseTimestamp.now(), + timestampCreated: timestampPreciseToDb(TalerPreciseTimestamp.now()), status: PeerPushDebitStatus.PendingCreatePurse, contractEncNonce, coinSel: { diff --git a/packages/taler-wallet-core/src/operations/pending.ts b/packages/taler-wallet-core/src/operations/pending.ts index 6115f848b..1819aa1b8 100644 --- a/packages/taler-wallet-core/src/operations/pending.ts +++ b/packages/taler-wallet-core/src/operations/pending.ts @@ -21,42 +21,46 @@ /** * Imports. */ +import { GlobalIDB } from "@gnu-taler/idb-bridge"; +import { AbsoluteTime, TransactionRecordFilter } from "@gnu-taler/taler-util"; import { - PurchaseStatus, - WalletStoresV1, BackupProviderStateTag, - RefreshCoinStatus, - PeerPushDebitStatus, - PeerPullDebitRecordStatus, - PeerPushCreditStatus, - PeerPullPaymentCreditStatus, - WithdrawalGroupStatus, - RewardRecordStatus, - DepositOperationStatus, - RefreshGroupRecord, - WithdrawalGroupRecord, + DepositElementStatus, DepositGroupRecord, - RewardRecord, - PurchaseRecord, + DepositOperationStatus, + ExchangeEntryDbUpdateStatus, PeerPullCreditRecord, + PeerPullDebitRecordStatus, + PeerPullPaymentCreditStatus, PeerPullPaymentIncomingRecord, + PeerPushCreditStatus, PeerPushDebitRecord, + PeerPushDebitStatus, PeerPushPaymentIncomingRecord, + PurchaseRecord, + PurchaseStatus, + RefreshCoinStatus, + RefreshGroupRecord, + RefreshOperationStatus, RefundGroupRecord, RefundGroupStatus, - ExchangeEntryDbUpdateStatus, - RefreshOperationStatus, - DepositElementStatus, + RewardRecord, + RewardRecordStatus, + WalletStoresV1, + WithdrawalGroupRecord, + WithdrawalGroupStatus, + timestampAbsoluteFromDb, + timestampOptionalAbsoluteFromDb, + timestampPreciseFromDb, + timestampPreciseToDb, } from "../db.js"; +import { InternalWalletState } from "../internal-wallet-state.js"; import { PendingOperationsResponse, PendingTaskType, TaskId, } from "../pending-types.js"; -import { AbsoluteTime, TransactionRecordFilter } from "@gnu-taler/taler-util"; -import { InternalWalletState } from "../internal-wallet-state.js"; import { GetReadOnlyAccess } from "../util/query.js"; -import { GlobalIDB } from "@gnu-taler/idb-bridge"; import { TaskIdentifiers } from "./common.js"; function getPendingCommon( @@ -99,12 +103,14 @@ async function gatherExchangePending( } const opTag = TaskIdentifiers.forExchangeUpdate(exch); let opr = await tx.operationRetries.get(opTag); - const timestampDue = - opr?.retryInfo.nextRetry ?? - AbsoluteTime.fromStampMs(exch.nextUpdateStampMs); + const timestampDue = opr?.retryInfo.nextRetry ?? exch.nextRefreshCheckStamp; resp.pendingOperations.push({ type: PendingTaskType.ExchangeUpdate, - ...getPendingCommon(ws, opTag, timestampDue), + ...getPendingCommon( + ws, + opTag, + AbsoluteTime.fromPreciseTimestamp(timestampPreciseFromDb(timestampDue)), + ), givesLifeness: false, exchangeBaseUrl: exch.baseUrl, lastError: opr?.lastError, @@ -115,8 +121,16 @@ async function gatherExchangePending( if (!opr?.lastError) { resp.pendingOperations.push({ type: PendingTaskType.ExchangeCheckRefresh, - ...getPendingCommon(ws, opTag, timestampDue), - timestampDue: AbsoluteTime.fromStampMs(exch.nextRefreshCheckStampMs), + ...getPendingCommon( + ws, + opTag, + AbsoluteTime.fromPreciseTimestamp( + timestampPreciseFromDb(timestampDue), + ), + ), + timestampDue: AbsoluteTime.fromPreciseTimestamp( + timestampPreciseFromDb(exch.nextRefreshCheckStamp), + ), givesLifeness: false, exchangeBaseUrl: exch.baseUrl, }); @@ -165,7 +179,9 @@ async function gatherRefreshPending( } const opId = TaskIdentifiers.forRefresh(r); const retryRecord = await tx.operationRetries.get(opId); - const timestampDue = retryRecord?.retryInfo.nextRetry ?? AbsoluteTime.now(); + const timestampDue = + timestampOptionalAbsoluteFromDb(retryRecord?.retryInfo.nextRetry) ?? + AbsoluteTime.now(); resp.pendingOperations.push({ type: PendingTaskType.Refresh, ...getPendingCommon(ws, opId, timestampDue), @@ -222,8 +238,8 @@ async function gatherWithdrawalPending( opr = { id: opTag, retryInfo: { - firstTry: now, - nextRetry: now, + firstTry: timestampPreciseToDb(AbsoluteTime.toPreciseTimestamp(now)), + nextRetry: timestampPreciseToDb(AbsoluteTime.toPreciseTimestamp(now)), retryCounter: 0, }, }; @@ -233,7 +249,8 @@ async function gatherWithdrawalPending( ...getPendingCommon( ws, opTag, - opr.retryInfo?.nextRetry ?? AbsoluteTime.now(), + timestampOptionalAbsoluteFromDb(opr.retryInfo?.nextRetry) ?? + AbsoluteTime.now(), ), givesLifeness: true, withdrawalGroupId: wsr.withdrawalGroupId, @@ -285,7 +302,9 @@ async function gatherDepositPending( } const opId = TaskIdentifiers.forDeposit(dg); const retryRecord = await tx.operationRetries.get(opId); - const timestampDue = retryRecord?.retryInfo.nextRetry ?? AbsoluteTime.now(); + const timestampDue = + timestampOptionalAbsoluteFromDb(retryRecord?.retryInfo.nextRetry) ?? + AbsoluteTime.now(); resp.pendingOperations.push({ type: PendingTaskType.Deposit, ...getPendingCommon(ws, opId, timestampDue), @@ -330,13 +349,15 @@ async function gatherRewardPending( await iterRecordsForReward(tx, { onlyState: "nonfinal" }, async (tip) => { const opId = TaskIdentifiers.forTipPickup(tip); const retryRecord = await tx.operationRetries.get(opId); - const timestampDue = retryRecord?.retryInfo.nextRetry ?? AbsoluteTime.now(); + const timestampDue = + timestampOptionalAbsoluteFromDb(retryRecord?.retryInfo.nextRetry) ?? + AbsoluteTime.now(); if (tip.acceptedTimestamp) { resp.pendingOperations.push({ type: PendingTaskType.RewardPickup, ...getPendingCommon(ws, opId, timestampDue), givesLifeness: true, - timestampDue: retryRecord?.retryInfo.nextRetry ?? AbsoluteTime.now(), + timestampDue, merchantBaseUrl: tip.merchantBaseUrl, tipId: tip.walletRewardId, merchantTipId: tip.merchantRewardId, @@ -390,7 +411,9 @@ async function gatherPurchasePending( await iterRecordsForPurchase(tx, { onlyState: "nonfinal" }, async (pr) => { const opId = TaskIdentifiers.forPay(pr); const retryRecord = await tx.operationRetries.get(opId); - const timestampDue = retryRecord?.retryInfo.nextRetry ?? AbsoluteTime.now(); + const timestampDue = + timestampOptionalAbsoluteFromDb(retryRecord?.retryInfo.nextRetry) ?? + AbsoluteTime.now(); resp.pendingOperations.push({ type: PendingTaskType.Purchase, ...getPendingCommon(ws, opId, timestampDue), @@ -419,7 +442,9 @@ async function gatherRecoupPending( } const opId = TaskIdentifiers.forRecoup(rg); const retryRecord = await tx.operationRetries.get(opId); - const timestampDue = retryRecord?.retryInfo.nextRetry ?? AbsoluteTime.now(); + const timestampDue = + timestampOptionalAbsoluteFromDb(retryRecord?.retryInfo.nextRetry) ?? + AbsoluteTime.now(); resp.pendingOperations.push({ type: PendingTaskType.Recoup, ...getPendingCommon(ws, opId, timestampDue), @@ -444,7 +469,7 @@ async function gatherBackupPending( const opId = TaskIdentifiers.forBackup(bp); const retryRecord = await tx.operationRetries.get(opId); if (bp.state.tag === BackupProviderStateTag.Ready) { - const timestampDue = AbsoluteTime.fromPreciseTimestamp( + const timestampDue = timestampAbsoluteFromDb( bp.state.nextBackupTimestamp, ); resp.pendingOperations.push({ @@ -456,7 +481,8 @@ async function gatherBackupPending( }); } else if (bp.state.tag === BackupProviderStateTag.Retrying) { const timestampDue = - retryRecord?.retryInfo?.nextRetry ?? AbsoluteTime.now(); + timestampOptionalAbsoluteFromDb(retryRecord?.retryInfo?.nextRetry) ?? + AbsoluteTime.now(); resp.pendingOperations.push({ type: PendingTaskType.Backup, ...getPendingCommon(ws, opId, timestampDue), @@ -503,7 +529,8 @@ async function gatherPeerPullInitiationPending( const opId = TaskIdentifiers.forPeerPullPaymentInitiation(pi); const retryRecord = await tx.operationRetries.get(opId); const timestampDue = - retryRecord?.retryInfo.nextRetry ?? AbsoluteTime.now(); + timestampOptionalAbsoluteFromDb(retryRecord?.retryInfo.nextRetry) ?? + AbsoluteTime.now(); resp.pendingOperations.push({ type: PendingTaskType.PeerPullCredit, ...getPendingCommon(ws, opId, timestampDue), @@ -549,7 +576,8 @@ async function gatherPeerPullDebitPending( const opId = TaskIdentifiers.forPeerPullPaymentDebit(pi); const retryRecord = await tx.operationRetries.get(opId); const timestampDue = - retryRecord?.retryInfo.nextRetry ?? AbsoluteTime.now(); + timestampOptionalAbsoluteFromDb(retryRecord?.retryInfo.nextRetry) ?? + AbsoluteTime.now(); resp.pendingOperations.push({ type: PendingTaskType.PeerPullDebit, ...getPendingCommon(ws, opId, timestampDue), @@ -595,7 +623,8 @@ async function gatherPeerPushInitiationPending( const opId = TaskIdentifiers.forPeerPushPaymentInitiation(pi); const retryRecord = await tx.operationRetries.get(opId); const timestampDue = - retryRecord?.retryInfo.nextRetry ?? AbsoluteTime.now(); + timestampOptionalAbsoluteFromDb(retryRecord?.retryInfo.nextRetry) ?? + AbsoluteTime.now(); resp.pendingOperations.push({ type: PendingTaskType.PeerPushDebit, ...getPendingCommon(ws, opId, timestampDue), @@ -645,7 +674,8 @@ async function gatherPeerPushCreditPending( const opId = TaskIdentifiers.forPeerPushCredit(pi); const retryRecord = await tx.operationRetries.get(opId); const timestampDue = - retryRecord?.retryInfo.nextRetry ?? AbsoluteTime.now(); + timestampOptionalAbsoluteFromDb(retryRecord?.retryInfo.nextRetry) ?? + AbsoluteTime.now(); resp.pendingOperations.push({ type: PendingTaskType.PeerPushCredit, ...getPendingCommon(ws, opId, timestampDue), diff --git a/packages/taler-wallet-core/src/operations/recoup.ts b/packages/taler-wallet-core/src/operations/recoup.ts index 6a18e5de6..782e98d1c 100644 --- a/packages/taler-wallet-core/src/operations/recoup.ts +++ b/packages/taler-wallet-core/src/operations/recoup.ts @@ -47,6 +47,7 @@ import { WithdrawCoinSource, WithdrawalGroupStatus, WithdrawalRecordType, + timestampPreciseToDb, } from "../db.js"; import { InternalWalletState } from "../internal-wallet-state.js"; import { checkDbInvariant } from "../util/invariants.js"; @@ -391,7 +392,7 @@ export async function processRecoupGroup( if (!rg2) { return; } - rg2.timestampFinished = TalerPreciseTimestamp.now(); + rg2.timestampFinished = timestampPreciseToDb(TalerPreciseTimestamp.now()); if (rg2.scheduleRefreshCoins.length > 0) { const refreshGroupId = await createRefreshGroup( ws, @@ -424,7 +425,7 @@ export async function createRecoupGroup( exchangeBaseUrl: exchangeBaseUrl, coinPubs: coinPubs, timestampFinished: undefined, - timestampStarted: TalerPreciseTimestamp.now(), + timestampStarted: timestampPreciseToDb(TalerPreciseTimestamp.now()), recoupFinishedPerCoin: coinPubs.map(() => false), scheduleRefreshCoins: [], }; diff --git a/packages/taler-wallet-core/src/operations/refresh.ts b/packages/taler-wallet-core/src/operations/refresh.ts index 75adbc860..95aedbbd6 100644 --- a/packages/taler-wallet-core/src/operations/refresh.ts +++ b/packages/taler-wallet-core/src/operations/refresh.ts @@ -80,6 +80,8 @@ import { isWithdrawableDenom, PendingTaskType, RefreshSessionRecord, + timestampPreciseToDb, + timestampProtocolFromDb, } from "../index.js"; import { EXCHANGE_COINS_LOCK, @@ -157,10 +159,10 @@ function updateGroupStatus(rg: RefreshGroupRecord): { final: boolean } { ); if (allFinal) { if (anyFailed) { - rg.timestampFinished = TalerPreciseTimestamp.now(); + rg.timestampFinished = timestampPreciseToDb(TalerPreciseTimestamp.now()); rg.operationStatus = RefreshOperationStatus.Failed; } else { - rg.timestampFinished = TalerPreciseTimestamp.now(); + rg.timestampFinished = timestampPreciseToDb(TalerPreciseTimestamp.now()); rg.operationStatus = RefreshOperationStatus.Finished; } return { final: true }; @@ -1099,12 +1101,14 @@ export async function createRefreshGroup( expectedOutputPerCoin: estimatedOutputPerCoin.map((x) => Amounts.stringify(x), ), - timestampCreated: TalerPreciseTimestamp.now(), + timestampCreated: timestampPreciseToDb(TalerPreciseTimestamp.now()), }; if (oldCoinPubs.length == 0) { logger.warn("created refresh group with zero coins"); - refreshGroup.timestampFinished = TalerPreciseTimestamp.now(); + refreshGroup.timestampFinished = timestampPreciseToDb( + TalerPreciseTimestamp.now(), + ); refreshGroup.operationStatus = RefreshOperationStatus.Finished; } @@ -1122,10 +1126,10 @@ export async function createRefreshGroup( */ function getAutoRefreshCheckThreshold(d: DenominationRecord): AbsoluteTime { const expireWithdraw = AbsoluteTime.fromProtocolTimestamp( - d.stampExpireWithdraw, + timestampProtocolFromDb(d.stampExpireWithdraw), ); const expireDeposit = AbsoluteTime.fromProtocolTimestamp( - d.stampExpireDeposit, + timestampProtocolFromDb(d.stampExpireDeposit), ); const delta = AbsoluteTime.difference(expireWithdraw, expireDeposit); const deltaDiv = durationMul(delta, 0.75); @@ -1137,10 +1141,10 @@ function getAutoRefreshCheckThreshold(d: DenominationRecord): AbsoluteTime { */ function getAutoRefreshExecuteThreshold(d: DenominationRecord): AbsoluteTime { const expireWithdraw = AbsoluteTime.fromProtocolTimestamp( - d.stampExpireWithdraw, + timestampProtocolFromDb(d.stampExpireWithdraw), ); const expireDeposit = AbsoluteTime.fromProtocolTimestamp( - d.stampExpireDeposit, + timestampProtocolFromDb(d.stampExpireDeposit), ); const delta = AbsoluteTime.difference(expireWithdraw, expireDeposit); const deltaDiv = durationMul(delta, 0.5); @@ -1224,8 +1228,9 @@ export async function autoRefresh( logger.trace( `next refresh check at ${AbsoluteTime.toIsoString(minCheckThreshold)}`, ); - exchange.nextRefreshCheckStampMs = - AbsoluteTime.toStampMs(minCheckThreshold); + exchange.nextRefreshCheckStamp = timestampPreciseToDb( + AbsoluteTime.toPreciseTimestamp(minCheckThreshold), + ); await tx.exchanges.put(exchange); }); return TaskRunResult.finished(); diff --git a/packages/taler-wallet-core/src/operations/reward.ts b/packages/taler-wallet-core/src/operations/reward.ts index 6ae021174..4e16d977d 100644 --- a/packages/taler-wallet-core/src/operations/reward.ts +++ b/packages/taler-wallet-core/src/operations/reward.ts @@ -50,6 +50,10 @@ import { DenominationRecord, RewardRecord, RewardRecordStatus, + timestampPreciseFromDb, + timestampPreciseToDb, + timestampProtocolFromDb, + timestampProtocolToDb, } from "../db.js"; import { makeErrorDetail } from "@gnu-taler/taler-util"; import { InternalWalletState } from "../internal-wallet-state.js"; @@ -199,11 +203,11 @@ export async function prepareTip( acceptedTimestamp: undefined, status: RewardRecordStatus.DialogAccept, rewardAmountRaw: Amounts.stringify(amount), - rewardExpiration: tipPickupStatus.expiration, + rewardExpiration: timestampProtocolToDb(tipPickupStatus.expiration), exchangeBaseUrl: tipPickupStatus.exchange_url, next_url: tipPickupStatus.next_url, merchantBaseUrl: res.merchantBaseUrl, - createdTimestamp: TalerPreciseTimestamp.now(), + createdTimestamp: timestampPreciseToDb(TalerPreciseTimestamp.now()), merchantRewardId: res.merchantRewardId, rewardAmountEffective: Amounts.stringify(selectedDenoms.totalCoinValue), denomsSel: selectedDenoms, @@ -229,7 +233,7 @@ export async function prepareTip( rewardAmountRaw: Amounts.stringify(tipRecord.rewardAmountRaw), exchangeBaseUrl: tipRecord.exchangeBaseUrl, merchantBaseUrl: tipRecord.merchantBaseUrl, - expirationTimestamp: tipRecord.rewardExpiration, + expirationTimestamp: timestampProtocolFromDb(tipRecord.rewardExpiration), rewardAmountEffective: Amounts.stringify(tipRecord.rewardAmountEffective), walletRewardId: tipRecord.walletRewardId, transactionId, @@ -300,13 +304,16 @@ export async function processTip( } const tipStatusUrl = new URL( - `tips/${tipRecord.merchantRewardId}/pickup`, + `rewards/${tipRecord.merchantRewardId}/pickup`, tipRecord.merchantBaseUrl, ); const req = { planchets: planchetsDetail }; logger.trace(`sending tip request: ${j2s(req)}`); - const merchantResp = await ws.http.postJson(tipStatusUrl.href, req); + const merchantResp = await ws.http.fetch(tipStatusUrl.href, { + method: "POST", + body: req, + }); logger.trace(`got tip response, status ${merchantResp.status}`); @@ -411,7 +418,7 @@ export async function processTip( return; } const oldTxState = computeRewardTransactionStatus(tr); - tr.pickedUpTimestamp = TalerPreciseTimestamp.now(); + tr.pickedUpTimestamp = timestampPreciseToDb(TalerPreciseTimestamp.now()); tr.status = RewardRecordStatus.Done; await tx.rewards.put(tr); const newTxState = computeRewardTransactionStatus(tr); @@ -448,7 +455,9 @@ export async function acceptTip( return { tipRecord }; } const oldTxState = computeRewardTransactionStatus(tipRecord); - tipRecord.acceptedTimestamp = TalerPreciseTimestamp.now(); + tipRecord.acceptedTimestamp = timestampPreciseToDb( + TalerPreciseTimestamp.now(), + ); tipRecord.status = RewardRecordStatus.PendingPickup; await tx.rewards.put(tipRecord); const newTxState = computeRewardTransactionStatus(tipRecord); diff --git a/packages/taler-wallet-core/src/operations/testing.ts b/packages/taler-wallet-core/src/operations/testing.ts index f71d842c7..607d03470 100644 --- a/packages/taler-wallet-core/src/operations/testing.ts +++ b/packages/taler-wallet-core/src/operations/testing.ts @@ -23,12 +23,16 @@ import { ConfirmPayResultType, Duration, IntegrationTestV2Args, + j2s, Logger, NotificationType, + RegisterAccountRequest, stringToBytes, + TalerCorebankApiClient, TestPayResult, TransactionMajorState, TransactionMinorState, + TransactionState, TransactionType, WithdrawTestBalanceRequest, } from "@gnu-taler/taler-util"; @@ -73,16 +77,6 @@ import { getTransactionById, getTransactions } from "./transactions.js"; const logger = new Logger("operations/testing.ts"); -interface BankUser { - username: string; - password: string; -} - -interface BankWithdrawalResponse { - taler_withdraw_uri: string; - withdrawal_id: string; -} - interface MerchantBackendInfo { baseUrl: string; authToken?: string; @@ -102,34 +96,27 @@ function makeId(length: number): string { return result; } -/** - * Helper function to generate the "Authorization" HTTP header. - * FIXME: redundant, put in taler-util - */ -function makeBasicAuthHeader(username: string, password: string): string { - const auth = `${username}:${password}`; - const authEncoded: string = base64FromArrayBuffer(stringToBytes(auth)); - return `Basic ${authEncoded}`; -} - export async function withdrawTestBalance( ws: InternalWalletState, req: WithdrawTestBalanceRequest, ): Promise<void> { const amount = req.amount; const exchangeBaseUrl = req.exchangeBaseUrl; - const bankAccessApiBaseUrl = req.bankAccessApiBaseUrl; + const corebankApiBaseUrl = req.corebankApiBaseUrl; logger.trace( - `Registered bank user, bank access base url ${bankAccessApiBaseUrl}`, + `Registering bank user, bank access base url ${corebankApiBaseUrl}`, ); - const bankUser = await registerRandomBankUser(ws.http, bankAccessApiBaseUrl); + + const corebankClient = new TalerCorebankApiClient(corebankApiBaseUrl); + + const bankUser = await corebankClient.createRandomBankUser(); logger.trace(`Registered bank user ${JSON.stringify(bankUser)}`); - const wresp = await createDemoBankWithdrawalUri( - ws.http, - bankAccessApiBaseUrl, - bankUser, + corebankClient.setAuth(bankUser); + + const wresp = await corebankClient.createWithdrawalOperation( + bankUser.username, amount, ); @@ -139,14 +126,14 @@ export async function withdrawTestBalance( forcedDenomSel: req.forcedDenomSel, }); - await confirmBankWithdrawalUri( - ws.http, - bankAccessApiBaseUrl, - bankUser, - wresp.withdrawal_id, - ); + await corebankClient.confirmWithdrawalOperation(bankUser.username, { + withdrawalOperationId: wresp.withdrawal_id, + }); } +/** + * FIXME: User MerchantApiClient instead. + */ function getMerchantAuthHeader(m: MerchantBackendInfo): Record<string, string> { if (m.authToken) { return { @@ -157,80 +144,8 @@ function getMerchantAuthHeader(m: MerchantBackendInfo): Record<string, string> { } /** - * Use the testing API of a demobank to create a taler://withdraw URI - * that the wallet can then use to make a withdrawal. + * FIXME: User MerchantApiClient instead. */ -export async function createDemoBankWithdrawalUri( - http: HttpRequestLibrary, - bankAccessApiBaseUrl: string, - bankUser: BankUser, - amount: AmountString, -): Promise<BankWithdrawalResponse> { - const reqUrl = new URL( - `accounts/${bankUser.username}/withdrawals`, - bankAccessApiBaseUrl, - ).href; - const resp = await http.postJson( - reqUrl, - { - amount, - }, - { - headers: { - Authorization: makeBasicAuthHeader( - bankUser.username, - bankUser.password, - ), - }, - }, - ); - const respJson = await readSuccessResponseJsonOrThrow(resp, codecForAny()); - return respJson; -} - -async function confirmBankWithdrawalUri( - http: HttpRequestLibrary, - bankAccessApiBaseUrl: string, - bankUser: BankUser, - withdrawalId: string, -): Promise<void> { - const reqUrl = new URL( - `accounts/${bankUser.username}/withdrawals/${withdrawalId}/confirm`, - bankAccessApiBaseUrl, - ).href; - const resp = await http.postJson( - reqUrl, - {}, - { - headers: { - Authorization: makeBasicAuthHeader( - bankUser.username, - bankUser.password, - ), - }, - }, - ); - await readSuccessResponseJsonOrThrow(resp, codecForAny()); - return; -} - -async function registerRandomBankUser( - http: HttpRequestLibrary, - bankAccessApiBaseUrl: string, -): Promise<BankUser> { - const reqUrl = new URL("testing/register", bankAccessApiBaseUrl).href; - const randId = makeId(8); - const bankUser: BankUser = { - // euFin doesn't allow resource names to have upper case letters. - username: `testuser-${randId.toLowerCase()}`, - password: `testpw-${randId}`, - }; - - const resp = await http.postJson(reqUrl, bankUser); - await checkSuccessResponseOrThrow(resp); - return bankUser; -} - async function refund( http: HttpRequestLibrary, merchantBackend: MerchantBackendInfo, @@ -258,6 +173,9 @@ async function refund( return refundUri; } +/** + * FIXME: User MerchantApiClient instead. + */ async function createOrder( http: HttpRequestLibrary, merchantBackend: MerchantBackendInfo, @@ -287,6 +205,9 @@ async function createOrder( return { orderId }; } +/** + * FIXME: User MerchantApiClient instead. + */ async function checkPayment( http: HttpRequestLibrary, merchantBackend: MerchantBackendInfo, @@ -300,16 +221,6 @@ async function checkPayment( return readSuccessResponseJsonOrThrow(resp, codecForCheckPaymentResponse()); } -interface BankUser { - username: string; - password: string; -} - -interface BankWithdrawalResponse { - taler_withdraw_uri: string; - withdrawal_id: string; -} - async function makePayment( ws: InternalWalletState, merchant: MerchantBackendInfo, @@ -376,7 +287,7 @@ export async function runIntegrationTest( logger.info("withdrawing test balance"); await withdrawTestBalance(ws, { amount: args.amountToWithdraw, - bankAccessApiBaseUrl: args.bankAccessApiBaseUrl, + corebankApiBaseUrl: args.corebankApiBaseUrl, exchangeBaseUrl: args.exchangeBaseUrl, }); await waitUntilDone(ws); @@ -404,7 +315,7 @@ export async function runIntegrationTest( await withdrawTestBalance(ws, { amount: Amounts.stringify(withdrawAmountTwo), - bankAccessApiBaseUrl: args.bankAccessApiBaseUrl, + corebankApiBaseUrl: args.corebankApiBaseUrl, exchangeBaseUrl: args.exchangeBaseUrl, }); @@ -585,6 +496,49 @@ async function waitUntilPendingReady( cancelNotifs(); } +/** + * Wait until a transaction is in a particular state. + */ +export async function waitTransactionState( + ws: InternalWalletState, + transactionId: string, + txState: TransactionState, +): Promise<void> { + logger.info( + `starting waiting for ${transactionId} to be in ${JSON.stringify( + txState, + )})`, + ); + ws.ensureTaskLoopRunning(); + let p: OpenedPromise<void> | undefined = undefined; + const cancelNotifs = ws.addNotificationListener((notif) => { + if (!p) { + return; + } + if (notif.type === NotificationType.TransactionStateTransition) { + p.resolve(); + } + }); + while (1) { + p = openPromise(); + const tx = await getTransactionById(ws, { + transactionId, + }); + if ( + tx.txState.major === txState.major && + tx.txState.minor === txState.minor + ) { + break; + } + // Wait until transaction state changed + await p.promise; + } + logger.info( + `done waiting for ${transactionId} to be in ${JSON.stringify(txState)}`, + ); + cancelNotifs(); +} + export async function runIntegrationTest2( ws: InternalWalletState, args: IntegrationTestV2Args, @@ -603,7 +557,7 @@ export async function runIntegrationTest2( logger.info("withdrawing test balance"); await withdrawTestBalance(ws, { amount: Amounts.stringify(amountToWithdraw), - bankAccessApiBaseUrl: args.bankAccessApiBaseUrl, + corebankApiBaseUrl: args.corebankApiBaseUrl, exchangeBaseUrl: args.exchangeBaseUrl, }); await waitUntilDone(ws); @@ -636,7 +590,7 @@ export async function runIntegrationTest2( await withdrawTestBalance(ws, { amount: Amounts.stringify(withdrawAmountTwo), - bankAccessApiBaseUrl: args.bankAccessApiBaseUrl, + corebankApiBaseUrl: args.corebankApiBaseUrl, exchangeBaseUrl: args.exchangeBaseUrl, }); diff --git a/packages/taler-wallet-core/src/operations/transactions.ts b/packages/taler-wallet-core/src/operations/transactions.ts index d7b277faf..cf2006406 100644 --- a/packages/taler-wallet-core/src/operations/transactions.ts +++ b/packages/taler-wallet-core/src/operations/transactions.ts @@ -20,6 +20,7 @@ import { AbsoluteTime, Amounts, + DepositTransactionTrackingState, j2s, Logger, NotificationType, @@ -65,7 +66,13 @@ import { WithdrawalGroupStatus, WithdrawalRecordType, } from "../db.js"; -import { GetReadOnlyAccess, WalletStoresV1 } from "../index.js"; +import { + GetReadOnlyAccess, + timestampOptionalPreciseFromDb, + timestampPreciseFromDb, + timestampProtocolFromDb, + WalletStoresV1, +} from "../index.js"; import { InternalWalletState } from "../internal-wallet-state.js"; import { PendingTaskType } from "../pending-types.js"; import { assertUnreachable } from "../util/assertUnreachable.js"; @@ -470,7 +477,7 @@ function buildTransactionForPushPaymentDebit( expiration: contractTerms.purse_expiration, summary: contractTerms.summary, }, - timestamp: pi.timestampCreated, + timestamp: timestampPreciseFromDb(pi.timestampCreated), talerUri: stringifyPayPushUri({ exchangeBaseUrl: pi.exchangeBaseUrl, contractPriv: pi.contractPriv, @@ -501,7 +508,7 @@ function buildTransactionForPullPaymentDebit( expiration: contractTerms.purse_expiration, summary: contractTerms.summary, }, - timestamp: pi.timestampCreated, + timestamp: timestampPreciseFromDb(pi.timestampCreated), transactionId: constructTransactionIdentifier({ tag: TransactionType.PeerPullDebit, peerPullDebitId: pi.peerPullDebitId, @@ -543,8 +550,7 @@ function buildTransactionForPeerPullCredit( amountEffective: Amounts.stringify(wsr.denomsSel.totalCoinValue), amountRaw: Amounts.stringify(wsr.instructedAmount), exchangeBaseUrl: wsr.exchangeBaseUrl, - // Old transactions don't have it! - timestamp: pullCredit.mergeTimestamp ?? TalerPreciseTimestamp.now(), + timestamp: timestampPreciseFromDb(pullCredit.mergeTimestamp), info: { expiration: peerContractTerms.purse_expiration, summary: peerContractTerms.summary, @@ -575,8 +581,7 @@ function buildTransactionForPeerPullCredit( amountEffective: Amounts.stringify(pullCredit.estimatedAmountEffective), amountRaw: Amounts.stringify(peerContractTerms.amount), exchangeBaseUrl: pullCredit.exchangeBaseUrl, - // Old transactions don't have it! - timestamp: pullCredit.mergeTimestamp ?? TalerProtocolTimestamp.now(), + timestamp: timestampPreciseFromDb(pullCredit.mergeTimestamp), info: { expiration: peerContractTerms.purse_expiration, summary: peerContractTerms.summary, @@ -617,7 +622,7 @@ function buildTransactionForPeerPushCredit( expiration: peerContractTerms.purse_expiration, summary: peerContractTerms.summary, }, - timestamp: wsr.timestampStart, + timestamp: timestampPreciseFromDb(wsr.timestampStart), transactionId: constructTransactionIdentifier({ tag: TransactionType.PeerPushCredit, peerPushCreditId: pushInc.peerPushCreditId, @@ -640,7 +645,7 @@ function buildTransactionForPeerPushCredit( summary: peerContractTerms.summary, }, kycUrl: pushInc.kycUrl, - timestamp: pushInc.timestamp, + timestamp: timestampPreciseFromDb(pushInc.timestamp), transactionId: constructTransactionIdentifier({ tag: TransactionType.PeerPushCredit, peerPushCreditId: pushInc.peerPushCreditId, @@ -673,7 +678,7 @@ function buildTransactionForBankIntegratedWithdraw( }, kycUrl: wgRecord.kycUrl, exchangeBaseUrl: wgRecord.exchangeBaseUrl, - timestamp: wgRecord.timestampStart, + timestamp: timestampPreciseFromDb(wgRecord.timestampStart), transactionId: constructTransactionIdentifier({ tag: TransactionType.Withdrawal, withdrawalGroupId: wgRecord.withdrawalGroupId, @@ -717,7 +722,7 @@ function buildTransactionForManualWithdraw( }, kycUrl: withdrawalGroup.kycUrl, exchangeBaseUrl: withdrawalGroup.exchangeBaseUrl, - timestamp: withdrawalGroup.timestampStart, + timestamp: timestampPreciseFromDb(withdrawalGroup.timestampStart), transactionId: constructTransactionIdentifier({ tag: TransactionType.Withdrawal, withdrawalGroupId: withdrawalGroup.withdrawalGroupId, @@ -748,7 +753,7 @@ function buildTransactionForRefund( tag: TransactionType.Payment, proposalId: refundRecord.proposalId, }), - timestamp: refundRecord.timestampCreated, + timestamp: timestampPreciseFromDb(refundRecord.timestampCreated), transactionId: constructTransactionIdentifier({ tag: TransactionType.Refund, refundGroupId: refundRecord.refundGroupId, @@ -786,7 +791,7 @@ function buildTransactionForRefresh( refreshOutputAmount: Amounts.stringify(outputAmount), originatingTransactionId: refreshGroupRecord.reasonDetails?.originatingTransactionId, - timestamp: refreshGroupRecord.timestampCreated, + timestamp: timestampPreciseFromDb(refreshGroupRecord.timestampCreated), transactionId: constructTransactionIdentifier({ tag: TransactionType.Refresh, refreshGroupId: refreshGroupRecord.refreshGroupId, @@ -806,15 +811,26 @@ function buildTransactionForDeposit( } } + const trackingState: DepositTransactionTrackingState[] = []; + + for (const ts of Object.values(dg.trackingState ?? {})) { + trackingState.push({ + amountRaw: ts.amountRaw, + timestampExecuted: timestampProtocolFromDb(ts.timestampExecuted), + wireFee: ts.wireFee, + wireTransferId: ts.wireTransferId, + }); + } + return { type: TransactionType.Deposit, txState: computeDepositTransactionStatus(dg), txActions: computeDepositTransactionActions(dg), amountRaw: Amounts.stringify(dg.counterpartyEffectiveDepositAmount), amountEffective: Amounts.stringify(dg.totalPayCost), - timestamp: dg.timestampCreated, + timestamp: timestampPreciseFromDb(dg.timestampCreated), targetPaytoUri: dg.wire.payto_uri, - wireTransferDeadline: dg.wireTransferDeadline, + wireTransferDeadline: timestampProtocolFromDb(dg.wireTransferDeadline), transactionId: constructTransactionIdentifier({ tag: TransactionType.Deposit, depositGroupId: dg.depositGroupId, @@ -827,7 +843,7 @@ function buildTransactionForDeposit( )) / dg.statusPerCoin.length, depositGroupId: dg.depositGroupId, - trackingState: Object.values(dg.trackingState ?? {}), + trackingState, deposited, ...(ort?.lastError ? { error: ort.lastError } : {}), }; @@ -845,7 +861,7 @@ function buildTransactionForTip( txActions: computeTipTransactionActions(tipRecord), amountEffective: Amounts.stringify(tipRecord.rewardAmountEffective), amountRaw: Amounts.stringify(tipRecord.rewardAmountRaw), - timestamp: tipRecord.acceptedTimestamp, + timestamp: timestampPreciseFromDb(tipRecord.acceptedTimestamp), transactionId: constructTransactionIdentifier({ tag: TransactionType.Reward, walletRewardId: tipRecord.walletRewardId, @@ -922,7 +938,7 @@ async function buildTransactionForPurchase( : Amounts.stringify(purchaseRecord.refundAmountAwaiting), refunds, posConfirmation: purchaseRecord.posConfirmation, - timestamp, + timestamp: timestampPreciseFromDb(timestamp), transactionId: constructTransactionIdentifier({ tag: TransactionType.Payment, proposalId: purchaseRecord.proposalId, diff --git a/packages/taler-wallet-core/src/operations/withdraw.test.ts b/packages/taler-wallet-core/src/operations/withdraw.test.ts index 2d9286610..cb8aa5e81 100644 --- a/packages/taler-wallet-core/src/operations/withdraw.test.ts +++ b/packages/taler-wallet-core/src/operations/withdraw.test.ts @@ -16,7 +16,11 @@ import { Amounts, DenomKeyType } from "@gnu-taler/taler-util"; import test from "ava"; -import { DenominationRecord, DenominationVerificationStatus } from "../db.js"; +import { + DenominationRecord, + DenominationVerificationStatus, + timestampProtocolToDb, +} from "../db.js"; import { selectWithdrawalDenominations } from "../util/coinSelection.js"; test("withdrawal selection bug repro", (t) => { @@ -64,22 +68,22 @@ test("withdrawal selection bug repro", (t) => { isRevoked: false, masterSig: "4F0P456CNNTTWK8BFJHGM3JTD6FVVNZY8EP077GYAHDJ5Y81S5RQ3SMS925NXMDVG9A88JAAP0E2GDZBC21PP5NHFFVWHAW3AVT8J3R", - stampExpireDeposit: { + stampExpireDeposit: timestampProtocolToDb({ t_s: 1742909388, - }, - stampExpireLegal: { + }), + stampExpireLegal: timestampProtocolToDb({ t_s: 1900589388, - }, - stampExpireWithdraw: { + }), + stampExpireWithdraw: timestampProtocolToDb({ t_s: 1679837388, - }, - stampStart: { + }), + stampStart: timestampProtocolToDb({ t_s: 1585229388, - }, + }), verificationStatus: DenominationVerificationStatus.Unverified, currency: "KUDOS", value: "KUDOS:1000", - listIssueDate: { t_s: 0 }, + listIssueDate: timestampProtocolToDb({ t_s: 0 }), }, { denomPub: { @@ -119,22 +123,22 @@ test("withdrawal selection bug repro", (t) => { isRevoked: false, masterSig: "P99AW82W46MZ0AKW7Z58VQPXFNTJQM9DVTYPBDF6KVYF38PPVDAZTV7JQ8TY7HGEC7JJJAY4E7AY7J3W1WV10DAZZQHHKTAVTSRAC20", - stampExpireDeposit: { + stampExpireDeposit: timestampProtocolToDb({ t_s: 1742909388, - }, - stampExpireLegal: { + }), + stampExpireLegal: timestampProtocolToDb({ t_s: 1900589388, - }, - stampExpireWithdraw: { + }), + stampExpireWithdraw: timestampProtocolToDb({ t_s: 1679837388, - }, - stampStart: { + }), + stampStart: timestampProtocolToDb({ t_s: 1585229388, - }, + }), verificationStatus: DenominationVerificationStatus.Unverified, value: "KUDOS:10", currency: "KUDOS", - listIssueDate: { t_s: 0 }, + listIssueDate: timestampProtocolToDb({ t_s: 0 }), }, { denomPub: { @@ -173,22 +177,22 @@ test("withdrawal selection bug repro", (t) => { isRevoked: false, masterSig: "8S4VZGHE5WE0N5ZVCHYW9KZZR4YAKK15S46MV1HR1QB9AAMH3NWPW4DCR4NYGJK33Q8YNFY80SWNS6XKAP5DEVK933TM894FJ2VGE3G", - stampExpireDeposit: { + stampExpireDeposit: timestampProtocolToDb({ t_s: 1742909388, - }, - stampExpireLegal: { + }), + stampExpireLegal: timestampProtocolToDb({ t_s: 1900589388, - }, - stampExpireWithdraw: { + }), + stampExpireWithdraw: timestampProtocolToDb({ t_s: 1679837388, - }, - stampStart: { + }), + stampStart: timestampProtocolToDb({ t_s: 1585229388, - }, + }), verificationStatus: DenominationVerificationStatus.Unverified, value: "KUDOS:5", currency: "KUDOS", - listIssueDate: { t_s: 0 }, + listIssueDate: timestampProtocolToDb({ t_s: 0 }), }, { denomPub: { @@ -228,22 +232,22 @@ test("withdrawal selection bug repro", (t) => { isRevoked: false, masterSig: "E3AWGAG8VB42P3KXM8B04Z6M483SX59R3Y4T53C3NXCA2NPB6C7HVCMVX05DC6S58E9X40NGEBQNYXKYMYCF3ASY2C4WP1WCZ4ME610", - stampExpireDeposit: { + stampExpireDeposit: timestampProtocolToDb({ t_s: 1742909388, - }, - stampExpireLegal: { + }), + stampExpireLegal: timestampProtocolToDb({ t_s: 1900589388, - }, - stampExpireWithdraw: { + }), + stampExpireWithdraw: timestampProtocolToDb({ t_s: 1679837388, - }, - stampStart: { + }), + stampStart: timestampProtocolToDb({ t_s: 1585229388, - }, + }), verificationStatus: DenominationVerificationStatus.Unverified, value: "KUDOS:1", currency: "KUDOS", - listIssueDate: { t_s: 0 }, + listIssueDate: timestampProtocolToDb({ t_s: 0 }), }, { denomPub: { @@ -282,18 +286,18 @@ test("withdrawal selection bug repro", (t) => { isRevoked: false, masterSig: "0ES1RKV002XB4YP21SN0QB7RSDHGYT0XAE65JYN8AVJAA6H7JZFN7JADXT521DJS89XMGPZGR8GCXF1516Y0Q9QDV00E6NMFA6CF838", - stampExpireDeposit: { + stampExpireDeposit: timestampProtocolToDb({ t_s: 1742909388, - }, - stampExpireLegal: { + }), + stampExpireLegal: timestampProtocolToDb({ t_s: 1900589388, - }, - stampExpireWithdraw: { + }), + stampExpireWithdraw: timestampProtocolToDb({ t_s: 1679837388, - }, - stampStart: { + }), + stampStart: timestampProtocolToDb({ t_s: 1585229388, - }, + }), verificationStatus: DenominationVerificationStatus.Unverified, value: Amounts.stringify({ currency: "KUDOS", @@ -301,7 +305,7 @@ test("withdrawal selection bug repro", (t) => { value: 0, }), currency: "KUDOS", - listIssueDate: { t_s: 0 }, + listIssueDate: timestampProtocolToDb({ t_s: 0 }), }, { denomPub: { @@ -340,22 +344,22 @@ test("withdrawal selection bug repro", (t) => { isRevoked: false, masterSig: "58QEB6C6N7602E572E3JYANVVJ9BRW0V9E2ZFDW940N47YVQDK9SAFPWBN5YGT3G1742AFKQ0CYR4DM2VWV0Z0T1XMEKWN6X2EZ9M0R", - stampExpireDeposit: { + stampExpireDeposit: timestampProtocolToDb({ t_s: 1742909388, - }, - stampExpireLegal: { + }), + stampExpireLegal: timestampProtocolToDb({ t_s: 1900589388, - }, - stampExpireWithdraw: { + }), + stampExpireWithdraw: timestampProtocolToDb({ t_s: 1679837388, - }, - stampStart: { + }), + stampStart: timestampProtocolToDb({ t_s: 1585229388, - }, + }), verificationStatus: DenominationVerificationStatus.Unverified, value: "KUDOS:2", currency: "KUDOS", - listIssueDate: { t_s: 0 }, + listIssueDate: timestampProtocolToDb({ t_s: 0 }), }, ]; diff --git a/packages/taler-wallet-core/src/operations/withdraw.ts b/packages/taler-wallet-core/src/operations/withdraw.ts index bae348dc1..eff427bec 100644 --- a/packages/taler-wallet-core/src/operations/withdraw.ts +++ b/packages/taler-wallet-core/src/operations/withdraw.ts @@ -135,6 +135,7 @@ import { ExchangeEntryDbUpdateStatus, PendingTaskType, isWithdrawableDenom, + timestampPreciseToDb, } from "../index.js"; import { TransitionInfo, @@ -573,7 +574,7 @@ export async function getBankWithdrawalInfo( throw TalerError.fromDetail( TalerErrorCode.WALLET_BANK_INTEGRATION_PROTOCOL_VERSION_INCOMPATIBLE, { - exchangeProtocolVersion: config.version, + bankProtocolVersion: config.version, walletProtocolVersion: WALLET_BANK_INTEGRATION_PROTOCOL_VERSION, }, "bank integration protocol version not compatible with wallet", @@ -816,10 +817,10 @@ async function handleKycRequired( amlStatus === AmlStatus.normal || amlStatus === undefined ? WithdrawalGroupStatus.PendingKyc : amlStatus === AmlStatus.pending - ? WithdrawalGroupStatus.PendingAml - : amlStatus === AmlStatus.fronzen - ? WithdrawalGroupStatus.SuspendedAml - : assertUnreachable(amlStatus); + ? WithdrawalGroupStatus.PendingAml + : amlStatus === AmlStatus.fronzen + ? WithdrawalGroupStatus.SuspendedAml + : assertUnreachable(amlStatus); await tx.withdrawalGroups.put(wg2); const newTxState = computeWithdrawalTransactionStatus(wg2); @@ -1149,8 +1150,7 @@ export async function updateWithdrawalDenoms( denom.verificationStatus === DenominationVerificationStatus.Unverified ) { logger.trace( - `Validating denomination (${current + 1}/${ - denominations.length + `Validating denomination (${current + 1}/${denominations.length }) signature of ${denom.denomPubHash}`, ); let valid = false; @@ -1244,7 +1244,7 @@ async function queryReserve( if ( resp.status === 404 && result.talerErrorResponse.code === - TalerErrorCode.EXCHANGE_RESERVES_STATUS_UNKNOWN + TalerErrorCode.EXCHANGE_RESERVES_STATUS_UNKNOWN ) { return { ready: false }; } else { @@ -1330,7 +1330,7 @@ async function processWithdrawalGroupAbortingBank( } const txStatusOld = computeWithdrawalTransactionStatus(wg); wg.status = WithdrawalGroupStatus.AbortedBank; - wg.timestampFinish = TalerPreciseTimestamp.now(); + wg.timestampFinish = timestampPreciseToDb(TalerPreciseTimestamp.now()); const txStatusNew = computeWithdrawalTransactionStatus(wg); await tx.withdrawalGroups.put(wg); return { @@ -1463,7 +1463,7 @@ async function processWithdrawalGroupPendingReady( } const txStatusOld = computeWithdrawalTransactionStatus(wg); wg.status = WithdrawalGroupStatus.Done; - wg.timestampFinish = TalerPreciseTimestamp.now(); + wg.timestampFinish = timestampPreciseToDb(TalerPreciseTimestamp.now()); const txStatusNew = computeWithdrawalTransactionStatus(wg); await tx.withdrawalGroups.put(wg); return { @@ -1559,7 +1559,7 @@ async function processWithdrawalGroupPendingReady( const oldTxState = computeWithdrawalTransactionStatus(wg); logger.info(`now withdrawn ${numFinished} of ${numTotalCoins} coins`); if (wg.timestampFinish === undefined && numFinished === numTotalCoins) { - wg.timestampFinish = TalerPreciseTimestamp.now(); + wg.timestampFinish = timestampPreciseToDb(TalerPreciseTimestamp.now()); wg.status = WithdrawalGroupStatus.Done; await makeCoinsVisible(ws, tx, transactionId); } @@ -1779,7 +1779,7 @@ export async function getExchangeWithdrawalInfo( ) { logger.warn( `wallet's support for exchange protocol version ${WALLET_EXCHANGE_PROTOCOL_VERSION} might be outdated ` + - `(exchange has ${exchangeDetails.protocolVersionRange}), checking for updates`, + `(exchange has ${exchangeDetails.protocolVersionRange}), checking for updates`, ); } } @@ -2052,8 +2052,9 @@ async function registerReserveWithBank( if (r.wgInfo.withdrawalType !== WithdrawalRecordType.BankIntegrated) { throw Error("invariant failed"); } - r.wgInfo.bankInfo.timestampReserveInfoPosted = - AbsoluteTime.toPreciseTimestamp(AbsoluteTime.now()); + r.wgInfo.bankInfo.timestampReserveInfoPosted = timestampPreciseToDb( + AbsoluteTime.toPreciseTimestamp(AbsoluteTime.now()), + ); const oldTxState = computeWithdrawalTransactionStatus(r); r.status = WithdrawalGroupStatus.PendingWaitConfirmBank; const newTxState = computeWithdrawalTransactionStatus(r); @@ -2135,7 +2136,7 @@ async function processReserveBankStatus( } const now = AbsoluteTime.toPreciseTimestamp(AbsoluteTime.now()); const oldTxState = computeWithdrawalTransactionStatus(r); - r.wgInfo.bankInfo.timestampBankConfirmed = now; + r.wgInfo.bankInfo.timestampBankConfirmed = timestampPreciseToDb(now); r.status = WithdrawalGroupStatus.FailedBankAborted; const newTxState = computeWithdrawalTransactionStatus(r); await tx.withdrawalGroups.put(r); @@ -2184,7 +2185,7 @@ async function processReserveBankStatus( if (status.transfer_done) { logger.info("withdrawal: transfer confirmed by bank."); const now = AbsoluteTime.toPreciseTimestamp(AbsoluteTime.now()); - r.wgInfo.bankInfo.timestampBankConfirmed = now; + r.wgInfo.bankInfo.timestampBankConfirmed = timestampPreciseToDb(now); r.status = WithdrawalGroupStatus.PendingQueryingStatus; } else { logger.info("withdrawal: transfer not yet confirmed by bank"); @@ -2290,7 +2291,7 @@ export async function internalPrepareCreateWithdrawalGroup( denomsSel: initialDenomSel, exchangeBaseUrl: canonExchange, instructedAmount: Amounts.stringify(amount), - timestampStart: now, + timestampStart: timestampPreciseToDb(now), rawWithdrawalAmount: initialDenomSel.totalWithdrawCost, effectiveWithdrawalAmount: initialDenomSel.totalCoinValue, secretSeed, @@ -2344,8 +2345,7 @@ export async function internalPerformCreateWithdrawalGroup( if (!prep.creationInfo) { return { withdrawalGroup, transitionInfo: undefined }; } - const { amount, canonExchange, exchangeDetails } = - prep.creationInfo; + const { amount, canonExchange, exchangeDetails } = prep.creationInfo; await tx.withdrawalGroups.add(withdrawalGroup); await tx.reserves.put({ @@ -2355,7 +2355,7 @@ export async function internalPerformCreateWithdrawalGroup( const exchange = await tx.exchanges.get(withdrawalGroup.exchangeBaseUrl); if (exchange) { - exchange.lastWithdrawal = TalerPreciseTimestamp.now(); + exchange.lastWithdrawal = timestampPreciseToDb(TalerPreciseTimestamp.now()); exchange.entryStatus = ExchangeEntryDbRecordStatus.Used; await tx.exchanges.put(exchange); } @@ -2546,11 +2546,7 @@ export async function createManualWithdrawal( }); const exchangePaytoUris = await ws.db - .mktx((x) => [ - x.withdrawalGroups, - x.exchanges, - x.exchangeDetails, - ]) + .mktx((x) => [x.withdrawalGroups, x.exchanges, x.exchangeDetails]) .runReadOnly(async (tx) => { return await getFundingPaytoUris(tx, withdrawalGroup.withdrawalGroupId); }); diff --git a/packages/taler-wallet-core/src/pending-types.ts b/packages/taler-wallet-core/src/pending-types.ts index 627888b4d..e7a40e81b 100644 --- a/packages/taler-wallet-core/src/pending-types.ts +++ b/packages/taler-wallet-core/src/pending-types.ts @@ -25,7 +25,7 @@ * Imports. */ import { TalerErrorDetail, AbsoluteTime } from "@gnu-taler/taler-util"; -import { RetryInfo } from "./operations/common.js"; +import { DbRetryInfo } from "./operations/common.js"; export enum PendingTaskType { ExchangeUpdate = "exchange-update", @@ -137,7 +137,7 @@ export interface PendingRefreshTask { lastError?: TalerErrorDetail; refreshGroupId: string; finishedPerCoin: boolean[]; - retryInfo?: RetryInfo; + retryInfo?: DbRetryInfo; } /** @@ -156,7 +156,7 @@ export interface PendingTipPickupTask { export interface PendingPurchaseTask { type: PendingTaskType.Purchase; proposalId: string; - retryInfo?: RetryInfo; + retryInfo?: DbRetryInfo; /** * Status of the payment as string, used only for debugging. */ @@ -167,7 +167,7 @@ export interface PendingPurchaseTask { export interface PendingRecoupTask { type: PendingTaskType.Recoup; recoupGroupId: string; - retryInfo?: RetryInfo; + retryInfo?: DbRetryInfo; lastError: TalerErrorDetail | undefined; } @@ -177,7 +177,7 @@ export interface PendingRecoupTask { export interface PendingWithdrawTask { type: PendingTaskType.Withdraw; lastError: TalerErrorDetail | undefined; - retryInfo?: RetryInfo; + retryInfo?: DbRetryInfo; withdrawalGroupId: string; } @@ -187,7 +187,7 @@ export interface PendingWithdrawTask { export interface PendingDepositTask { type: PendingTaskType.Deposit; lastError: TalerErrorDetail | undefined; - retryInfo: RetryInfo | undefined; + retryInfo: DbRetryInfo | undefined; depositGroupId: string; } @@ -233,7 +233,7 @@ export interface PendingTaskInfoCommon { * Retry info. Currently used to stop the wallet after any operation * exceeds a number of retries. */ - retryInfo?: RetryInfo; + retryInfo?: DbRetryInfo; } /** diff --git a/packages/taler-wallet-core/src/util/coinSelection.test.ts b/packages/taler-wallet-core/src/util/coinSelection.test.ts index 2a322c4a9..81a656f8a 100644 --- a/packages/taler-wallet-core/src/util/coinSelection.test.ts +++ b/packages/taler-wallet-core/src/util/coinSelection.test.ts @@ -15,65 +15,58 @@ */ import { AbsoluteTime, - AgeRestriction, - AmountJson, AmountString, Amounts, DenomKeyType, Duration, - TransactionAmountMode, + j2s, } from "@gnu-taler/taler-util"; import test, { ExecutionContext } from "ava"; -import { AvailableDenom, testing_greedySelectPeer } from "./coinSelection.js" - -type Tester<T> = { - deep: { - equal(another: T): ReturnType<ExecutionContext["deepEqual"]>; - equals(another: T): ReturnType<ExecutionContext["deepEqual"]>; - } -} - -function expect<T>(t: ExecutionContext, thing: T): Tester<T> { - return { - deep: { - equal: (another: T) => t.deepEqual(thing, another), - equals: (another: T) => t.deepEqual(thing, another), - }, - }; -} +import { + AvailableDenom, + testing_greedySelectPeer, + testing_selectGreedy, +} from "./coinSelection.js"; const inTheDistantFuture = AbsoluteTime.toProtocolTimestamp( - AbsoluteTime.addDuration(AbsoluteTime.now(), Duration.fromSpec({ hours: 1 })) -) + AbsoluteTime.addDuration(AbsoluteTime.now(), Duration.fromSpec({ hours: 1 })), +); const inThePast = AbsoluteTime.toProtocolTimestamp( - AbsoluteTime.subtractDuraction(AbsoluteTime.now(), Duration.fromSpec({ hours: 1 })) -) - -test("should select the coin", (t) => { - const instructedAmount = Amounts.parseOrThrow("LOCAL:2") + AbsoluteTime.subtractDuraction( + AbsoluteTime.now(), + Duration.fromSpec({ hours: 1 }), + ), +); + +test("p2p: should select the coin", (t) => { + const instructedAmount = Amounts.parseOrThrow("LOCAL:2"); const tally = { amountAcc: Amounts.zeroOfCurrency(instructedAmount.currency), depositFeesAcc: Amounts.zeroOfCurrency(instructedAmount.currency), lastDepositFee: Amounts.zeroOfCurrency(instructedAmount.currency), }; const coins = testing_greedySelectPeer( - createCandidates([{ - amount: "LOCAL:10", - numAvailable: 5, - depositFee: "LOCAL:0.1", - fromExchange: "http://exchange.localhost/", - }]), - instructedAmount, - tally + createCandidates([ + { + amount: "LOCAL:10", + numAvailable: 5, + depositFee: "LOCAL:0.1", + fromExchange: "http://exchange.localhost/", + }, + ]), + instructedAmount, + tally, ); + t.log(j2s(coins)); + expect(t, coins).deep.equal({ "hash0;32;http://exchange.localhost/": { exchangeBaseUrl: "http://exchange.localhost/", denomPubHash: "hash0", maxAge: 32, - contributions: [Amounts.parseOrThrow("LOCAL:2")], - } + contributions: [Amounts.parseOrThrow("LOCAL:2.1")], + }, }); expect(t, tally).deep.equal({ @@ -81,25 +74,26 @@ test("should select the coin", (t) => { depositFeesAcc: Amounts.parseOrThrow("LOCAL:0.1"), lastDepositFee: Amounts.parseOrThrow("LOCAL:0.1"), }); - }); -test("should select 3 coins", (t) => { - const instructedAmount = Amounts.parseOrThrow("LOCAL:20") +test("p2p: should select 3 coins", (t) => { + const instructedAmount = Amounts.parseOrThrow("LOCAL:20"); const tally = { amountAcc: Amounts.zeroOfCurrency(instructedAmount.currency), depositFeesAcc: Amounts.zeroOfCurrency(instructedAmount.currency), lastDepositFee: Amounts.zeroOfCurrency(instructedAmount.currency), }; const coins = testing_greedySelectPeer( - createCandidates([{ - amount: "LOCAL:10", - numAvailable: 5, - depositFee: "LOCAL:0.1", - fromExchange: "http://exchange.localhost/", - }]), - instructedAmount, - tally + createCandidates([ + { + amount: "LOCAL:10", + numAvailable: 5, + depositFee: "LOCAL:0.1", + fromExchange: "http://exchange.localhost/", + }, + ]), + instructedAmount, + tally, ); expect(t, coins).deep.equal({ @@ -110,9 +104,9 @@ test("should select 3 coins", (t) => { contributions: [ Amounts.parseOrThrow("LOCAL:9.9"), Amounts.parseOrThrow("LOCAL:9.9"), - Amounts.parseOrThrow("LOCAL:0.2") + Amounts.parseOrThrow("LOCAL:0.5"), ], - } + }, }); expect(t, tally).deep.equal({ @@ -120,62 +114,142 @@ test("should select 3 coins", (t) => { depositFeesAcc: Amounts.parseOrThrow("LOCAL:0.3"), lastDepositFee: Amounts.parseOrThrow("LOCAL:0.1"), }); - }); -test("can't select since the instructed amount is too high", (t) => { - const instructedAmount = Amounts.parseOrThrow("LOCAL:60") +test("p2p: can't select since the instructed amount is too high", (t) => { + const instructedAmount = Amounts.parseOrThrow("LOCAL:60"); const tally = { amountAcc: Amounts.zeroOfCurrency(instructedAmount.currency), depositFeesAcc: Amounts.zeroOfCurrency(instructedAmount.currency), lastDepositFee: Amounts.zeroOfCurrency(instructedAmount.currency), }; const coins = testing_greedySelectPeer( - createCandidates([{ - amount: "LOCAL:10", - numAvailable: 5, - depositFee: "LOCAL:0.1", - fromExchange: "http://exchange.localhost/", - }]), - instructedAmount, - tally + createCandidates([ + { + amount: "LOCAL:10", + numAvailable: 5, + depositFee: "LOCAL:0.1", + fromExchange: "http://exchange.localhost/", + }, + ]), + instructedAmount, + tally, ); expect(t, coins).deep.equal(undefined); expect(t, tally).deep.equal({ - amountAcc: Amounts.parseOrThrow("LOCAL:49.5"), + amountAcc: Amounts.parseOrThrow("LOCAL:49"), depositFeesAcc: Amounts.parseOrThrow("LOCAL:0.5"), lastDepositFee: Amounts.parseOrThrow("LOCAL:0.1"), }); - }); +test("pay: select one coin to pay with fee", (t) => { + const payment = Amounts.parseOrThrow("LOCAL:2"); + const exchangeWireFee = Amounts.parseOrThrow("LOCAL:0.1"); + const zero = Amounts.zeroOfCurrency(payment.currency); + const tally = { + amountPayRemaining: payment, + amountWireFeeLimitRemaining: zero, + amountDepositFeeLimitRemaining: zero, + customerDepositFees: zero, + customerWireFees: zero, + wireFeeCoveredForExchange: new Set<string>(), + lastDepositFee: zero, + }; + const coins = testing_selectGreedy( + { + auditors: [], + exchanges: [ + { + exchangeBaseUrl: "http://exchange.localhost/", + exchangePub: "E5M8CGRDHXF1RCVP3B8TQCTDYNQ7T4XHWR5SVEQRGVVMVME41VJ0", + }, + ], + contractTermsAmount: payment, + depositFeeLimit: zero, + wireFeeAmortization: 1, + wireFeeLimit: zero, + prevPayCoins: [], + wireMethod: "x-taler-bank", + }, + createCandidates([ + { + amount: "LOCAL:10", + numAvailable: 5, + depositFee: "LOCAL:0.1", + fromExchange: "http://exchange.localhost/", + }, + ]), + { "http://exchange.localhost/": exchangeWireFee }, + tally, + ); + expect(t, coins).deep.equal({ + "hash0;32;http://exchange.localhost/": { + exchangeBaseUrl: "http://exchange.localhost/", + denomPubHash: "hash0", + maxAge: 32, + contributions: [Amounts.parseOrThrow("LOCAL:2.2")], + }, + }); + expect(t, tally).deep.equal({ + amountPayRemaining: Amounts.parseOrThrow("LOCAL:2"), + amountWireFeeLimitRemaining: zero, + amountDepositFeeLimitRemaining: zero, + customerDepositFees: zero, + customerWireFees: zero, + wireFeeCoveredForExchange: new Set(), + lastDepositFee: zero, + }); +}); -function createCandidates(ar: {amount: AmountString, depositFee: AmountString, numAvailable: number, fromExchange: string}[]): AvailableDenom[] { - return ar.map((r,idx) => { +function createCandidates( + ar: { + amount: AmountString; + depositFee: AmountString; + numAvailable: number; + fromExchange: string; + }[], +): AvailableDenom[] { + return ar.map((r, idx) => { return { - "denomPub": { - "age_mask": 0, - "cipher": DenomKeyType.Rsa, - "rsa_public_key": "PPP" + denomPub: { + age_mask: 0, + cipher: DenomKeyType.Rsa, + rsa_public_key: "PPP", }, - "denomPubHash": `hash${idx}`, - "value": r.amount, - "feeDeposit": r.depositFee, - "feeRefresh": "LOCAL:0", - "feeRefund": "LOCAL:0", - "feeWithdraw": "LOCAL:0", - "stampExpireDeposit": inTheDistantFuture, - "stampExpireLegal": inTheDistantFuture, - "stampExpireWithdraw": inTheDistantFuture, - "stampStart": inThePast, - "exchangeBaseUrl": r.fromExchange, - "numAvailable": r.numAvailable, - "maxAge": 32, - - } - }) + denomPubHash: `hash${idx}`, + value: r.amount, + feeDeposit: r.depositFee, + feeRefresh: "LOCAL:0", + feeRefund: "LOCAL:0", + feeWithdraw: "LOCAL:0", + stampExpireDeposit: inTheDistantFuture, + stampExpireLegal: inTheDistantFuture, + stampExpireWithdraw: inTheDistantFuture, + stampStart: inThePast, + exchangeBaseUrl: r.fromExchange, + numAvailable: r.numAvailable, + maxAge: 32, + }; + }); +} + +type Tester<T> = { + deep: { + equal(another: T): ReturnType<ExecutionContext["deepEqual"]>; + equals(another: T): ReturnType<ExecutionContext["deepEqual"]>; + }; +}; + +function expect<T>(t: ExecutionContext, thing: T): Tester<T> { + return { + deep: { + equal: (another: T) => t.deepEqual(thing, another), + equals: (another: T) => t.deepEqual(thing, another), + }, + }; } diff --git a/packages/taler-wallet-core/src/util/coinSelection.ts b/packages/taler-wallet-core/src/util/coinSelection.ts index 0885215dd..8c90f26f1 100644 --- a/packages/taler-wallet-core/src/util/coinSelection.ts +++ b/packages/taler-wallet-core/src/util/coinSelection.ts @@ -419,6 +419,11 @@ interface SelResult { }; } +export function testing_selectGreedy( + ...args: Parameters<typeof selectGreedy> +): ReturnType<typeof selectGreedy> { + return selectGreedy(...args); +} function selectGreedy( req: SelectPayCoinRequestNg, candidateDenoms: AvailableDenom[], @@ -897,9 +902,12 @@ interface PeerCoinSelectionTally { /** * exporting for testing */ -export function testing_greedySelectPeer(...args: Parameters<typeof greedySelectPeer>): ReturnType<typeof greedySelectPeer> { - return greedySelectPeer(...args) +export function testing_greedySelectPeer( + ...args: Parameters<typeof greedySelectPeer> +): ReturnType<typeof greedySelectPeer> { + return greedySelectPeer(...args); } + function greedySelectPeer( candidates: AvailableDenom[], instructedAmount: AmountLike, @@ -918,11 +926,16 @@ function greedySelectPeer( instructedAmount, tally.amountAcc, ).amount; - const coinContrib = Amounts.sub(denom.value, denom.feeDeposit).amount + // Maximum amount the coin could effectively contribute. + const maxCoinContrib = Amounts.sub(denom.value, denom.feeDeposit).amount; + + const coinSpend = Amounts.min( + Amounts.add(amountPayRemaining, denom.feeDeposit).amount, + maxCoinContrib, + ); - const coinSpend = Amounts.min(amountPayRemaining, coinContrib) - tally.amountAcc = Amounts.add(tally.amountAcc, coinSpend).amount; + tally.amountAcc = Amounts.sub(tally.amountAcc, denom.feeDeposit).amount; tally.depositFeesAcc = Amounts.add( tally.depositFeesAcc, @@ -930,7 +943,7 @@ function greedySelectPeer( ).amount; tally.lastDepositFee = Amounts.parseOrThrow(denom.feeDeposit); - + contributions.push(coinSpend); } if (contributions.length > 0) { diff --git a/packages/taler-wallet-core/src/util/denominations.ts b/packages/taler-wallet-core/src/util/denominations.ts index 76716cf7a..db6e69956 100644 --- a/packages/taler-wallet-core/src/util/denominations.ts +++ b/packages/taler-wallet-core/src/util/denominations.ts @@ -26,10 +26,9 @@ import { FeeDescriptionPair, TalerProtocolTimestamp, TimePoint, - WireFee, } from "@gnu-taler/taler-util"; import { DenominationRecord } from "../db.js"; -import { WalletConfig } from "../index.js"; +import { timestampProtocolFromDb } from "../index.js"; /** * Given a list of denominations with the same value and same period of time: @@ -457,9 +456,11 @@ export function isWithdrawableDenom( denomselAllowLate?: boolean, ): boolean { const now = AbsoluteTime.now(); - const start = AbsoluteTime.fromProtocolTimestamp(d.stampStart); + const start = AbsoluteTime.fromProtocolTimestamp( + timestampProtocolFromDb(d.stampStart), + ); const withdrawExpire = AbsoluteTime.fromProtocolTimestamp( - d.stampExpireWithdraw, + timestampProtocolFromDb(d.stampExpireWithdraw), ); const started = AbsoluteTime.cmp(now, start) >= 0; let lastPossibleWithdraw: AbsoluteTime; diff --git a/packages/taler-wallet-core/src/util/instructedAmountConversion.ts b/packages/taler-wallet-core/src/util/instructedAmountConversion.ts index 54c08eee4..a0394a687 100644 --- a/packages/taler-wallet-core/src/util/instructedAmountConversion.ts +++ b/packages/taler-wallet-core/src/util/instructedAmountConversion.ts @@ -14,6 +14,7 @@ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ +import { GlobalIDB } from "@gnu-taler/idb-bridge"; import { AbsoluteTime, AgeRestriction, @@ -29,14 +30,14 @@ import { parsePaytoUri, strcmp, } from "@gnu-taler/taler-util"; -import { checkDbInvariant } from "./invariants.js"; import { DenominationRecord, InternalWalletState, getExchangeDetails, + timestampProtocolFromDb, } from "../index.js"; import { CoinInfo } from "./coinSelection.js"; -import { GlobalIDB } from "@gnu-taler/idb-bridge"; +import { checkDbInvariant } from "./invariants.js"; /** * If the operation going to be plan subtracts @@ -224,10 +225,10 @@ async function getAvailableDenoms( ); for (const denom of ds) { const expiresWithdraw = AbsoluteTime.fromProtocolTimestamp( - denom.stampExpireWithdraw, + timestampProtocolFromDb(denom.stampExpireWithdraw), ); const expiresDeposit = AbsoluteTime.fromProtocolTimestamp( - denom.stampExpireDeposit, + timestampProtocolFromDb(denom.stampExpireDeposit), ); creditDeadline = AbsoluteTime.min(deadline, expiresWithdraw); debitDeadline = AbsoluteTime.min(deadline, expiresDeposit); @@ -270,10 +271,10 @@ async function getAvailableDenoms( continue; } const expiresWithdraw = AbsoluteTime.fromProtocolTimestamp( - denom.stampExpireWithdraw, + timestampProtocolFromDb(denom.stampExpireWithdraw), ); const expiresDeposit = AbsoluteTime.fromProtocolTimestamp( - denom.stampExpireDeposit, + timestampProtocolFromDb(denom.stampExpireDeposit), ); creditDeadline = AbsoluteTime.min(deadline, expiresWithdraw); debitDeadline = AbsoluteTime.min(deadline, expiresDeposit); @@ -318,7 +319,9 @@ function buildCoinInfoFromDenom( exchangeBaseUrl: denom.exchangeBaseUrl, duration: AbsoluteTime.difference( AbsoluteTime.now(), - AbsoluteTime.fromProtocolTimestamp(denom.stampExpireDeposit), + AbsoluteTime.fromProtocolTimestamp( + timestampProtocolFromDb(denom.stampExpireDeposit), + ), ), totalAvailable: total, value: Amounts.parseOrThrow(denom.value), diff --git a/packages/taler-wallet-core/src/versions.ts b/packages/taler-wallet-core/src/versions.ts index 8b9177bc3..022f4900d 100644 --- a/packages/taler-wallet-core/src/versions.ts +++ b/packages/taler-wallet-core/src/versions.ts @@ -29,7 +29,7 @@ export const WALLET_EXCHANGE_PROTOCOL_VERSION = "17:0:0"; export const WALLET_MERCHANT_PROTOCOL_VERSION = "2:0:1"; /** - * Protocol version spoken with the merchant. + * Protocol version spoken with the bank. * * Uses libtool's current:revision:age versioning. */ diff --git a/packages/taler-wallet-core/src/wallet-api-types.ts b/packages/taler-wallet-core/src/wallet-api-types.ts index 67c05a42f..375e0a1b2 100644 --- a/packages/taler-wallet-core/src/wallet-api-types.ts +++ b/packages/taler-wallet-core/src/wallet-api-types.ts @@ -104,11 +104,13 @@ import { TestPayArgs, TestPayResult, TestingSetTimetravelRequest, + TestingWaitTransactionRequest, Transaction, TransactionByIdRequest, TransactionsRequest, TransactionsResponse, TxIdResponse, + UpdateExchangeEntryRequest, UserAttentionByIdRequest, UserAttentionsCountResponse, UserAttentionsRequest, @@ -118,7 +120,6 @@ import { WalletContractData, WalletCoreVersion, WalletCurrencyInfo, - WithdrawFakebankRequest, WithdrawTestBalanceRequest, WithdrawUriInfoResponse, } from "@gnu-taler/taler-util"; @@ -199,7 +200,6 @@ export enum WalletApiOperation { GenerateDepositGroupTxId = "generateDepositGroupTxId", CreateDepositGroup = "createDepositGroup", SetWalletDeviceId = "setWalletDeviceId", - WithdrawFakebank = "withdrawFakebank", ImportDb = "importDb", ExportDb = "exportDb", PreparePeerPushCredit = "preparePeerPushCredit", @@ -216,12 +216,14 @@ export enum WalletApiOperation { ValidateIban = "validateIban", TestingWaitTransactionsFinal = "testingWaitTransactionsFinal", TestingWaitRefreshesFinal = "testingWaitRefreshesFinal", + TestingWaitTransactionState = "testingWaitTransactionState", TestingSetTimetravel = "testingSetTimetravel", GetScopedCurrencyInfo = "getScopedCurrencyInfo", ListStoredBackups = "listStoredBackups", CreateStoredBackup = "createStoredBackup", DeleteStoredBackup = "deleteStoredBackup", RecoverStoredBackup = "recoverStoredBackup", + UpdateExchangeEntry = "updateExchangeEntry", } // group: Initialization @@ -252,6 +254,11 @@ export type GetVersionOp = { */ export type WalletConfigParameter = RecursivePartial<WalletConfig>; +export interface BuiltinExchange { + exchangeBaseUrl: string; + currencyHint?: string; +} + export interface WalletConfig { /** * Initialization values useful for a complete startup. @@ -259,7 +266,7 @@ export interface WalletConfig { * These are values may be overridden by different wallets */ builtin: { - exchanges: string[]; + exchanges: BuiltinExchange[]; }; /** @@ -557,6 +564,15 @@ export type AddExchangeOp = { response: EmptyObject; }; +/** + * Update an exchange entry. + */ +export type UpdateExchangeEntryOp = { + op: WalletApiOperation.UpdateExchangeEntry; + request: UpdateExchangeEntryRequest; + response: EmptyObject; +}; + export type ListKnownBankAccountsOp = { op: WalletApiOperation.ListKnownBankAccounts; request: ListKnownBankAccountsRequest; @@ -935,17 +951,6 @@ export type TestPayOp = { }; /** - * Make a withdrawal from a fakebank, i.e. - * a bank where test users can be registered freely - * and testing APIs are available. - */ -export type WithdrawFakebankOp = { - op: WalletApiOperation.WithdrawFakebank; - request: WithdrawFakebankRequest; - response: EmptyObject; -}; - -/** * Get wallet-internal pending tasks. */ export type GetUserAttentionRequests = { @@ -1018,6 +1023,15 @@ export type TestingWaitRefreshesFinal = { }; /** + * Wait until a transaction is in a particular state. + */ +export type TestingWaitTransactionStateOp = { + op: WalletApiOperation.TestingWaitTransactionState; + request: TestingWaitTransactionRequest; + response: EmptyObject; +}; + +/** * Set a coin as (un-)suspended. * Suspended coins won't be used for payments. */ @@ -1040,7 +1054,6 @@ export type ForceRefreshOp = { export type WalletOperations = { [WalletApiOperation.InitWallet]: InitWalletOp; [WalletApiOperation.GetVersion]: GetVersionOp; - [WalletApiOperation.WithdrawFakebank]: WithdrawFakebankOp; [WalletApiOperation.PreparePayForUri]: PreparePayForUriOp; [WalletApiOperation.SharePayment]: SharePaymentOp; [WalletApiOperation.PreparePayForTemplate]: PreparePayForTemplateOp; @@ -1122,11 +1135,13 @@ export type WalletOperations = { [WalletApiOperation.TestingWaitTransactionsFinal]: TestingWaitTransactionsFinal; [WalletApiOperation.TestingWaitRefreshesFinal]: TestingWaitRefreshesFinal; [WalletApiOperation.TestingSetTimetravel]: TestingSetTimetravelOp; + [WalletApiOperation.TestingWaitTransactionState]: TestingWaitTransactionStateOp; [WalletApiOperation.GetScopedCurrencyInfo]: GetScopedCurrencyInfoOp; [WalletApiOperation.CreateStoredBackup]: CreateStoredBackupsOp; [WalletApiOperation.ListStoredBackups]: ListStoredBackupsOp; [WalletApiOperation.DeleteStoredBackup]: DeleteStoredBackupOp; [WalletApiOperation.RecoverStoredBackup]: RecoverStoredBackupsOp; + [WalletApiOperation.UpdateExchangeEntry]: UpdateExchangeEntryOp; }; export type WalletCoreRequestType< diff --git a/packages/taler-wallet-core/src/wallet.ts b/packages/taler-wallet-core/src/wallet.ts index 2d0878afc..ead0ee407 100644 --- a/packages/taler-wallet-core/src/wallet.ts +++ b/packages/taler-wallet-core/src/wallet.ts @@ -127,6 +127,8 @@ import { codecForRecoverStoredBackupRequest, codecForTestingSetTimetravelRequest, setDangerousTimetravel, + TestingWaitTransactionRequest, + codecForUpdateExchangeEntryRequest, } from "@gnu-taler/taler-util"; import type { HttpRequestLibrary } from "@gnu-taler/taler-util/http"; import { readSuccessResponseJsonOrThrow } from "@gnu-taler/taler-util/http"; @@ -198,7 +200,6 @@ import { downloadTosFromAcceptedFormat, getExchangeDetails, getExchangeRequestTimeout, - provideExchangeRecordInTx, updateExchangeFromUrl, updateExchangeFromUrlHandler, } from "./operations/exchanges.js"; @@ -250,6 +251,7 @@ import { runIntegrationTest, runIntegrationTest2, testPay, + waitTransactionState, waitUntilDone, waitUntilRefreshesDone, withdrawTestBalance, @@ -532,10 +534,12 @@ async function fillDefaults(ws: InternalWalletState): Promise<void> { logger.trace("defaults already applied"); return; } - for (const baseUrl of ws.config.builtin.exchanges) { - await addPresetExchangeEntry(tx, baseUrl); - const now = AbsoluteTime.now(); - provideExchangeRecordInTx(ws, tx, baseUrl, now); + for (const exch of ws.config.builtin.exchanges) { + await addPresetExchangeEntry( + tx, + exch.exchangeBaseUrl, + exch.currencyHint, + ); } await tx.config.put({ key: ConfigRecordKey.CurrencyDefaultsApplied, @@ -923,9 +927,9 @@ async function dumpCoins(ws: InternalWalletState): Promise<CoinDumpJson> { ageCommitmentProof: c.ageCommitmentProof, spend_allocation: c.spendAllocation ? { - amount: c.spendAllocation.amount, - id: c.spendAllocation.id, - } + amount: c.spendAllocation.amount, + id: c.spendAllocation.id, + } : undefined, }); } @@ -1069,8 +1073,7 @@ async function dispatchRequestInternal<Op extends WalletApiOperation>( case WalletApiOperation.WithdrawTestkudos: { await withdrawTestBalance(ws, { amount: "TESTKUDOS:10", - bankAccessApiBaseUrl: - "https://bank.test.taler.net/demobanks/default/access-api/", + corebankApiBaseUrl: "https://bank.test.taler.net/", exchangeBaseUrl: "https://exchange.test.taler.net/", }); return { @@ -1120,6 +1123,11 @@ async function dispatchRequestInternal<Op extends WalletApiOperation>( }); return {}; } + case WalletApiOperation.UpdateExchangeEntry: { + const req = codecForUpdateExchangeEntryRequest().decode(payload); + await updateExchangeFromUrl(ws, req.exchangeBaseUrl, {}); + return {}; + } case WalletApiOperation.ListExchanges: { return await getExchanges(ws); } @@ -1261,7 +1269,10 @@ async function dispatchRequestInternal<Op extends WalletApiOperation>( `templates/${url.templateId}`, url.merchantBaseUrl, ); - const httpReq = await ws.http.postJson(reqUrl.href, templateDetails); + const httpReq = await ws.http.fetch(reqUrl.href, { + method: "POST", + body: templateDetails, + }); const resp = await readSuccessResponseJsonOrThrow( httpReq, codecForMerchantPostOrderResponse(), @@ -1411,6 +1422,11 @@ async function dispatchRequestInternal<Op extends WalletApiOperation>( const resp = await getBackupRecovery(ws); return resp; } + case WalletApiOperation.TestingWaitTransactionState: { + const req = payload as TestingWaitTransactionRequest; + await waitTransactionState(ws, req.transactionId, req.txState); + return {}; + } case WalletApiOperation.GetScopedCurrencyInfo: { // Ignore result, just validate in this mock implementation codecForGetCurrencyInfoRequest().decode(payload); @@ -1489,40 +1505,6 @@ async function dispatchRequestInternal<Op extends WalletApiOperation>( trustedExchanges: [], }; } - case WalletApiOperation.WithdrawFakebank: { - const req = codecForWithdrawFakebankRequest().decode(payload); - const amount = Amounts.parseOrThrow(req.amount); - const details = await getExchangeWithdrawalInfo( - ws, - req.exchange, - amount, - undefined, - ); - const wres = await createManualWithdrawal(ws, { - amount: amount, - exchangeBaseUrl: req.exchange, - }); - const paytoUri = details.exchangePaytoUris[0]; - const pt = parsePaytoUri(paytoUri); - if (!pt) { - throw Error("failed to parse payto URI"); - } - const components = pt.targetPath.split("/"); - const creditorAcct = components[components.length - 1]; - logger.info(`making testbank transfer to '${creditorAcct}'`); - const fbReq = await ws.http.postJson( - new URL(`${creditorAcct}/admin/add-incoming`, req.bank).href, - { - amount: Amounts.stringify(amount), - reserve_pub: wres.reservePub, - debit_account: - "payto://x-taler-bank/localhost/testdebtor?receiver-name=Foo", - }, - ); - const fbResp = await readSuccessResponseJsonOrThrow(fbReq, codecForAny()); - logger.info(`started fakebank withdrawal: ${j2s(fbResp)}`); - return {}; - } case WalletApiOperation.TestCrypto: { return await ws.cryptoApi.hashString({ str: "hello world" }); } @@ -1691,7 +1673,12 @@ export class Wallet { public static defaultConfig: Readonly<WalletConfig> = { builtin: { - exchanges: ["https://exchange.demo.taler.net/"], + exchanges: [ + { + exchangeBaseUrl: "https://exchange.demo.taler.net/", + currencyHint: "KUDOS", + }, + ], }, features: { allowHttp: false, |