commit 6d9b300703d82093f340e6498aeb970fd811beba
parent c67217eea575b863b92b63061e2c7152d07dedbb
Author: Florian Dold <florian@dold.me>
Date: Mon, 21 Oct 2024 18:57:31 +0200
wallet-core: improve scopeInfo support for p2p transactions
Diffstat:
8 files changed, 265 insertions(+), 79 deletions(-)
diff --git a/packages/taler-util/src/types-taler-wallet.ts b/packages/taler-util/src/types-taler-wallet.ts
@@ -1498,6 +1498,11 @@ export interface AcceptManualWithdrawalResult {
export interface WithdrawalDetailsForAmount {
/**
+ * Exchange base URL for the withdrawal.
+ */
+ exchangeBaseUrl: string;
+
+ /**
* Did the user accept the current version of the exchange's
* terms of service?
*
@@ -1839,8 +1844,12 @@ export const codecForAcceptManualWithdrawalRequest =
.build("AcceptManualWithdrawalRequest");
export interface GetWithdrawalDetailsForAmountRequest {
- exchangeBaseUrl: string;
+ exchangeBaseUrl?: string;
+
+ restrictScope?: ScopeInfo;
+
amount: AmountString;
+
restrictAge?: number;
/**
@@ -1917,7 +1926,8 @@ export const codecForAcceptBankIntegratedWithdrawalRequest =
export const codecForGetWithdrawalDetailsForAmountRequest =
(): Codec<GetWithdrawalDetailsForAmountRequest> =>
buildCodecForObject<GetWithdrawalDetailsForAmountRequest>()
- .property("exchangeBaseUrl", codecForCanonBaseUrl())
+ .property("exchangeBaseUrl", codecOptional(codecForCanonBaseUrl()))
+ .property("restrictScope", codecOptional(codecForScopeInfo()))
.property("amount", codecForAmountString())
.property("restrictAge", codecOptional(codecForNumber()))
.property("clientCancellationId", codecOptional(codecForString()))
@@ -2824,14 +2834,21 @@ export const codecForCheckPeerPushDebitRequest =
(): Codec<CheckPeerPushDebitRequest> =>
buildCodecForObject<CheckPeerPushDebitRequest>()
.property("exchangeBaseUrl", codecOptional(codecForCanonBaseUrl()))
+ .property("restrictScope", codecOptional(codecForScopeInfo()))
.property("amount", codecForAmountString())
.property("clientCancellationId", codecOptional(codecForString()))
.build("CheckPeerPushDebitRequest");
export interface CheckPeerPushDebitResponse {
amountRaw: AmountString;
+
amountEffective: AmountString;
+
+ /**
+ * Exchange base URL.
+ */
exchangeBaseUrl: string;
+
/**
* Maximum expiration date, based on how close the coins
* used for the payment are to expiry.
@@ -2887,6 +2904,8 @@ export interface PreparePeerPushCreditResponse {
exchangeBaseUrl: string;
+ scopeInfo: ScopeInfo;
+
/**
* @deprecated use transaction ID instead.
*/
@@ -2900,17 +2919,25 @@ export interface PreparePeerPushCreditResponse {
export interface PreparePeerPullDebitResponse {
contractTerms: PeerContractTerms;
- /**
- * @deprecated Redundant field with bad name, will be removed soon.
- */
- amount: AmountString;
amountRaw: AmountString;
amountEffective: AmountString;
+ transactionId: TransactionIdStr;
+
+ exchangeBaseUrl: string;
+
+ scopeInfo: ScopeInfo;
+
+ /**
+ * @deprecated Use transactionId instead
+ */
peerPullDebitId: string;
- transactionId: TransactionIdStr;
+ /**
+ * @deprecated Redundant field with bad name, will be removed soon.
+ */
+ amount: AmountString;
}
export const codecForPreparePeerPushCreditRequest =
@@ -2963,7 +2990,13 @@ export const codecForAcceptPeerPullPaymentRequest =
.build("ConfirmPeerPullDebitRequest");
export interface CheckPeerPullCreditRequest {
+ /**
+ * Require using this particular exchange for this operation.
+ */
exchangeBaseUrl?: string;
+
+ restrictScope?: ScopeInfo;
+
amount: AmountString;
/**
@@ -2985,6 +3018,7 @@ export const codecForPreparePeerPullPaymentRequest =
buildCodecForObject<CheckPeerPullCreditRequest>()
.property("amount", codecForAmountString())
.property("exchangeBaseUrl", codecOptional(codecForCanonBaseUrl()))
+ .property("restrictScope", codecOptional(codecForScopeInfo()))
.property("clientCancellationId", codecOptional(codecForString()))
.build("CheckPeerPullCreditRequest");
diff --git a/packages/taler-wallet-core/src/exchanges.ts b/packages/taler-wallet-core/src/exchanges.ts
@@ -3640,3 +3640,116 @@ export async function checkExchangeInScopeTx(
throw Error("auditor scope not supported yet");
}
}
+
+/**
+ * Find a preferred exchange based on when we withdrew last from this exchange.
+ */
+export async function getPreferredExchangeForCurrency(
+ wex: WalletExecutionContext,
+ currency: string,
+): Promise<string | undefined> {
+ // Find an exchange with the matching currency.
+ // Prefer exchanges with the most recent withdrawal.
+ const url = await wex.db.runReadOnlyTx(
+ { storeNames: ["exchanges"] },
+ async (tx) => {
+ const exchanges = await tx.exchanges.iter().toArray();
+ let candidate = undefined;
+ for (const e of exchanges) {
+ if (e.detailsPointer?.currency !== currency) {
+ continue;
+ }
+ if (!candidate) {
+ candidate = e;
+ continue;
+ }
+ if (candidate.lastWithdrawal && !e.lastWithdrawal) {
+ continue;
+ }
+ const exchangeLastWithdrawal = timestampOptionalPreciseFromDb(
+ e.lastWithdrawal,
+ );
+ const candidateLastWithdrawal = timestampOptionalPreciseFromDb(
+ candidate.lastWithdrawal,
+ );
+ if (exchangeLastWithdrawal && candidateLastWithdrawal) {
+ if (
+ AbsoluteTime.cmp(
+ AbsoluteTime.fromPreciseTimestamp(exchangeLastWithdrawal),
+ AbsoluteTime.fromPreciseTimestamp(candidateLastWithdrawal),
+ ) > 0
+ ) {
+ candidate = e;
+ }
+ }
+ }
+ if (candidate) {
+ return candidate.baseUrl;
+ }
+ return undefined;
+ },
+ );
+ return url;
+}
+
+/**
+ * Find a preferred exchange based on when we withdrew last from this exchange.
+ */
+export async function getPreferredExchangeForScope(
+ wex: WalletExecutionContext,
+ scope: ScopeInfo,
+): Promise<string | undefined> {
+ // Find an exchange with the matching currency.
+ // Prefer exchanges with the most recent withdrawal.
+ const url = await wex.db.runReadOnlyTx(
+ {
+ storeNames: [
+ "exchanges",
+ "globalCurrencyAuditors",
+ "globalCurrencyExchanges",
+ "exchangeDetails",
+ ],
+ },
+ async (tx) => {
+ const exchanges = await tx.exchanges.iter().toArray();
+ let candidate = undefined;
+ for (const e of exchanges) {
+ const inScope = await checkExchangeInScopeTx(wex, tx, e.baseUrl, scope);
+ if (!inScope) {
+ continue;
+ }
+ if (e.detailsPointer?.currency !== scope.currency) {
+ continue;
+ }
+ if (!candidate) {
+ candidate = e;
+ continue;
+ }
+ if (candidate.lastWithdrawal && !e.lastWithdrawal) {
+ continue;
+ }
+ const exchangeLastWithdrawal = timestampOptionalPreciseFromDb(
+ e.lastWithdrawal,
+ );
+ const candidateLastWithdrawal = timestampOptionalPreciseFromDb(
+ candidate.lastWithdrawal,
+ );
+ if (exchangeLastWithdrawal && candidateLastWithdrawal) {
+ if (
+ AbsoluteTime.cmp(
+ AbsoluteTime.fromPreciseTimestamp(exchangeLastWithdrawal),
+ AbsoluteTime.fromPreciseTimestamp(candidateLastWithdrawal),
+ ) > 0
+ ) {
+ candidate = e;
+ }
+ }
+ }
+ if (candidate) {
+ return candidate.baseUrl;
+ }
+ return undefined;
+ },
+ );
+ return url;
+}
diff --git a/packages/taler-wallet-core/src/pay-peer-pull-credit.ts b/packages/taler-wallet-core/src/pay-peer-pull-credit.ts
@@ -18,7 +18,6 @@
* Imports.
*/
import {
- AbsoluteTime,
Amounts,
CheckPeerPullCreditRequest,
CheckPeerPullCreditResponse,
@@ -31,6 +30,8 @@ import {
Logger,
NotificationType,
PeerContractTerms,
+ ScopeInfo,
+ ScopeType,
TalerErrorCode,
TalerErrorDetail,
TalerPreciseTimestamp,
@@ -84,7 +85,6 @@ import {
WithdrawalGroupRecord,
WithdrawalGroupStatus,
WithdrawalRecordType,
- timestampOptionalPreciseFromDb,
timestampPreciseFromDb,
timestampPreciseToDb,
} from "./db.js";
@@ -92,6 +92,8 @@ import {
BalanceThresholdCheckResult,
checkIncomingAmountLegalUnderKycBalanceThreshold,
fetchFreshExchange,
+ getPreferredExchangeForCurrency,
+ getPreferredExchangeForScope,
getScopeForAllExchanges,
handleStartExchangeWalletKyc,
} from "./exchanges.js";
@@ -1346,14 +1348,34 @@ export async function internalCheckPeerPullCredit(
): Promise<CheckPeerPullCreditResponse> {
// FIXME: We don't support exchanges with purse fees yet.
// Select an exchange where we have money in the specified currency
- // FIXME: How do we handle regional currency scopes here? Is it an additional input?
+
+ const instructedAmount = Amounts.parseOrThrow(req.amount);
+ const currency = instructedAmount.currency;
+
+ logger.trace(
+ `checking peer push debit for ${Amounts.stringify(instructedAmount)}`,
+ );
+
+ let restrictScope: ScopeInfo;
+ if (req.restrictScope) {
+ restrictScope = req.restrictScope;
+ } else if (req.exchangeBaseUrl) {
+ restrictScope = {
+ type: ScopeType.Exchange,
+ currency,
+ url: req.exchangeBaseUrl,
+ };
+ } else {
+ throw Error("client must either specify exchangeBaseUrl or restrictScope");
+ }
logger.trace("checking peer-pull-credit fees");
- const currency = Amounts.currencyOf(req.amount);
- let exchangeUrl;
+ let exchangeUrl: string | undefined;
if (req.exchangeBaseUrl) {
exchangeUrl = req.exchangeBaseUrl;
+ } else if (req.restrictScope) {
+ exchangeUrl = await getPreferredExchangeForScope(wex, req.restrictScope);
} else {
exchangeUrl = await getPreferredExchangeForCurrency(wex, currency);
}
@@ -1392,57 +1414,6 @@ export async function internalCheckPeerPullCredit(
}
/**
- * Find a preferred exchange based on when we withdrew last from this exchange.
- */
-async function getPreferredExchangeForCurrency(
- wex: WalletExecutionContext,
- currency: string,
-): Promise<string | undefined> {
- // Find an exchange with the matching currency.
- // Prefer exchanges with the most recent withdrawal.
- const url = await wex.db.runReadOnlyTx(
- { storeNames: ["exchanges"] },
- async (tx) => {
- const exchanges = await tx.exchanges.iter().toArray();
- let candidate = undefined;
- for (const e of exchanges) {
- if (e.detailsPointer?.currency !== currency) {
- continue;
- }
- if (!candidate) {
- candidate = e;
- continue;
- }
- if (candidate.lastWithdrawal && !e.lastWithdrawal) {
- continue;
- }
- const exchangeLastWithdrawal = timestampOptionalPreciseFromDb(
- e.lastWithdrawal,
- );
- const candidateLastWithdrawal = timestampOptionalPreciseFromDb(
- candidate.lastWithdrawal,
- );
- if (exchangeLastWithdrawal && candidateLastWithdrawal) {
- if (
- AbsoluteTime.cmp(
- AbsoluteTime.fromPreciseTimestamp(exchangeLastWithdrawal),
- AbsoluteTime.fromPreciseTimestamp(candidateLastWithdrawal),
- ) > 0
- ) {
- candidate = e;
- }
- }
- }
- if (candidate) {
- return candidate.baseUrl;
- }
- return undefined;
- },
- );
- return url;
-}
-
-/**
* Initiate a peer pull payment.
*/
export async function initiatePeerPullPayment(
diff --git a/packages/taler-wallet-core/src/pay-peer-pull-debit.ts b/packages/taler-wallet-core/src/pay-peer-pull-debit.ts
@@ -90,7 +90,7 @@ import {
timestampPreciseFromDb,
timestampPreciseToDb,
} from "./db.js";
-import { getScopeForAllExchanges } from "./exchanges.js";
+import { getExchangeScopeInfo, getScopeForAllExchanges } from "./exchanges.js";
import {
codecForExchangePurseStatus,
getTotalPeerPaymentCost,
@@ -897,7 +897,16 @@ export async function preparePeerPullDebit(
}
const existing = await wex.db.runReadOnlyTx(
- { storeNames: ["peerPullDebit", "contractTerms"] },
+ {
+ storeNames: [
+ "peerPullDebit",
+ "contractTerms",
+ "exchangeDetails",
+ "exchanges",
+ "globalCurrencyAuditors",
+ "globalCurrencyExchanges",
+ ],
+ },
async (tx) => {
const peerPullDebitRecord =
await tx.peerPullDebit.indexes.byExchangeAndContractPriv.get([
@@ -913,7 +922,18 @@ export async function preparePeerPullDebit(
if (!contractTerms) {
return;
}
- return { peerPullDebitRecord, contractTerms };
+ const currency = Amounts.currencyOf(peerPullDebitRecord.amount);
+ const scopeInfo = await getExchangeScopeInfo(
+ tx,
+ peerPullDebitRecord.exchangeBaseUrl,
+ currency,
+ );
+ return {
+ peerPullDebitRecord,
+ contractTerms,
+ scopeInfo,
+ exchangeBaseUrl: peerPullDebitRecord.exchangeBaseUrl,
+ };
},
);
@@ -924,6 +944,8 @@ export async function preparePeerPullDebit(
amountEffective: existing.peerPullDebitRecord.totalCostEstimated,
contractTerms: existing.contractTerms.contractTermsRaw,
peerPullDebitId: existing.peerPullDebitRecord.peerPullDebitId,
+ scopeInfo: existing.scopeInfo,
+ exchangeBaseUrl: existing.exchangeBaseUrl,
transactionId: constructTransactionIdentifier({
tag: TransactionType.PeerPullDebit,
peerPullDebitId: existing.peerPullDebitRecord.peerPullDebitId,
@@ -1008,6 +1030,7 @@ export async function preparePeerPullDebit(
}
const totalAmount = await getTotalPeerPaymentCost(wex, coins);
+ const currency = Amounts.currencyOf(totalAmount);
const ctx = new PeerPullDebitTransactionContext(wex, peerPullDebitId);
@@ -1033,16 +1056,19 @@ export async function preparePeerPullDebit(
},
);
+ const scopeInfo = await wex.db.runAllStoresReadOnlyTx({}, (tx) => {
+ return getExchangeScopeInfo(tx, exchangeBaseUrl, currency);
+ });
+
return {
amount: contractTerms.amount,
amountEffective: Amounts.stringify(totalAmount),
amountRaw: contractTerms.amount,
contractTerms: contractTerms,
peerPullDebitId,
- transactionId: constructTransactionIdentifier({
- tag: TransactionType.PeerPullDebit,
- peerPullDebitId: peerPullDebitId,
- }),
+ scopeInfo,
+ exchangeBaseUrl,
+ transactionId: ctx.transactionId,
};
}
diff --git a/packages/taler-wallet-core/src/pay-peer-push-credit.ts b/packages/taler-wallet-core/src/pay-peer-push-credit.ts
@@ -29,6 +29,7 @@ import {
PeerContractTerms,
PreparePeerPushCreditRequest,
PreparePeerPushCreditResponse,
+ ScopeType,
TalerErrorDetail,
TalerPreciseTimestamp,
Transaction,
@@ -630,6 +631,7 @@ export async function preparePeerPushCredit(
);
if (existing) {
+ const currency = Amounts.currencyOf(existing.existingContractTerms.amount);
return {
amount: existing.existingContractTerms.amount,
amountEffective: existing.existingPushInc.estimatedAmountEffective,
@@ -640,6 +642,12 @@ export async function preparePeerPushCredit(
tag: TransactionType.PeerPushCredit,
peerPushCreditId: existing.existingPushInc.peerPushCreditId,
}),
+ // FIXME: Shouldn't we place this in a tighter scope?
+ scopeInfo: {
+ type: ScopeType.Exchange,
+ currency,
+ url: existing.existingPushInc.exchangeBaseUrl,
+ },
exchangeBaseUrl: existing.existingPushInc.exchangeBaseUrl,
...getPeerCreditLimitInfo(
exchange,
@@ -742,12 +750,9 @@ export async function preparePeerPushCredit(
},
);
- const transactionId = constructTransactionIdentifier({
- tag: TransactionType.PeerPushCredit,
- peerPushCreditId,
- });
+ const currency = Amounts.currencyOf(wi.withdrawalAmountRaw);
- notifyTransition(wex, transactionId, transitionInfo);
+ notifyTransition(wex, ctx.transactionId, transitionInfo);
return {
amount: purseStatus.balance,
@@ -755,8 +760,14 @@ export async function preparePeerPushCredit(
amountRaw: purseStatus.balance,
contractTerms: dec.contractTerms,
peerPushCreditId,
- transactionId,
+ transactionId: ctx.transactionId,
exchangeBaseUrl,
+ // FIXME: Shouldn't we place this in a tighter scope?
+ scopeInfo: {
+ type: ScopeType.Exchange,
+ currency,
+ url: exchangeBaseUrl,
+ },
...getPeerCreditLimitInfo(exchange, purseStatus.balance),
};
}
diff --git 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,8 @@ import {
InitiatePeerPushDebitResponse,
Logger,
RefreshReason,
+ ScopeInfo,
+ ScopeType,
SelectedProspectiveCoin,
TalerError,
TalerErrorCode,
@@ -452,12 +454,25 @@ async function internalCheckPeerPushDebit(
req: CheckPeerPushDebitRequest,
): Promise<CheckPeerPushDebitResponse> {
const instructedAmount = Amounts.parseOrThrow(req.amount);
+ const currency = instructedAmount.currency;
logger.trace(
`checking peer push debit for ${Amounts.stringify(instructedAmount)}`,
);
+ let restrictScope: ScopeInfo;
+ if (req.restrictScope) {
+ restrictScope = req.restrictScope;
+ } else if (req.exchangeBaseUrl) {
+ restrictScope = {
+ type: ScopeType.Exchange,
+ currency,
+ url: req.exchangeBaseUrl,
+ };
+ } else {
+ throw Error("client must either specify exchangeBaseUrl or restrictScope");
+ }
const coinSelRes = await selectPeerCoins(wex, {
instructedAmount,
- restrictScope: req.restrictScope,
+ restrictScope,
feesCoveredByCounterparty: false,
});
let coins: SelectedProspectiveCoin[] | undefined = undefined;
diff --git a/packages/taler-wallet-core/src/withdraw.ts b/packages/taler-wallet-core/src/withdraw.ts
@@ -167,6 +167,7 @@ import {
fetchFreshExchange,
getExchangePaytoUri,
getExchangeWireDetailsInTx,
+ getPreferredExchangeForScope,
getScopeForAllExchanges,
handleStartExchangeWalletKyc,
listExchanges,
@@ -4044,9 +4045,21 @@ export async function internalGetWithdrawalDetailsForAmount(
wex: WalletExecutionContext,
req: GetWithdrawalDetailsForAmountRequest,
): Promise<WithdrawalDetailsForAmount> {
+ let exchangeBaseUrl: string | undefined;
+ if (req.exchangeBaseUrl) {
+ exchangeBaseUrl = req.exchangeBaseUrl;
+ } else if (req.restrictScope) {
+ exchangeBaseUrl = await getPreferredExchangeForScope(
+ wex,
+ req.restrictScope,
+ );
+ }
+ if (!exchangeBaseUrl) {
+ throw Error("could not find exchange for withdrawal");
+ }
const wi = await getExchangeWithdrawalInfo(
wex,
- req.exchangeBaseUrl,
+ exchangeBaseUrl,
Amounts.parseOrThrow(req.amount),
req.restrictAge,
);
@@ -4055,6 +4068,7 @@ export async function internalGetWithdrawalDetailsForAmount(
numCoins += x.count;
}
const resp: WithdrawalDetailsForAmount = {
+ exchangeBaseUrl,
amountRaw: req.amount,
amountEffective: Amounts.stringify(wi.selectedDenoms.totalCoinValue),
paytoUris: wi.exchangePaytoUris,
diff --git a/packages/taler-wallet-webextension/src/cta/Withdraw/test.ts b/packages/taler-wallet-webextension/src/cta/Withdraw/test.ts
@@ -29,8 +29,8 @@ import {
TransactionIdStr,
} from "@gnu-taler/taler-util";
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
-import { expect } from "chai";
import * as tests from "@gnu-taler/web-util/testing";
+import { expect } from "chai";
import { createWalletApiMock } from "../../test-utils.js";
import { useComponentStateFromURI } from "./state.js";
@@ -186,6 +186,7 @@ describe("Withdraw CTA states", () => {
type: ScopeType.Exchange,
url: "http://asd",
},
+ exchangeBaseUrl: "http://asd",
withdrawalAccountsList: [],
ageRestrictionOptions: [],
numCoins: 42,
@@ -262,6 +263,7 @@ describe("Withdraw CTA states", () => {
type: ScopeType.Exchange,
url: "http://asd",
},
+ exchangeBaseUrl: "http://asd",
tosAccepted: false,
withdrawalAccountsList: [],
ageRestrictionOptions: [],