taler-typescript-core

Wallet core logic and WebUIs for various components
Log | Files | Refs | Submodules | README | LICENSE

commit d424216e5cacef7640c0a8b982071236578b2d61
parent 1997c2bb4945ec318de58d6f4c88b6649a514d61
Author: Florian Dold <florian@dold.me>
Date:   Thu, 12 Sep 2024 02:35:50 +0200

wallet-core: implement getMaxPeerPushDebitAmount, fix p2p push instructed amount semantics

Diffstat:
Mpackages/taler-harness/src/integrationtests/test-exchange-purse.ts | 1+
Mpackages/taler-harness/src/integrationtests/test-peer-to-peer-push.ts | 4++--
Mpackages/taler-util/src/types-taler-wallet.ts | 44++++++++++++++++++++++++++++++++++++--------
Mpackages/taler-wallet-core/src/balance.ts | 20++++++++++++--------
Mpackages/taler-wallet-core/src/coinSelection.test.ts | 17+++++++++++++----
Mpackages/taler-wallet-core/src/coinSelection.ts | 157++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----
Mpackages/taler-wallet-core/src/crypto/cryptoImplementation.ts | 1+
Mpackages/taler-wallet-core/src/db.ts | 12+++++++++++-
Mpackages/taler-wallet-core/src/pay-peer-common.ts | 1+
Mpackages/taler-wallet-core/src/pay-peer-push-credit.ts | 4++--
Mpackages/taler-wallet-core/src/pay-peer-push-debit.ts | 64+++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------
Mpackages/taler-wallet-core/src/refresh.ts | 20++++++++++++++++++--
Mpackages/taler-wallet-core/src/wallet-api-types.ts | 10++++++++++
Mpackages/taler-wallet-core/src/wallet.ts | 10+++++++++-
14 files changed, 321 insertions(+), 44 deletions(-)

