summaryrefslogtreecommitdiff
path: root/packages/taler-wallet-core/src/deposits.ts
diff options
context:
space:
mode:
authorFlorian Dold <florian@dold.me>2024-04-03 16:21:33 +0200
committerFlorian Dold <florian@dold.me>2024-04-03 16:21:33 +0200
commit65a656163797e9dd298b34ec916b982082db7f52 (patch)
tree1a226c657639c69194ddf7682a805bf2aa14191c /packages/taler-wallet-core/src/deposits.ts
parent5417b8b7b866f1c4f4d99d6ec9ad001af67822b6 (diff)
downloadwallet-core-65a656163797e9dd298b34ec916b982082db7f52.tar.gz
wallet-core-65a656163797e9dd298b34ec916b982082db7f52.tar.bz2
wallet-core-65a656163797e9dd298b34ec916b982082db7f52.zip
wallet-core: allow deposits with balance locked behind refresh
Diffstat (limited to 'packages/taler-wallet-core/src/deposits.ts')
-rw-r--r--packages/taler-wallet-core/src/deposits.ts230
1 files changed, 170 insertions, 60 deletions
diff --git a/packages/taler-wallet-core/src/deposits.ts b/packages/taler-wallet-core/src/deposits.ts
index 05a5d780a..5b23d8325 100644
--- a/packages/taler-wallet-core/src/deposits.ts
+++ b/packages/taler-wallet-core/src/deposits.ts
@@ -413,16 +413,28 @@ async function refundDepositGroup(
wex: WalletExecutionContext,
depositGroup: DepositGroupRecord,
): Promise<TaskRunResult> {
- const newTxPerCoin = [...depositGroup.statusPerCoin];
+ const statusPerCoin = depositGroup.statusPerCoin;
+ const payCoinSelection = depositGroup.payCoinSelection;
+ if (!statusPerCoin) {
+ throw Error(
+ "unable to refund deposit group without coin selection (status missing)",
+ );
+ }
+ if (!payCoinSelection) {
+ throw Error(
+ "unable to refund deposit group without coin selection (selection missing)",
+ );
+ }
+ const newTxPerCoin = [...statusPerCoin];
logger.info(`status per coin: ${j2s(depositGroup.statusPerCoin)}`);
- for (let i = 0; i < depositGroup.statusPerCoin.length; i++) {
- const st = depositGroup.statusPerCoin[i];
+ for (let i = 0; i < statusPerCoin.length; i++) {
+ const st = statusPerCoin[i];
switch (st) {
case DepositElementStatus.RefundFailed:
case DepositElementStatus.RefundSuccess:
break;
default: {
- const coinPub = depositGroup.payCoinSelection.coinPubs[i];
+ const coinPub = payCoinSelection.coinPubs[i];
const coinExchange = await wex.db.runReadOnlyTx(
["coins"],
async (tx) => {
@@ -431,7 +443,7 @@ async function refundDepositGroup(
return coinRecord.exchangeBaseUrl;
},
);
- const refundAmount = depositGroup.payCoinSelection.coinContributions[i];
+ const refundAmount = payCoinSelection.coinContributions[i];
// We use a constant refund transaction ID, since there can
// only be one refund.
const rtid = 1;
@@ -503,8 +515,8 @@ async function refundDepositGroup(
const refreshCoins: CoinRefreshRequest[] = [];
for (let i = 0; i < newTxPerCoin.length; i++) {
refreshCoins.push({
- amount: depositGroup.payCoinSelection.coinContributions[i],
- coinPub: depositGroup.payCoinSelection.coinPubs[i],
+ amount: payCoinSelection.coinContributions[i],
+ coinPub: payCoinSelection.coinPubs[i],
});
}
let refreshRes: CreateRefreshGroupResult | undefined = undefined;
@@ -740,9 +752,21 @@ async function processDepositGroupPendingTrack(
wex: WalletExecutionContext,
depositGroup: DepositGroupRecord,
): Promise<TaskRunResult> {
+ const statusPerCoin = depositGroup.statusPerCoin;
+ const payCoinSelection = depositGroup.payCoinSelection;
+ if (!statusPerCoin) {
+ throw Error(
+ "unable to refund deposit group without coin selection (status missing)",
+ );
+ }
+ if (!payCoinSelection) {
+ throw Error(
+ "unable to refund deposit group without coin selection (selection missing)",
+ );
+ }
const { depositGroupId } = depositGroup;
- for (let i = 0; i < depositGroup.statusPerCoin.length; i++) {
- const coinPub = depositGroup.payCoinSelection.coinPubs[i];
+ for (let i = 0; i < statusPerCoin.length; i++) {
+ const coinPub = payCoinSelection.coinPubs[i];
// FIXME: Make the URL part of the coin selection?
const exchangeBaseUrl = await wex.db.runReadWriteTx(
["coins"],
@@ -761,7 +785,7 @@ async function processDepositGroupPendingTrack(
}
| undefined;
- if (depositGroup.statusPerCoin[i] !== DepositElementStatus.Wired) {
+ if (statusPerCoin[i] !== DepositElementStatus.Wired) {
const track = await trackDeposit(
wex,
depositGroup,
@@ -826,6 +850,9 @@ async function processDepositGroupPendingTrack(
if (!dg) {
return;
}
+ if (!dg.statusPerCoin) {
+ return;
+ }
if (updatedTxStatus !== undefined) {
dg.statusPerCoin[i] = updatedTxStatus;
}
@@ -858,9 +885,12 @@ async function processDepositGroupPendingTrack(
if (!dg) {
return undefined;
}
+ if (!dg.statusPerCoin) {
+ return undefined;
+ }
const oldTxState = computeDepositTransactionStatus(dg);
- for (let i = 0; i < depositGroup.statusPerCoin.length; i++) {
- if (depositGroup.statusPerCoin[i] !== DepositElementStatus.Wired) {
+ for (let i = 0; i < dg.statusPerCoin.length; i++) {
+ if (dg.statusPerCoin[i] !== DepositElementStatus.Wired) {
allWired = false;
break;
}
@@ -924,6 +954,87 @@ async function processDepositGroupPendingDeposit(
// Check for cancellation before expensive operations.
cancellationToken?.throwIfCancelled();
+ if (!depositGroup.payCoinSelection) {
+ logger.info("missing coin selection for deposit group, selecting now");
+ // FIXME: Consider doing the coin selection inside the txn
+ const payCoinSel = await selectPayCoins(wex, {
+ restrictExchanges: {
+ auditors: [],
+ exchanges: contractData.allowedExchanges,
+ },
+ restrictWireMethod: contractData.wireMethod,
+ contractTermsAmount: Amounts.parseOrThrow(contractData.amount),
+ depositFeeLimit: Amounts.parseOrThrow(contractData.maxDepositFee),
+ wireFeeAmortization: 1, // FIXME #8653
+ prevPayCoins: [],
+ });
+
+ switch (payCoinSel.type) {
+ case "success":
+ logger.info("coin selection success");
+ break;
+ case "failure":
+ logger.info("coin selection failure");
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_DEPOSIT_GROUP_INSUFFICIENT_BALANCE,
+ {
+ insufficientBalanceDetails: payCoinSel.insufficientBalanceDetails,
+ },
+ );
+ case "prospective":
+ logger.info("coin selection prospective");
+ throw Error("insufficient balance (waiting on pending refresh)");
+ default:
+ assertUnreachable(payCoinSel);
+ }
+
+ const transitionDone = await wex.db.runReadWriteTx(
+ [
+ "depositGroups",
+ "coins",
+ "coinAvailability",
+ "refreshGroups",
+ "refreshSessions",
+ "denominations",
+ ],
+ async (tx) => {
+ const dg = await tx.depositGroups.get(depositGroupId);
+ if (!dg) {
+ return false;
+ }
+ if (dg.statusPerCoin) {
+ return false;
+ }
+ dg.payCoinSelection = {
+ coinContributions: payCoinSel.coinSel.coins.map(
+ (x) => x.contribution,
+ ),
+ coinPubs: payCoinSel.coinSel.coins.map((x) => x.coinPub),
+ };
+ dg.payCoinSelectionUid = encodeCrock(getRandomBytes(32));
+ dg.statusPerCoin = payCoinSel.coinSel.coins.map(
+ () => DepositElementStatus.DepositPending,
+ );
+ await tx.depositGroups.put(dg);
+ await spendCoins(wex, tx, {
+ allocationId: transactionId,
+ coinPubs: dg.payCoinSelection.coinPubs,
+ contributions: dg.payCoinSelection.coinContributions.map((x) =>
+ Amounts.parseOrThrow(x),
+ ),
+ refreshReason: RefreshReason.PayDeposit,
+ });
+ return true;
+ },
+ );
+
+ if (transitionDone) {
+ return TaskRunResult.progress();
+ } else {
+ return TaskRunResult.backoff();
+ }
+ }
+
// FIXME: Cache these!
const depositPermissions = await generateDepositPermissions(
wex,
@@ -990,6 +1101,9 @@ async function processDepositGroupPendingDeposit(
if (!dg) {
return;
}
+ if (!dg.statusPerCoin) {
+ return;
+ }
for (const batchIndex of batchIndexes) {
const coinStatus = dg.statusPerCoin[batchIndex];
switch (coinStatus) {
@@ -1360,8 +1474,11 @@ export async function createDepositGroup(
prevPayCoins: [],
});
+ let coins: SelectedProspectiveCoin[] | undefined = undefined;
+
switch (payCoinSel.type) {
case "success":
+ coins = payCoinSel.coinSel.coins;
break;
case "failure":
throw TalerError.fromDetail(
@@ -1371,17 +1488,13 @@ export async function createDepositGroup(
},
);
case "prospective":
- // FIXME: Here we need to create the deposit group without a full coin selection!
- throw Error("insufficient balance (pending refresh)");
+ coins = payCoinSel.result.prospectiveCoins;
+ break;
default:
assertUnreachable(payCoinSel);
}
- const totalDepositCost = await getTotalPaymentCost(
- wex,
- currency,
- payCoinSel.coinSel.coins,
- );
+ const totalDepositCost = await getTotalPaymentCost(wex, currency, coins);
let depositGroupId: string;
if (req.transactionId) {
@@ -1396,34 +1509,23 @@ export async function createDepositGroup(
const infoPerExchange: Record<string, DepositInfoPerExchange> = {};
- await wex.db.runReadOnlyTx(["coins"], async (tx) => {
- for (let i = 0; i < payCoinSel.coinSel.coins.length; i++) {
- const coin = await tx.coins.get(payCoinSel.coinSel.coins[i].coinPub);
- if (!coin) {
- logger.error("coin not found anymore");
- continue;
- }
- let depPerExchange = infoPerExchange[coin.exchangeBaseUrl];
- if (!depPerExchange) {
- infoPerExchange[coin.exchangeBaseUrl] = depPerExchange = {
- amountEffective: Amounts.stringify(
- Amounts.zeroOfAmount(totalDepositCost),
- ),
- };
- }
- const contrib = payCoinSel.coinSel.coins[i].contribution;
- depPerExchange.amountEffective = Amounts.stringify(
- Amounts.add(depPerExchange.amountEffective, contrib).amount,
- );
+ for (let i = 0; i < coins.length; i++) {
+ let depPerExchange = infoPerExchange[coins[i].exchangeBaseUrl];
+ if (!depPerExchange) {
+ infoPerExchange[coins[i].exchangeBaseUrl] = depPerExchange = {
+ amountEffective: Amounts.stringify(
+ Amounts.zeroOfAmount(totalDepositCost),
+ ),
+ };
}
- });
+ const contrib = coins[i].contribution;
+ depPerExchange.amountEffective = Amounts.stringify(
+ Amounts.add(depPerExchange.amountEffective, contrib).amount,
+ );
+ }
const counterpartyEffectiveDepositAmount =
- await getCounterpartyEffectiveDepositAmount(
- wex,
- p.targetType,
- payCoinSel.coinSel.coins,
- );
+ await getCounterpartyEffectiveDepositAmount(wex, p.targetType, coins);
const depositGroup: DepositGroupRecord = {
contractTermsHash,
@@ -1436,14 +1538,9 @@ export async function createDepositGroup(
AbsoluteTime.toPreciseTimestamp(now),
),
timestampFinished: undefined,
- statusPerCoin: payCoinSel.coinSel.coins.map(
- () => DepositElementStatus.DepositPending,
- ),
- payCoinSelection: {
- coinContributions: payCoinSel.coinSel.coins.map((x) => x.contribution),
- coinPubs: payCoinSel.coinSel.coins.map((x) => x.coinPub),
- },
- payCoinSelectionUid: encodeCrock(getRandomBytes(32)),
+ statusPerCoin: undefined,
+ payCoinSelection: undefined,
+ payCoinSelectionUid: undefined,
merchantPriv: merchantPair.priv,
merchantPub: merchantPair.pub,
totalPayCost: Amounts.stringify(totalDepositCost),
@@ -1461,6 +1558,17 @@ export async function createDepositGroup(
infoPerExchange,
};
+ if (payCoinSel.type === "success") {
+ depositGroup.payCoinSelection = {
+ coinContributions: payCoinSel.coinSel.coins.map((x) => x.contribution),
+ coinPubs: payCoinSel.coinSel.coins.map((x) => x.coinPub),
+ };
+ depositGroup.payCoinSelectionUid = encodeCrock(getRandomBytes(32));
+ depositGroup.statusPerCoin = payCoinSel.coinSel.coins.map(
+ () => DepositElementStatus.DepositPending,
+ );
+ }
+
const ctx = new DepositTransactionContext(wex, depositGroupId);
const transactionId = ctx.transactionId;
@@ -1476,14 +1584,16 @@ export async function createDepositGroup(
"contractTerms",
],
async (tx) => {
- await spendCoins(wex, tx, {
- allocationId: transactionId,
- coinPubs: payCoinSel.coinSel.coins.map((x) => x.coinPub),
- contributions: payCoinSel.coinSel.coins.map((x) =>
- Amounts.parseOrThrow(x.contribution),
- ),
- refreshReason: RefreshReason.PayDeposit,
- });
+ if (depositGroup.payCoinSelection) {
+ await spendCoins(wex, tx, {
+ allocationId: transactionId,
+ coinPubs: depositGroup.payCoinSelection.coinPubs,
+ contributions: depositGroup.payCoinSelection.coinContributions.map(
+ (x) => Amounts.parseOrThrow(x),
+ ),
+ refreshReason: RefreshReason.PayDeposit,
+ });
+ }
await tx.depositGroups.put(depositGroup);
await tx.contractTerms.put({
contractTermsRaw: contractTerms,