summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--packages/taler-harness/src/integrationtests/test-wallet-blocked-pay-merchant.ts2
-rw-r--r--packages/taler-harness/src/integrationtests/test-wallet-blocked-pay-peer-push.ts149
-rw-r--r--packages/taler-harness/src/integrationtests/testrunner.ts2
-rw-r--r--packages/taler-wallet-core/src/coinSelection.ts9
-rw-r--r--packages/taler-wallet-core/src/db.ts2
-rw-r--r--packages/taler-wallet-core/src/pay-peer-common.ts14
-rw-r--r--packages/taler-wallet-core/src/pay-peer-pull-debit.ts3
-rw-r--r--packages/taler-wallet-core/src/pay-peer-push-debit.ts177
8 files changed, 287 insertions, 71 deletions
diff --git a/packages/taler-harness/src/integrationtests/test-wallet-blocked-pay-merchant.ts b/packages/taler-harness/src/integrationtests/test-wallet-blocked-pay-merchant.ts
index a74f4fd1d..15167d133 100644
--- a/packages/taler-harness/src/integrationtests/test-wallet-blocked-pay-merchant.ts
+++ b/packages/taler-harness/src/integrationtests/test-wallet-blocked-pay-merchant.ts
@@ -45,7 +45,7 @@ const coinCommon = {
};
/**
- * Run test for refreshe after a payment.
+ * Run test for paying a merchant with balance locked behind a pending refresh.
*/
export async function runWalletBlockedPayMerchantTest(t: GlobalTestState) {
// Set up test environment
diff --git a/packages/taler-harness/src/integrationtests/test-wallet-blocked-pay-peer-push.ts b/packages/taler-harness/src/integrationtests/test-wallet-blocked-pay-peer-push.ts
new file mode 100644
index 000000000..7427f2b07
--- /dev/null
+++ b/packages/taler-harness/src/integrationtests/test-wallet-blocked-pay-peer-push.ts
@@ -0,0 +1,149 @@
+/*
+ This file is part of GNU Taler
+ (C) 2020 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ * Imports.
+ */
+import {
+ AbsoluteTime,
+ AmountString,
+ Duration,
+ NotificationType,
+ TransactionMajorState,
+ TransactionMinorState,
+ j2s,
+} from "@gnu-taler/taler-util";
+import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+import { CoinConfig } from "../harness/denomStructures.js";
+import { GlobalTestState } from "../harness/harness.js";
+import {
+ createSimpleTestkudosEnvironmentV2,
+ createWalletDaemonWithClient,
+ makeTestPaymentV2,
+ withdrawViaBankV2,
+} from "../harness/helpers.js";
+
+const coinCommon = {
+ cipher: "RSA" as const,
+ durationLegal: "3 years",
+ durationSpend: "2 years",
+ durationWithdraw: "7 days",
+ feeDeposit: "TESTKUDOS:0",
+ feeRefresh: "TESTKUDOS:0",
+ feeRefund: "TESTKUDOS:0",
+ feeWithdraw: "TESTKUDOS:0",
+ rsaKeySize: 1024,
+};
+
+/**
+ * Run test for a peer push payment with balance locked behind a pending refresh.
+ */
+export async function runWalletBlockedPayPeerPushTest(t: GlobalTestState) {
+ // Set up test environment
+
+ const coinConfigList: CoinConfig[] = [
+ {
+ ...coinCommon,
+ name: "n1",
+ value: "TESTKUDOS:1",
+ },
+ {
+ ...coinCommon,
+ name: "n5",
+ value: "TESTKUDOS:5",
+ },
+ ];
+
+ const { bank, exchange, merchant } = await createSimpleTestkudosEnvironmentV2(
+ t,
+ coinConfigList,
+ );
+
+ // Withdraw digital cash into the wallet.
+
+ const { walletClient: w1 } = await createWalletDaemonWithClient(t, {
+ name: "w1",
+ persistent: true,
+ config: {
+ testing: {
+ devModeActive: true,
+ },
+ },
+ });
+
+ await withdrawViaBankV2(t, {
+ walletClient: w1,
+ bank,
+ exchange,
+ amount: "TESTKUDOS:20",
+ });
+
+ await w1.call(WalletApiOperation.TestingWaitTransactionsFinal, {});
+
+ // Prevent the wallet from doing refreshes by injecting a 5xx
+ // status for all refresh requests.
+ await w1.call(WalletApiOperation.ApplyDevExperiment, {
+ devExperimentUri: "taler://dev-experiment/start-block-refresh",
+ });
+
+ // Do a payment that causes a refresh.
+ await makeTestPaymentV2(t, {
+ merchant,
+ walletClient: w1,
+ order: {
+ summary: "test",
+ amount: "TESTKUDOS:2",
+ },
+ });
+
+ const checkResp = await w1.call(WalletApiOperation.CheckPeerPushDebit, {
+ amount: "TESTKUDOS:18" as AmountString,
+ });
+
+ console.log(`check resp ${j2s(checkResp)}`);
+
+ const readyCond = w1.waitForNotificationCond((n) => {
+ return (
+ n.type === NotificationType.TransactionStateTransition &&
+ n.transactionId.startsWith("txn:peer-push-debit:") &&
+ n.newTxState.major === TransactionMajorState.Pending &&
+ n.newTxState.minor === TransactionMinorState.Ready
+ );
+ });
+
+ const confirmResp = await w1.call(WalletApiOperation.InitiatePeerPushDebit, {
+ partialContractTerms: {
+ summary: "hi!",
+ amount: "TESTKUDOS:18" as AmountString,
+ purse_expiration: AbsoluteTime.toProtocolTimestamp(
+ AbsoluteTime.addDuration(
+ AbsoluteTime.now(),
+ Duration.fromSpec({ hours: 1 }),
+ ),
+ ),
+ },
+ });
+
+ console.log(`confirm resp ${j2s(confirmResp)}`);
+
+ await w1.call(WalletApiOperation.ApplyDevExperiment, {
+ devExperimentUri: "taler://dev-experiment/stop-block-refresh",
+ });
+
+ await readyCond;
+}
+
+runWalletBlockedPayPeerPushTest.suites = ["wallet"];
diff --git a/packages/taler-harness/src/integrationtests/testrunner.ts b/packages/taler-harness/src/integrationtests/testrunner.ts
index 2934e36e3..acc9f5e29 100644
--- a/packages/taler-harness/src/integrationtests/testrunner.ts
+++ b/packages/taler-harness/src/integrationtests/testrunner.ts
@@ -92,6 +92,7 @@ import { runWalletBalanceZeroTest } from "./test-wallet-balance-zero.js";
import { runWalletBalanceTest } from "./test-wallet-balance.js";
import { runWalletBlockedDepositTest } from "./test-wallet-blocked-deposit.js";
import { runWalletBlockedPayMerchantTest } from "./test-wallet-blocked-pay-merchant.js";
+import { runWalletBlockedPayPeerPushTest } from "./test-wallet-blocked-pay-peer-push.js";
import { runWalletCliTerminationTest } from "./test-wallet-cli-termination.js";
import { runWalletConfigTest } from "./test-wallet-config.js";
import { runWalletCryptoWorkerTest } from "./test-wallet-cryptoworker.js";
@@ -216,6 +217,7 @@ const allTests: TestMainFunction[] = [
runWalletDenomExpireTest,
runWalletBlockedDepositTest,
runWalletBlockedPayMerchantTest,
+ runWalletBlockedPayPeerPushTest,
];
export interface TestRunSpec {
diff --git a/packages/taler-wallet-core/src/coinSelection.ts b/packages/taler-wallet-core/src/coinSelection.ts
index bce51fd91..0027241c4 100644
--- a/packages/taler-wallet-core/src/coinSelection.ts
+++ b/packages/taler-wallet-core/src/coinSelection.ts
@@ -1001,13 +1001,6 @@ export interface PeerCoinSelectionRequest {
* selection instead of selecting completely new coins.
*/
repair?: PreviousPayCoins;
-
- /**
- * If set to true, the coin selection will also use coins that are not
- * materially available yet, but that are expected to become available
- * as the output of a refresh operation.
- */
- includePendingCoins: boolean;
}
export async function computeCoinSelMaxExpirationDate(
@@ -1191,7 +1184,7 @@ export async function selectPeerCoins(
false,
);
- if (!avRes && req.includePendingCoins) {
+ if (!avRes) {
// Try to see if we can do a prospective selection
const prospectiveAvRes = await internalSelectPeerCoins(
wex,
diff --git a/packages/taler-wallet-core/src/db.ts b/packages/taler-wallet-core/src/db.ts
index a675fa8dd..6db85d8f6 100644
--- a/packages/taler-wallet-core/src/db.ts
+++ b/packages/taler-wallet-core/src/db.ts
@@ -1892,7 +1892,7 @@ export interface PeerPushDebitRecord {
totalCost: AmountString;
- coinSel: DbPeerPushPaymentCoinSelection;
+ coinSel?: DbPeerPushPaymentCoinSelection;
contractTermsHash: HashCodeString;
diff --git a/packages/taler-wallet-core/src/pay-peer-common.ts b/packages/taler-wallet-core/src/pay-peer-common.ts
index 599010c1d..6ad8ecb70 100644
--- a/packages/taler-wallet-core/src/pay-peer-common.ts
+++ b/packages/taler-wallet-core/src/pay-peer-common.ts
@@ -22,7 +22,7 @@ import {
AmountString,
Amounts,
Codec,
- SelectedCoin,
+ SelectedProspectiveCoin,
TalerProtocolTimestamp,
buildCodecForObject,
checkDbInvariant,
@@ -74,21 +74,17 @@ export async function queryCoinInfosForSelection(
export async function getTotalPeerPaymentCost(
wex: WalletExecutionContext,
- pcs: SelectedCoin[],
+ pcs: SelectedProspectiveCoin[],
): Promise<AmountJson> {
const currency = Amounts.currencyOf(pcs[0].contribution);
return wex.db.runReadOnlyTx(["coins", "denominations"], async (tx) => {
const costs: AmountJson[] = [];
for (let i = 0; i < pcs.length; i++) {
- const coin = await tx.coins.get(pcs[i].coinPub);
- if (!coin) {
- throw Error("can't calculate payment cost, coin not found");
- }
const denomInfo = await getDenomInfo(
wex,
tx,
- coin.exchangeBaseUrl,
- coin.denomPubHash,
+ pcs[i].exchangeBaseUrl,
+ pcs[i].denomPubHash,
);
if (!denomInfo) {
throw Error(
@@ -98,7 +94,7 @@ export async function getTotalPeerPaymentCost(
const allDenoms = await getCandidateWithdrawalDenomsTx(
wex,
tx,
- coin.exchangeBaseUrl,
+ pcs[i].exchangeBaseUrl,
currency,
);
const amountLeft = Amounts.sub(
diff --git a/packages/taler-wallet-core/src/pay-peer-pull-debit.ts b/packages/taler-wallet-core/src/pay-peer-pull-debit.ts
index 2cc241187..9bfa14ca2 100644
--- a/packages/taler-wallet-core/src/pay-peer-pull-debit.ts
+++ b/packages/taler-wallet-core/src/pay-peer-pull-debit.ts
@@ -373,7 +373,6 @@ async function handlePurseCreationConflict(
const coinSelRes = await selectPeerCoins(ws, {
instructedAmount,
repair,
- includePendingCoins: false,
});
switch (coinSelRes.type) {
@@ -600,7 +599,6 @@ export async function confirmPeerPullDebit(
const coinSelRes = await selectPeerCoins(wex, {
instructedAmount,
- includePendingCoins: false,
});
if (logger.shouldLogTrace()) {
logger.trace(`selected p2p coins (pull): ${j2s(coinSelRes)}`);
@@ -785,7 +783,6 @@ export async function preparePeerPullDebit(
const coinSelRes = await selectPeerCoins(wex, {
instructedAmount,
- includePendingCoins: true,
});
if (logger.shouldLogTrace()) {
logger.trace(`selected p2p coins (pull): ${j2s(coinSelRes)}`);
diff --git a/packages/taler-wallet-core/src/pay-peer-push-debit.ts b/packages/taler-wallet-core/src/pay-peer-push-debit.ts
index 51b865b99..b6771be89 100644
--- a/packages/taler-wallet-core/src/pay-peer-push-debit.ts
+++ b/packages/taler-wallet-core/src/pay-peer-push-debit.ts
@@ -26,6 +26,7 @@ import {
Logger,
NotificationType,
RefreshReason,
+ SelectedProspectiveCoin,
TalerError,
TalerErrorCode,
TalerPreciseTimestamp,
@@ -38,6 +39,7 @@ import {
TransactionState,
TransactionType,
assertUnreachable,
+ checkDbInvariant,
checkLogicInvariant,
encodeCrock,
getRandomBytes,
@@ -345,8 +347,8 @@ export async function checkPeerPushDebit(
);
const coinSelRes = await selectPeerCoins(wex, {
instructedAmount,
- includePendingCoins: true,
});
+ let coins: SelectedProspectiveCoin[] | undefined = undefined;
switch (coinSelRes.type) {
case "failure":
throw TalerError.fromDetail(
@@ -356,17 +358,16 @@ export async function checkPeerPushDebit(
},
);
case "prospective":
- throw Error("not supported");
+ coins = coinSelRes.result.prospectiveCoins;
+ break;
case "success":
+ coins = coinSelRes.result.coins;
break;
default:
assertUnreachable(coinSelRes);
}
- logger.trace(`selected peer coins (len=${coinSelRes.result.coins.length})`);
- const totalAmount = await getTotalPeerPaymentCost(
- wex,
- coinSelRes.result.coins,
- );
+ logger.trace(`selected peer coins (len=${coins.length})`);
+ const totalAmount = await getTotalPeerPaymentCost(wex, coins);
logger.trace("computed total peer payment cost");
return {
exchangeBaseUrl: coinSelRes.result.exchangeBaseUrl,
@@ -401,6 +402,8 @@ async function handlePurseCreationConflict(
const instructedAmount = Amounts.parseOrThrow(peerPushInitiation.amount);
const sel = peerPushInitiation.coinSel;
+ checkDbInvariant(!!sel);
+
const repair: PreviousPayCoins = [];
for (let i = 0; i < sel.coinPubs.length; i++) {
@@ -415,7 +418,6 @@ async function handlePurseCreationConflict(
const coinSelRes = await selectPeerCoins(wex, {
instructedAmount,
repair,
- includePendingCoins: false,
});
switch (coinSelRes.type) {
@@ -479,6 +481,75 @@ async function processPeerPushDebitCreateReserve(
);
}
+ if (!peerPushInitiation.coinSel) {
+ const coinSelRes = await selectPeerCoins(wex, {
+ instructedAmount: Amounts.parseOrThrow(peerPushInitiation.amount),
+ });
+
+ switch (coinSelRes.type) {
+ case "failure":
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_PEER_PUSH_PAYMENT_INSUFFICIENT_BALANCE,
+ {
+ insufficientBalanceDetails: coinSelRes.insufficientBalanceDetails,
+ },
+ );
+ case "prospective":
+ throw Error("insufficient funds (blocked on refresh)");
+ case "success":
+ break;
+ default:
+ assertUnreachable(coinSelRes);
+ }
+ const transitionDone = await wex.db.runReadWriteTx(
+ [
+ "exchanges",
+ "contractTerms",
+ "coins",
+ "coinAvailability",
+ "denominations",
+ "refreshGroups",
+ "refreshSessions",
+ "peerPushDebit",
+ ],
+ async (tx) => {
+ const ppi = await tx.peerPushDebit.get(pursePub);
+ if (!ppi) {
+ return false;
+ }
+ if (ppi.coinSel) {
+ return false;
+ }
+
+ ppi.coinSel = {
+ coinPubs: coinSelRes.result.coins.map((x) => x.coinPub),
+ contributions: coinSelRes.result.coins.map((x) => x.contribution),
+ };
+ // FIXME: Instead of directly doing a spendCoin here,
+ // we might want to mark the coins as used and spend them
+ // after we've been able to create the purse.
+ await spendCoins(wex, tx, {
+ allocationId: constructTransactionIdentifier({
+ tag: TransactionType.PeerPushDebit,
+ pursePub,
+ }),
+ coinPubs: coinSelRes.result.coins.map((x) => x.coinPub),
+ contributions: coinSelRes.result.coins.map((x) =>
+ Amounts.parseOrThrow(x.contribution),
+ ),
+ refreshReason: RefreshReason.PayPeerPush,
+ });
+
+ await tx.peerPushDebit.put(ppi);
+ return true;
+ },
+ );
+ if (transitionDone) {
+ return TaskRunResult.progress();
+ }
+ return TaskRunResult.backoff();
+ }
+
const purseSigResp = await wex.cryptoApi.signPurseCreation({
hContractTerms,
mergePub: peerPushInitiation.mergePub,
@@ -625,6 +696,10 @@ async function processPeerPushDebitAbortingDeletePurse(
const oldTxState = computePeerPushDebitTransactionState(ppiRec);
const coinPubs: CoinRefreshRequest[] = [];
+ if (!ppiRec.coinSel) {
+ return undefined;
+ }
+
for (let i = 0; i < ppiRec.coinSel.coinPubs.length; i++) {
coinPubs.push({
amount: ppiRec.coinSel.contributions[i],
@@ -859,23 +934,26 @@ async function processPeerPushDebitReady(
const oldTxState = computePeerPushDebitTransactionState(ppiRec);
const coinPubs: CoinRefreshRequest[] = [];
- for (let i = 0; i < ppiRec.coinSel.coinPubs.length; i++) {
- coinPubs.push({
- amount: ppiRec.coinSel.contributions[i],
- coinPub: ppiRec.coinSel.coinPubs[i],
- });
+ if (ppiRec.coinSel) {
+ for (let i = 0; i < ppiRec.coinSel.coinPubs.length; i++) {
+ coinPubs.push({
+ amount: ppiRec.coinSel.contributions[i],
+ coinPub: ppiRec.coinSel.coinPubs[i],
+ });
+ }
+
+ const refresh = await createRefreshGroup(
+ wex,
+ tx,
+ currency,
+ coinPubs,
+ RefreshReason.AbortPeerPushDebit,
+ transactionId,
+ );
+
+ ppiRec.abortRefreshGroupId = refresh.refreshGroupId;
}
-
- const refresh = await createRefreshGroup(
- wex,
- tx,
- currency,
- coinPubs,
- RefreshReason.AbortPeerPushDebit,
- transactionId,
- );
ppiRec.status = PeerPushDebitStatus.AbortingRefreshExpired;
- ppiRec.abortRefreshGroupId = refresh.refreshGroupId;
await tx.peerPushDebit.put(ppiRec);
const newTxState = computePeerPushDebitTransactionState(ppiRec);
return {
@@ -954,12 +1032,12 @@ export async function initiatePeerPushDebit(
const contractKeyPair = await wex.cryptoApi.createEddsaKeypair({});
- // FIXME: Check first if possible with pending coins, in that case defer coin selection
const coinSelRes = await selectPeerCoins(wex, {
instructedAmount,
- includePendingCoins: false,
});
+ let coins: SelectedProspectiveCoin[] | undefined = undefined;
+
switch (coinSelRes.type) {
case "failure":
throw TalerError.fromDetail(
@@ -969,8 +1047,10 @@ export async function initiatePeerPushDebit(
},
);
case "prospective":
- throw Error("blocked on pending refresh");
+ coins = coinSelRes.result.prospectiveCoins;
+ break;
case "success":
+ coins = coinSelRes.result.coins;
break;
default:
assertUnreachable(coinSelRes);
@@ -981,10 +1061,7 @@ export async function initiatePeerPushDebit(
logger.info(`selected p2p coins (push):`);
logger.trace(`${j2s(coinSelRes)}`);
- const totalAmount = await getTotalPeerPaymentCost(
- wex,
- coinSelRes.result.coins,
- );
+ const totalAmount = await getTotalPeerPaymentCost(wex, coins);
logger.info(`computed total peer payment cost`);
@@ -1008,21 +1085,6 @@ export async function initiatePeerPushDebit(
"peerPushDebit",
],
async (tx) => {
- // FIXME: Instead of directly doing a spendCoin here,
- // we might want to mark the coins as used and spend them
- // after we've been able to create the purse.
- await spendCoins(wex, tx, {
- allocationId: constructTransactionIdentifier({
- tag: TransactionType.PeerPushDebit,
- pursePub: pursePair.pub,
- }),
- coinPubs: sel.coins.map((x) => x.coinPub),
- contributions: sel.coins.map((x) =>
- Amounts.parseOrThrow(x.contribution),
- ),
- refreshReason: RefreshReason.PayPeerPush,
- });
-
const ppi: PeerPushDebitRecord = {
amount: Amounts.stringify(instructedAmount),
contractPriv: contractKeyPair.priv,
@@ -1037,13 +1099,30 @@ export async function initiatePeerPushDebit(
timestampCreated: timestampPreciseToDb(TalerPreciseTimestamp.now()),
status: PeerPushDebitStatus.PendingCreatePurse,
contractEncNonce,
- coinSel: {
- coinPubs: sel.coins.map((x) => x.coinPub),
- contributions: sel.coins.map((x) => x.contribution),
- },
totalCost: Amounts.stringify(totalAmount),
};
+ if (coinSelRes.type === "success") {
+ ppi.coinSel = {
+ coinPubs: coinSelRes.result.coins.map((x) => x.coinPub),
+ contributions: coinSelRes.result.coins.map((x) => x.contribution),
+ };
+ // FIXME: Instead of directly doing a spendCoin here,
+ // we might want to mark the coins as used and spend them
+ // after we've been able to create the purse.
+ await spendCoins(wex, tx, {
+ allocationId: constructTransactionIdentifier({
+ tag: TransactionType.PeerPushDebit,
+ pursePub: pursePair.pub,
+ }),
+ coinPubs: coinSelRes.result.coins.map((x) => x.coinPub),
+ contributions: coinSelRes.result.coins.map((x) =>
+ Amounts.parseOrThrow(x.contribution),
+ ),
+ refreshReason: RefreshReason.PayPeerPush,
+ });
+ }
+
await tx.peerPushDebit.add(ppi);
await tx.contractTerms.put({