diff --git a/packages/taler-harness/src/integrationtests/test-exchange-purse.ts b/packages/taler-harness/src/integrationtests/test-exchange-purse.ts @@ -148,6 +148,7 @@ export async function runExchangePurseTest(t: GlobalTestState) { contribution: amount, denomPubHash: coin.denomPubHash, denomSig: coin.denomSig, + feeDeposit: d1.fees.feeDeposit, }; const depositSigsResp = await cryptoApi.signPurseDeposits({ diff --git a/packages/taler-harness/src/integrationtests/test-peer-to-peer-push.ts b/packages/taler-harness/src/integrationtests/test-peer-to-peer-push.ts @@ -83,7 +83,7 @@ export async function runPeerToPeerPushTest(t: GlobalTestState) { }, ); - t.assertAmountEquals(checkResp0.amountEffective, "TESTKUDOS:5.49"); + t.assertAmountEquals(checkResp0.amountEffective, "TESTKUDOS:5.3"); { const resp = await w1.walletClient.call( @@ -102,7 +102,7 @@ export async function runPeerToPeerPushTest(t: GlobalTestState) { { const bal = await w1.walletClient.call(WalletApiOperation.GetBalances, {}); - t.assertAmountEquals(bal.balances[0].pendingOutgoing, "TESTKUDOS:5.49"); + t.assertAmountEquals(bal.balances[0].pendingOutgoing, "TESTKUDOS:5.3"); } await w1.walletClient.call(WalletApiOperation.TestingWaitRefreshesFinal, {}); diff --git a/packages/taler-util/src/types-taler-wallet.ts b/packages/taler-util/src/types-taler-wallet.ts @@ -175,6 +175,14 @@ export function codecForCanonBaseUrl(): Codec<string> { }; } +export const codecForScopeInfo = (): Codec<ScopeInfo> => + buildCodecForUnion<ScopeInfo>() + .discriminateOn("type") + .alternative(ScopeType.Global, codecForScopeInfoGlobal()) + .alternative(ScopeType.Exchange, codecForScopeInfoExchange()) + .alternative(ScopeType.Auditor, codecForScopeInfoAuditor()) + .build("ScopeInfo"); + /** * Response for the create reserve request to the wallet. */ @@ -243,11 +251,34 @@ export const codecForGetMaxDepositAmountRequest = .property("depositPaytoUri", codecOptional(codecForString())) .build("GetAmountRequest"); +export interface GetMaxPeerPushDebitAmountRequest { + currency: string; + /** + * Preferred exchange to use for the p2p payment. + */ + exchangeBaseUrl?: string; + restrictScope?: ScopeInfo; +} + +export const codecForGetMaxPeerPushDebitAmountRequest = + (): Codec<GetMaxPeerPushDebitAmountRequest> => + buildCodecForObject<GetMaxPeerPushDebitAmountRequest>() + .property("currency", codecForString()) + .property("exchangeBaseUrl", codecForString()) + .property("restrictScope", codecOptional(codecForScopeInfo())) + .build("GetMaxPeerPushDebitRequest"); + export interface GetMaxDepositAmountResponse { effectiveAmount: AmountString; rawAmount: AmountString; } +export interface GetMaxPeerPushDebitAmountResponse { + effectiveAmount: AmountString; + rawAmount: AmountString; + exchangeBaseUrl?: string; +} + export interface AmountResponse { effectiveAmount: AmountString; rawAmount: AmountString; @@ -310,14 +341,6 @@ export const codecForScopeInfoAuditor = (): Codec<ScopeInfoAuditor> => .property("url", codecForString()) .build("ScopeInfoAuditor"); -export const codecForScopeInfo = (): Codec<ScopeInfo> => - buildCodecForUnion<ScopeInfo>() - .discriminateOn("type") - .alternative(ScopeType.Global, codecForScopeInfoGlobal()) - .alternative(ScopeType.Exchange, codecForScopeInfoExchange()) - .alternative(ScopeType.Auditor, codecForScopeInfoAuditor()) - .build("ScopeInfo"); - export interface GetCurrencySpecificationRequest { scope: ScopeInfo; } @@ -2720,6 +2743,11 @@ export interface PayCoinSelection { * How much of the deposit fees is the customer paying? */ customerDepositFees: AmountString; + + /** + * How much of the deposit fees does the exchange charge in total? + */ + totalDepositFees: AmountString; } export interface ProspectivePayCoinSelection { diff --git a/packages/taler-wallet-core/src/balance.ts b/packages/taler-wallet-core/src/balance.ts @@ -551,12 +551,12 @@ export interface PaymentBalanceDetails { balanceAgeAcceptable: AmountJson; /** - * Balance of type "merchant-acceptable" (see balance.ts for definition). + * Balance of type "receiver-acceptable" (see balance.ts for definition). */ balanceReceiverAcceptable: AmountJson; /** - * Balance of type "merchant-depositable" (see balance.ts for definition). + * Balance of type "receiver-depositable" (see balance.ts for definition). */ balanceReceiverDepositable: AmountJson; @@ -567,7 +567,11 @@ export interface PaymentBalanceDetails { */ balanceExchangeDepositable: AmountJson; - maxEffectiveSpendAmount: AmountJson; + /** + * Estimated maximum amount that the wallet could pay for, under the assumption + * that the merchant pays absolutely no fees. + */ + maxMerchantEffectiveDepositAmount: AmountJson; } export async function getPaymentBalanceDetails( @@ -609,7 +613,7 @@ export async function getPaymentBalanceDetailsInTx( balanceAgeAcceptable: Amounts.zeroOfCurrency(req.currency), balanceReceiverAcceptable: Amounts.zeroOfCurrency(req.currency), balanceReceiverDepositable: Amounts.zeroOfCurrency(req.currency), - maxEffectiveSpendAmount: Amounts.zeroOfCurrency(req.currency), + maxMerchantEffectiveDepositAmount: Amounts.zeroOfCurrency(req.currency), balanceExchangeDepositable: Amounts.zeroOfCurrency(req.currency), }; @@ -718,13 +722,13 @@ export async function getPaymentBalanceDetailsInTx( merchantExchangeAcceptable && merchantExchangeDepositable ) { - d.maxEffectiveSpendAmount = Amounts.add( - d.maxEffectiveSpendAmount, + d.maxMerchantEffectiveDepositAmount = Amounts.add( + d.maxMerchantEffectiveDepositAmount, Amounts.mult(ca.value, ca.freshCoinCount).amount, ).amount; - d.maxEffectiveSpendAmount = Amounts.sub( - d.maxEffectiveSpendAmount, + d.maxMerchantEffectiveDepositAmount = Amounts.sub( + d.maxMerchantEffectiveDepositAmount, Amounts.mult(denom.feeDeposit, ca.freshCoinCount).amount, ).amount; } diff --git a/packages/taler-wallet-core/src/coinSelection.test.ts b/packages/taler-wallet-core/src/coinSelection.test.ts @@ -46,7 +46,9 @@ const inThePast = AbsoluteTime.toProtocolTimestamp( test("p2p: should select the coin", (t) => { const instructedAmount = Amounts.parseOrThrow("LOCAL:2"); - const tally = emptyTallyForPeerPayment(instructedAmount); + const tally = emptyTallyForPeerPayment({ + instructedAmount, + }); t.log(`tally before: ${j2s(tally)}`); const coins = testing_selectGreedy( { @@ -80,7 +82,9 @@ test("p2p: should select the coin", (t) => { test("p2p: should select 3 coins", (t) => { const instructedAmount = Amounts.parseOrThrow("LOCAL:20"); - const tally = emptyTallyForPeerPayment(instructedAmount); + const tally = emptyTallyForPeerPayment({ + instructedAmount, + }); const coins = testing_selectGreedy( { wireFeesPerExchange: {}, @@ -112,7 +116,9 @@ test("p2p: should select 3 coins", (t) => { test("p2p: can't select since the instructed amount is too high", (t) => { const instructedAmount = Amounts.parseOrThrow("LOCAL:60"); - const tally = emptyTallyForPeerPayment(instructedAmount); + const tally = emptyTallyForPeerPayment({ + instructedAmount, + }); const coins = testing_selectGreedy( { wireFeesPerExchange: {}, @@ -142,6 +148,7 @@ test("pay: select one coin to pay with fee", (t) => { customerWireFees: zero, wireFeeCoveredForExchange: new Set<string>(), lastDepositFee: zero, + totalDepositFees: zero, } satisfies CoinSelectionTally; const coins = testing_selectGreedy( { @@ -273,7 +280,9 @@ test("p2p: regression STATER", (t) => { }, ]; const instructedAmount = Amounts.parseOrThrow("STATER:1"); - const tally = emptyTallyForPeerPayment(instructedAmount); + const tally = emptyTallyForPeerPayment({ + instructedAmount, + }); const res = testing_selectGreedy( { wireFeesPerExchange: {}, diff --git a/packages/taler-wallet-core/src/coinSelection.ts b/packages/taler-wallet-core/src/coinSelection.ts @@ -40,6 +40,8 @@ import { ForcedCoinSel, GetMaxDepositAmountRequest, GetMaxDepositAmountResponse, + GetMaxPeerPushDebitAmountRequest, + GetMaxPeerPushDebitAmountResponse, j2s, Logger, parsePaytoUri, @@ -90,6 +92,8 @@ export interface CoinSelectionTally { customerDepositFees: AmountJson; + totalDepositFees: AmountJson; + customerWireFees: AmountJson; wireFeeCoveredForExchange: Set<string>; @@ -156,6 +160,10 @@ function tallyFees( dfRemaining, ).amount; tally.lastDepositFee = feeDeposit; + tally.totalDepositFees = Amounts.add( + tally.totalDepositFees, + feeDeposit, + ).amount; } export type SelectPayCoinsResult = @@ -213,6 +221,7 @@ async function internalSelectPayCoins( amountDepositFeeLimitRemaining: depositFeeLimit, customerDepositFees: Amounts.zeroOfCurrency(currency), customerWireFees: Amounts.zeroOfCurrency(currency), + totalDepositFees: Amounts.zeroOfCurrency(currency), wireFeeCoveredForExchange: new Set(), lastDepositFee: Amounts.zeroOfCurrency(currency), }; @@ -455,6 +464,7 @@ async function assembleSelectPayCoinsSuccessResult( coins: coinRes, customerDepositFees: Amounts.stringify(tally.customerDepositFees), customerWireFees: Amounts.stringify(tally.customerWireFees), + totalDepositFees: Amounts.stringify(tally.totalDepositFees), }; } @@ -536,7 +546,7 @@ export async function reportInsufficientBalanceDetails( exchDet.balanceReceiverDepositable, ), maxEffectiveSpendAmount: Amounts.stringify( - exchDet.maxEffectiveSpendAmount, + exchDet.maxMerchantEffectiveDepositAmount, ), missingGlobalFees, }; @@ -556,7 +566,9 @@ export async function reportInsufficientBalanceDetails( balanceReceiverDepositable: Amounts.stringify( details.balanceReceiverDepositable, ), - maxEffectiveSpendAmount: Amounts.stringify(details.maxEffectiveSpendAmount), + maxEffectiveSpendAmount: Amounts.stringify( + details.maxMerchantEffectiveDepositAmount, + ), perExchange, }; } @@ -965,7 +977,9 @@ export interface PeerCoinSelectionDetails { /** * How much of the deposit fees is the customer paying? */ - depositFees: AmountJson; + customerDepositFees: AmountJson; + + totalDepositFees: AmountJson; maxExpirationDate: TalerProtocolTimestamp; } @@ -978,7 +992,9 @@ export interface ProspectivePeerCoinSelectionDetails { /** * How much of the deposit fees is the customer paying? */ - depositFees: AmountJson; + customerDepositFees: AmountJson; + + totalDepositFees: AmountJson; maxExpirationDate: TalerProtocolTimestamp; } @@ -996,6 +1012,13 @@ export interface PeerCoinSelectionRequest { instructedAmount: AmountJson; /** + * Are deposit fees covered by the counterparty? + * + * Defaults to false. + */ + feesCoveredByCounterparty?: boolean; + + /** * Restrict the scope of funds that can be spent via the given * scope info. */ @@ -1041,17 +1064,21 @@ export async function computeCoinSelMaxExpirationDate( } export function emptyTallyForPeerPayment( - instructedAmount: AmountJson, + req: PeerCoinSelectionRequest, ): CoinSelectionTally { + const instructedAmount = req.instructedAmount; const currency = instructedAmount.currency; const zero = Amounts.zeroOfCurrency(currency); return { amountPayRemaining: instructedAmount, customerDepositFees: zero, lastDepositFee: zero, - amountDepositFeeLimitRemaining: zero, + amountDepositFeeLimitRemaining: req.feesCoveredByCounterparty + ? instructedAmount + : zero, customerWireFees: zero, wireFeeCoveredForExchange: new Set(), + totalDepositFees: zero, }; } @@ -1111,7 +1138,7 @@ async function internalSelectPeerCoins( if (logger.shouldLogTrace()) { logger.trace(`peer payment candidate coins: ${j2s(candidates)}`); } - const tally = emptyTallyForPeerPayment(req.instructedAmount); + const tally = emptyTallyForPeerPayment(req); const resCoins: SelectedCoin[] = []; await maybeRepairCoinSelection(wex, tx, req.repair ?? [], resCoins, tally, { @@ -1224,7 +1251,8 @@ export async function selectPeerCoinsInTx( type: "prospective", result: { prospectiveCoins, - depositFees: prospectiveAvRes.tally.customerDepositFees, + customerDepositFees: prospectiveAvRes.tally.customerDepositFees, + totalDepositFees: prospectiveAvRes.tally.totalDepositFees, exchangeBaseUrl: exch.baseUrl, maxExpirationDate, }, @@ -1248,7 +1276,8 @@ export async function selectPeerCoinsInTx( type: "success", result: { coins: r.coins, - depositFees: Amounts.parseOrThrow(r.customerDepositFees), + customerDepositFees: Amounts.parseOrThrow(r.customerDepositFees), + totalDepositFees: Amounts.parseOrThrow(r.totalDepositFees), exchangeBaseUrl: exch.baseUrl, maxExpirationDate, }, @@ -1373,3 +1402,113 @@ export async function getMaxDepositAmount( }, ); } + +function getMaxPeerPushDebitAmountForAvailableCoins( + req: GetMaxDepositAmountRequest, + exchangeBaseUrl: string, + candidateRes: PayCoinCandidates, +): GetMaxPeerPushDebitAmountResponse { + let amountEffective = Amounts.zeroOfCurrency(req.currency); + let fees = Amounts.zeroOfCurrency(req.currency); + + for (const cc of candidateRes.coinAvailability) { + amountEffective = Amounts.add( + amountEffective, + Amounts.mult(cc.value, cc.numAvailable).amount, + ).amount; + + fees = Amounts.add( + fees, + Amounts.mult(cc.feeDeposit, cc.numAvailable).amount, + ).amount; + } + + return { + exchangeBaseUrl, + effectiveAmount: Amounts.stringify(amountEffective), + rawAmount: Amounts.stringify(Amounts.sub(amountEffective, fees).amount), + }; +} + +export async function getMaxPeerPushDebitAmount( + wex: WalletExecutionContext, + req: GetMaxPeerPushDebitAmountRequest, +): Promise<GetMaxPeerPushDebitAmountResponse> { + logger.trace(`getting max deposit amount for: ${j2s(req)}`); + + return await wex.db.runReadOnlyTx( + { + storeNames: [ + "exchanges", + "coinAvailability", + "denominations", + "exchangeDetails", + ], + }, + async (tx): Promise<GetMaxPeerPushDebitAmountResponse> => { + let result: GetMaxDepositAmountResponse | undefined = undefined; + const currency = req.currency; + const exchanges = await tx.exchanges.iter().toArray(); + for (const exch of exchanges) { + if (exch.detailsPointer?.currency !== currency) { + continue; + } + const exchWire = await getExchangeWireDetailsInTx(tx, exch.baseUrl); + if (!exchWire) { + continue; + } + const isInScope = req.restrictScope + ? await checkExchangeInScope(wex, exch.baseUrl, req.restrictScope) + : true; + if (!isInScope) { + continue; + } + if ( + req.restrictScope && + req.restrictScope.type === ScopeType.Exchange && + req.restrictScope.url !== exch.baseUrl + ) { + continue; + } + const globalFees = getGlobalFees(exchWire); + if (!globalFees) { + continue; + } + + const candidatesRes = await selectPayCandidates(wex, tx, { + currency, + restrictExchanges: { + auditors: [], + exchanges: [ + { + exchangeBaseUrl: exchWire.exchangeBaseUrl, + exchangePub: exchWire.masterPublicKey, + }, + ], + }, + restrictWireMethod: undefined, + includePendingCoins: true, + }); + + const myExchangeRes = getMaxPeerPushDebitAmountForAvailableCoins( + req, + exchWire.exchangeBaseUrl, + candidatesRes, + ); + + if (!result) { + result = myExchangeRes; + } else if (Amounts.cmp(result.rawAmount, myExchangeRes.rawAmount) < 0) { + result = myExchangeRes; + } + } + if (!result) { + return { + effectiveAmount: Amounts.stringify(Amounts.zeroOfCurrency(currency)), + rawAmount: Amounts.stringify(Amounts.zeroOfCurrency(currency)), + }; + } + return result; + }, + ); +} diff --git a/packages/taler-wallet-core/src/crypto/cryptoImplementation.ts b/packages/taler-wallet-core/src/crypto/cryptoImplementation.ts @@ -521,6 +521,7 @@ export interface SpendCoinDetails { coinPub: string; coinPriv: string; contribution: AmountString; + feeDeposit: AmountString; denomPubHash: string; denomSig: UnblindedSignature; ageCommitmentProof: AgeCommitmentProof | undefined; diff --git a/packages/taler-wallet-core/src/db.ts b/packages/taler-wallet-core/src/db.ts @@ -1899,7 +1899,7 @@ export interface PeerPushDebitRecord { /** * Restricted scope for this transaction. - * + * * Relevant for coin reselection. */ restrictScope?: ScopeInfo; @@ -1909,6 +1909,16 @@ export interface PeerPushDebitRecord { */ amount: AmountString; + /** + * Optional for backwards compatibility (added 2024-09-12). + */ + amountPurse?: AmountString; + + /** + * Effective amount. + * + * (Called totalCost for historical reasons.) + */ totalCost: AmountString; coinSel?: DbPeerPushPaymentCoinSelection; diff --git a/packages/taler-wallet-core/src/pay-peer-common.ts b/packages/taler-wallet-core/src/pay-peer-common.ts @@ -71,6 +71,7 @@ export async function queryCoinInfosForSelection( denomSig: coin.denomSig, ageCommitmentProof: coin.ageCommitmentProof, contribution: csel.contributions[i], + feeDeposit: denom.feeDeposit, }); } }, diff --git a/packages/taler-wallet-core/src/pay-peer-push-credit.ts b/packages/taler-wallet-core/src/pay-peer-push-credit.ts @@ -657,12 +657,12 @@ export async function preparePeerPushCredit( pursePub: pursePub, }); + const contractTerms = codecForPeerContractTerms().decode(dec.contractTerms); + const getPurseUrl = new URL(`purses/${pursePub}/deposit`, exchangeBaseUrl); const purseHttpResp = await wex.http.fetch(getPurseUrl.href); - const contractTerms = codecForPeerContractTerms().decode(dec.contractTerms); - const purseStatus = await readSuccessResponseJsonOrThrow( purseHttpResp, codecForExchangePurseStatus(), diff --git a/packages/taler-wallet-core/src/pay-peer-push-debit.ts b/packages/taler-wallet-core/src/pay-peer-push-debit.ts @@ -456,6 +456,7 @@ async function internalCheckPeerPushDebit( const coinSelRes = await selectPeerCoins(wex, { instructedAmount, restrictScope: req.restrictScope, + feesCoveredByCounterparty: true, }); let coins: SelectedProspectiveCoin[] | undefined = undefined; switch (coinSelRes.type) { @@ -528,9 +529,12 @@ async function handlePurseCreationConflict( } const coinSelRes = await selectPeerCoins(wex, { - instructedAmount, + instructedAmount: Amounts.parseOrThrow( + peerPushInitiation.amountPurse ?? peerPushInitiation.amount, + ), restrictScope: peerPushInitiation.restrictScope, repair, + feesCoveredByCounterparty: false, }); switch (coinSelRes.type) { @@ -602,6 +606,7 @@ async function processPeerPushDebitCreateReserve( const coinSelRes = await selectPeerCoins(wex, { instructedAmount: Amounts.parseOrThrow(peerPushInitiation.amount), restrictScope: peerPushInitiation.restrictScope, + feesCoveredByCounterparty: true, }); switch (coinSelRes.type) { @@ -670,11 +675,14 @@ async function processPeerPushDebitCreateReserve( return TaskRunResult.backoff(); } + const purseAmount = + peerPushInitiation.amountPurse ?? peerPushInitiation.amount; + const purseSigResp = await wex.cryptoApi.signPurseCreation({ hContractTerms, mergePub: peerPushInitiation.mergePub, minAge: 0, - purseAmount: peerPushInitiation.amount, + purseAmount, purseExpiration: timestampProtocolFromDb(purseExpiration), pursePriv: peerPushInitiation.pursePriv, }); @@ -721,7 +729,8 @@ async function processPeerPushDebitCreateReserve( ); const reqBody = { - amount: peerPushInitiation.amount, + // Older wallets do not have amountPurse + amount: purseAmount, merge_pub: peerPushInitiation.mergePub, purse_sig: purseSigResp.sig, h_contract_terms: hContractTerms, @@ -746,6 +755,8 @@ async function processPeerPushDebitCreateReserve( // Possibly on to the next batch. continue; case HttpStatusCode.Forbidden: { + const errResp = await readTalerErrorResponse(httpResp); + logger.error(`${j2s(errResp)}`); // FIXME: Store this error! await ctx.failTransaction(); return TaskRunResult.finished(); @@ -781,7 +792,7 @@ async function processPeerPushDebitCreateReserve( switch (httpResp.status) { case HttpStatusCode.Ok: // Possibly on to the next batch. - continue; + break; case HttpStatusCode.Forbidden: { // FIXME: Store this error! await ctx.failTransaction(); @@ -804,6 +815,22 @@ async function processPeerPushDebitCreateReserve( // All batches done! + const getPurseUrl = new URL( + `purses/${pursePub}/deposit`, + peerPushInitiation.exchangeBaseUrl, + ); + + const purseHttpResp = await wex.http.fetch(getPurseUrl.href); + + const purseStatus = await readSuccessResponseJsonOrThrow( + purseHttpResp, + codecForExchangePurseStatus(), + ); + + if (logger.shouldLogTrace()) { + logger.trace(`purse status: ${j2s(purseStatus)}`); + } + await transitionPeerPushDebitTransaction(wex, pursePub, { stFrom: PeerPushDebitStatus.PendingCreatePurse, stTo: PeerPushDebitStatus.PendingReady, @@ -1195,13 +1222,11 @@ export async function initiatePeerPushDebit( req.partialContractTerms.amount, ); const purseExpiration = req.partialContractTerms.purse_expiration; - const contractTerms = req.partialContractTerms; + const contractTerms = { ...req.partialContractTerms }; const pursePair = await wex.cryptoApi.createEddsaKeypair({}); const mergePair = await wex.cryptoApi.createEddsaKeypair({}); - const hContractTerms = ContractTermsUtil.hashContractTerms(contractTerms); - const contractKeyPair = await wex.cryptoApi.createEddsaKeypair({}); const pursePub = pursePair.pub; @@ -1232,6 +1257,7 @@ export async function initiatePeerPushDebit( const coinSelRes = await selectPeerCoinsInTx(wex, tx, { instructedAmount, restrictScope: req.restrictScope, + feesCoveredByCounterparty: true, }); let coins: SelectedProspectiveCoin[] | undefined = undefined; @@ -1254,8 +1280,31 @@ export async function initiatePeerPushDebit( assertUnreachable(coinSelRes); } + logger.trace(j2s(coinSelRes)); + const sel = coinSelRes.result; + // Adjust the contract terms amount. + // Change it from the instructed amount to the raw amount + // of the counterparty. + contractTerms.amount = Amounts.stringify( + Amounts.sub(instructedAmount, sel.totalDepositFees).amount, + ); + + const hContractTerms = ContractTermsUtil.hashContractTerms(contractTerms); + + logger.trace( + `peer debit instructed amount: ${Amounts.stringify(instructedAmount)}`, + ); + logger.trace( + `peer debit contract terms amount: ${Amounts.stringify( + contractTerms.amount, + )}`, + ); + logger.trace( + `peer debit deposit fees: ${Amounts.stringify(sel.totalDepositFees)}`, + ); + const totalAmount = await getTotalPeerPaymentCostInTx(wex, tx, coins); const ppi: PeerPushDebitRecord = { amount: Amounts.stringify(instructedAmount), @@ -1273,6 +1322,7 @@ export async function initiatePeerPushDebit( status: PeerPushDebitStatus.PendingCreatePurse, contractEncNonce, totalCost: Amounts.stringify(totalAmount), + amountPurse: contractTerms.amount, }; if (coinSelRes.type === "success") { diff --git a/packages/taler-wallet-core/src/refresh.ts b/packages/taler-wallet-core/src/refresh.ts @@ -110,6 +110,7 @@ import { WalletDbStoresArr, } from "./db.js"; import { selectWithdrawalDenominations } from "./denomSelection.js"; +import { getScopeForAllExchanges } from "./exchanges.js"; import { constructTransactionIdentifier, isUnsuccessfulTransaction, @@ -122,7 +123,6 @@ import { WalletExecutionContext, } from "./wallet.js"; import { getCandidateWithdrawalDenomsTx } from "./withdraw.js"; -import { getScopeForAllExchanges } from "./exchanges.js"; const logger = new Logger("refresh.ts"); @@ -187,7 +187,12 @@ export class RefreshTransactionContext implements TransactionContext { return { type: TransactionType.Refresh, txState, - scopes: await getScopeForAllExchanges(tx, !refreshGroupRecord.infoPerExchange? []: Object.keys(refreshGroupRecord.infoPerExchange)), + scopes: await getScopeForAllExchanges( + tx, + !refreshGroupRecord.infoPerExchange + ? [] + : Object.keys(refreshGroupRecord.infoPerExchange), + ), txActions: computeRefreshTransactionActions(refreshGroupRecord), refreshReason: refreshGroupRecord.reason, amountEffective: isUnsuccessfulTransaction(txState) @@ -406,6 +411,11 @@ export function getTotalRefreshCostInternal( refreshedDenom: DenominationInfo, amountLeft: AmountJson, ): AmountJson { + logger.trace( + `computing total refresh cost, denom value ${ + refreshedDenom.value + }, amount left ${Amounts.stringify(amountLeft)}`, + ); const withdrawAmount = Amounts.sub( amountLeft, refreshedDenom.feeRefresh, @@ -1748,6 +1758,12 @@ export async function createRefreshGroup( const estimatedOutputPerCoin = outInfo.outputPerCoin; + if (logger.shouldLogTrace()) { + logger.trace( + `creating refresh group, inputs ${j2s(oldCoinPubs.map((x) => x.amount))}`, + ); + } + await applyRefreshToOldCoins(wex, tx, oldCoinPubs, refreshGroupId); const refreshGroup: RefreshGroupRecord = { diff --git a/packages/taler-wallet-core/src/wallet-api-types.ts b/packages/taler-wallet-core/src/wallet-api-types.ts @@ -87,6 +87,8 @@ import { GetExchangeTosResult, GetMaxDepositAmountRequest, GetMaxDepositAmountResponse, + GetMaxPeerPushDebitAmountRequest, + GetMaxPeerPushDebitAmountResponse, GetQrCodesForPaytoRequest, GetQrCodesForPaytoResponse, GetWithdrawalDetailsForAmountRequest, @@ -199,6 +201,7 @@ export enum WalletApiOperation { GetBalanceDetail = "getBalanceDetail", ConvertDepositAmount = "convertDepositAmount", GetMaxDepositAmount = "getMaxDepositAmount", + GetMaxPeerPushDebitAmount = "getMaxPeerPushDebitAmount", GetUserAttentionRequests = "getUserAttentionRequests", GetUserAttentionUnreadCount = "getUserAttentionUnreadCount", MarkAttentionRequestAsRead = "markAttentionRequestAsRead", @@ -373,6 +376,12 @@ export type GetMaxDepositAmountOp = { response: GetMaxDepositAmountResponse; }; +export type GetMaxPeerPushDebitAmountOp = { + op: WalletApiOperation.GetMaxPeerPushDebitAmount; + request: GetMaxPeerPushDebitAmountRequest; + response: GetMaxPeerPushDebitAmountResponse; +}; + // group: Managing Transactions /** @@ -1297,6 +1306,7 @@ export type WalletOperations = { [WalletApiOperation.GetBalances]: GetBalancesOp; [WalletApiOperation.ConvertDepositAmount]: ConvertDepositAmountOp; [WalletApiOperation.GetMaxDepositAmount]: GetMaxDepositAmountOp; + [WalletApiOperation.GetMaxPeerPushDebitAmount]: GetMaxPeerPushDebitAmountOp; [WalletApiOperation.GetBalanceDetail]: GetBalancesDetailOp; [WalletApiOperation.GetTransactions]: GetTransactionsOp; [WalletApiOperation.TestingGetSampleTransactions]: TestingGetSampleTransactionsOp; diff --git a/packages/taler-wallet-core/src/wallet.ts b/packages/taler-wallet-core/src/wallet.ts @@ -158,6 +158,7 @@ import { codecForGetExchangeResourcesRequest, codecForGetExchangeTosRequest, codecForGetMaxDepositAmountRequest, + codecForGetMaxPeerPushDebitAmountRequest, codecForGetQrCodesForPaytoRequest, codecForGetWithdrawalDetailsForAmountRequest, codecForGetWithdrawalDetailsForUri, @@ -233,7 +234,10 @@ import { setWalletDeviceId, } from "./backup/index.js"; import { getBalanceDetail, getBalances } from "./balance.js"; -import { getMaxDepositAmount } from "./coinSelection.js"; +import { + getMaxDepositAmount, + getMaxPeerPushDebitAmount, +} from "./coinSelection.js"; import { TalerCryptoInterface } from "./crypto/cryptoImplementation.js"; import { CryptoDispatcher, @@ -1895,6 +1899,10 @@ const handlers: { [T in WalletApiOperation]: HandlerWithValidator<T> } = { codec: codecForGetMaxDepositAmountRequest, handler: getMaxDepositAmount, }, + [WalletApiOperation.GetMaxPeerPushDebitAmount]: { + codec: codecForGetMaxPeerPushDebitAmountRequest(), + handler: getMaxPeerPushDebitAmount, + }, [WalletApiOperation.GetBackupInfo]: { codec: codecForEmptyObject(), handler: getBackupInfo,