From d4fda1eea86ef901d125078f1f4fe0fe4a141afb Mon Sep 17 00:00:00 2001 From: Florian Dold Date: Mon, 20 Feb 2023 03:22:43 +0100 Subject: wallet-core: raw/effective amount for push transactions, fix transactions list for push/pull credit --- packages/taler-util/src/wallet-types.ts | 7 +- packages/taler-wallet-core/src/db.ts | 29 +++ .../taler-wallet-core/src/operations/pay-peer.ts | 24 +- .../src/operations/transactions.ts | 280 +++++++++++++++------ packages/taler-wallet-core/src/util/retries.ts | 4 +- packages/taler-wallet-core/src/wallet-api-types.ts | 4 +- 6 files changed, 265 insertions(+), 83 deletions(-) diff --git a/packages/taler-util/src/wallet-types.ts b/packages/taler-util/src/wallet-types.ts index e89d143c8..aff83da14 100644 --- a/packages/taler-util/src/wallet-types.ts +++ b/packages/taler-util/src/wallet-types.ts @@ -2084,9 +2084,14 @@ export interface PreparePeerPullDebitRequest { talerUri: string; } -export interface CheckPeerPushPaymentResponse { +export interface PreparePeerPushCreditResponse { contractTerms: PeerContractTerms; + /** + * @deprecated + */ amount: AmountString; + amountRaw: AmountString; + amountEffective: AmountString; peerPushPaymentIncomingId: string; } diff --git a/packages/taler-wallet-core/src/db.ts b/packages/taler-wallet-core/src/db.ts index 5f7a6a4c4..7e1906351 100644 --- a/packages/taler-wallet-core/src/db.ts +++ b/packages/taler-wallet-core/src/db.ts @@ -1850,6 +1850,8 @@ export interface PeerPushPaymentIncomingRecord { timestamp: TalerProtocolTimestamp; + estimatedAmountEffective: AmountString; + /** * Hash of the contract terms. Also * used to look up the contract terms in the DB. @@ -1865,6 +1867,14 @@ export interface PeerPushPaymentIncomingRecord { * Associated withdrawal group. */ withdrawalGroupId: string | undefined; + + /** + * Currency of the peer push payment credit transaction. + * + * Mandatory in current schema version, optional for compatibility + * with older (ver_minor<4) DB versions. + */ + currency: string | undefined; } export enum PeerPullPaymentIncomingStatus { @@ -2567,6 +2577,25 @@ export const walletDbFixups: FixupDescription[] = [ }); }, }, + { + name: "PeerPushPaymentIncomingRecord_totalCostEstimated_add", + async fn(tx): Promise { + await tx.peerPushPaymentIncoming.iter().forEachAsync(async (pi) => { + if (pi.estimatedAmountEffective) { + return; + } + const contractTerms = await tx.contractTerms.get(pi.contractTermsHash); + if (!contractTerms) { + // Not sure what we can do here! + } else { + // Not really the cost, but a good substitute for older transactions + // that don't sture the effective cost of the transaction. + pi.estimatedAmountEffective = contractTerms.contractTermsRaw.amount; + await tx.peerPushPaymentIncoming.put(pi); + } + }); + }, + }, ]; const logger = new Logger("db.ts"); diff --git a/packages/taler-wallet-core/src/operations/pay-peer.ts b/packages/taler-wallet-core/src/operations/pay-peer.ts index ef2c19c33..6e5f1b89b 100644 --- a/packages/taler-wallet-core/src/operations/pay-peer.ts +++ b/packages/taler-wallet-core/src/operations/pay-peer.ts @@ -31,7 +31,7 @@ import { PreparePeerPullDebitRequest, PreparePeerPullDebitResponse, PreparePeerPushCredit, - CheckPeerPushPaymentResponse, + PreparePeerPushCreditResponse, Codec, codecForAmountString, codecForAny, @@ -100,7 +100,10 @@ import { import { getPeerPaymentBalanceDetailsInTx } from "./balance.js"; import { updateExchangeFromUrl } from "./exchanges.js"; import { getTotalRefreshCost } from "./refresh.js"; -import { internalCreateWithdrawalGroup } from "./withdraw.js"; +import { + getExchangeWithdrawalInfo, + internalCreateWithdrawalGroup, +} from "./withdraw.js"; const logger = new Logger("operations/peer-to-peer.ts"); @@ -623,7 +626,7 @@ export const codecForExchangePurseStatus = (): Codec => export async function preparePeerPushCredit( ws: InternalWalletState, req: PreparePeerPushCredit, -): Promise { +): Promise { const uri = parsePayPushUri(req.talerUri); if (!uri) { @@ -658,6 +661,8 @@ export async function preparePeerPushCredit( if (existing) { return { amount: existing.existingContractTerms.amount, + amountEffective: existing.existingPushInc.estimatedAmountEffective, + amountRaw: existing.existingContractTerms.amount, contractTerms: existing.existingContractTerms, peerPushPaymentIncomingId: existing.existingPushInc.peerPushPaymentIncomingId, @@ -705,6 +710,13 @@ export async function preparePeerPushCredit( const withdrawalGroupId = encodeCrock(getRandomBytes(32)); + const wi = await getExchangeWithdrawalInfo( + ws, + exchangeBaseUrl, + Amounts.parseOrThrow(purseStatus.balance), + undefined, + ); + await ws.db .mktx((x) => [x.contractTerms, x.peerPushPaymentIncoming]) .runReadWrite(async (tx) => { @@ -718,6 +730,10 @@ export async function preparePeerPushCredit( contractTermsHash, status: PeerPushPaymentIncomingStatus.Proposed, withdrawalGroupId, + currency: Amounts.currencyOf(purseStatus.balance), + estimatedAmountEffective: Amounts.stringify( + wi.withdrawalAmountEffective, + ), }); await tx.contractTerms.put({ @@ -728,6 +744,8 @@ export async function preparePeerPushCredit( return { amount: purseStatus.balance, + amountEffective: wi.withdrawalAmountEffective, + amountRaw: purseStatus.balance, contractTerms: dec.contractTerms, peerPushPaymentIncomingId, }; diff --git a/packages/taler-wallet-core/src/operations/transactions.ts b/packages/taler-wallet-core/src/operations/transactions.ts index 1864a0b50..c988e1e84 100644 --- a/packages/taler-wallet-core/src/operations/transactions.ts +++ b/packages/taler-wallet-core/src/operations/transactions.ts @@ -58,6 +58,9 @@ import { WithdrawalGroupStatus, RefreshGroupRecord, RefreshOperationStatus, + PeerPushPaymentIncomingRecord, + PeerPushPaymentIncomingStatus, + PeerPullPaymentInitiationRecord, } from "../db.js"; import { InternalWalletState } from "../internal-wallet-state.js"; import { checkDbInvariant } from "../util/invariants.js"; @@ -135,8 +138,7 @@ export async function getTransactionById( const { type, args: rest } = parseId("txn", req.transactionId); if ( type === TransactionType.Withdrawal || - type === TransactionType.PeerPullCredit || - type === TransactionType.PeerPushCredit + type === TransactionType.PeerPullCredit ) { const withdrawalGroupId = rest[0]; return await ws.db @@ -165,24 +167,6 @@ export async function getTransactionById( ort, ); } - if ( - withdrawalGroupRecord.wgInfo.withdrawalType === - WithdrawalRecordType.PeerPullCredit - ) { - return buildTransactionForPullPaymentCredit( - withdrawalGroupRecord, - ort, - ); - } - if ( - withdrawalGroupRecord.wgInfo.withdrawalType === - WithdrawalRecordType.PeerPushCredit - ) { - return buildTransactionForPushPaymentCredit( - withdrawalGroupRecord, - ort, - ); - } const exchangeDetails = await getExchangeDetails( tx, withdrawalGroupRecord.exchangeBaseUrl, @@ -356,9 +340,15 @@ export async function getTransactionById( checkDbInvariant(!!ct); return buildTransactionForPushPaymentDebit(debit, ct.contractTermsRaw); }); + } else if (type === TransactionType.PeerPushCredit) { + // FIXME: Implement! + throw Error("getTransaction not yet implemented for PeerPushCredit"); + } else if (type === TransactionType.PeerPushCredit) { + // FIXME: Implement! + throw Error("getTransaction not yet implemented for PeerPullCredit"); } else { const unknownTxType: never = type; - throw Error(`can't delete a '${unknownTxType}' transaction`); + throw Error(`can't retrieve a '${unknownTxType}' transaction`); } } @@ -422,82 +412,144 @@ function buildTransactionForPullPaymentDebit( }; } -function buildTransactionForPullPaymentCredit( - wsr: WithdrawalGroupRecord, - ort?: OperationRetryRecord, +function buildTransactionForPeerPullCredit( + pullCredit: PeerPullPaymentInitiationRecord, + pullCreditOrt: OperationRetryRecord | undefined, + peerContractTerms: PeerContractTerms, + wsr: WithdrawalGroupRecord | undefined, + wsrOrt: OperationRetryRecord | undefined, ): Transaction { - if (wsr.wgInfo.withdrawalType !== WithdrawalRecordType.PeerPullCredit) { - throw Error(`Unexpected withdrawalType: ${wsr.wgInfo.withdrawalType}`); + if (wsr) { + if (wsr.wgInfo.withdrawalType !== WithdrawalRecordType.PeerPullCredit) { + throw Error(`Unexpected withdrawalType: ${wsr.wgInfo.withdrawalType}`); + } + /** + * FIXME: this should be handled in the withdrawal process. + * PeerPull withdrawal fails until reserve have funds but it is not + * an error from the user perspective. + */ + const silentWithdrawalErrorForInvoice = + wsrOrt?.lastError && + wsrOrt.lastError.code === + TalerErrorCode.WALLET_WITHDRAWAL_GROUP_INCOMPLETE && + Object.values(wsrOrt.lastError.errorsPerCoin ?? {}).every((e) => { + return ( + e.code === TalerErrorCode.WALLET_UNEXPECTED_REQUEST_ERROR && + e.httpStatusCode === 409 + ); + }); + return { + type: TransactionType.PeerPullCredit, + amountEffective: Amounts.stringify(wsr.denomsSel.totalCoinValue), + amountRaw: Amounts.stringify(wsr.instructedAmount), + exchangeBaseUrl: wsr.exchangeBaseUrl, + extendedStatus: wsr.timestampFinish + ? ExtendedStatus.Done + : ExtendedStatus.Pending, + pending: !wsr.timestampFinish, + timestamp: pullCredit.mergeTimestamp, + info: { + expiration: wsr.wgInfo.contractTerms.purse_expiration, + summary: wsr.wgInfo.contractTerms.summary, + }, + talerUri: constructPayPullUri({ + exchangeBaseUrl: wsr.exchangeBaseUrl, + contractPriv: wsr.wgInfo.contractPriv, + }), + transactionId: makeTransactionId( + TransactionType.PeerPullCredit, + pullCredit.pursePub, + ), + frozen: false, + ...(wsrOrt?.lastError + ? { + error: silentWithdrawalErrorForInvoice + ? undefined + : wsrOrt.lastError, + } + : {}), + }; } - /** - * FIXME: this should be handled in the withdrawal process. - * PeerPull withdrawal fails until reserve have funds but it is not - * an error from the user perspective. - */ - const silentWithdrawalErrorForInvoice = - ort?.lastError && - ort.lastError.code === TalerErrorCode.WALLET_WITHDRAWAL_GROUP_INCOMPLETE && - Object.values(ort.lastError.errorsPerCoin ?? {}).every((e) => { - return ( - e.code === TalerErrorCode.WALLET_UNEXPECTED_REQUEST_ERROR && - e.httpStatusCode === 409 - ); - }); + return { type: TransactionType.PeerPullCredit, - amountEffective: Amounts.stringify(wsr.denomsSel.totalCoinValue), - amountRaw: Amounts.stringify(wsr.instructedAmount), - exchangeBaseUrl: wsr.exchangeBaseUrl, - extendedStatus: wsr.timestampFinish - ? ExtendedStatus.Done - : ExtendedStatus.Pending, - pending: !wsr.timestampFinish, - timestamp: wsr.timestampStart, + amountEffective: Amounts.stringify(peerContractTerms.amount), + amountRaw: Amounts.stringify(peerContractTerms.amount), + exchangeBaseUrl: pullCredit.exchangeBaseUrl, + extendedStatus: ExtendedStatus.Pending, + pending: true, + timestamp: pullCredit.mergeTimestamp, info: { - expiration: wsr.wgInfo.contractTerms.purse_expiration, - summary: wsr.wgInfo.contractTerms.summary, + expiration: peerContractTerms.purse_expiration, + summary: peerContractTerms.summary, }, talerUri: constructPayPullUri({ - exchangeBaseUrl: wsr.exchangeBaseUrl, - contractPriv: wsr.wgInfo.contractPriv, + exchangeBaseUrl: pullCredit.exchangeBaseUrl, + contractPriv: pullCredit.contractPriv, }), transactionId: makeTransactionId( TransactionType.PeerPullCredit, - wsr.withdrawalGroupId, + pullCredit.pursePub, ), frozen: false, - ...(ort?.lastError - ? { error: silentWithdrawalErrorForInvoice ? undefined : ort.lastError } - : {}), + ...(pullCreditOrt?.lastError ? { error: pullCreditOrt.lastError } : {}), }; } -function buildTransactionForPushPaymentCredit( - wsr: WithdrawalGroupRecord, - ort?: OperationRetryRecord, +function buildTransactionForPeerPushCredit( + pushInc: PeerPushPaymentIncomingRecord, + pushOrt: OperationRetryRecord | undefined, + peerContractTerms: PeerContractTerms, + wsr: WithdrawalGroupRecord | undefined, + wsrOrt: OperationRetryRecord | undefined, ): Transaction { - if (wsr.wgInfo.withdrawalType !== WithdrawalRecordType.PeerPushCredit) - throw Error(""); + if (wsr) { + if (wsr.wgInfo.withdrawalType !== WithdrawalRecordType.PeerPushCredit) { + throw Error("invalid withdrawal group type for push payment credit"); + } + + return { + type: TransactionType.PeerPushCredit, + amountEffective: Amounts.stringify(wsr.denomsSel.totalCoinValue), + amountRaw: Amounts.stringify(wsr.instructedAmount), + exchangeBaseUrl: wsr.exchangeBaseUrl, + info: { + expiration: wsr.wgInfo.contractTerms.purse_expiration, + summary: wsr.wgInfo.contractTerms.summary, + }, + extendedStatus: wsr.timestampFinish + ? ExtendedStatus.Done + : ExtendedStatus.Pending, + pending: !wsr.timestampFinish, + timestamp: wsr.timestampStart, + transactionId: makeTransactionId( + TransactionType.PeerPushCredit, + pushInc.peerPushPaymentIncomingId, + ), + frozen: false, + ...(wsrOrt?.lastError ? { error: wsrOrt.lastError } : {}), + }; + } + return { type: TransactionType.PeerPushCredit, - amountEffective: Amounts.stringify(wsr.denomsSel.totalCoinValue), - amountRaw: Amounts.stringify(wsr.instructedAmount), - exchangeBaseUrl: wsr.exchangeBaseUrl, + // FIXME: This is wrong, needs to consider fees! + amountEffective: Amounts.stringify(peerContractTerms.amount), + amountRaw: Amounts.stringify(peerContractTerms.amount), + exchangeBaseUrl: pushInc.exchangeBaseUrl, info: { - expiration: wsr.wgInfo.contractTerms.purse_expiration, - summary: wsr.wgInfo.contractTerms.summary, + expiration: peerContractTerms.purse_expiration, + summary: peerContractTerms.summary, }, - extendedStatus: wsr.timestampFinish - ? ExtendedStatus.Done - : ExtendedStatus.Pending, - pending: !wsr.timestampFinish, - timestamp: wsr.timestampStart, + extendedStatus: ExtendedStatus.Pending, + pending: true, + timestamp: pushInc.timestamp, transactionId: makeTransactionId( TransactionType.PeerPushCredit, - wsr.withdrawalGroupId, + pushInc.peerPushPaymentIncomingId, ), frozen: false, - ...(ort?.lastError ? { error: ort.lastError } : {}), + ...(pushOrt?.lastError ? { error: pushOrt.lastError } : {}), }; } @@ -926,6 +978,8 @@ export async function getTransactions( x.operationRetries, x.peerPullPaymentIncoming, x.peerPushPaymentInitiations, + x.peerPushPaymentIncoming, + x.peerPullPaymentInitiations, x.planchets, x.purchases, x.contractTerms, @@ -970,6 +1024,80 @@ export async function getTransactions( transactions.push(buildTransactionForPullPaymentDebit(pi)); }); + tx.peerPushPaymentIncoming.iter().forEachAsync(async (pi) => { + if (!pi.currency) { + // Legacy transaction + return; + } + if (shouldSkipCurrency(transactionsRequest, pi.currency)) { + return; + } + if (shouldSkipSearch(transactionsRequest, [])) { + return; + } + if (pi.status === PeerPushPaymentIncomingStatus.Proposed) { + // We don't report proposed push credit transactions, user needs + // to scan URI again and confirm to see it. + return; + } + const ct = await tx.contractTerms.get(pi.contractTermsHash); + let wg: WithdrawalGroupRecord | undefined = undefined; + let wgOrt: OperationRetryRecord | undefined = undefined; + if (pi.withdrawalGroupId) { + wg = await tx.withdrawalGroups.get(pi.withdrawalGroupId); + if (wg) { + const withdrawalOpId = RetryTags.forWithdrawal(wg); + wgOrt = await tx.operationRetries.get(withdrawalOpId); + } + } + const pushIncOpId = RetryTags.forPeerPushCredit(pi); + let pushIncOrt = await tx.operationRetries.get(pushIncOpId); + + checkDbInvariant(!!ct); + transactions.push( + buildTransactionForPeerPushCredit( + pi, + pushIncOrt, + ct.contractTermsRaw, + wg, + wgOrt, + ), + ); + }); + + tx.peerPullPaymentInitiations.iter().forEachAsync(async (pi) => { + const currency = Amounts.currencyOf(pi.amount); + if (shouldSkipCurrency(transactionsRequest, currency)) { + return; + } + if (shouldSkipSearch(transactionsRequest, [])) { + return; + } + const ct = await tx.contractTerms.get(pi.contractTermsHash); + let wg: WithdrawalGroupRecord | undefined = undefined; + let wgOrt: OperationRetryRecord | undefined = undefined; + if (pi.withdrawalGroupId) { + wg = await tx.withdrawalGroups.get(pi.withdrawalGroupId); + if (wg) { + const withdrawalOpId = RetryTags.forWithdrawal(wg); + wgOrt = await tx.operationRetries.get(withdrawalOpId); + } + } + const pushIncOpId = RetryTags.forPeerPullPaymentInitiation(pi); + let pushIncOrt = await tx.operationRetries.get(pushIncOpId); + + checkDbInvariant(!!ct); + transactions.push( + buildTransactionForPeerPullCredit( + pi, + pushIncOrt, + ct.contractTermsRaw, + wg, + wgOrt, + ), + ); + }); + tx.refreshGroups.iter().forEachAsync(async (rg) => { if (shouldSkipCurrency(transactionsRequest, rg.currency)) { return; @@ -1009,10 +1137,12 @@ export async function getTransactions( switch (wsr.wgInfo.withdrawalType) { case WithdrawalRecordType.PeerPullCredit: - transactions.push(buildTransactionForPullPaymentCredit(wsr, ort)); + // Will be reported by the corresponding p2p transaction. + // FIXME: If this is an orphan withdrawal, still report it as a withdrawal! return; case WithdrawalRecordType.PeerPushCredit: - transactions.push(buildTransactionForPushPaymentCredit(wsr, ort)); + // Will be reported by the corresponding p2p transaction. + // FIXME: If this is an orphan withdrawal, still report it as a withdrawal! return; case WithdrawalRecordType.BankIntegrated: transactions.push( diff --git a/packages/taler-wallet-core/src/util/retries.ts b/packages/taler-wallet-core/src/util/retries.ts index ffa4d5b9e..5744bf8fe 100644 --- a/packages/taler-wallet-core/src/util/retries.ts +++ b/packages/taler-wallet-core/src/util/retries.ts @@ -220,12 +220,12 @@ export namespace RetryTags { export function forPeerPullPaymentDebit( ppi: PeerPullPaymentIncomingRecord, ): string { - return `${PendingTaskType.PeerPullDebit}:${ppi.pursePub}`; + return `${PendingTaskType.PeerPullDebit}:${ppi.peerPullPaymentIncomingId}`; } export function forPeerPushCredit( ppi: PeerPushPaymentIncomingRecord, ): string { - return `${PendingTaskType.PeerPushCredit}:${ppi.pursePub}`; + return `${PendingTaskType.PeerPushCredit}:${ppi.peerPushPaymentIncomingId}`; } export function byPaymentProposalId(proposalId: string): string { return `${PendingTaskType.Purchase}:${proposalId}`; diff --git a/packages/taler-wallet-core/src/wallet-api-types.ts b/packages/taler-wallet-core/src/wallet-api-types.ts index 39d3f3d1f..904462c36 100644 --- a/packages/taler-wallet-core/src/wallet-api-types.ts +++ b/packages/taler-wallet-core/src/wallet-api-types.ts @@ -45,7 +45,7 @@ import { PreparePeerPullDebitRequest, PreparePeerPullDebitResponse, PreparePeerPushCredit, - CheckPeerPushPaymentResponse, + PreparePeerPushCreditResponse, CoinDumpJson, ConfirmPayRequest, ConfirmPayResult, @@ -615,7 +615,7 @@ export type InitiatePeerPushDebitOp = { export type PreparePeerPushCreditOp = { op: WalletApiOperation.PreparePeerPushCredit; request: PreparePeerPushCredit; - response: CheckPeerPushPaymentResponse; + response: PreparePeerPushCreditResponse; }; /** -- cgit v1.2.3