diff options
-rw-r--r-- | packages/taler-util/src/wallet-types.ts | 18 | ||||
-rw-r--r-- | packages/taler-wallet-core/src/coinSelection.ts | 38 | ||||
-rw-r--r-- | packages/taler-wallet-core/src/db.ts | 10 | ||||
-rw-r--r-- | packages/taler-wallet-core/src/exchanges.ts | 64 |
4 files changed, 130 insertions, 0 deletions
diff --git a/packages/taler-util/src/wallet-types.ts b/packages/taler-util/src/wallet-types.ts index 723e5a282..7b6da8a40 100644 --- a/packages/taler-util/src/wallet-types.ts +++ b/packages/taler-util/src/wallet-types.ts @@ -884,6 +884,11 @@ export interface PaymentInsufficientBalanceDetails { balanceReceiverAcceptable: AmountString; balanceReceiverDepositable: AmountString; maxEffectiveSpendAmount: AmountString; + /** + * Exchange doesn't have global fees configured for the relevant year, + * p2p payments aren't possible. + */ + missingGlobalFees: boolean; }; }; } @@ -1394,6 +1399,17 @@ export interface ExchangeListItem { exchangeUpdateStatus: ExchangeUpdateStatus; ageRestrictionOptions: number[]; + /** + * P2P payments are disabled with this exchange + * (e.g. because no global fees are configured). + */ + peerPaymentsDisabled: boolean; + + /** + * Set to true if this exchange doesn't charge any fees. + */ + noFees: boolean; + scopeInfo: ScopeInfo | undefined; lastUpdateTimestamp: TalerPreciseTimestamp | undefined; @@ -1473,6 +1489,8 @@ export const codecForExchangeListItem = (): Codec<ExchangeListItem> => .property("scopeInfo", codecForScopeInfo()) .property("lastUpdateErrorInfo", codecForAny()) .property("lastUpdateTimestamp", codecOptional(codecForPreciseTimestamp)) + .property("noFees", codecForBoolean()) + .property("peerPaymentsDisabled", codecForBoolean()) .build("ExchangeListItem"); export const codecForExchangesListResponse = (): Codec<ExchangesListResponse> => diff --git a/packages/taler-wallet-core/src/coinSelection.ts b/packages/taler-wallet-core/src/coinSelection.ts index 7c22f63db..cf323e586 100644 --- a/packages/taler-wallet-core/src/coinSelection.ts +++ b/packages/taler-wallet-core/src/coinSelection.ts @@ -36,6 +36,7 @@ import { checkLogicInvariant, CoinStatus, DenominationInfo, + ExchangeGlobalFees, ForcedCoinSel, InternationalizedString, j2s, @@ -400,6 +401,16 @@ export async function reportInsufficientBalanceDetails( if (!exch.detailsPointer) { continue; } + let missingGlobalFees = false; + const exchWire = await getExchangeWireDetailsInTx(tx, exch.baseUrl); + if (!exchWire) { + missingGlobalFees = true; + } else { + const globalFees = getGlobalFees(exchWire); + if (!globalFees) { + missingGlobalFees = true; + } + } const exchDet = await getPaymentBalanceDetailsInTx(wex, tx, { restrictExchanges: { exchanges: [ @@ -431,6 +442,7 @@ export async function reportInsufficientBalanceDetails( maxEffectiveSpendAmount: Amounts.stringify( exchDet.maxEffectiveSpendAmount, ), + missingGlobalFees, }; } @@ -900,6 +912,24 @@ export function emptyTallyForPeerPayment( }; } +function getGlobalFees( + wireDetails: ExchangeWireDetails, +): ExchangeGlobalFees | undefined { + const now = AbsoluteTime.now(); + for (let gf of wireDetails.globalFees) { + const isActive = AbsoluteTime.isBetween( + now, + AbsoluteTime.fromProtocolTimestamp(gf.startDate), + AbsoluteTime.fromProtocolTimestamp(gf.endDate), + ); + if (!isActive) { + continue; + } + return gf; + } + return undefined; +} + export async function selectPeerCoins( wex: WalletExecutionContext, req: PeerCoinSelectionRequest, @@ -928,6 +958,14 @@ export async function selectPeerCoins( if (exch.detailsPointer?.currency !== currency) { continue; } + const exchWire = await getExchangeWireDetailsInTx(tx, exch.baseUrl); + if (!exchWire) { + continue; + } + const globalFees = getGlobalFees(exchWire); + if (!globalFees) { + continue; + } const candidatesRes = await selectPayCandidates(wex, tx, { instructedAmount, restrictExchanges: { diff --git a/packages/taler-wallet-core/src/db.ts b/packages/taler-wallet-core/src/db.ts index aad3b2d7b..92cf63ae1 100644 --- a/packages/taler-wallet-core/src/db.ts +++ b/packages/taler-wallet-core/src/db.ts @@ -688,6 +688,16 @@ export interface ExchangeEntryRecord { * receiving P2P payments. */ currentMergeReserveRowId?: number; + + /** + * Defaults to false. + */ + peerPaymentsDisabled?: boolean; + + /** + * Defaults to false. + */ + noFees?: boolean; } export enum PlanchetStatus { diff --git a/packages/taler-wallet-core/src/exchanges.ts b/packages/taler-wallet-core/src/exchanges.ts index d501789a8..152bc76ce 100644 --- a/packages/taler-wallet-core/src/exchanges.ts +++ b/packages/taler-wallet-core/src/exchanges.ts @@ -309,6 +309,8 @@ async function makeExchangeListItem( return { exchangeBaseUrl: r.baseUrl, masterPub: exchangeDetails?.masterPublicKey, + noFees: r.noFees ?? false, + peerPaymentsDisabled: r.peerPaymentsDisabled ?? false, currency: exchangeDetails?.currency ?? r.presetCurrencyHint, exchangeUpdateStatus: getExchangeUpdateStatusFromRecord(r), exchangeEntryStatus: getExchangeEntryStatusFromRecord(r), @@ -1177,6 +1179,61 @@ async function waitReadyExchange( } } +function checkPeerPaymentsDisabled( + keysInfo: ExchangeKeysDownloadResult, +): boolean { + const now = AbsoluteTime.now(); + for (let gf of keysInfo.globalFees) { + const isActive = AbsoluteTime.isBetween( + now, + AbsoluteTime.fromProtocolTimestamp(gf.start_date), + AbsoluteTime.fromProtocolTimestamp(gf.end_date), + ); + if (!isActive) { + continue; + } + return false; + } + // No global fees, we can't do p2p payments! + return true; +} + +function checkNoFees(keysInfo: ExchangeKeysDownloadResult): boolean { + for (const gf of keysInfo.globalFees) { + if (!Amounts.isZero(gf.account_fee)) { + return false; + } + if (!Amounts.isZero(gf.history_fee)) { + return false; + } + if (!Amounts.isZero(gf.purse_fee)) { + return false; + } + } + for (const denom of keysInfo.currentDenominations) { + if (!Amounts.isZero(denom.fees.feeWithdraw)) { + return false; + } + if (!Amounts.isZero(denom.fees.feeDeposit)) { + return false; + } + if (!Amounts.isZero(denom.fees.feeRefund)) { + return false; + } + if (!Amounts.isZero(denom.fees.feeRefresh)) { + return false; + } + } + for (const wft of Object.values(keysInfo.wireFees)) { + for (const wf of wft) { + if (!Amounts.isZero(wf.wire_fee)) { + return false; + } + } + } + return true; +} + /** * Update an exchange entry in the wallet's database * by fetching the /keys and /wire information. @@ -1305,6 +1362,7 @@ export async function updateExchangeFromUrlHandler( keysInfo.globalFees, keysInfo.masterPublicKey, ); + if (keysInfo.baseUrl != exchangeBaseUrl) { logger.warn("exchange base URL mismatch"); const errorDetail: TalerErrorDetail = makeErrorDetail( @@ -1349,6 +1407,10 @@ export async function updateExchangeFromUrlHandler( } } + const now = AbsoluteTime.now(); + let noFees = checkNoFees(keysInfo); + let peerPaymentsDisabled = checkPeerPaymentsDisabled(keysInfo); + const updated = await wex.db.runReadWriteTx( [ "exchanges", @@ -1390,6 +1452,8 @@ export async function updateExchangeFromUrlHandler( wireInfo, ageMask, }; + r.noFees = noFees; + r.peerPaymentsDisabled = peerPaymentsDisabled; r.tosCurrentEtag = tosDownload.tosEtag; if (existingDetails?.rowId) { newDetails.rowId = existingDetails.rowId; |