summaryrefslogtreecommitdiff
path: root/packages/taler-wallet-core
diff options
context:
space:
mode:
authorFlorian Dold <florian@dold.me>2024-04-04 15:43:12 +0200
committerFlorian Dold <florian@dold.me>2024-04-04 15:43:12 +0200
commite7a966f755e78bf9ec200f29e49706045d1e1a54 (patch)
treef6843b80fbf190b547aed3f3c37d4ae1720b5d7e /packages/taler-wallet-core
parentac8adbc4a1a93664763d7e35bade940d598d0f74 (diff)
downloadwallet-core-e7a966f755e78bf9ec200f29e49706045d1e1a54.tar.gz
wallet-core-e7a966f755e78bf9ec200f29e49706045d1e1a54.tar.bz2
wallet-core-e7a966f755e78bf9ec200f29e49706045d1e1a54.zip
wallet-core: allow peer-push with coins locked behind refresh
Diffstat (limited to 'packages/taler-wallet-core')
-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
5 files changed, 135 insertions, 70 deletions
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({