aboutsummaryrefslogtreecommitdiff
path: root/packages/taler-wallet-core/src/operations
diff options
context:
space:
mode:
Diffstat (limited to 'packages/taler-wallet-core/src/operations')
-rw-r--r--packages/taler-wallet-core/src/operations/backup/export.ts24
-rw-r--r--packages/taler-wallet-core/src/operations/backup/import.ts185
-rw-r--r--packages/taler-wallet-core/src/operations/balance.ts13
-rw-r--r--packages/taler-wallet-core/src/operations/peer-to-peer.ts149
-rw-r--r--packages/taler-wallet-core/src/operations/pending.ts40
-rw-r--r--packages/taler-wallet-core/src/operations/recoup.ts65
-rw-r--r--packages/taler-wallet-core/src/operations/reserves.ts843
-rw-r--r--packages/taler-wallet-core/src/operations/testing.ts15
-rw-r--r--packages/taler-wallet-core/src/operations/transactions.ts88
-rw-r--r--packages/taler-wallet-core/src/operations/withdraw.ts657
10 files changed, 813 insertions, 1266 deletions
diff --git a/packages/taler-wallet-core/src/operations/backup/export.ts b/packages/taler-wallet-core/src/operations/backup/export.ts
index d4c822972..c77ce1a85 100644
--- a/packages/taler-wallet-core/src/operations/backup/export.ts
+++ b/packages/taler-wallet-core/src/operations/backup/export.ts
@@ -88,7 +88,6 @@ export async function exportBackup(
backupProviders: x.backupProviders,
tips: x.tips,
recoupGroups: x.recoupGroups,
- reserves: x.reserves,
withdrawalGroups: x.withdrawalGroups,
}))
.runReadWrite(async (tx) => {
@@ -128,29 +127,6 @@ export async function exportBackup(
});
});
- await tx.reserves.iter().forEach((reserve) => {
- const backupReserve: BackupReserve = {
- initial_selected_denoms: reserve.initialDenomSel.selectedDenoms.map(
- (x) => ({
- count: x.count,
- denom_pub_hash: x.denomPubHash,
- }),
- ),
- initial_withdrawal_group_id: reserve.initialWithdrawalGroupId,
- instructed_amount: Amounts.stringify(reserve.instructedAmount),
- reserve_priv: reserve.reservePriv,
- timestamp_created: reserve.timestampCreated,
- withdrawal_groups:
- withdrawalGroupsByReserve[reserve.reservePub] ?? [],
- // FIXME!
- timestamp_last_activity: reserve.timestampCreated,
- };
- const backupReserves = (backupReservesByExchange[
- reserve.exchangeBaseUrl
- ] ??= []);
- backupReserves.push(backupReserve);
- });
-
await tx.tips.iter().forEach((tip) => {
backupTips.push({
exchange_base_url: tip.exchangeBaseUrl,
diff --git a/packages/taler-wallet-core/src/operations/backup/import.ts b/packages/taler-wallet-core/src/operations/backup/import.ts
index e099fae57..f26c42770 100644
--- a/packages/taler-wallet-core/src/operations/backup/import.ts
+++ b/packages/taler-wallet-core/src/operations/backup/import.ts
@@ -236,7 +236,6 @@ export async function importBackup(
backupProviders: x.backupProviders,
tips: x.tips,
recoupGroups: x.recoupGroups,
- reserves: x.reserves,
withdrawalGroups: x.withdrawalGroups,
tombstones: x.tombstones,
depositGroups: x.depositGroups,
@@ -427,94 +426,98 @@ export async function importBackup(
}
}
- for (const backupReserve of backupExchangeDetails.reserves) {
- const reservePub =
- cryptoComp.reservePrivToPub[backupReserve.reserve_priv];
- const ts = makeEventId(TombstoneTag.DeleteReserve, reservePub);
- if (tombstoneSet.has(ts)) {
- continue;
- }
- checkLogicInvariant(!!reservePub);
- const existingReserve = await tx.reserves.get(reservePub);
- const instructedAmount = Amounts.parseOrThrow(
- backupReserve.instructed_amount,
- );
- if (!existingReserve) {
- let bankInfo: ReserveBankInfo | undefined;
- if (backupReserve.bank_info) {
- bankInfo = {
- exchangePaytoUri: backupReserve.bank_info.exchange_payto_uri,
- statusUrl: backupReserve.bank_info.status_url,
- confirmUrl: backupReserve.bank_info.confirm_url,
- };
- }
- await tx.reserves.put({
- currency: instructedAmount.currency,
- instructedAmount,
- exchangeBaseUrl: backupExchangeDetails.base_url,
- reservePub,
- reservePriv: backupReserve.reserve_priv,
- bankInfo,
- timestampCreated: backupReserve.timestamp_created,
- timestampBankConfirmed:
- backupReserve.bank_info?.timestamp_bank_confirmed,
- timestampReserveInfoPosted:
- backupReserve.bank_info?.timestamp_reserve_info_posted,
- senderWire: backupReserve.sender_wire,
- retryInfo: RetryInfo.reset(),
- lastError: undefined,
- initialWithdrawalGroupId:
- backupReserve.initial_withdrawal_group_id,
- initialWithdrawalStarted:
- backupReserve.withdrawal_groups.length > 0,
- // FIXME!
- reserveStatus: ReserveRecordStatus.QueryingStatus,
- initialDenomSel: await getDenomSelStateFromBackup(
- tx,
- backupExchangeDetails.base_url,
- backupReserve.initial_selected_denoms,
- ),
- // FIXME!
- operationStatus: OperationStatus.Pending,
- });
- }
- for (const backupWg of backupReserve.withdrawal_groups) {
- const ts = makeEventId(
- TombstoneTag.DeleteWithdrawalGroup,
- backupWg.withdrawal_group_id,
- );
- if (tombstoneSet.has(ts)) {
- continue;
- }
- const existingWg = await tx.withdrawalGroups.get(
- backupWg.withdrawal_group_id,
- );
- if (!existingWg) {
- await tx.withdrawalGroups.put({
- denomsSel: await getDenomSelStateFromBackup(
- tx,
- backupExchangeDetails.base_url,
- backupWg.selected_denoms,
- ),
- exchangeBaseUrl: backupExchangeDetails.base_url,
- lastError: undefined,
- rawWithdrawalAmount: Amounts.parseOrThrow(
- backupWg.raw_withdrawal_amount,
- ),
- reservePub,
- retryInfo: RetryInfo.reset(),
- secretSeed: backupWg.secret_seed,
- timestampStart: backupWg.timestamp_created,
- timestampFinish: backupWg.timestamp_finish,
- withdrawalGroupId: backupWg.withdrawal_group_id,
- denomSelUid: backupWg.selected_denoms_id,
- operationStatus: backupWg.timestamp_finish
- ? OperationStatus.Finished
- : OperationStatus.Pending,
- });
- }
- }
- }
+
+ // FIXME: import reserves with new schema
+
+ // for (const backupReserve of backupExchangeDetails.reserves) {
+ // const reservePub =
+ // cryptoComp.reservePrivToPub[backupReserve.reserve_priv];
+ // const ts = makeEventId(TombstoneTag.DeleteReserve, reservePub);
+ // if (tombstoneSet.has(ts)) {
+ // continue;
+ // }
+ // checkLogicInvariant(!!reservePub);
+ // const existingReserve = await tx.reserves.get(reservePub);
+ // const instructedAmount = Amounts.parseOrThrow(
+ // backupReserve.instructed_amount,
+ // );
+ // if (!existingReserve) {
+ // let bankInfo: ReserveBankInfo | undefined;
+ // if (backupReserve.bank_info) {
+ // bankInfo = {
+ // exchangePaytoUri: backupReserve.bank_info.exchange_payto_uri,
+ // statusUrl: backupReserve.bank_info.status_url,
+ // confirmUrl: backupReserve.bank_info.confirm_url,
+ // };
+ // }
+ // await tx.reserves.put({
+ // currency: instructedAmount.currency,
+ // instructedAmount,
+ // exchangeBaseUrl: backupExchangeDetails.base_url,
+ // reservePub,
+ // reservePriv: backupReserve.reserve_priv,
+ // bankInfo,
+ // timestampCreated: backupReserve.timestamp_created,
+ // timestampBankConfirmed:
+ // backupReserve.bank_info?.timestamp_bank_confirmed,
+ // timestampReserveInfoPosted:
+ // backupReserve.bank_info?.timestamp_reserve_info_posted,
+ // senderWire: backupReserve.sender_wire,
+ // retryInfo: RetryInfo.reset(),
+ // lastError: undefined,
+ // initialWithdrawalGroupId:
+ // backupReserve.initial_withdrawal_group_id,
+ // initialWithdrawalStarted:
+ // backupReserve.withdrawal_groups.length > 0,
+ // // FIXME!
+ // reserveStatus: ReserveRecordStatus.QueryingStatus,
+ // initialDenomSel: await getDenomSelStateFromBackup(
+ // tx,
+ // backupExchangeDetails.base_url,
+ // backupReserve.initial_selected_denoms,
+ // ),
+ // // FIXME!
+ // operationStatus: OperationStatus.Pending,
+ // });
+ // }
+ // for (const backupWg of backupReserve.withdrawal_groups) {
+ // const ts = makeEventId(
+ // TombstoneTag.DeleteWithdrawalGroup,
+ // backupWg.withdrawal_group_id,
+ // );
+ // if (tombstoneSet.has(ts)) {
+ // continue;
+ // }
+ // const existingWg = await tx.withdrawalGroups.get(
+ // backupWg.withdrawal_group_id,
+ // );
+ // if (!existingWg) {
+ // await tx.withdrawalGroups.put({
+ // denomsSel: await getDenomSelStateFromBackup(
+ // tx,
+ // backupExchangeDetails.base_url,
+ // backupWg.selected_denoms,
+ // ),
+ // exchangeBaseUrl: backupExchangeDetails.base_url,
+ // lastError: undefined,
+ // rawWithdrawalAmount: Amounts.parseOrThrow(
+ // backupWg.raw_withdrawal_amount,
+ // ),
+ // reservePub,
+ // retryInfo: RetryInfo.reset(),
+ // secretSeed: backupWg.secret_seed,
+ // timestampStart: backupWg.timestamp_created,
+ // timestampFinish: backupWg.timestamp_finish,
+ // withdrawalGroupId: backupWg.withdrawal_group_id,
+ // denomSelUid: backupWg.selected_denoms_id,
+ // operationStatus: backupWg.timestamp_finish
+ // ? OperationStatus.Finished
+ // : OperationStatus.Pending,
+ // });
+ // }
+ // }
+ // }
+
}
for (const backupProposal of backupBlob.proposals) {
@@ -920,10 +923,6 @@ export async function importBackup(
} else if (type === TombstoneTag.DeleteRefund) {
// Nothing required, will just prevent display
// in the transactions list
- } else if (type === TombstoneTag.DeleteReserve) {
- // FIXME: Once we also have account (=kyc) reserves,
- // we need to check if the reserve is an account before deleting here
- await tx.reserves.delete(rest[0]);
} else if (type === TombstoneTag.DeleteTip) {
await tx.tips.delete(rest[0]);
} else if (type === TombstoneTag.DeleteWithdrawalGroup) {
diff --git a/packages/taler-wallet-core/src/operations/balance.ts b/packages/taler-wallet-core/src/operations/balance.ts
index c26eb0cfc..4590f5051 100644
--- a/packages/taler-wallet-core/src/operations/balance.ts
+++ b/packages/taler-wallet-core/src/operations/balance.ts
@@ -41,7 +41,6 @@ interface WalletBalance {
export async function getBalancesInsideTransaction(
ws: InternalWalletState,
tx: GetReadOnlyAccess<{
- reserves: typeof WalletStoresV1.reserves;
coins: typeof WalletStoresV1.coins;
refreshGroups: typeof WalletStoresV1.refreshGroups;
withdrawalGroups: typeof WalletStoresV1.withdrawalGroups;
@@ -65,17 +64,6 @@ export async function getBalancesInsideTransaction(
return balanceStore[currency];
};
- // Initialize balance to zero, even if we didn't start withdrawing yet.
- await tx.reserves.iter().forEach((r) => {
- const b = initBalance(r.currency);
- if (!r.initialWithdrawalStarted) {
- b.pendingIncoming = Amounts.add(
- b.pendingIncoming,
- r.initialDenomSel.totalCoinValue,
- ).amount;
- }
- });
-
await tx.coins.iter().forEach((c) => {
// Only count fresh coins, as dormant coins will
// already be in a refresh session.
@@ -154,7 +142,6 @@ export async function getBalances(
.mktx((x) => ({
coins: x.coins,
refreshGroups: x.refreshGroups,
- reserves: x.reserves,
purchases: x.purchases,
withdrawalGroups: x.withdrawalGroups,
}))
diff --git a/packages/taler-wallet-core/src/operations/peer-to-peer.ts b/packages/taler-wallet-core/src/operations/peer-to-peer.ts
index 658cbe4f7..4d2f2bb5f 100644
--- a/packages/taler-wallet-core/src/operations/peer-to-peer.ts
+++ b/packages/taler-wallet-core/src/operations/peer-to-peer.ts
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2019 GNUnet e.V.
+ (C) 2022 GNUnet e.V.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
@@ -30,35 +30,35 @@ import {
codecForAmountString,
codecForAny,
codecForExchangeGetContractResponse,
+ constructPayPushUri,
ContractTermsUtil,
decodeCrock,
Duration,
eddsaGetPublic,
encodeCrock,
ExchangePurseMergeRequest,
+ getRandomBytes,
InitiatePeerPushPaymentRequest,
InitiatePeerPushPaymentResponse,
j2s,
Logger,
+ parsePayPushUri,
strcmp,
TalerProtocolTimestamp,
UnblindedSignature,
WalletAccountMergeFlags,
} from "@gnu-taler/taler-util";
-import { url } from "inspector";
import {
CoinStatus,
+ MergeReserveInfo,
OperationStatus,
- ReserveRecord,
ReserveRecordStatus,
+ WithdrawalGroupRecord,
} from "../db.js";
-import {
- checkSuccessResponseOrThrow,
- readSuccessResponseJsonOrThrow,
- throwUnexpectedRequestError,
-} from "../util/http.js";
+import { readSuccessResponseJsonOrThrow } from "../util/http.js";
import { InternalWalletState } from "../internal-wallet-state.js";
import { checkDbInvariant } from "../util/invariants.js";
+import { internalCreateWithdrawalGroup } from "./withdraw.js";
const logger = new Logger("operations/peer-to-peer.ts");
@@ -265,6 +265,10 @@ export async function initiatePeerToPeerPush(
mergePriv: mergePair.priv,
pursePub: pursePair.pub,
exchangeBaseUrl: coinSelRes.exchangeBaseUrl,
+ talerUri: constructPayPushUri({
+ exchangeBaseUrl: coinSelRes.exchangeBaseUrl,
+ contractPriv: econtractResp.contractPriv,
+ }),
};
}
@@ -281,26 +285,19 @@ export async function checkPeerPushPayment(
ws: InternalWalletState,
req: CheckPeerPushPaymentRequest,
): Promise<CheckPeerPushPaymentResponse> {
- const getPurseUrl = new URL(
- `purses/${req.pursePub}/deposit`,
- req.exchangeBaseUrl,
- );
+ // FIXME: Check if existing record exists!
- const contractPub = encodeCrock(
- eddsaGetPublic(decodeCrock(req.contractPriv)),
- );
+ const uri = parsePayPushUri(req.talerUri);
- const purseHttpResp = await ws.http.get(getPurseUrl.href);
+ if (!uri) {
+ throw Error("got invalid taler://pay-push URI");
+ }
- const purseStatus = await readSuccessResponseJsonOrThrow(
- purseHttpResp,
- codecForExchangePurseStatus(),
- );
+ const exchangeBaseUrl = uri.exchangeBaseUrl;
+ const contractPriv = uri.contractPriv;
+ const contractPub = encodeCrock(eddsaGetPublic(decodeCrock(contractPriv)));
- const getContractUrl = new URL(
- `contracts/${contractPub}`,
- req.exchangeBaseUrl,
- );
+ const getContractUrl = new URL(`contracts/${contractPub}`, exchangeBaseUrl);
const contractHttpResp = await ws.http.get(getContractUrl.href);
@@ -309,22 +306,36 @@ export async function checkPeerPushPayment(
codecForExchangeGetContractResponse(),
);
+ const pursePub = contractResp.purse_pub;
+
const dec = await ws.cryptoApi.decryptContractForMerge({
ciphertext: contractResp.econtract,
- contractPriv: req.contractPriv,
- pursePub: req.pursePub,
+ contractPriv: contractPriv,
+ pursePub: pursePub,
});
+ const getPurseUrl = new URL(`purses/${pursePub}/deposit`, exchangeBaseUrl);
+
+ const purseHttpResp = await ws.http.get(getPurseUrl.href);
+
+ const purseStatus = await readSuccessResponseJsonOrThrow(
+ purseHttpResp,
+ codecForExchangePurseStatus(),
+ );
+
+ const peerPushPaymentIncomingId = encodeCrock(getRandomBytes(32));
+
await ws.db
.mktx((x) => ({
peerPushPaymentIncoming: x.peerPushPaymentIncoming,
}))
.runReadWrite(async (tx) => {
await tx.peerPushPaymentIncoming.add({
- contractPriv: req.contractPriv,
- exchangeBaseUrl: req.exchangeBaseUrl,
+ peerPushPaymentIncomingId,
+ contractPriv: contractPriv,
+ exchangeBaseUrl: exchangeBaseUrl,
mergePriv: dec.mergePriv,
- pursePub: req.pursePub,
+ pursePub: pursePub,
timestampAccepted: TalerProtocolTimestamp.now(),
contractTerms: dec.contractTerms,
});
@@ -333,6 +344,7 @@ export async function checkPeerPushPayment(
return {
amount: purseStatus.balance,
contractTerms: dec.contractTerms,
+ peerPushPaymentIncomingId,
};
}
@@ -343,9 +355,9 @@ export function talerPaytoFromExchangeReserve(
const url = new URL(exchangeBaseUrl);
let proto: string;
if (url.protocol === "http:") {
- proto = "taler+http";
+ proto = "taler-reserve-http";
} else if (url.protocol === "https:") {
- proto = "taler";
+ proto = "taler-reserve";
} else {
throw Error(`unsupported exchange base URL protocol (${url.protocol})`);
}
@@ -365,69 +377,45 @@ export async function acceptPeerPushPayment(
const peerInc = await ws.db
.mktx((x) => ({ peerPushPaymentIncoming: x.peerPushPaymentIncoming }))
.runReadOnly(async (tx) => {
- return tx.peerPushPaymentIncoming.get([
- req.exchangeBaseUrl,
- req.pursePub,
- ]);
+ return tx.peerPushPaymentIncoming.get(req.peerPushPaymentIncomingId);
});
if (!peerInc) {
- throw Error("can't accept unknown incoming p2p push payment");
+ throw Error(
+ `can't accept unknown incoming p2p push payment (${req.peerPushPaymentIncomingId})`,
+ );
}
const amount = Amounts.parseOrThrow(peerInc.contractTerms.amount);
- // We have to create the key pair outside of the transaction,
+ // We have to eagerly create the key pair outside of the transaction,
// due to the async crypto API.
const newReservePair = await ws.cryptoApi.createEddsaKeypair({});
- const reserve: ReserveRecord | undefined = await ws.db
+ const mergeReserveInfo: MergeReserveInfo = await ws.db
.mktx((x) => ({
exchanges: x.exchanges,
- reserves: x.reserves,
+ withdrawalGroups: x.withdrawalGroups,
}))
.runReadWrite(async (tx) => {
- const ex = await tx.exchanges.get(req.exchangeBaseUrl);
+ const ex = await tx.exchanges.get(peerInc.exchangeBaseUrl);
checkDbInvariant(!!ex);
- if (ex.currentMergeReservePub) {
- return await tx.reserves.get(ex.currentMergeReservePub);
+ if (ex.currentMergeReserveInfo) {
+ return ex.currentMergeReserveInfo;
}
- const rec: ReserveRecord = {
- exchangeBaseUrl: req.exchangeBaseUrl,
- // FIXME: field will be removed in the future, folded into withdrawal/p2p record.
- reserveStatus: ReserveRecordStatus.Dormant,
- timestampCreated: TalerProtocolTimestamp.now(),
- instructedAmount: Amounts.getZero(amount.currency),
- currency: amount.currency,
- reservePub: newReservePair.pub,
+ await tx.exchanges.put(ex);
+ ex.currentMergeReserveInfo = {
reservePriv: newReservePair.priv,
- timestampBankConfirmed: undefined,
- timestampReserveInfoPosted: undefined,
- // FIXME!
- initialDenomSel: undefined as any,
- // FIXME!
- initialWithdrawalGroupId: "",
- initialWithdrawalStarted: false,
- lastError: undefined,
- operationStatus: OperationStatus.Pending,
- retryInfo: undefined,
- bankInfo: undefined,
- restrictAge: undefined,
- senderWire: undefined,
+ reservePub: newReservePair.pub,
};
- await tx.reserves.put(rec);
- return rec;
+ return ex.currentMergeReserveInfo;
});
- if (!reserve) {
- throw Error("can't create reserve");
- }
-
const mergeTimestamp = TalerProtocolTimestamp.now();
const reservePayto = talerPaytoFromExchangeReserve(
- reserve.exchangeBaseUrl,
- reserve.reservePub,
+ peerInc.exchangeBaseUrl,
+ mergeReserveInfo.reservePub,
);
const sigRes = await ws.cryptoApi.signPurseMerge({
@@ -442,12 +430,12 @@ export async function acceptPeerPushPayment(
purseFee: Amounts.stringify(Amounts.getZero(amount.currency)),
pursePub: peerInc.pursePub,
reservePayto,
- reservePriv: reserve.reservePriv,
+ reservePriv: mergeReserveInfo.reservePriv,
});
const mergePurseUrl = new URL(
- `purses/${req.pursePub}/merge`,
- req.exchangeBaseUrl,
+ `purses/${peerInc.pursePub}/merge`,
+ peerInc.exchangeBaseUrl,
);
const mergeReq: ExchangePurseMergeRequest = {
@@ -459,6 +447,17 @@ export async function acceptPeerPushPayment(
const mergeHttpReq = await ws.http.postJson(mergePurseUrl.href, mergeReq);
+ logger.info(`merge request: ${j2s(mergeReq)}`);
const res = await readSuccessResponseJsonOrThrow(mergeHttpReq, codecForAny());
- logger.info(`merge result: ${j2s(res)}`);
+ logger.info(`merge response: ${j2s(res)}`);
+
+ await internalCreateWithdrawalGroup(ws, {
+ amount,
+ exchangeBaseUrl: peerInc.exchangeBaseUrl,
+ reserveStatus: ReserveRecordStatus.QueryingStatus,
+ reserveKeyPair: {
+ priv: mergeReserveInfo.reservePriv,
+ pub: mergeReserveInfo.reservePub,
+ },
+ });
}
diff --git a/packages/taler-wallet-core/src/operations/pending.ts b/packages/taler-wallet-core/src/operations/pending.ts
index 0a262d3bb..ae93711f9 100644
--- a/packages/taler-wallet-core/src/operations/pending.ts
+++ b/packages/taler-wallet-core/src/operations/pending.ts
@@ -70,44 +70,6 @@ async function gatherExchangePending(
});
}
-async function gatherReservePending(
- tx: GetReadOnlyAccess<{ reserves: typeof WalletStoresV1.reserves }>,
- now: AbsoluteTime,
- resp: PendingOperationsResponse,
-): Promise<void> {
- const reserves = await tx.reserves.indexes.byStatus.getAll(
- OperationStatus.Pending,
- );
- for (const reserve of reserves) {
- const reserveType = reserve.bankInfo
- ? ReserveType.TalerBankWithdraw
- : ReserveType.Manual;
- switch (reserve.reserveStatus) {
- case ReserveRecordStatus.Dormant:
- // nothing to report as pending
- break;
- case ReserveRecordStatus.WaitConfirmBank:
- case ReserveRecordStatus.QueryingStatus:
- case ReserveRecordStatus.RegisteringBank: {
- resp.pendingOperations.push({
- type: PendingTaskType.Reserve,
- givesLifeness: true,
- timestampDue: reserve.retryInfo?.nextRetry ?? AbsoluteTime.now(),
- stage: reserve.reserveStatus,
- timestampCreated: reserve.timestampCreated,
- reserveType,
- reservePub: reserve.reservePub,
- retryInfo: reserve.retryInfo,
- });
- break;
- }
- default:
- // FIXME: report problem!
- break;
- }
- }
-}
-
async function gatherRefreshPending(
tx: GetReadOnlyAccess<{ refreshGroups: typeof WalletStoresV1.refreshGroups }>,
now: AbsoluteTime,
@@ -336,7 +298,6 @@ export async function getPendingOperations(
backupProviders: x.backupProviders,
exchanges: x.exchanges,
exchangeDetails: x.exchangeDetails,
- reserves: x.reserves,
refreshGroups: x.refreshGroups,
coins: x.coins,
withdrawalGroups: x.withdrawalGroups,
@@ -352,7 +313,6 @@ export async function getPendingOperations(
pendingOperations: [],
};
await gatherExchangePending(tx, now, resp);
- await gatherReservePending(tx, now, resp);
await gatherRefreshPending(tx, now, resp);
await gatherWithdrawalPending(tx, now, resp);
await gatherProposalPending(tx, now, resp);
diff --git a/packages/taler-wallet-core/src/operations/recoup.ts b/packages/taler-wallet-core/src/operations/recoup.ts
index d36a10287..7c0f79daf 100644
--- a/packages/taler-wallet-core/src/operations/recoup.ts
+++ b/packages/taler-wallet-core/src/operations/recoup.ts
@@ -26,28 +26,35 @@
*/
import {
Amounts,
- codecForRecoupConfirmation, encodeCrock, getRandomBytes, j2s, Logger, NotificationType,
+ codecForRecoupConfirmation,
+ encodeCrock,
+ getRandomBytes,
+ j2s,
+ Logger,
+ NotificationType,
RefreshReason,
TalerErrorDetail,
- TalerProtocolTimestamp, URL
+ TalerProtocolTimestamp,
+ URL,
} from "@gnu-taler/taler-util";
import {
CoinRecord,
CoinSourceType,
- CoinStatus, OperationStatus, RecoupGroupRecord,
+ CoinStatus,
+ OperationStatus,
+ RecoupGroupRecord,
RefreshCoinSource,
- ReserveRecordStatus, WalletStoresV1, WithdrawCoinSource
+ ReserveRecordStatus,
+ WalletStoresV1,
+ WithdrawCoinSource,
} from "../db.js";
import { InternalWalletState } from "../internal-wallet-state.js";
import { readSuccessResponseJsonOrThrow } from "../util/http.js";
import { GetReadWriteAccess } from "../util/query.js";
-import {
- RetryInfo
-} from "../util/retries.js";
+import { RetryInfo } from "../util/retries.js";
import { guardOperationException } from "./common.js";
import { createRefreshGroup, processRefreshGroup } from "./refresh.js";
-import { getReserveRequestTimeout, processReserve } from "./reserves.js";
-
+import { internalCreateWithdrawalGroup } from "./withdraw.js";
const logger = new Logger("operations/recoup.ts");
@@ -182,34 +189,24 @@ async function recoupWithdrawCoin(
cs: WithdrawCoinSource,
): Promise<void> {
const reservePub = cs.reservePub;
- const d = await ws.db
+ const denomInfo = await ws.db
.mktx((x) => ({
- reserves: x.reserves,
denominations: x.denominations,
}))
.runReadOnly(async (tx) => {
- const reserve = await tx.reserves.get(reservePub);
- if (!reserve) {
- return;
- }
const denomInfo = await ws.getDenomInfo(
ws,
tx,
- reserve.exchangeBaseUrl,
+ coin.exchangeBaseUrl,
coin.denomPubHash,
);
- if (!denomInfo) {
- return;
- }
- return { reserve, denomInfo };
+ return denomInfo;
});
- if (!d) {
+ if (!denomInfo) {
// FIXME: We should at least emit some pending operation / warning for this?
return;
}
- const { reserve, denomInfo } = d;
-
ws.notify({
type: NotificationType.RecoupStarted,
});
@@ -224,9 +221,7 @@ async function recoupWithdrawCoin(
});
const reqUrl = new URL(`/coins/${coin.coinPub}/recoup`, coin.exchangeBaseUrl);
logger.trace(`requesting recoup via ${reqUrl.href}`);
- const resp = await ws.http.postJson(reqUrl.href, recoupRequest, {
- timeout: getReserveRequestTimeout(reserve),
- });
+ const resp = await ws.http.postJson(reqUrl.href, recoupRequest);
const recoupConfirmation = await readSuccessResponseJsonOrThrow(
resp,
codecForRecoupConfirmation(),
@@ -244,7 +239,6 @@ async function recoupWithdrawCoin(
.mktx((x) => ({
coins: x.coins,
denominations: x.denominations,
- reserves: x.reserves,
recoupGroups: x.recoupGroups,
refreshGroups: x.refreshGroups,
}))
@@ -260,18 +254,12 @@ async function recoupWithdrawCoin(
if (!updatedCoin) {
return;
}
- const updatedReserve = await tx.reserves.get(reserve.reservePub);
- if (!updatedReserve) {
- return;
- }
updatedCoin.status = CoinStatus.Dormant;
const currency = updatedCoin.currentAmount.currency;
updatedCoin.currentAmount = Amounts.getZero(currency);
- updatedReserve.reserveStatus = ReserveRecordStatus.QueryingStatus;
- updatedReserve.retryInfo = RetryInfo.reset();
- updatedReserve.operationStatus = OperationStatus.Pending;
await tx.coins.put(updatedCoin);
- await tx.reserves.put(updatedReserve);
+ // FIXME: Actually withdraw here!
+ // await internalCreateWithdrawalGroup(ws, {...});
await putGroupAsFinished(ws, tx, recoupGroup, coinIdx);
});
@@ -341,7 +329,6 @@ async function recoupRefreshCoin(
.mktx((x) => ({
coins: x.coins,
denominations: x.denominations,
- reserves: x.reserves,
recoupGroups: x.recoupGroups,
refreshGroups: x.refreshGroups,
}))
@@ -446,12 +433,6 @@ async function processRecoupGroupImpl(
reserveSet.add(coin.coinSource.reservePub);
}
}
-
- for (const r of reserveSet.values()) {
- processReserve(ws, r, { forceNow: true }).catch((e) => {
- logger.error(`processing reserve ${r} after recoup failed`);
- });
- }
}
export async function createRecoupGroup(
diff --git a/packages/taler-wallet-core/src/operations/reserves.ts b/packages/taler-wallet-core/src/operations/reserves.ts
deleted file mode 100644
index b33f574f4..000000000
--- a/packages/taler-wallet-core/src/operations/reserves.ts
+++ /dev/null
@@ -1,843 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2019 GNUnet e.V.
-
- GNU Taler is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
-import {
- AbsoluteTime,
- AcceptWithdrawalResponse,
- addPaytoQueryParams,
- Amounts,
- canonicalizeBaseUrl,
- codecForBankWithdrawalOperationPostResponse,
- codecForReserveStatus,
- codecForWithdrawOperationStatusResponse,
- CreateReserveRequest,
- CreateReserveResponse,
- Duration,
- durationMax,
- durationMin,
- encodeCrock,
- ForcedDenomSel,
- getRandomBytes,
- j2s,
- Logger,
- NotificationType,
- randomBytes,
- TalerErrorCode,
- TalerErrorDetail,
- URL,
-} from "@gnu-taler/taler-util";
-import {
- DenomSelectionState,
- OperationStatus,
- ReserveBankInfo,
- ReserveRecord,
- ReserveRecordStatus,
- WalletStoresV1,
- WithdrawalGroupRecord,
-} from "../db.js";
-import { TalerError } from "../errors.js";
-import { InternalWalletState } from "../internal-wallet-state.js";
-import { assertUnreachable } from "../util/assertUnreachable.js";
-import {
- readSuccessResponseJsonOrErrorCode,
- readSuccessResponseJsonOrThrow,
- throwUnexpectedRequestError,
-} from "../util/http.js";
-import { GetReadOnlyAccess } from "../util/query.js";
-import { RetryInfo } from "../util/retries.js";
-import { guardOperationException } from "./common.js";
-import {
- getExchangeDetails,
- getExchangePaytoUri,
- getExchangeTrust,
- updateExchangeFromUrl,
-} from "./exchanges.js";
-import {
- getBankWithdrawalInfo,
- getCandidateWithdrawalDenoms,
- processWithdrawGroup,
- selectForcedWithdrawalDenominations,
- selectWithdrawalDenominations,
- updateWithdrawalDenoms,
-} from "./withdraw.js";
-
-const logger = new Logger("taler-wallet-core:reserves.ts");
-
-/**
- * Set up the reserve's retry timeout in preparation for
- * processing the reserve.
- */
-async function setupReserveRetry(
- ws: InternalWalletState,
- reservePub: string,
- options: {
- reset: boolean;
- },
-): Promise<void> {
- await ws.db
- .mktx((x) => ({
- reserves: x.reserves,
- }))
- .runReadWrite(async (tx) => {
- const r = await tx.reserves.get(reservePub);
- if (!r) {
- return;
- }
- if (options.reset) {
- r.retryInfo = RetryInfo.reset();
- } else {
- r.retryInfo = RetryInfo.increment(r.retryInfo);
- }
- delete r.lastError;
- await tx.reserves.put(r);
- });
-}
-
-/**
- * Report an error that happened while processing the reserve.
- *
- * Logs the error via a notification and by storing it in the database.
- */
-async function reportReserveError(
- ws: InternalWalletState,
- reservePub: string,
- err: TalerErrorDetail,
-): Promise<void> {
- await ws.db
- .mktx((x) => ({
- reserves: x.reserves,
- }))
- .runReadWrite(async (tx) => {
- const r = await tx.reserves.get(reservePub);
- if (!r) {
- return;
- }
- if (!r.retryInfo) {
- logger.error(`got reserve error for inactive reserve (no retryInfo)`);
- return;
- }
- r.lastError = err;
- await tx.reserves.put(r);
- });
- ws.notify({
- type: NotificationType.ReserveOperationError,
- error: err,
- });
-}
-
-/**
- * Create a reserve, but do not flag it as confirmed yet.
- *
- * Adds the corresponding exchange as a trusted exchange if it is neither
- * audited nor trusted already.
- */
-export async function createReserve(
- ws: InternalWalletState,
- req: CreateReserveRequest,
-): Promise<CreateReserveResponse> {
- const keypair = await ws.cryptoApi.createEddsaKeypair({});
- const now = AbsoluteTime.toTimestamp(AbsoluteTime.now());
- const canonExchange = canonicalizeBaseUrl(req.exchange);
-
- let reserveStatus;
- if (req.bankWithdrawStatusUrl) {
- reserveStatus = ReserveRecordStatus.RegisteringBank;
- } else {
- reserveStatus = ReserveRecordStatus.QueryingStatus;
- }
-
- let bankInfo: ReserveBankInfo | undefined;
-
- if (req.bankWithdrawStatusUrl) {
- if (!req.exchangePaytoUri) {
- throw Error(
- "Exchange payto URI must be specified for a bank-integrated withdrawal",
- );
- }
- bankInfo = {
- statusUrl: req.bankWithdrawStatusUrl,
- exchangePaytoUri: req.exchangePaytoUri,
- };
- }
-
- const initialWithdrawalGroupId = encodeCrock(getRandomBytes(32));
-
- await updateWithdrawalDenoms(ws, canonExchange);
- const denoms = await getCandidateWithdrawalDenoms(ws, canonExchange);
-
- let initialDenomSel: DenomSelectionState;
- if (req.forcedDenomSel) {
- logger.warn("using forced denom selection");
- initialDenomSel = selectForcedWithdrawalDenominations(
- req.amount,
- denoms,
- req.forcedDenomSel,
- );
- } else {
- initialDenomSel = selectWithdrawalDenominations(req.amount, denoms);
- }
-
- const reserveRecord: ReserveRecord = {
- instructedAmount: req.amount,
- initialWithdrawalGroupId,
- initialDenomSel,
- initialWithdrawalStarted: false,
- timestampCreated: now,
- exchangeBaseUrl: canonExchange,
- reservePriv: keypair.priv,
- reservePub: keypair.pub,
- senderWire: req.senderWire,
- timestampBankConfirmed: undefined,
- timestampReserveInfoPosted: undefined,
- bankInfo,
- reserveStatus,
- retryInfo: RetryInfo.reset(),
- lastError: undefined,
- currency: req.amount.currency,
- operationStatus: OperationStatus.Pending,
- restrictAge: req.restrictAge,
- };
-
- const exchangeInfo = await updateExchangeFromUrl(ws, req.exchange);
- const exchangeDetails = exchangeInfo.exchangeDetails;
- if (!exchangeDetails) {
- logger.trace(exchangeDetails);
- throw Error("exchange not updated");
- }
- const { isAudited, isTrusted } = await getExchangeTrust(
- ws,
- exchangeInfo.exchange,
- );
-
- const resp = await ws.db
- .mktx((x) => ({
- exchangeTrust: x.exchangeTrust,
- reserves: x.reserves,
- bankWithdrawUris: x.bankWithdrawUris,
- }))
- .runReadWrite(async (tx) => {
- // Check if we have already created a reserve for that bankWithdrawStatusUrl
- if (reserveRecord.bankInfo?.statusUrl) {
- const bwi = await tx.bankWithdrawUris.get(
- reserveRecord.bankInfo.statusUrl,
- );
- if (bwi) {
- const otherReserve = await tx.reserves.get(bwi.reservePub);
- if (otherReserve) {
- logger.trace(
- "returning existing reserve for bankWithdrawStatusUri",
- );
- return {
- exchange: otherReserve.exchangeBaseUrl,
- reservePub: otherReserve.reservePub,
- };
- }
- }
- await tx.bankWithdrawUris.put({
- reservePub: reserveRecord.reservePub,
- talerWithdrawUri: reserveRecord.bankInfo.statusUrl,
- });
- }
- if (!isAudited && !isTrusted) {
- await tx.exchangeTrust.put({
- currency: reserveRecord.currency,
- exchangeBaseUrl: reserveRecord.exchangeBaseUrl,
- exchangeMasterPub: exchangeDetails.masterPublicKey,
- uids: [encodeCrock(getRandomBytes(32))],
- });
- }
- await tx.reserves.put(reserveRecord);
- const r: CreateReserveResponse = {
- exchange: canonExchange,
- reservePub: keypair.pub,
- };
- return r;
- });
-
- if (reserveRecord.reservePub === resp.reservePub) {
- // Only emit notification when a new reserve was created.
- ws.notify({
- type: NotificationType.ReserveCreated,
- reservePub: reserveRecord.reservePub,
- });
- }
-
- // Asynchronously process the reserve, but return
- // to the caller already.
- processReserve(ws, resp.reservePub, { forceNow: true }).catch((e) => {
- logger.error("Processing reserve (after createReserve) failed:", e);
- });
-
- return resp;
-}
-
-/**
- * Re-query the status of a reserve.
- */
-export async function forceQueryReserve(
- ws: InternalWalletState,
- reservePub: string,
-): Promise<void> {
- await ws.db
- .mktx((x) => ({
- reserves: x.reserves,
- }))
- .runReadWrite(async (tx) => {
- const reserve = await tx.reserves.get(reservePub);
- if (!reserve) {
- return;
- }
- // Only force status query where it makes sense
- switch (reserve.reserveStatus) {
- case ReserveRecordStatus.Dormant:
- reserve.reserveStatus = ReserveRecordStatus.QueryingStatus;
- reserve.operationStatus = OperationStatus.Pending;
- reserve.retryInfo = RetryInfo.reset();
- break;
- default:
- break;
- }
- await tx.reserves.put(reserve);
- });
- await processReserve(ws, reservePub, { forceNow: true });
-}
-
-/**
- * First fetch information required to withdraw from the reserve,
- * then deplete the reserve, withdrawing coins until it is empty.
- *
- * The returned promise resolves once the reserve is set to the
- * state "Dormant".
- */
-export async function processReserve(
- ws: InternalWalletState,
- reservePub: string,
- options: {
- forceNow?: boolean;
- } = {},
-): Promise<void> {
- return ws.memoProcessReserve.memo(reservePub, async () => {
- const onOpError = (err: TalerErrorDetail): Promise<void> =>
- reportReserveError(ws, reservePub, err);
- await guardOperationException(
- () => processReserveImpl(ws, reservePub, options),
- onOpError,
- );
- });
-}
-
-async function registerReserveWithBank(
- ws: InternalWalletState,
- reservePub: string,
-): Promise<void> {
- const reserve = await ws.db
- .mktx((x) => ({
- reserves: x.reserves,
- }))
- .runReadOnly(async (tx) => {
- return await tx.reserves.get(reservePub);
- });
- switch (reserve?.reserveStatus) {
- case ReserveRecordStatus.WaitConfirmBank:
- case ReserveRecordStatus.RegisteringBank:
- break;
- default:
- return;
- }
- const bankInfo = reserve.bankInfo;
- if (!bankInfo) {
- return;
- }
- const bankStatusUrl = bankInfo.statusUrl;
- const httpResp = await ws.http.postJson(
- bankStatusUrl,
- {
- reserve_pub: reservePub,
- selected_exchange: bankInfo.exchangePaytoUri,
- },
- {
- timeout: getReserveRequestTimeout(reserve),
- },
- );
- await readSuccessResponseJsonOrThrow(
- httpResp,
- codecForBankWithdrawalOperationPostResponse(),
- );
- await ws.db
- .mktx((x) => ({
- reserves: x.reserves,
- }))
- .runReadWrite(async (tx) => {
- const r = await tx.reserves.get(reservePub);
- if (!r) {
- return;
- }
- switch (r.reserveStatus) {
- case ReserveRecordStatus.RegisteringBank:
- case ReserveRecordStatus.WaitConfirmBank:
- break;
- default:
- return;
- }
- r.timestampReserveInfoPosted = AbsoluteTime.toTimestamp(
- AbsoluteTime.now(),
- );
- r.reserveStatus = ReserveRecordStatus.WaitConfirmBank;
- r.operationStatus = OperationStatus.Pending;
- if (!r.bankInfo) {
- throw Error("invariant failed");
- }
- r.retryInfo = RetryInfo.reset();
- await tx.reserves.put(r);
- });
- ws.notify({ type: NotificationType.ReserveRegisteredWithBank });
- return processReserveBankStatus(ws, reservePub);
-}
-
-export function getReserveRequestTimeout(r: ReserveRecord): Duration {
- return durationMax(
- { d_ms: 60000 },
- durationMin({ d_ms: 5000 }, RetryInfo.getDuration(r.retryInfo)),
- );
-}
-
-async function processReserveBankStatus(
- ws: InternalWalletState,
- reservePub: string,
-): Promise<void> {
- const reserve = await ws.db
- .mktx((x) => ({
- reserves: x.reserves,
- }))
- .runReadOnly(async (tx) => {
- return tx.reserves.get(reservePub);
- });
- switch (reserve?.reserveStatus) {
- case ReserveRecordStatus.WaitConfirmBank:
- case ReserveRecordStatus.RegisteringBank:
- break;
- default:
- return;
- }
- const bankStatusUrl = reserve.bankInfo?.statusUrl;
- if (!bankStatusUrl) {
- return;
- }
-
- const statusResp = await ws.http.get(bankStatusUrl, {
- timeout: getReserveRequestTimeout(reserve),
- });
- const status = await readSuccessResponseJsonOrThrow(
- statusResp,
- codecForWithdrawOperationStatusResponse(),
- );
-
- if (status.aborted) {
- logger.info("bank aborted the withdrawal");
- await ws.db
- .mktx((x) => ({
- reserves: x.reserves,
- }))
- .runReadWrite(async (tx) => {
- const r = await tx.reserves.get(reservePub);
- if (!r) {
- return;
- }
- switch (r.reserveStatus) {
- case ReserveRecordStatus.RegisteringBank:
- case ReserveRecordStatus.WaitConfirmBank:
- break;
- default:
- return;
- }
- const now = AbsoluteTime.toTimestamp(AbsoluteTime.now());
- r.timestampBankConfirmed = now;
- r.reserveStatus = ReserveRecordStatus.BankAborted;
- r.operationStatus = OperationStatus.Finished;
- r.retryInfo = RetryInfo.reset();
- await tx.reserves.put(r);
- });
- return;
- }
-
- // Bank still needs to know our reserve info
- if (!status.selection_done) {
- await registerReserveWithBank(ws, reservePub);
- return await processReserveBankStatus(ws, reservePub);
- }
-
- // FIXME: Why do we do this?!
- if (reserve.reserveStatus === ReserveRecordStatus.RegisteringBank) {
- await registerReserveWithBank(ws, reservePub);
- return await processReserveBankStatus(ws, reservePub);
- }
-
- await ws.db
- .mktx((x) => ({
- reserves: x.reserves,
- }))
- .runReadWrite(async (tx) => {
- const r = await tx.reserves.get(reservePub);
- if (!r) {
- return;
- }
- // Re-check reserve status within transaction
- switch (r.reserveStatus) {
- case ReserveRecordStatus.RegisteringBank:
- case ReserveRecordStatus.WaitConfirmBank:
- break;
- default:
- return;
- }
- if (status.transfer_done) {
- const now = AbsoluteTime.toTimestamp(AbsoluteTime.now());
- r.timestampBankConfirmed = now;
- r.reserveStatus = ReserveRecordStatus.QueryingStatus;
- r.operationStatus = OperationStatus.Pending;
- r.retryInfo = RetryInfo.reset();
- } else {
- logger.info("Withdrawal operation not yet confirmed by bank");
- if (r.bankInfo) {
- r.bankInfo.confirmUrl = status.confirm_transfer_url;
- }
- r.retryInfo = RetryInfo.increment(r.retryInfo);
- }
- await tx.reserves.put(r);
- });
-}
-
-/**
- * Update the information about a reserve that is stored in the wallet
- * by querying the reserve's exchange.
- *
- * If the reserve have funds that are not allocated in a withdrawal group yet
- * and are big enough to withdraw with available denominations,
- * create a new withdrawal group for the remaining amount.
- */
-async function updateReserve(
- ws: InternalWalletState,
- reservePub: string,
-): Promise<{ ready: boolean }> {
- const reserve = await ws.db
- .mktx((x) => ({
- reserves: x.reserves,
- }))
- .runReadOnly(async (tx) => {
- return tx.reserves.get(reservePub);
- });
- if (!reserve) {
- throw Error("reserve not in db");
- }
-
- if (reserve.reserveStatus !== ReserveRecordStatus.QueryingStatus) {
- return { ready: true };
- }
-
- const reserveUrl = new URL(`reserves/${reservePub}`, reserve.exchangeBaseUrl);
- reserveUrl.searchParams.set("timeout_ms", "30000");
-
- logger.info(`querying reserve status via ${reserveUrl}`);
-
- const resp = await ws.http.get(reserveUrl.href, {
- timeout: getReserveRequestTimeout(reserve),
- });
-
- const result = await readSuccessResponseJsonOrErrorCode(
- resp,
- codecForReserveStatus(),
- );
-
- if (result.isError) {
- if (
- resp.status === 404 &&
- result.talerErrorResponse.code ===
- TalerErrorCode.EXCHANGE_RESERVES_STATUS_UNKNOWN
- ) {
- ws.notify({
- type: NotificationType.ReserveNotYetFound,
- reservePub,
- });
- return { ready: false };
- } else {
- throwUnexpectedRequestError(resp, result.talerErrorResponse);
- }
- }
-
- logger.trace(`got reserve status ${j2s(result.response)}`);
-
- const reserveInfo = result.response;
- const reserveBalance = Amounts.parseOrThrow(reserveInfo.balance);
- const currency = reserveBalance.currency;
-
- await updateWithdrawalDenoms(ws, reserve.exchangeBaseUrl);
- const denoms = await getCandidateWithdrawalDenoms(
- ws,
- reserve.exchangeBaseUrl,
- );
-
- const newWithdrawalGroup = await ws.db
- .mktx((x) => ({
- planchets: x.planchets,
- withdrawalGroups: x.withdrawalGroups,
- reserves: x.reserves,
- denominations: x.denominations,
- }))
- .runReadWrite(async (tx) => {
- const newReserve = await tx.reserves.get(reserve.reservePub);
- if (!newReserve) {
- return;
- }
-
- let amountReservePlus = reserveBalance;
- let amountReserveMinus = Amounts.getZero(currency);
-
- // Subtract amount allocated in unfinished withdrawal groups
- // for this reserve from the available amount.
- await tx.withdrawalGroups.indexes.byReservePub
- .iter(reservePub)
- .forEachAsync(async (wg) => {
- if (wg.timestampFinish) {
- return;
- }
- await tx.planchets.indexes.byGroup
- .iter(wg.withdrawalGroupId)
- .forEachAsync(async (pr) => {
- if (pr.withdrawalDone) {
- return;
- }
- const denomInfo = await ws.getDenomInfo(
- ws,
- tx,
- wg.exchangeBaseUrl,
- pr.denomPubHash,
- );
- if (!denomInfo) {
- logger.error(`no denom info found for ${pr.denomPubHash}`);
- return;
- }
- amountReserveMinus = Amounts.add(
- amountReserveMinus,
- denomInfo.value,
- denomInfo.feeWithdraw,
- ).amount;
- });
- });
-
- const remainingAmount = Amounts.sub(
- amountReservePlus,
- amountReserveMinus,
- ).amount;
-
- let withdrawalGroupId: string;
- let denomSel: DenomSelectionState;
-
- if (!newReserve.initialWithdrawalStarted) {
- withdrawalGroupId = newReserve.initialWithdrawalGroupId;
- newReserve.initialWithdrawalStarted = true;
- denomSel = newReserve.initialDenomSel;
- } else {
- withdrawalGroupId = encodeCrock(randomBytes(32));
-
- denomSel = selectWithdrawalDenominations(remainingAmount, denoms);
-
- logger.trace(
- `Remaining unclaimed amount in reseve is ${Amounts.stringify(
- remainingAmount,
- )} and can be withdrawn with ${denomSel.selectedDenoms.length} coins`,
- );
-
- if (denomSel.selectedDenoms.length === 0) {
- newReserve.reserveStatus = ReserveRecordStatus.Dormant;
- newReserve.operationStatus = OperationStatus.Finished;
- delete newReserve.lastError;
- delete newReserve.retryInfo;
- await tx.reserves.put(newReserve);
- return;
- }
- }
-
- const withdrawalRecord: WithdrawalGroupRecord = {
- withdrawalGroupId: withdrawalGroupId,
- exchangeBaseUrl: reserve.exchangeBaseUrl,
- reservePub: reserve.reservePub,
- rawWithdrawalAmount: remainingAmount,
- timestampStart: AbsoluteTime.toTimestamp(AbsoluteTime.now()),
- retryInfo: RetryInfo.reset(),
- lastError: undefined,
- denomsSel: denomSel,
- secretSeed: encodeCrock(getRandomBytes(64)),
- denomSelUid: encodeCrock(getRandomBytes(32)),
- operationStatus: OperationStatus.Pending,
- };
-
- delete newReserve.lastError;
- delete newReserve.retryInfo;
- newReserve.reserveStatus = ReserveRecordStatus.Dormant;
- newReserve.operationStatus = OperationStatus.Finished;
-
- await tx.reserves.put(newReserve);
- await tx.withdrawalGroups.put(withdrawalRecord);
- return withdrawalRecord;
- });
-
- if (newWithdrawalGroup) {
- logger.trace("processing new withdraw group");
- ws.notify({
- type: NotificationType.WithdrawGroupCreated,
- withdrawalGroupId: newWithdrawalGroup.withdrawalGroupId,
- });
- await processWithdrawGroup(ws, newWithdrawalGroup.withdrawalGroupId);
- }
-
- return { ready: true };
-}
-
-async function processReserveImpl(
- ws: InternalWalletState,
- reservePub: string,
- options: {
- forceNow?: boolean;
- } = {},
-): Promise<void> {
- const forceNow = options.forceNow ?? false;
- await setupReserveRetry(ws, reservePub, { reset: forceNow });
- const reserve = await ws.db
- .mktx((x) => ({
- reserves: x.reserves,
- }))
- .runReadOnly(async (tx) => {
- return tx.reserves.get(reservePub);
- });
- if (!reserve) {
- logger.error(
- `not processing reserve: reserve ${reservePub} does not exist`,
- );
- return;
- }
- logger.trace(
- `Processing reserve ${reservePub} with status ${reserve.reserveStatus}`,
- );
- switch (reserve.reserveStatus) {
- case ReserveRecordStatus.RegisteringBank:
- await processReserveBankStatus(ws, reservePub);
- return await processReserveImpl(ws, reservePub, { forceNow: true });
- case ReserveRecordStatus.QueryingStatus: {
- const res = await updateReserve(ws, reservePub);
- if (res.ready) {
- return await processReserveImpl(ws, reservePub, { forceNow: true });
- }
- break;
- }
- case ReserveRecordStatus.Dormant:
- // nothing to do
- break;
- case ReserveRecordStatus.WaitConfirmBank:
- await processReserveBankStatus(ws, reservePub);
- break;
- case ReserveRecordStatus.BankAborted:
- break;
- default:
- console.warn("unknown reserve record status:", reserve.reserveStatus);
- assertUnreachable(reserve.reserveStatus);
- break;
- }
-}
-
-/**
- * Create a reserve for a bank-integrated withdrawal from
- * a taler://withdraw URI.
- */
-export async function createTalerWithdrawReserve(
- ws: InternalWalletState,
- talerWithdrawUri: string,
- selectedExchange: string,
- options: {
- forcedDenomSel?: ForcedDenomSel;
- restrictAge?: number;
- } = {},
-): Promise<AcceptWithdrawalResponse> {
- await updateExchangeFromUrl(ws, selectedExchange);
- const withdrawInfo = await getBankWithdrawalInfo(ws.http, talerWithdrawUri);
- const exchangePaytoUri = await getExchangePaytoUri(
- ws,
- selectedExchange,
- withdrawInfo.wireTypes,
- );
- const reserve = await createReserve(ws, {
- amount: withdrawInfo.amount,
- bankWithdrawStatusUrl: withdrawInfo.extractedStatusUrl,
- exchange: selectedExchange,
- senderWire: withdrawInfo.senderWire,
- exchangePaytoUri: exchangePaytoUri,
- restrictAge: options.restrictAge,
- forcedDenomSel: options.forcedDenomSel,
- });
- // We do this here, as the reserve should be registered before we return,
- // so that we can redirect the user to the bank's status page.
- await processReserveBankStatus(ws, reserve.reservePub);
- const processedReserve = await ws.db
- .mktx((x) => ({
- reserves: x.reserves,
- }))
- .runReadOnly(async (tx) => {
- return tx.reserves.get(reserve.reservePub);
- });
- if (processedReserve?.reserveStatus === ReserveRecordStatus.BankAborted) {
- throw TalerError.fromDetail(
- TalerErrorCode.WALLET_WITHDRAWAL_OPERATION_ABORTED_BY_BANK,
- {},
- );
- }
- return {
- reservePub: reserve.reservePub,
- confirmTransferUrl: withdrawInfo.confirmTransferUrl,
- };
-}
-
-/**
- * Get payto URIs that can be used to fund a reserve.
- */
-export async function getFundingPaytoUris(
- tx: GetReadOnlyAccess<{
- reserves: typeof WalletStoresV1.reserves;
- exchanges: typeof WalletStoresV1.exchanges;
- exchangeDetails: typeof WalletStoresV1.exchangeDetails;
- }>,
- reservePub: string,
-): Promise<string[]> {
- const r = await tx.reserves.get(reservePub);
- if (!r) {
- logger.error(`reserve ${reservePub} not found (DB corrupted?)`);
- return [];
- }
- const exchangeDetails = await getExchangeDetails(tx, r.exchangeBaseUrl);
- if (!exchangeDetails) {
- logger.error(`exchange ${r.exchangeBaseUrl} not found (DB corrupted?)`);
- return [];
- }
- const plainPaytoUris =
- exchangeDetails.wireInfo?.accounts.map((x) => x.payto_uri) ?? [];
- if (!plainPaytoUris) {
- logger.error(`exchange ${r.exchangeBaseUrl} has no wire info`);
- return [];
- }
- return plainPaytoUris.map((x) =>
- addPaytoQueryParams(x, {
- amount: Amounts.stringify(r.instructedAmount),
- message: `Taler Withdrawal ${r.reservePub}`,
- }),
- );
-}
diff --git a/packages/taler-wallet-core/src/operations/testing.ts b/packages/taler-wallet-core/src/operations/testing.ts
index d609011ca..bec8ec8f8 100644
--- a/packages/taler-wallet-core/src/operations/testing.ts
+++ b/packages/taler-wallet-core/src/operations/testing.ts
@@ -39,12 +39,12 @@ import {
URL,
PreparePayResultType,
} from "@gnu-taler/taler-util";
-import { createTalerWithdrawReserve } from "./reserves.js";
import { InternalWalletState } from "../internal-wallet-state.js";
import { confirmPay, preparePayForUri } from "./pay.js";
import { getBalances } from "./balance.js";
import { applyRefund } from "./refund.js";
import { checkLogicInvariant } from "../util/invariants.js";
+import { acceptWithdrawalFromUri } from "./withdraw.js";
const logger = new Logger("operations/testing.ts");
@@ -104,14 +104,11 @@ export async function withdrawTestBalance(
amount,
);
- await createTalerWithdrawReserve(
- ws,
- wresp.taler_withdraw_uri,
- exchangeBaseUrl,
- {
- forcedDenomSel: req.forcedDenomSel,
- },
- );
+ await acceptWithdrawalFromUri(ws, {
+ talerWithdrawUri: wresp.taler_withdraw_uri,
+ selectedExchange: exchangeBaseUrl,
+ forcedDenomSel: req.forcedDenomSel,
+ });
await confirmBankWithdrawalUri(
ws.http,
diff --git a/packages/taler-wallet-core/src/operations/transactions.ts b/packages/taler-wallet-core/src/operations/transactions.ts
index ebc223b23..ae4ce6999 100644
--- a/packages/taler-wallet-core/src/operations/transactions.ts
+++ b/packages/taler-wallet-core/src/operations/transactions.ts
@@ -36,7 +36,6 @@ import { InternalWalletState } from "../internal-wallet-state.js";
import {
AbortStatus,
RefundState,
- ReserveRecord,
ReserveRecordStatus,
WalletRefundItem,
} from "../db.js";
@@ -44,9 +43,8 @@ import { processDepositGroup } from "./deposits.js";
import { getExchangeDetails } from "./exchanges.js";
import { processPurchasePay } from "./pay.js";
import { processRefreshGroup } from "./refresh.js";
-import { getFundingPaytoUris } from "./reserves.js";
import { processTip } from "./tip.js";
-import { processWithdrawGroup } from "./withdraw.js";
+import { processWithdrawalGroup } from "./withdraw.js";
const logger = new Logger("taler-wallet-core:transactions.ts");
@@ -127,7 +125,6 @@ export async function getTransactions(
proposals: x.proposals,
purchases: x.purchases,
refreshGroups: x.refreshGroups,
- reserves: x.reserves,
tips: x.tips,
withdrawalGroups: x.withdrawalGroups,
planchets: x.planchets,
@@ -151,24 +148,13 @@ export async function getTransactions(
if (shouldSkipSearch(transactionsRequest, [])) {
return;
}
-
- const r = await tx.reserves.get(wsr.reservePub);
- if (!r) {
- return;
- }
- let amountRaw: AmountJson | undefined = undefined;
- if (wsr.withdrawalGroupId === r.initialWithdrawalGroupId) {
- amountRaw = r.instructedAmount;
- } else {
- amountRaw = wsr.denomsSel.totalWithdrawCost;
- }
let withdrawalDetails: WithdrawalDetails;
- if (r.bankInfo) {
+ if (wsr.bankInfo) {
withdrawalDetails = {
type: WithdrawalType.TalerBankIntegrationApi,
- confirmed: r.timestampBankConfirmed ? true : false,
+ confirmed: wsr.bankInfo.timestampBankConfirmed ? true : false,
reservePub: wsr.reservePub,
- bankConfirmationUrl: r.bankInfo.confirmUrl,
+ bankConfirmationUrl: wsr.bankInfo.confirmUrl,
};
} else {
const exchangeDetails = await getExchangeDetails(
@@ -191,7 +177,7 @@ export async function getTransactions(
transactions.push({
type: TransactionType.Withdrawal,
amountEffective: Amounts.stringify(wsr.denomsSel.totalCoinValue),
- amountRaw: Amounts.stringify(amountRaw),
+ amountRaw: Amounts.stringify(wsr.rawWithdrawalAmount),
withdrawalDetails,
exchangeBaseUrl: wsr.exchangeBaseUrl,
pending: !wsr.timestampFinish,
@@ -205,56 +191,6 @@ export async function getTransactions(
});
});
- // Report pending withdrawals based on reserves that
- // were created, but where the actual withdrawal group has
- // not started yet.
- tx.reserves.iter().forEachAsync(async (r) => {
- if (shouldSkipCurrency(transactionsRequest, r.currency)) {
- return;
- }
- if (shouldSkipSearch(transactionsRequest, [])) {
- return;
- }
- if (r.initialWithdrawalStarted) {
- return;
- }
- if (r.reserveStatus === ReserveRecordStatus.BankAborted) {
- return;
- }
- let withdrawalDetails: WithdrawalDetails;
- if (r.bankInfo) {
- withdrawalDetails = {
- type: WithdrawalType.TalerBankIntegrationApi,
- confirmed: false,
- reservePub: r.reservePub,
- bankConfirmationUrl: r.bankInfo.confirmUrl,
- };
- } else {
- withdrawalDetails = {
- type: WithdrawalType.ManualTransfer,
- reservePub: r.reservePub,
- exchangePaytoUris: await getFundingPaytoUris(tx, r.reservePub),
- };
- }
- transactions.push({
- type: TransactionType.Withdrawal,
- amountRaw: Amounts.stringify(r.instructedAmount),
- amountEffective: Amounts.stringify(
- r.initialDenomSel.totalCoinValue,
- ),
- exchangeBaseUrl: r.exchangeBaseUrl,
- pending: true,
- timestamp: r.timestampCreated,
- withdrawalDetails: withdrawalDetails,
- transactionId: makeEventId(
- TransactionType.Withdrawal,
- r.initialWithdrawalGroupId,
- ),
- frozen: false,
- ...(r.lastError ? { error: r.lastError } : {}),
- });
- });
-
tx.depositGroups.iter().forEachAsync(async (dg) => {
const amount = Amounts.parseOrThrow(dg.contractTermsRaw.amount);
if (shouldSkipCurrency(transactionsRequest, amount.currency)) {
@@ -499,7 +435,7 @@ export async function retryTransaction(
}
case TransactionType.Withdrawal: {
const withdrawalGroupId = rest[0];
- await processWithdrawGroup(ws, withdrawalGroupId, { forceNow: true });
+ await processWithdrawalGroup(ws, withdrawalGroupId, { forceNow: true });
break;
}
case TransactionType.Payment: {
@@ -536,7 +472,6 @@ export async function deleteTransaction(
await ws.db
.mktx((x) => ({
withdrawalGroups: x.withdrawalGroups,
- reserves: x.reserves,
tombstones: x.tombstones,
}))
.runReadWrite(async (tx) => {
@@ -550,17 +485,6 @@ export async function deleteTransaction(
});
return;
}
- const reserveRecord: ReserveRecord | undefined =
- await tx.reserves.indexes.byInitialWithdrawalGroupId.get(
- withdrawalGroupId,
- );
- if (reserveRecord && !reserveRecord.initialWithdrawalStarted) {
- const reservePub = reserveRecord.reservePub;
- await tx.reserves.delete(reservePub);
- await tx.tombstones.put({
- id: TombstoneTag.DeleteReserve + ":" + reservePub,
- });
- }
});
} else if (type === TransactionType.Payment) {
const proposalId = rest[0];
diff --git a/packages/taler-wallet-core/src/operations/withdraw.ts b/packages/taler-wallet-core/src/operations/withdraw.ts
index ea9e22331..484b9b962 100644
--- a/packages/taler-wallet-core/src/operations/withdraw.ts
+++ b/packages/taler-wallet-core/src/operations/withdraw.ts
@@ -19,20 +19,29 @@
*/
import {
AbsoluteTime,
+ AcceptManualWithdrawalResult,
+ AcceptWithdrawalResponse,
+ addPaytoQueryParams,
AmountJson,
+ AmountLike,
Amounts,
AmountString,
BankWithdrawDetails,
+ canonicalizeBaseUrl,
+ codecForBankWithdrawalOperationPostResponse,
+ codecForReserveStatus,
codecForTalerConfigResponse,
codecForWithdrawBatchResponse,
codecForWithdrawOperationStatusResponse,
codecForWithdrawResponse,
DenomKeyType,
Duration,
- durationFromSpec,
+ durationFromSpec, encodeCrock,
ExchangeListItem,
ExchangeWithdrawRequest,
ForcedDenomSel,
+ getRandomBytes,
+ j2s,
LibtoolVersion,
Logger,
NotificationType,
@@ -45,8 +54,9 @@ import {
VersionMatchResult,
WithdrawBatchResponse,
WithdrawResponse,
- WithdrawUriInfoResponse,
+ WithdrawUriInfoResponse
} from "@gnu-taler/taler-util";
+import { EddsaKeypair } from "../crypto/cryptoImplementation.js";
import {
CoinRecord,
CoinSourceType,
@@ -58,26 +68,42 @@ import {
ExchangeRecord,
OperationStatus,
PlanchetRecord,
- WithdrawalGroupRecord,
+ ReserveBankInfo,
+ ReserveRecordStatus,
+ WalletStoresV1,
+ WithdrawalGroupRecord
} from "../db.js";
import {
getErrorDetailFromException,
makeErrorDetail,
- TalerError,
+ TalerError
} from "../errors.js";
import { InternalWalletState } from "../internal-wallet-state.js";
+import { assertUnreachable } from "../util/assertUnreachable.js";
import { walletCoreDebugFlags } from "../util/debugFlags.js";
import {
HttpRequestLibrary,
+ readSuccessResponseJsonOrErrorCode,
readSuccessResponseJsonOrThrow,
+ throwUnexpectedRequestError
} from "../util/http.js";
import { checkDbInvariant, checkLogicInvariant } from "../util/invariants.js";
+import {
+ DbAccess,
+ GetReadOnlyAccess
+} from "../util/query.js";
import { RetryInfo } from "../util/retries.js";
import {
WALLET_BANK_INTEGRATION_PROTOCOL_VERSION,
- WALLET_EXCHANGE_PROTOCOL_VERSION,
+ WALLET_EXCHANGE_PROTOCOL_VERSION
} from "../versions.js";
import { guardOperationException } from "./common.js";
+import {
+ getExchangeDetails,
+ getExchangePaytoUri,
+ getExchangeTrust,
+ updateExchangeFromUrl
+} from "./exchanges.js";
/**
* Logger for this file.
@@ -215,7 +241,7 @@ export function selectWithdrawalDenominations(
for (const d of denoms) {
let count = 0;
const cost = Amounts.add(d.value, d.feeWithdraw).amount;
- for (; ;) {
+ for (;;) {
if (Amounts.cmp(remaining, cost) < 0) {
break;
}
@@ -410,47 +436,42 @@ async function processPlanchetGenerate(
return;
}
let ci = 0;
- let denomPubHash: string | undefined;
+ let maybeDenomPubHash: string | undefined;
for (let di = 0; di < withdrawalGroup.denomsSel.selectedDenoms.length; di++) {
const d = withdrawalGroup.denomsSel.selectedDenoms[di];
if (coinIdx >= ci && coinIdx < ci + d.count) {
- denomPubHash = d.denomPubHash;
+ maybeDenomPubHash = d.denomPubHash;
break;
}
ci += d.count;
}
- if (!denomPubHash) {
+ if (!maybeDenomPubHash) {
throw Error("invariant violated");
}
+ const denomPubHash = maybeDenomPubHash;
- const { denom, reserve } = await ws.db
+ const denom = await ws.db
.mktx((x) => ({
- reserves: x.reserves,
denominations: x.denominations,
}))
.runReadOnly(async (tx) => {
- const denom = await tx.denominations.get([
+ return ws.getDenomInfo(
+ ws,
+ tx,
withdrawalGroup.exchangeBaseUrl,
- denomPubHash!,
- ]);
- if (!denom) {
- throw Error("invariant violated");
- }
- const reserve = await tx.reserves.get(withdrawalGroup.reservePub);
- if (!reserve) {
- throw Error("invariant violated");
- }
- return { denom, reserve };
+ denomPubHash,
+ );
});
+ checkDbInvariant(!!denom);
const r = await ws.cryptoApi.createPlanchet({
denomPub: denom.denomPub,
feeWithdraw: denom.feeWithdraw,
- reservePriv: reserve.reservePriv,
- reservePub: reserve.reservePub,
+ reservePriv: withdrawalGroup.reservePriv,
+ reservePub: withdrawalGroup.reservePub,
value: denom.value,
coinIndex: coinIdx,
secretSeed: withdrawalGroup.secretSeed,
- restrictAge: reserve.restrictAge,
+ restrictAge: withdrawalGroup.restrictAge,
});
const newPlanchet: PlanchetRecord = {
blindingKey: r.blindingKey,
@@ -806,11 +827,13 @@ async function processPlanchetVerifyAndStoreCoin(
const planchetCoinPub = planchet.coinPub;
+ // Check if this is the first time that the whole
+ // withdrawal succeeded. If so, mark the withdrawal
+ // group as finished.
const firstSuccess = await ws.db
.mktx((x) => ({
coins: x.coins,
withdrawalGroups: x.withdrawalGroups,
- reserves: x.reserves,
planchets: x.planchets,
}))
.runReadWrite(async (tx) => {
@@ -875,7 +898,8 @@ 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;
@@ -960,7 +984,80 @@ async function reportWithdrawalError(
ws.notify({ type: NotificationType.WithdrawOperationError, error: err });
}
-export async function processWithdrawGroup(
+/**
+ * Update the information about a reserve that is stored in the wallet
+ * by querying the reserve's exchange.
+ *
+ * If the reserve have funds that are not allocated in a withdrawal group yet
+ * and are big enough to withdraw with available denominations,
+ * create a new withdrawal group for the remaining amount.
+ */
+async function queryReserve(
+ ws: InternalWalletState,
+ withdrawalGroupId: string,
+): Promise<{ ready: boolean }> {
+ const withdrawalGroup = await getWithdrawalGroupRecordTx(ws.db, {
+ withdrawalGroupId,
+ });
+ checkDbInvariant(!!withdrawalGroup);
+ if (withdrawalGroup.reserveStatus !== ReserveRecordStatus.QueryingStatus) {
+ return { ready: true };
+ }
+ const reservePub = withdrawalGroup.reservePub;
+
+ const reserveUrl = new URL(
+ `reserves/${reservePub}`,
+ withdrawalGroup.exchangeBaseUrl,
+ );
+ reserveUrl.searchParams.set("timeout_ms", "30000");
+
+ logger.info(`querying reserve status via ${reserveUrl}`);
+
+ const resp = await ws.http.get(reserveUrl.href, {
+ timeout: getReserveRequestTimeout(withdrawalGroup),
+ });
+
+ const result = await readSuccessResponseJsonOrErrorCode(
+ resp,
+ codecForReserveStatus(),
+ );
+
+ if (result.isError) {
+ if (
+ resp.status === 404 &&
+ result.talerErrorResponse.code ===
+ TalerErrorCode.EXCHANGE_RESERVES_STATUS_UNKNOWN
+ ) {
+ ws.notify({
+ type: NotificationType.ReserveNotYetFound,
+ reservePub,
+ });
+ return { ready: false };
+ } else {
+ throwUnexpectedRequestError(resp, result.talerErrorResponse);
+ }
+ }
+
+ logger.trace(`got reserve status ${j2s(result.response)}`);
+
+ await ws.db
+ .mktx((x) => ({
+ withdrawalGroups: x.withdrawalGroups,
+ }))
+ .runReadWrite(async (tx) => {
+ const wg = await tx.withdrawalGroups.get(withdrawalGroupId);
+ if (!wg) {
+ logger.warn(`withdrawal group ${withdrawalGroupId} not found`);
+ return;
+ }
+ wg.reserveStatus = ReserveRecordStatus.Dormant;
+ await tx.withdrawalGroups.put(wg);
+ });
+
+ return { ready: true };
+}
+
+export async function processWithdrawalGroup(
ws: InternalWalletState,
withdrawalGroupId: string,
options: {
@@ -990,24 +1087,42 @@ async function processWithdrawGroupImpl(
.runReadOnly(async (tx) => {
return tx.withdrawalGroups.get(withdrawalGroupId);
});
+
if (!withdrawalGroup) {
- // Withdrawal group doesn't exist yet, but reserve might exist
- // (and reference the yet to be created withdrawal group)
- const reservePub = await ws.db
- .mktx((x) => ({ reserves: x.reserves }))
- .runReadOnly(async (tx) => {
- const r = await tx.reserves.indexes.byInitialWithdrawalGroupId.get(
- withdrawalGroupId,
- );
- return r?.reservePub;
+ throw Error(`withdrawal group ${withdrawalGroupId} not found`);
+ }
+
+ switch (withdrawalGroup.reserveStatus) {
+ case ReserveRecordStatus.RegisteringBank:
+ await processReserveBankStatus(ws, withdrawalGroupId);
+ return await processWithdrawGroupImpl(ws, withdrawalGroupId, {
+ forceNow: true,
});
- if (!reservePub) {
- logger.warn(
- "withdrawal group doesn't exist (and reserve doesn't exist either)",
- );
+ case ReserveRecordStatus.QueryingStatus: {
+ const res = await queryReserve(ws, withdrawalGroupId);
+ if (res.ready) {
+ return await processWithdrawGroupImpl(ws, withdrawalGroupId, {
+ forceNow: true,
+ });
+ }
return;
}
- return await ws.reserveOps.processReserve(ws, reservePub, { forceNow });
+ case ReserveRecordStatus.WaitConfirmBank:
+ await processReserveBankStatus(ws, withdrawalGroupId);
+ return;
+ case ReserveRecordStatus.BankAborted:
+ // FIXME
+ return;
+ case ReserveRecordStatus.Dormant:
+ // We can try to withdraw, nothing needs to be done with the reserve.
+ break;
+ default:
+ logger.warn(
+ "unknown reserve record status:",
+ withdrawalGroup.reserveStatus,
+ );
+ assertUnreachable(withdrawalGroup.reserveStatus);
+ break;
}
await ws.exchangeOps.updateExchangeFromUrl(
@@ -1071,7 +1186,6 @@ async function processWithdrawGroupImpl(
.mktx((x) => ({
coins: x.coins,
withdrawalGroups: x.withdrawalGroups,
- reserves: x.reserves,
planchets: x.planchets,
}))
.runReadWrite(async (tx) => {
@@ -1200,9 +1314,9 @@ export async function getExchangeWithdrawalInfo(
!versionMatch.compatible &&
versionMatch.currentCmp === -1
) {
- console.warn(
+ logger.warn(
`wallet's support for exchange protocol version ${WALLET_EXCHANGE_PROTOCOL_VERSION} might be outdated ` +
- `(exchange has ${exchangeDetails.protocolVersion}), checking for updates`,
+ `(exchange has ${exchangeDetails.protocolVersion}), checking for updates`,
);
}
}
@@ -1308,3 +1422,456 @@ export async function getWithdrawalDetailsForUri(
possibleExchanges: exchanges,
};
}
+
+export async function getFundingPaytoUrisTx(
+ ws: InternalWalletState,
+ withdrawalGroupId: string,
+): Promise<string[]> {
+ return await ws.db
+ .mktx((x) => ({
+ exchanges: x.exchanges,
+ exchangeDetails: x.exchangeDetails,
+ withdrawalGroups: x.withdrawalGroups,
+ }))
+ .runReadWrite((tx) => getFundingPaytoUris(tx, withdrawalGroupId));
+}
+
+/**
+ * Get payto URIs that can be used to fund a withdrawal operation.
+ */
+export async function getFundingPaytoUris(
+ tx: GetReadOnlyAccess<{
+ withdrawalGroups: typeof WalletStoresV1.withdrawalGroups;
+ exchanges: typeof WalletStoresV1.exchanges;
+ exchangeDetails: typeof WalletStoresV1.exchangeDetails;
+ }>,
+ withdrawalGroupId: string,
+): Promise<string[]> {
+ const withdrawalGroup = await tx.withdrawalGroups.get(withdrawalGroupId);
+ checkDbInvariant(!!withdrawalGroup);
+ const exchangeDetails = await getExchangeDetails(
+ tx,
+ withdrawalGroup.exchangeBaseUrl,
+ );
+ if (!exchangeDetails) {
+ logger.error(`exchange ${withdrawalGroup.exchangeBaseUrl} not found`);
+ return [];
+ }
+ const plainPaytoUris =
+ exchangeDetails.wireInfo?.accounts.map((x) => x.payto_uri) ?? [];
+ if (!plainPaytoUris) {
+ logger.error(
+ `exchange ${withdrawalGroup.exchangeBaseUrl} has no wire info`,
+ );
+ return [];
+ }
+ return plainPaytoUris.map((x) =>
+ addPaytoQueryParams(x, {
+ amount: Amounts.stringify(withdrawalGroup.instructedAmount),
+ message: `Taler Withdrawal ${withdrawalGroup.reservePub}`,
+ }),
+ );
+}
+
+async function getWithdrawalGroupRecordTx(
+ db: DbAccess<typeof WalletStoresV1>,
+ req: {
+ withdrawalGroupId: string;
+ },
+): Promise<WithdrawalGroupRecord | undefined> {
+ return await db
+ .mktx((x) => ({
+ withdrawalGroups: x.withdrawalGroups,
+ }))
+ .runReadOnly(async (tx) => {
+ return tx.withdrawalGroups.get(req.withdrawalGroupId);
+ });
+}
+
+export function getReserveRequestTimeout(r: WithdrawalGroupRecord): Duration {
+ return Duration.max(
+ { d_ms: 60000 },
+ Duration.min({ d_ms: 5000 }, RetryInfo.getDuration(r.retryInfo)),
+ );
+}
+
+async function registerReserveWithBank(
+ ws: InternalWalletState,
+ withdrawalGroupId: string,
+): Promise<void> {
+ const withdrawalGroup = await ws.db
+ .mktx((x) => ({
+ withdrawalGroups: x.withdrawalGroups,
+ }))
+ .runReadOnly(async (tx) => {
+ return await tx.withdrawalGroups.get(withdrawalGroupId);
+ });
+ switch (withdrawalGroup?.reserveStatus) {
+ case ReserveRecordStatus.WaitConfirmBank:
+ case ReserveRecordStatus.RegisteringBank:
+ break;
+ default:
+ return;
+ }
+ const bankInfo = withdrawalGroup.bankInfo;
+ if (!bankInfo) {
+ return;
+ }
+ const bankStatusUrl = bankInfo.statusUrl;
+ const httpResp = await ws.http.postJson(
+ bankStatusUrl,
+ {
+ reserve_pub: withdrawalGroup.reservePub,
+ selected_exchange: bankInfo.exchangePaytoUri,
+ },
+ {
+ timeout: getReserveRequestTimeout(withdrawalGroup),
+ },
+ );
+ await readSuccessResponseJsonOrThrow(
+ httpResp,
+ codecForBankWithdrawalOperationPostResponse(),
+ );
+ await ws.db
+ .mktx((x) => ({
+ withdrawalGroups: x.withdrawalGroups,
+ }))
+ .runReadWrite(async (tx) => {
+ const r = await tx.withdrawalGroups.get(withdrawalGroupId);
+ if (!r) {
+ return;
+ }
+ switch (r.reserveStatus) {
+ case ReserveRecordStatus.RegisteringBank:
+ case ReserveRecordStatus.WaitConfirmBank:
+ break;
+ default:
+ return;
+ }
+ if (!r.bankInfo) {
+ throw Error("invariant failed");
+ }
+ r.bankInfo.timestampReserveInfoPosted = AbsoluteTime.toTimestamp(
+ AbsoluteTime.now(),
+ );
+ r.reserveStatus = ReserveRecordStatus.WaitConfirmBank;
+ r.operationStatus = OperationStatus.Pending;
+ r.retryInfo = RetryInfo.reset();
+ await tx.withdrawalGroups.put(r);
+ });
+ ws.notify({ type: NotificationType.ReserveRegisteredWithBank });
+ return processReserveBankStatus(ws, withdrawalGroupId);
+}
+
+async function processReserveBankStatus(
+ ws: InternalWalletState,
+ withdrawalGroupId: string,
+): Promise<void> {
+ const withdrawalGroup = await getWithdrawalGroupRecordTx(ws.db, {
+ withdrawalGroupId,
+ });
+ switch (withdrawalGroup?.reserveStatus) {
+ case ReserveRecordStatus.WaitConfirmBank:
+ case ReserveRecordStatus.RegisteringBank:
+ break;
+ default:
+ return;
+ }
+ const bankStatusUrl = withdrawalGroup.bankInfo?.statusUrl;
+ if (!bankStatusUrl) {
+ return;
+ }
+
+ const statusResp = await ws.http.get(bankStatusUrl, {
+ timeout: getReserveRequestTimeout(withdrawalGroup),
+ });
+ const status = await readSuccessResponseJsonOrThrow(
+ statusResp,
+ codecForWithdrawOperationStatusResponse(),
+ );
+
+ if (status.aborted) {
+ logger.info("bank aborted the withdrawal");
+ await ws.db
+ .mktx((x) => ({
+ withdrawalGroups: x.withdrawalGroups,
+ }))
+ .runReadWrite(async (tx) => {
+ const r = await tx.withdrawalGroups.get(withdrawalGroupId);
+ if (!r) {
+ return;
+ }
+ switch (r.reserveStatus) {
+ case ReserveRecordStatus.RegisteringBank:
+ case ReserveRecordStatus.WaitConfirmBank:
+ break;
+ default:
+ return;
+ }
+ if (!r.bankInfo) {
+ throw Error("invariant failed");
+ }
+ const now = AbsoluteTime.toTimestamp(AbsoluteTime.now());
+ r.bankInfo.timestampBankConfirmed = now;
+ r.reserveStatus = ReserveRecordStatus.BankAborted;
+ r.operationStatus = OperationStatus.Finished;
+ r.retryInfo = RetryInfo.reset();
+ await tx.withdrawalGroups.put(r);
+ });
+ return;
+ }
+
+ // Bank still needs to know our reserve info
+ if (!status.selection_done) {
+ await registerReserveWithBank(ws, withdrawalGroupId);
+ return await processReserveBankStatus(ws, withdrawalGroupId);
+ }
+
+ // FIXME: Why do we do this?!
+ if (withdrawalGroup.reserveStatus === ReserveRecordStatus.RegisteringBank) {
+ await registerReserveWithBank(ws, withdrawalGroupId);
+ return await processReserveBankStatus(ws, withdrawalGroupId);
+ }
+
+ await ws.db
+ .mktx((x) => ({
+ withdrawalGroups: x.withdrawalGroups,
+ }))
+ .runReadWrite(async (tx) => {
+ const r = await tx.withdrawalGroups.get(withdrawalGroupId);
+ if (!r) {
+ return;
+ }
+ // Re-check reserve status within transaction
+ switch (r.reserveStatus) {
+ case ReserveRecordStatus.RegisteringBank:
+ case ReserveRecordStatus.WaitConfirmBank:
+ break;
+ default:
+ return;
+ }
+ if (status.transfer_done) {
+ logger.info("withdrawal: transfer confirmed by bank.");
+ const now = AbsoluteTime.toTimestamp(AbsoluteTime.now());
+ if (!r.bankInfo) {
+ throw Error("invariant failed");
+ }
+ r.bankInfo.timestampBankConfirmed = now;
+ r.reserveStatus = ReserveRecordStatus.QueryingStatus;
+ r.operationStatus = OperationStatus.Pending;
+ r.retryInfo = RetryInfo.reset();
+ } else {
+ logger.info("withdrawal: transfer not yet confirmed by bank");
+ if (r.bankInfo) {
+ r.bankInfo.confirmUrl = status.confirm_transfer_url;
+ }
+ r.retryInfo = RetryInfo.increment(r.retryInfo);
+ }
+ await tx.withdrawalGroups.put(r);
+ });
+}
+
+export async function internalCreateWithdrawalGroup(
+ ws: InternalWalletState,
+ args: {
+ reserveStatus: ReserveRecordStatus;
+ amount: AmountJson;
+ bankInfo?: ReserveBankInfo;
+ exchangeBaseUrl: string;
+ forcedDenomSel?: ForcedDenomSel;
+ reserveKeyPair?: EddsaKeypair;
+ restrictAge?: number;
+ },
+): Promise<WithdrawalGroupRecord> {
+ const reserveKeyPair =
+ args.reserveKeyPair ?? (await ws.cryptoApi.createEddsaKeypair({}));
+ const now = AbsoluteTime.toTimestamp(AbsoluteTime.now());
+ const secretSeed = encodeCrock(getRandomBytes(32));
+ const canonExchange = canonicalizeBaseUrl(args.exchangeBaseUrl);
+ const withdrawalGroupId = encodeCrock(getRandomBytes(32));
+ const amount = args.amount;
+
+ await updateWithdrawalDenoms(ws, canonExchange);
+ const denoms = await getCandidateWithdrawalDenoms(ws, canonExchange);
+
+ let initialDenomSel: DenomSelectionState;
+ const denomSelUid = encodeCrock(getRandomBytes(16));
+ if (args.forcedDenomSel) {
+ logger.warn("using forced denom selection");
+ initialDenomSel = selectForcedWithdrawalDenominations(
+ amount,
+ denoms,
+ args.forcedDenomSel,
+ );
+ } else {
+ initialDenomSel = selectWithdrawalDenominations(amount, denoms);
+ }
+
+ const withdrawalGroup: WithdrawalGroupRecord = {
+ denomSelUid,
+ denomsSel: initialDenomSel,
+ exchangeBaseUrl: canonExchange,
+ instructedAmount: amount,
+ timestampStart: now,
+ lastError: undefined,
+ operationStatus: OperationStatus.Pending,
+ rawWithdrawalAmount: initialDenomSel.totalWithdrawCost,
+ secretSeed,
+ reservePriv: reserveKeyPair.priv,
+ reservePub: reserveKeyPair.pub,
+ reserveStatus: args.reserveStatus,
+ retryInfo: RetryInfo.reset(),
+ withdrawalGroupId,
+ bankInfo: args.bankInfo,
+ restrictAge: args.restrictAge,
+ senderWire: undefined,
+ timestampFinish: undefined,
+ };
+
+ const exchangeInfo = await updateExchangeFromUrl(ws, canonExchange);
+ const exchangeDetails = exchangeInfo.exchangeDetails;
+ if (!exchangeDetails) {
+ logger.trace(exchangeDetails);
+ throw Error("exchange not updated");
+ }
+ const { isAudited, isTrusted } = await getExchangeTrust(
+ ws,
+ exchangeInfo.exchange,
+ );
+
+ await ws.db
+ .mktx((x) => ({
+ withdrawalGroups: x.withdrawalGroups,
+ exchanges: x.exchanges,
+ exchangeDetails: x.exchangeDetails,
+ exchangeTrust: x.exchangeTrust,
+ }))
+ .runReadWrite(async (tx) => {
+ await tx.withdrawalGroups.add(withdrawalGroup);
+
+ if (!isAudited && !isTrusted) {
+ await tx.exchangeTrust.put({
+ currency: amount.currency,
+ exchangeBaseUrl: canonExchange,
+ exchangeMasterPub: exchangeDetails.masterPublicKey,
+ uids: [encodeCrock(getRandomBytes(32))],
+ });
+ }
+ });
+
+ return withdrawalGroup;
+}
+
+export async function acceptWithdrawalFromUri(
+ ws: InternalWalletState,
+ req: {
+ talerWithdrawUri: string;
+ selectedExchange: string;
+ forcedDenomSel?: ForcedDenomSel;
+ restrictAge?: number;
+ },
+): Promise<AcceptWithdrawalResponse> {
+ await updateExchangeFromUrl(ws, req.selectedExchange);
+ const withdrawInfo = await getBankWithdrawalInfo(
+ ws.http,
+ req.talerWithdrawUri,
+ );
+ const exchangePaytoUri = await getExchangePaytoUri(
+ ws,
+ req.selectedExchange,
+ withdrawInfo.wireTypes,
+ );
+
+ const withdrawalGroup = await internalCreateWithdrawalGroup(ws, {
+ amount: withdrawInfo.amount,
+ exchangeBaseUrl: req.selectedExchange,
+ forcedDenomSel: req.forcedDenomSel,
+ reserveStatus: ReserveRecordStatus.RegisteringBank,
+ bankInfo: {
+ exchangePaytoUri,
+ statusUrl: withdrawInfo.extractedStatusUrl,
+ confirmUrl: withdrawInfo.confirmTransferUrl,
+ },
+ });
+
+ const withdrawalGroupId = withdrawalGroup.withdrawalGroupId;
+
+ // We do this here, as the reserve should be registered before we return,
+ // so that we can redirect the user to the bank's status page.
+ await processReserveBankStatus(ws, withdrawalGroupId);
+ const processedWithdrawalGroup = await getWithdrawalGroupRecordTx(ws.db, {
+ withdrawalGroupId,
+ });
+ if (
+ processedWithdrawalGroup?.reserveStatus === ReserveRecordStatus.BankAborted
+ ) {
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_WITHDRAWAL_OPERATION_ABORTED_BY_BANK,
+ {},
+ );
+ }
+
+ // Start withdrawal in the background.
+ await processWithdrawalGroup(ws, withdrawalGroupId, { forceNow: true }).catch(
+ (err) => {
+ logger.error("Processing withdrawal (after creation) failed:", err);
+ },
+ );
+
+ return {
+ reservePub: withdrawalGroup.reservePub,
+ confirmTransferUrl: withdrawInfo.confirmTransferUrl,
+ };
+}
+
+/**
+ * Create a manual withdrawal operation.
+ *
+ * Adds the corresponding exchange as a trusted exchange if it is neither
+ * audited nor trusted already.
+ *
+ * Asynchronously starts the withdrawal.
+ */
+export async function createManualWithdrawal(
+ ws: InternalWalletState,
+ req: {
+ exchangeBaseUrl: string;
+ amount: AmountLike;
+ restrictAge?: number;
+ forcedDenomSel?: ForcedDenomSel;
+ },
+): Promise<AcceptManualWithdrawalResult> {
+ const withdrawalGroup = await internalCreateWithdrawalGroup(ws, {
+ amount: Amounts.jsonifyAmount(req.amount),
+ exchangeBaseUrl: req.exchangeBaseUrl,
+ bankInfo: undefined,
+ forcedDenomSel: req.forcedDenomSel,
+ restrictAge: req.restrictAge,
+ reserveStatus: ReserveRecordStatus.QueryingStatus,
+ });
+
+ const withdrawalGroupId = withdrawalGroup.withdrawalGroupId;
+
+ const exchangePaytoUris = await ws.db
+ .mktx((x) => ({
+ withdrawalGroups: x.withdrawalGroups,
+ exchanges: x.exchanges,
+ exchangeDetails: x.exchangeDetails,
+ exchangeTrust: x.exchangeTrust,
+ }))
+ .runReadWrite(async (tx) => {
+ return await getFundingPaytoUris(tx, withdrawalGroup.withdrawalGroupId);
+ });
+
+ // Start withdrawal in the background.
+ await processWithdrawalGroup(ws, withdrawalGroupId, { forceNow: true }).catch(
+ (err) => {
+ logger.error("Processing withdrawal (after creation) failed:", err);
+ },
+ );
+
+ return {
+ reservePub: withdrawalGroup.reservePub,
+ exchangePaytoUris: exchangePaytoUris,
+ };
+}