diff options
Diffstat (limited to 'packages')
16 files changed, 703 insertions, 327 deletions
diff --git a/packages/taler-util/src/time.ts b/packages/taler-util/src/time.ts index 1e79b943b..c3f1b30bd 100644 --- a/packages/taler-util/src/time.ts +++ b/packages/taler-util/src/time.ts @@ -56,6 +56,16 @@ export namespace TalerProtocolTimestamp { t_s: s, }; } + export function min(t1: TalerProtocolTimestamp, t2: TalerProtocolTimestamp): TalerProtocolTimestamp { + if (t1.t_s === "never") { + return { t_s: t2.t_s }; + } + if (t2.t_s === "never") { + return { t_s: t2.t_s }; + } + return { t_s: Math.min(t1.t_s, t2.t_s) }; + } + } export interface Duration { diff --git a/packages/taler-util/src/transactionsTypes.ts b/packages/taler-util/src/transactionsTypes.ts index a46f304d1..e5b0695f8 100644 --- a/packages/taler-util/src/transactionsTypes.ts +++ b/packages/taler-util/src/transactionsTypes.ts @@ -505,6 +505,15 @@ export interface TransactionDeposit extends TransactionCommon { amountEffective: AmountString; } +export interface TransactionByIdRequest { + transactionId: string; +} + +export const codecForTransactionByIdRequest = (): Codec<TransactionByIdRequest> => + buildCodecForObject<TransactionByIdRequest>() + .property("transactionId", codecForString()) + .build("TransactionByIdRequest"); + export const codecForTransactionsRequest = (): Codec<TransactionsRequest> => buildCodecForObject<TransactionsRequest>() .property("currency", codecOptional(codecForString())) diff --git a/packages/taler-util/src/walletTypes.ts b/packages/taler-util/src/walletTypes.ts index 701049c26..7fcb752b1 100644 --- a/packages/taler-util/src/walletTypes.ts +++ b/packages/taler-util/src/walletTypes.ts @@ -138,11 +138,12 @@ export enum ConfirmPayResultType { export interface ConfirmPayResultDone { type: ConfirmPayResultType.Done; contractTerms: ContractTerms; + transactionId: string; } export interface ConfirmPayResultPending { type: ConfirmPayResultType.Pending; - + transactionId: string; lastError: TalerErrorDetail | undefined; } @@ -152,12 +153,14 @@ export const codecForConfirmPayResultPending = (): Codec<ConfirmPayResultPending> => buildCodecForObject<ConfirmPayResultPending>() .property("lastError", codecForAny()) + .property("transactionId", codecForString()) .property("type", codecForConstString(ConfirmPayResultType.Pending)) .build("ConfirmPayResultPending"); export const codecForConfirmPayResultDone = (): Codec<ConfirmPayResultDone> => buildCodecForObject<ConfirmPayResultDone>() .property("type", codecForConstString(ConfirmPayResultType.Done)) + .property("transactionId", codecForString()) .property("contractTerms", codecForContractTerms()) .build("ConfirmPayResultDone"); @@ -334,6 +337,10 @@ export interface PrepareTipResult { expirationTimestamp: TalerProtocolTimestamp; } +export interface AcceptTipResponse { + transactionId: string; +} + export const codecForPrepareTipResult = (): Codec<PrepareTipResult> => buildCodecForObject<PrepareTipResult>() .property("accepted", codecForBoolean()) @@ -462,6 +469,7 @@ export interface BankWithdrawDetails { export interface AcceptWithdrawalResponse { reservePub: string; confirmTransferUrl?: string; + transactionId: string; } /** @@ -864,6 +872,8 @@ export interface AcceptManualWithdrawalResult { * Public key of the newly created reserve. */ reservePub: string; + + transactionId: string; } export interface ManualWithdrawalDetails { @@ -1252,6 +1262,8 @@ export const codecForWithdrawTestBalance = export interface ApplyRefundResponse { contractTermsHash: string; + transactionId: string; + proposalId: string; amountEffectivePaid: AmountString; @@ -1273,6 +1285,7 @@ export const codecForApplyRefundResponse = (): Codec<ApplyRefundResponse> => .property("contractTermsHash", codecForString()) .property("pendingAtExchange", codecForBoolean()) .property("proposalId", codecForString()) + .property("transactionId", codecForString()) .property("info", codecForOrderShortInfo()) .build("ApplyRefundResponse"); @@ -1374,6 +1387,7 @@ export const codecForCreateDepositGroupRequest = export interface CreateDepositGroupResponse { depositGroupId: string; + transactionId: string; } export interface TrackDepositGroupRequest { @@ -1539,6 +1553,7 @@ export interface InitiatePeerPushPaymentResponse { mergePriv: string; contractPriv: string; talerUri: string; + transactionId: string; } export const codecForInitiatePeerPushPaymentRequest = @@ -1586,6 +1601,13 @@ export interface AcceptPeerPushPaymentRequest { */ peerPushPaymentIncomingId: string; } +export interface AcceptPeerPushPaymentResponse { + transactionId: string; +} + +export interface AcceptPeerPullPaymentResponse { + transactionId: string; +} export const codecForAcceptPeerPushPaymentRequest = (): Codec<AcceptPeerPushPaymentRequest> => @@ -1629,4 +1651,6 @@ export interface InitiatePeerPullPaymentResponse { * that was requested. */ talerUri: string; + + transactionId: string; } diff --git a/packages/taler-wallet-core/src/operations/deposits.ts b/packages/taler-wallet-core/src/operations/deposits.ts index 612de8240..e6f1591ee 100644 --- a/packages/taler-wallet-core/src/operations/deposits.ts +++ b/packages/taler-wallet-core/src/operations/deposits.ts @@ -41,6 +41,7 @@ import { TalerProtocolTimestamp, TrackDepositGroupRequest, TrackDepositGroupResponse, + TransactionType, URL, } from "@gnu-taler/taler-util"; import { @@ -62,6 +63,7 @@ import { getTotalPaymentCost, } from "./pay.js"; import { getTotalRefreshCost } from "./refresh.js"; +import { makeEventId } from "./transactions.js"; /** * Logger. @@ -531,7 +533,10 @@ export async function createDepositGroup( await tx.depositGroups.put(depositGroup); }); - return { depositGroupId }; + return { + depositGroupId: depositGroupId, + transactionId: makeEventId(TransactionType.Deposit, depositGroupId) + }; } /** diff --git a/packages/taler-wallet-core/src/operations/pay.ts b/packages/taler-wallet-core/src/operations/pay.ts index 5a0d3cee3..5e3c3dd15 100644 --- a/packages/taler-wallet-core/src/operations/pay.ts +++ b/packages/taler-wallet-core/src/operations/pay.ts @@ -103,6 +103,7 @@ import { RetryInfo, RetryTags, scheduleRetry } from "../util/retries.js"; import { spendCoins } from "../wallet.js"; import { getExchangeDetails } from "./exchanges.js"; import { createRefreshGroup, getTotalRefreshCost } from "./refresh.js"; +import { makeEventId } from "./transactions.js"; /** * Logger. @@ -511,7 +512,7 @@ export function extractContractData( export async function processDownloadProposal( ws: InternalWalletState, proposalId: string, - options: {} = {}, + options: object = {}, ): Promise<OperationAttemptResult> { const proposal = await ws.db .mktx((x) => [x.proposals]) @@ -1312,6 +1313,7 @@ export async function runPayForConfirmPay( return { type: ConfirmPayResultType.Done, contractTerms: purchase.download.contractTermsRaw, + transactionId: makeEventId(TransactionType.Payment, proposalId) }; } case OperationAttemptResultType.Error: @@ -1320,6 +1322,7 @@ export async function runPayForConfirmPay( case OperationAttemptResultType.Pending: return { type: ConfirmPayResultType.Pending, + transactionId: makeEventId(TransactionType.Payment, proposalId), lastError: undefined, }; case OperationAttemptResultType.Longpoll: diff --git a/packages/taler-wallet-core/src/operations/peer-to-peer.ts b/packages/taler-wallet-core/src/operations/peer-to-peer.ts index 449a91c68..e71e8a709 100644 --- a/packages/taler-wallet-core/src/operations/peer-to-peer.ts +++ b/packages/taler-wallet-core/src/operations/peer-to-peer.ts @@ -20,11 +20,11 @@ import { AbsoluteTime, AcceptPeerPullPaymentRequest, + AcceptPeerPullPaymentResponse, AcceptPeerPushPaymentRequest, + AcceptPeerPushPaymentResponse, AgeCommitmentProof, - AmountJson, - AmountLike, - Amounts, + AmountJson, Amounts, AmountString, buildCodecForObject, CheckPeerPullPaymentRequest, @@ -34,9 +34,7 @@ import { Codec, codecForAmountString, codecForAny, - codecForExchangeGetContractResponse, - CoinPublicKey, - constructPayPullUri, + codecForExchangeGetContractResponse, constructPayPullUri, constructPayPushUri, ContractTermsUtil, decodeCrock, @@ -58,25 +56,25 @@ import { RefreshReason, strcmp, TalerProtocolTimestamp, + TransactionType, UnblindedSignature, - WalletAccountMergeFlags, + WalletAccountMergeFlags } from "@gnu-taler/taler-util"; import { CoinStatus, MergeReserveInfo, ReserveRecordStatus, WalletStoresV1, - WithdrawalRecordType, + WithdrawalRecordType } from "../db.js"; -import { readSuccessResponseJsonOrThrow } from "../util/http.js"; import { InternalWalletState } from "../internal-wallet-state.js"; +import { readSuccessResponseJsonOrThrow } from "../util/http.js"; import { checkDbInvariant } from "../util/invariants.js"; -import { internalCreateWithdrawalGroup } from "./withdraw.js"; import { GetReadOnlyAccess } from "../util/query.js"; -import { createRefreshGroup } from "./refresh.js"; -import { updateExchangeFromUrl } from "./exchanges.js"; import { spendCoins } from "../wallet.js"; -import { RetryTags } from "../util/retries.js"; +import { updateExchangeFromUrl } from "./exchanges.js"; +import { makeEventId } from "./transactions.js"; +import { internalCreateWithdrawalGroup } from "./withdraw.js"; const logger = new Logger("operations/peer-to-peer.ts"); @@ -338,6 +336,7 @@ export async function initiatePeerToPeerPush( exchangeBaseUrl: coinSelRes.exchangeBaseUrl, contractPriv: econtractResp.contractPriv, }), + transactionId: makeEventId(TransactionType.PeerPushDebit, pursePair.pub) }; } @@ -472,7 +471,7 @@ async function getMergeReserveInfo( export async function acceptPeerPushPayment( ws: InternalWalletState, req: AcceptPeerPushPaymentRequest, -): Promise<void> { +): Promise<AcceptPeerPushPaymentResponse> { const peerInc = await ws.db .mktx((x) => [x.peerPushPaymentIncoming]) .runReadOnly(async (tx) => { @@ -533,7 +532,7 @@ export async function acceptPeerPushPayment( const res = await readSuccessResponseJsonOrThrow(mergeHttpReq, codecForAny()); logger.info(`merge response: ${j2s(res)}`); - await internalCreateWithdrawalGroup(ws, { + const wg = await internalCreateWithdrawalGroup(ws, { amount, wgInfo: { withdrawalType: WithdrawalRecordType.PeerPushCredit, @@ -546,6 +545,13 @@ export async function acceptPeerPushPayment( pub: mergeReserveInfo.reservePub, }, }); + + return { + transactionId: makeEventId( + TransactionType.PeerPushCredit, + wg.withdrawalGroupId + ) + } } /** @@ -554,7 +560,7 @@ export async function acceptPeerPushPayment( export async function acceptPeerPullPayment( ws: InternalWalletState, req: AcceptPeerPullPaymentRequest, -): Promise<void> { +): Promise<AcceptPeerPullPaymentResponse> { const peerPullInc = await ws.db .mktx((x) => [x.peerPullPaymentIncoming]) .runReadOnly(async (tx) => { @@ -630,6 +636,13 @@ export async function acceptPeerPullPayment( const httpResp = await ws.http.postJson(purseDepositUrl.href, depositPayload); const resp = await readSuccessResponseJsonOrThrow(httpResp, codecForAny()); logger.trace(`purse deposit response: ${j2s(resp)}`); + + return { + transactionId: makeEventId( + TransactionType.PeerPullDebit, + req.peerPullPaymentIncomingId, + ) + } } export async function checkPeerPullPayment( @@ -801,7 +814,7 @@ export async function initiatePeerRequestForPay( logger.info(`reserve merge response: ${j2s(resp)}`); - await internalCreateWithdrawalGroup(ws, { + const wg = await internalCreateWithdrawalGroup(ws, { amount: Amounts.parseOrThrow(req.amount), wgInfo: { withdrawalType: WithdrawalRecordType.PeerPullCredit, @@ -821,5 +834,9 @@ export async function initiatePeerRequestForPay( exchangeBaseUrl: req.exchangeBaseUrl, contractPriv: econtractResp.contractPriv, }), + transactionId: makeEventId( + TransactionType.PeerPullCredit, + wg.withdrawalGroupId + ) }; } diff --git a/packages/taler-wallet-core/src/operations/refund.ts b/packages/taler-wallet-core/src/operations/refund.ts index f028dfbf1..644b07ef1 100644 --- a/packages/taler-wallet-core/src/operations/refund.ts +++ b/packages/taler-wallet-core/src/operations/refund.ts @@ -46,6 +46,7 @@ import { TalerErrorCode, TalerErrorDetail, TalerProtocolTimestamp, + TransactionType, URL, } from "@gnu-taler/taler-util"; import { @@ -63,6 +64,7 @@ import { readSuccessResponseJsonOrThrow } from "../util/http.js"; import { checkDbInvariant } from "../util/invariants.js"; import { GetReadWriteAccess } from "../util/query.js"; import { createRefreshGroup, getTotalRefreshCost } from "./refresh.js"; +import { makeEventId } from "./transactions.js"; const logger = new Logger("refund.ts"); @@ -573,6 +575,7 @@ export async function applyRefundFromPurchaseId( return { contractTermsHash: purchase.download.contractData.contractTermsHash, proposalId: purchase.proposalId, + transactionId: makeEventId(TransactionType.Payment, proposalId), //FIXME: can we have the tx id of the refund amountEffectivePaid: Amounts.stringify(summary.amountEffectivePaid), amountRefundGone: Amounts.stringify(summary.amountRefundGone), amountRefundGranted: Amounts.stringify(summary.amountRefundGranted), diff --git a/packages/taler-wallet-core/src/operations/tip.ts b/packages/taler-wallet-core/src/operations/tip.ts index c8f327a56..eef151cf2 100644 --- a/packages/taler-wallet-core/src/operations/tip.ts +++ b/packages/taler-wallet-core/src/operations/tip.ts @@ -18,6 +18,7 @@ * Imports. */ import { + AcceptTipResponse, Amounts, BlindedDenominationSignature, codecForMerchantTipResponseV2, @@ -32,6 +33,7 @@ import { TalerErrorCode, TalerProtocolTimestamp, TipPlanchetDetail, + TransactionType, URL, } from "@gnu-taler/taler-util"; import { DerivedTipPlanchet } from "../crypto/cryptoTypes.js"; @@ -53,6 +55,7 @@ import { import { checkDbInvariant, checkLogicInvariant } from "../util/invariants.js"; import { makeCoinAvailable } from "../wallet.js"; import { updateExchangeFromUrl } from "./exchanges.js"; +import { makeEventId } from "./transactions.js"; import { getCandidateWithdrawalDenoms, getExchangeWithdrawalInfo, @@ -341,7 +344,7 @@ export async function processTip( export async function acceptTip( ws: InternalWalletState, tipId: string, -): Promise<void> { +): Promise<AcceptTipResponse> { const found = await ws.db .mktx((x) => [x.tips]) .runReadWrite(async (tx) => { @@ -357,4 +360,10 @@ export async function acceptTip( if (found) { await processTip(ws, tipId); } + return { + transactionId: makeEventId( + TransactionType.Tip, + tipId + ) + } } diff --git a/packages/taler-wallet-core/src/operations/transactions.ts b/packages/taler-wallet-core/src/operations/transactions.ts index 95be50183..4c0ea7663 100644 --- a/packages/taler-wallet-core/src/operations/transactions.ts +++ b/packages/taler-wallet-core/src/operations/transactions.ts @@ -19,13 +19,16 @@ */ import { AbsoluteTime, - addPaytoQueryParams, Amounts, + addPaytoQueryParams, AmountJson, Amounts, constructPayPullUri, constructPayPushUri, Logger, OrderShortInfo, PaymentStatus, RefundInfoShort, + TalerProtocolTimestamp, Transaction, + TransactionByIdRequest, + TransactionRefund, TransactionsRequest, TransactionsResponse, TransactionType, @@ -34,8 +37,16 @@ import { } from "@gnu-taler/taler-util"; import { AbortStatus, + DepositGroupRecord, + ExchangeDetailsRecord, + OperationRetryRecord, + PeerPullPaymentIncomingRecord, + PeerPushPaymentInitiationRecord, + PurchaseRecord, RefundState, + TipRecord, WalletRefundItem, + WithdrawalGroupRecord, WithdrawalRecordType } from "../db.js"; import { InternalWalletState } from "../internal-wallet-state.js"; @@ -44,6 +55,7 @@ import { processDepositGroup } from "./deposits.js"; import { getExchangeDetails } from "./exchanges.js"; import { processPurchasePay } from "./pay.js"; import { processRefreshGroup } from "./refresh.js"; +import { applyRefundFromPurchaseId } from "./refund.js"; import { processTip } from "./tip.js"; import { processWithdrawalGroup } from "./withdraw.js"; @@ -114,6 +126,500 @@ const txOrder: { [t in TransactionType]: number } = { [TransactionType.Tip]: 11, }; +export async function getTransactionById( + ws: InternalWalletState, + req: TransactionByIdRequest, +): Promise<Transaction> { + const [typeStr, ...rest] = req.transactionId.split(":"); + const type = typeStr as TransactionType; + + if ( + type === TransactionType.Withdrawal || + type === TransactionType.PeerPullCredit || + type === TransactionType.PeerPushCredit + ) { + const withdrawalGroupId = rest[0]; + return await ws.db + .mktx((x) => [x.withdrawalGroups, x.exchangeDetails, x.exchanges, x.operationRetries]) + .runReadWrite(async (tx) => { + const withdrawalGroupRecord = await tx.withdrawalGroups.get( + withdrawalGroupId, + ); + + if (!withdrawalGroupRecord) throw Error("not found") + + const opId = RetryTags.forWithdrawal(withdrawalGroupRecord); + const ort = await tx.operationRetries.get(opId); + + if (withdrawalGroupRecord.wgInfo.withdrawalType === WithdrawalRecordType.BankIntegrated) { + return buildTransactionForBankIntegratedWithdraw(withdrawalGroupRecord, 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,); + if (!exchangeDetails) throw Error('not exchange details') + + return buildTransactionForManualWithdraw(withdrawalGroupRecord, exchangeDetails, ort); + + }); + + } else if (type === TransactionType.Payment) { + const proposalId = rest[0]; + return await ws.db + .mktx((x) => [x.purchases, x.tombstones, x.operationRetries]) + .runReadWrite(async (tx) => { + const purchase = await tx.purchases.get(proposalId); + if (!purchase) throw Error("not found") + + const filteredRefunds = await Promise.all(Object.values(purchase.refunds).map(async r => { + const t = await tx.tombstones.get(makeEventId( + TombstoneTag.DeleteRefund, + purchase.proposalId, + `${r.executionTime.t_s}`, + )) + if (!t) return r + return undefined + })); + + const cleanRefunds = filteredRefunds.filter((x): x is WalletRefundItem => !!x); + + const contractData = purchase.download.contractData; + const refunds = mergeRefundByExecutionTime(cleanRefunds, Amounts.getZero(contractData.amount.currency)); + + const payOpId = RetryTags.forPay(purchase); + const refundQueryOpId = RetryTags.forRefundQuery(purchase); + const payRetryRecord = await tx.operationRetries.get(payOpId); + const refundQueryRetryRecord = await tx.operationRetries.get( + refundQueryOpId, + ); + + const err = payRetryRecord !== undefined ? payRetryRecord : refundQueryRetryRecord + + return buildTransactionForPurchase(purchase, refunds, err); + }); + } else if (type === TransactionType.Refresh) { + const refreshGroupId = rest[0]; + throw Error(`no tx for refresh`); + + } else if (type === TransactionType.Tip) { + const tipId = rest[0]; + return await ws.db + .mktx((x) => [x.tips, x.operationRetries]) + .runReadWrite(async (tx) => { + const tipRecord = await tx.tips.get(tipId); + if (!tipRecord) throw Error("not found") + + const retries = await tx.operationRetries.get(RetryTags.forTipPickup(tipRecord)); + return buildTransactionForTip(tipRecord, retries) + }); + } else if (type === TransactionType.Deposit) { + const depositGroupId = rest[0]; + return await ws.db + .mktx((x) => [x.depositGroups, x.operationRetries]) + .runReadWrite(async (tx) => { + const depositRecord = await tx.depositGroups.get(depositGroupId); + if (!depositRecord) throw Error("not found") + + const retries = await tx.operationRetries.get(RetryTags.forDeposit(depositRecord)); + return buildTransactionForDeposit(depositRecord, retries) + }); + } else if (type === TransactionType.Refund) { + const proposalId = rest[0]; + const executionTimeStr = rest[1]; + + return await ws.db + .mktx((x) => [x.operationRetries, x.purchases, x.tombstones]) + .runReadWrite(async (tx) => { + const purchase = await tx.purchases.get(proposalId); + if (!purchase) throw Error("not found") + + const theRefund = Object.values(purchase.refunds).find(r => `${r.executionTime.t_s}` === executionTimeStr) + if (!theRefund) throw Error("not found") + + const t = await tx.tombstones.get(makeEventId( + TombstoneTag.DeleteRefund, + purchase.proposalId, + executionTimeStr, + )) + if (t) throw Error("deleted") + + const contractData = purchase.download.contractData; + const refunds = mergeRefundByExecutionTime([theRefund], Amounts.getZero(contractData.amount.currency)) + + const refundQueryOpId = RetryTags.forRefundQuery(purchase); + const refundQueryRetryRecord = await tx.operationRetries.get( + refundQueryOpId, + ); + + return buildTransactionForRefund(purchase, refunds[0], refundQueryRetryRecord); + }); + } else if (type === TransactionType.PeerPullDebit) { + const peerPullPaymentIncomingId = rest[0]; + return await ws.db + .mktx((x) => [x.peerPullPaymentIncoming]) + .runReadWrite(async (tx) => { + const debit = await tx.peerPullPaymentIncoming.get( + peerPullPaymentIncomingId, + ); + if (!debit) throw Error("not found"); + return buildTransactionForPullPaymentDebit(debit) + }); + } else if (type === TransactionType.PeerPushDebit) { + const pursePub = rest[0]; + return await ws.db + .mktx((x) => [x.peerPushPaymentInitiations]) + .runReadWrite(async (tx) => { + const debit = await tx.peerPushPaymentInitiations.get(pursePub); + if (!debit) throw Error("not found"); + return buildTransactionForPushPaymentDebit(debit) + }); + } else { + const unknownTxType: never = type; + throw Error(`can't delete a '${unknownTxType}' transaction`); + } +} + + +function buildTransactionForPushPaymentDebit(pi: PeerPushPaymentInitiationRecord, ort?: OperationRetryRecord): Transaction { + return { + type: TransactionType.PeerPushDebit, + amountEffective: pi.amount, + amountRaw: pi.amount, + exchangeBaseUrl: pi.exchangeBaseUrl, + info: { + expiration: pi.contractTerms.purse_expiration, + summary: pi.contractTerms.summary, + }, + frozen: false, + pending: !pi.purseCreated, + timestamp: pi.timestampCreated, + talerUri: constructPayPushUri({ + exchangeBaseUrl: pi.exchangeBaseUrl, + contractPriv: pi.contractPriv, + }), + transactionId: makeEventId( + TransactionType.PeerPushDebit, + pi.pursePub, + ), + ...(ort?.lastError ? { error: ort.lastError } : {}), + }; +} + +function buildTransactionForPullPaymentDebit(pi: PeerPullPaymentIncomingRecord, ort?: OperationRetryRecord): Transaction { + return { + type: TransactionType.PeerPullDebit, + amountEffective: Amounts.stringify(pi.contractTerms.amount), + amountRaw: Amounts.stringify(pi.contractTerms.amount), + exchangeBaseUrl: pi.exchangeBaseUrl, + frozen: false, + pending: false, + info: { + expiration: pi.contractTerms.purse_expiration, + summary: pi.contractTerms.summary, + }, + timestamp: pi.timestampCreated, + transactionId: makeEventId( + TransactionType.PeerPullDebit, + pi.peerPullPaymentIncomingId, + ), + ...(ort?.lastError ? { error: ort.lastError } : {}), + } +} + +function buildTransactionForPullPaymentCredit(wsr: WithdrawalGroupRecord, ort?: OperationRetryRecord): Transaction { + if (wsr.wgInfo.withdrawalType !== WithdrawalRecordType.PeerPullCredit) throw Error("") + return { + type: TransactionType.PeerPullCredit, + amountEffective: Amounts.stringify(wsr.denomsSel.totalCoinValue), + amountRaw: Amounts.stringify(wsr.rawWithdrawalAmount), + exchangeBaseUrl: wsr.exchangeBaseUrl, + pending: !wsr.timestampFinish, + timestamp: wsr.timestampStart, + info: { + expiration: wsr.wgInfo.contractTerms.purse_expiration, + summary: wsr.wgInfo.contractTerms.summary, + }, + talerUri: constructPayPullUri({ + exchangeBaseUrl: wsr.exchangeBaseUrl, + contractPriv: wsr.wgInfo.contractPriv, + }), + transactionId: makeEventId( + TransactionType.PeerPullCredit, + wsr.withdrawalGroupId, + ), + frozen: false, + ...(ort?.lastError ? { error: ort.lastError } : {}), + } +} + +function buildTransactionForPushPaymentCredit(wsr: WithdrawalGroupRecord, ort?: OperationRetryRecord): Transaction { + if (wsr.wgInfo.withdrawalType !== WithdrawalRecordType.PeerPushCredit) throw Error("") + return { + type: TransactionType.PeerPushCredit, + amountEffective: Amounts.stringify(wsr.denomsSel.totalCoinValue), + amountRaw: Amounts.stringify(wsr.rawWithdrawalAmount), + exchangeBaseUrl: wsr.exchangeBaseUrl, + info: { + expiration: wsr.wgInfo.contractTerms.purse_expiration, + summary: wsr.wgInfo.contractTerms.summary, + }, + pending: !wsr.timestampFinish, + timestamp: wsr.timestampStart, + transactionId: makeEventId( + TransactionType.PeerPushCredit, + wsr.withdrawalGroupId, + ), + frozen: false, + ...(ort?.lastError ? { error: ort.lastError } : {}), + } +} + +function buildTransactionForBankIntegratedWithdraw(wsr: WithdrawalGroupRecord, ort?: OperationRetryRecord): Transaction { + if (wsr.wgInfo.withdrawalType !== WithdrawalRecordType.BankIntegrated) throw Error("") + + return { + type: TransactionType.Withdrawal, + amountEffective: Amounts.stringify(wsr.denomsSel.totalCoinValue), + amountRaw: Amounts.stringify(wsr.rawWithdrawalAmount), + withdrawalDetails: { + type: WithdrawalType.TalerBankIntegrationApi, + confirmed: wsr.wgInfo.bankInfo.timestampBankConfirmed + ? true + : false, + reservePub: wsr.reservePub, + bankConfirmationUrl: wsr.wgInfo.bankInfo.confirmUrl, + }, + exchangeBaseUrl: wsr.exchangeBaseUrl, + pending: !wsr.timestampFinish, + timestamp: wsr.timestampStart, + transactionId: makeEventId( + TransactionType.Withdrawal, + wsr.withdrawalGroupId, + ), + frozen: false, + ...(ort?.lastError ? { error: ort.lastError } : {}), + } +} + +function buildTransactionForManualWithdraw(wsr: WithdrawalGroupRecord, exchangeDetails: ExchangeDetailsRecord, ort?: OperationRetryRecord): Transaction { + if (wsr.wgInfo.withdrawalType !== WithdrawalRecordType.BankManual) throw Error("") + + return { + type: TransactionType.Withdrawal, + amountEffective: Amounts.stringify(wsr.denomsSel.totalCoinValue), + amountRaw: Amounts.stringify(wsr.rawWithdrawalAmount), + withdrawalDetails: { + type: WithdrawalType.ManualTransfer, + reservePub: wsr.reservePub, + exchangePaytoUris: + exchangeDetails.wireInfo?.accounts.map( + (x) => addPaytoQueryParams(x.payto_uri, { subject: wsr.reservePub }), + ) ?? [], + }, + exchangeBaseUrl: wsr.exchangeBaseUrl, + pending: !wsr.timestampFinish, + timestamp: wsr.timestampStart, + transactionId: makeEventId( + TransactionType.Withdrawal, + wsr.withdrawalGroupId, + ), + frozen: false, + ...(ort?.lastError ? { error: ort.lastError } : {}), + } +} + +function buildTransactionForDeposit(dg: DepositGroupRecord, ort?: OperationRetryRecord): Transaction { + return { + type: TransactionType.Deposit, + amountRaw: Amounts.stringify(dg.effectiveDepositAmount), + amountEffective: Amounts.stringify(dg.totalPayCost), + pending: !dg.timestampFinished, + frozen: false, + timestamp: dg.timestampCreated, + targetPaytoUri: dg.wire.payto_uri, + transactionId: makeEventId( + TransactionType.Deposit, + dg.depositGroupId, + ), + depositGroupId: dg.depositGroupId, + ...(ort?.lastError ? { error: ort.lastError } : {}), + } +} + +function buildTransactionForTip(tipRecord: TipRecord, ort?: OperationRetryRecord): Transaction { + if (!tipRecord.acceptedTimestamp) throw Error("") + + return { + type: TransactionType.Tip, + amountEffective: Amounts.stringify(tipRecord.tipAmountEffective), + amountRaw: Amounts.stringify(tipRecord.tipAmountRaw), + pending: !tipRecord.pickedUpTimestamp, + frozen: false, + timestamp: tipRecord.acceptedTimestamp, + transactionId: makeEventId( + TransactionType.Tip, + tipRecord.walletTipId, + ), + merchantBaseUrl: tipRecord.merchantBaseUrl, + ...(ort?.lastError ? { error: ort.lastError } : {}), + } +} + +/** + * For a set of refund with the same executionTime. + * + */ +interface MergedRefundInfo { + executionTime: TalerProtocolTimestamp; + amountAppliedRaw: AmountJson; + amountAppliedEffective: AmountJson; + firstTimestamp: TalerProtocolTimestamp; +} + +function mergeRefundByExecutionTime(rs: WalletRefundItem[], zero: AmountJson): MergedRefundInfo[] { + const refundByExecTime = rs.reduce((prev, refund) => { + const key = `${refund.executionTime.t_s}`; + + //refunds counts if applied + const effective = refund.type === RefundState.Applied ? Amounts.sub( + refund.refundAmount, + refund.refundFee, + refund.totalRefreshCostBound, + ).amount : zero + const raw = refund.type === RefundState.Applied ? refund.refundAmount : zero + + const v = prev.get(key) + if (!v) { + prev.set(key, { + executionTime: refund.executionTime, + amountAppliedEffective: effective, + amountAppliedRaw: raw, + firstTimestamp: refund.obtainedTime + }) + } else { + //v.executionTime is the same + v.amountAppliedEffective = Amounts.add(v.amountAppliedEffective, effective).amount; + v.amountAppliedRaw = Amounts.add(v.amountAppliedRaw).amount + v.firstTimestamp = TalerProtocolTimestamp.min(v.firstTimestamp, refund.obtainedTime); + } + return prev + }, {} as Map<string, MergedRefundInfo>); + + return Array.from(refundByExecTime.values()); +} + +function buildTransactionForRefund(purchaseRecord: PurchaseRecord, refundInfo: MergedRefundInfo, ort?: OperationRetryRecord): Transaction { + + const contractData = purchaseRecord.download.contractData; + + const info: OrderShortInfo = { + merchant: contractData.merchant, + orderId: contractData.orderId, + products: contractData.products, + summary: contractData.summary, + summary_i18n: contractData.summaryI18n, + contractTermsHash: contractData.contractTermsHash, + }; + if (contractData.fulfillmentUrl !== "") { + info.fulfillmentUrl = contractData.fulfillmentUrl; + } + + return { + type: TransactionType.Refund, + info, + refundedTransactionId: makeEventId( + TransactionType.Payment, + purchaseRecord.proposalId, + ), + transactionId: makeEventId( + TransactionType.Refund, + purchaseRecord.proposalId, + `${refundInfo.executionTime.t_s}`, + ), + timestamp: refundInfo.firstTimestamp, + amountEffective: Amounts.stringify(refundInfo.amountAppliedEffective), + amountRaw: Amounts.stringify(refundInfo.amountAppliedRaw), + refundPending: + purchaseRecord.refundAwaiting === undefined + ? undefined + : Amounts.stringify(purchaseRecord.refundAwaiting), + pending: false, + frozen: false, + ...(ort?.lastError ? { error: ort.lastError } : {}), + } +} + +function buildTransactionForPurchase(purchaseRecord: PurchaseRecord, refundsInfo: MergedRefundInfo[], ort?: OperationRetryRecord): Transaction { + + const contractData = purchaseRecord.download.contractData; + const zero = Amounts.getZero(contractData.amount.currency) + + const info: OrderShortInfo = { + merchant: contractData.merchant, + orderId: contractData.orderId, + products: contractData.products, + summary: contractData.summary, + summary_i18n: contractData.summaryI18n, + contractTermsHash: contractData.contractTermsHash, + }; + + if (contractData.fulfillmentUrl !== "") { + info.fulfillmentUrl = contractData.fulfillmentUrl; + } + + const totalRefund = refundsInfo.reduce((prev, cur) => { + return { + raw: Amounts.add(prev.raw, cur.amountAppliedRaw).amount, + effective: Amounts.add(prev.effective, cur.amountAppliedEffective).amount, + } + }, { + raw: zero, effective: zero + } as { raw: AmountJson, effective: AmountJson }) + + const refunds: RefundInfoShort[] = refundsInfo.map(r => ({ + amountEffective: Amounts.stringify(r.amountAppliedEffective), + amountRaw: Amounts.stringify(r.amountAppliedRaw), + timestamp: r.executionTime, + transactionId: makeEventId( + TransactionType.Refund, + purchaseRecord.proposalId, + `${r.executionTime.t_s}` + ), + })) + + return { + type: TransactionType.Payment, + amountRaw: Amounts.stringify(contractData.amount), + amountEffective: Amounts.stringify(purchaseRecord.totalPayCost), + totalRefundRaw: Amounts.stringify(totalRefund.raw), + totalRefundEffective: Amounts.stringify(totalRefund.effective), + refundPending: + purchaseRecord.refundAwaiting === undefined + ? undefined + : Amounts.stringify(purchaseRecord.refundAwaiting), + status: purchaseRecord.timestampFirstSuccessfulPay + ? PaymentStatus.Paid + : PaymentStatus.Accepted, + pending: + !purchaseRecord.timestampFirstSuccessfulPay && + purchaseRecord.abortStatus === AbortStatus.None, + refunds, + timestamp: purchaseRecord.timestampAccept, + transactionId: makeEventId( + TransactionType.Payment, + purchaseRecord.proposalId, + ), + proposalId: purchaseRecord.proposalId, + info, + frozen: purchaseRecord.payFrozen ?? false, + ...(ort?.lastError ? { error: ort.lastError } : {}), + } +} + /** * Retrieve the full event history for this wallet. */ @@ -137,7 +643,6 @@ export async function getTransactions( x.proposals, x.purchases, x.recoupGroups, - x.recoupGroups, x.tips, x.tombstones, x.withdrawalGroups, @@ -152,27 +657,7 @@ export async function getTransactions( if (shouldSkipSearch(transactionsRequest, [])) { return; } - transactions.push({ - type: TransactionType.PeerPushDebit, - amountEffective: pi.amount, - amountRaw: pi.amount, - exchangeBaseUrl: pi.exchangeBaseUrl, - info: { - expiration: pi.contractTerms.purse_expiration, - summary: pi.contractTerms.summary, - }, - frozen: false, - pending: !pi.purseCreated, - timestamp: pi.timestampCreated, - talerUri: constructPayPushUri({ - exchangeBaseUrl: pi.exchangeBaseUrl, - contractPriv: pi.contractPriv, - }), - transactionId: makeEventId( - TransactionType.PeerPushDebit, - pi.pursePub, - ), - }); + transactions.push(buildTransactionForPushPaymentDebit(pi)); }); tx.peerPullPaymentIncoming.iter().forEachAsync(async (pi) => { @@ -187,23 +672,7 @@ export async function getTransactions( return; } - transactions.push({ - type: TransactionType.PeerPullDebit, - amountEffective: Amounts.stringify(amount), - amountRaw: Amounts.stringify(amount), - exchangeBaseUrl: pi.exchangeBaseUrl, - frozen: false, - pending: false, - info: { - expiration: pi.contractTerms.purse_expiration, - summary: pi.contractTerms.summary, - }, - timestamp: pi.timestampCreated, - transactionId: makeEventId( - TransactionType.PeerPullDebit, - pi.peerPullPaymentIncomingId, - ), - }); + transactions.push(buildTransactionForPullPaymentDebit(pi)); }); tx.withdrawalGroups.iter().forEachAsync(async (wsr) => { @@ -223,64 +692,18 @@ export async function getTransactions( const opId = RetryTags.forWithdrawal(wsr); const ort = await tx.operationRetries.get(opId); - let withdrawalDetails: WithdrawalDetails; if (wsr.wgInfo.withdrawalType === WithdrawalRecordType.PeerPullCredit) { - transactions.push({ - type: TransactionType.PeerPullCredit, - amountEffective: Amounts.stringify(wsr.denomsSel.totalCoinValue), - amountRaw: Amounts.stringify(wsr.rawWithdrawalAmount), - exchangeBaseUrl: wsr.exchangeBaseUrl, - pending: !wsr.timestampFinish, - timestamp: wsr.timestampStart, - info: { - expiration: wsr.wgInfo.contractTerms.purse_expiration, - summary: wsr.wgInfo.contractTerms.summary, - }, - talerUri: constructPayPullUri({ - exchangeBaseUrl: wsr.exchangeBaseUrl, - contractPriv: wsr.wgInfo.contractPriv, - }), - transactionId: makeEventId( - TransactionType.PeerPullCredit, - wsr.withdrawalGroupId, - ), - frozen: false, - ...(ort?.lastError ? { error: ort.lastError } : {}), - }); + transactions.push(buildTransactionForPullPaymentCredit(wsr, ort)); return; } else if ( wsr.wgInfo.withdrawalType === WithdrawalRecordType.PeerPushCredit ) { - transactions.push({ - type: TransactionType.PeerPushCredit, - amountEffective: Amounts.stringify(wsr.denomsSel.totalCoinValue), - amountRaw: Amounts.stringify(wsr.rawWithdrawalAmount), - exchangeBaseUrl: wsr.exchangeBaseUrl, - info: { - expiration: wsr.wgInfo.contractTerms.purse_expiration, - summary: wsr.wgInfo.contractTerms.summary, - }, - pending: !wsr.timestampFinish, - timestamp: wsr.timestampStart, - transactionId: makeEventId( - TransactionType.PeerPushCredit, - wsr.withdrawalGroupId, - ), - frozen: false, - ...(ort?.lastError ? { error: ort.lastError } : {}), - }); + transactions.push(buildTransactionForPushPaymentCredit(wsr, ort)); return; } else if ( wsr.wgInfo.withdrawalType === WithdrawalRecordType.BankIntegrated ) { - withdrawalDetails = { - type: WithdrawalType.TalerBankIntegrationApi, - confirmed: wsr.wgInfo.bankInfo.timestampBankConfirmed - ? true - : false, - reservePub: wsr.reservePub, - bankConfirmationUrl: wsr.wgInfo.bankInfo.confirmUrl, - }; + transactions.push(buildTransactionForBankIntegratedWithdraw(wsr, ort)); } else { const exchangeDetails = await getExchangeDetails( tx, @@ -290,31 +713,9 @@ export async function getTransactions( // FIXME: report somehow return; } - withdrawalDetails = { - type: WithdrawalType.ManualTransfer, - reservePub: wsr.reservePub, - exchangePaytoUris: - exchangeDetails.wireInfo?.accounts.map( - (x) => addPaytoQueryParams(x.payto_uri, { subject: wsr.reservePub }), - ) ?? [], - }; - } - transactions.push({ - type: TransactionType.Withdrawal, - amountEffective: Amounts.stringify(wsr.denomsSel.totalCoinValue), - amountRaw: Amounts.stringify(wsr.rawWithdrawalAmount), - withdrawalDetails, - exchangeBaseUrl: wsr.exchangeBaseUrl, - pending: !wsr.timestampFinish, - timestamp: wsr.timestampStart, - transactionId: makeEventId( - TransactionType.Withdrawal, - wsr.withdrawalGroupId, - ), - frozen: false, - ...(ort?.lastError ? { error: ort.lastError } : {}), - }); + transactions.push(buildTransactionForManualWithdraw(wsr, exchangeDetails, ort)); + } }); tx.depositGroups.iter().forEachAsync(async (dg) => { @@ -324,21 +725,8 @@ export async function getTransactions( } const opId = RetryTags.forDeposit(dg); const retryRecord = await tx.operationRetries.get(opId); - transactions.push({ - type: TransactionType.Deposit, - amountRaw: Amounts.stringify(dg.effectiveDepositAmount), - amountEffective: Amounts.stringify(dg.totalPayCost), - pending: !dg.timestampFinished, - frozen: false, - timestamp: dg.timestampCreated, - targetPaytoUri: dg.wire.payto_uri, - transactionId: makeEventId( - TransactionType.Deposit, - dg.depositGroupId, - ), - depositGroupId: dg.depositGroupId, - ...(retryRecord?.lastError ? { error: retryRecord.lastError } : {}), - }); + + transactions.push(buildTransactionForDeposit(dg, retryRecord)); }); tx.purchases.iter().forEachAsync(async (pr) => { @@ -358,107 +746,31 @@ export async function getTransactions( if (!proposal) { return; } - const info: OrderShortInfo = { - merchant: contractData.merchant, - orderId: contractData.orderId, - products: contractData.products, - summary: contractData.summary, - summary_i18n: contractData.summaryI18n, - contractTermsHash: contractData.contractTermsHash, - }; - if (contractData.fulfillmentUrl !== "") { - info.fulfillmentUrl = contractData.fulfillmentUrl; - } - const paymentTransactionId = makeEventId( - TransactionType.Payment, - pr.proposalId, - ); - const refundGroupKeys = new Set<string>(); - - for (const rk of Object.keys(pr.refunds)) { - const refund = pr.refunds[rk]; - const groupKey = `${refund.executionTime.t_s}`; - refundGroupKeys.add(groupKey); - } - - let totalRefundRaw = Amounts.getZero(contractData.amount.currency); - let totalRefundEffective = Amounts.getZero( - contractData.amount.currency, - ); - const refunds: RefundInfoShort[] = []; - for (const groupKey of refundGroupKeys.values()) { - const refundTombstoneId = makeEventId( + const filteredRefunds = await Promise.all(Object.values(pr.refunds).map(async r => { + const t = await tx.tombstones.get(makeEventId( TombstoneTag.DeleteRefund, pr.proposalId, - groupKey, - ); - const tombstone = await tx.tombstones.get(refundTombstoneId); - if (tombstone) { - continue; - } - const refundTransactionId = makeEventId( - TransactionType.Refund, - pr.proposalId, - groupKey, + `${r.executionTime.t_s}`, + )) + if (!t) return r + return undefined + })); + + const cleanRefunds = filteredRefunds.filter((x): x is WalletRefundItem => !!x); + + const refunds = mergeRefundByExecutionTime(cleanRefunds, Amounts.getZero(contractData.amount.currency)); + + refunds.forEach(async (refundInfo) => { + const refundQueryOpId = RetryTags.forRefundQuery(pr); + const refundQueryRetryRecord = await tx.operationRetries.get( + refundQueryOpId, ); - let r0: WalletRefundItem | undefined; - let amountRaw = Amounts.getZero(contractData.amount.currency); - let amountEffective = Amounts.getZero(contractData.amount.currency); - for (const rk of Object.keys(pr.refunds)) { - const refund = pr.refunds[rk]; - const myGroupKey = `${refund.executionTime.t_s}`; - if (myGroupKey !== groupKey) { - continue; - } - if (!r0) { - r0 = refund; - } - - if (refund.type === RefundState.Applied) { - amountRaw = Amounts.add(amountRaw, refund.refundAmount).amount; - amountEffective = Amounts.add( - amountEffective, - Amounts.sub( - refund.refundAmount, - refund.refundFee, - refund.totalRefreshCostBound, - ).amount, - ).amount; - - refunds.push({ - transactionId: refundTransactionId, - timestamp: r0.obtainedTime, - amountEffective: Amounts.stringify(amountEffective), - amountRaw: Amounts.stringify(amountRaw), - }); - } - } - if (!r0) { - throw Error("invariant violated"); - } - totalRefundRaw = Amounts.add(totalRefundRaw, amountRaw).amount; - totalRefundEffective = Amounts.add( - totalRefundEffective, - amountEffective, - ).amount; - transactions.push({ - type: TransactionType.Refund, - info, - refundedTransactionId: paymentTransactionId, - transactionId: refundTransactionId, - timestamp: r0.obtainedTime, - amountEffective: Amounts.stringify(amountEffective), - amountRaw: Amounts.stringify(amountRaw), - refundPending: - pr.refundAwaiting === undefined - ? undefined - : Amounts.stringify(pr.refundAwaiting), - pending: false, - frozen: false, - }); - } + transactions.push( + buildTransactionForRefund(pr, refundInfo, refundQueryRetryRecord) + ) + }) const payOpId = RetryTags.forPay(pr); const refundQueryOpId = RetryTags.forRefundQuery(pr); @@ -467,32 +779,9 @@ export async function getTransactions( refundQueryOpId, ); - const err = - refundQueryRetryRecord?.lastError ?? payRetryRecord?.lastError; - transactions.push({ - type: TransactionType.Payment, - amountRaw: Amounts.stringify(contractData.amount), - amountEffective: Amounts.stringify(pr.totalPayCost), - totalRefundRaw: Amounts.stringify(totalRefundRaw), - totalRefundEffective: Amounts.stringify(totalRefundEffective), - refundPending: - pr.refundAwaiting === undefined - ? undefined - : Amounts.stringify(pr.refundAwaiting), - status: pr.timestampFirstSuccessfulPay - ? PaymentStatus.Paid - : PaymentStatus.Accepted, - pending: - !pr.timestampFirstSuccessfulPay && - pr.abortStatus === AbortStatus.None, - refunds, - timestamp: pr.timestampAccept, - transactionId: paymentTransactionId, - proposalId: pr.proposalId, - info, - frozen: pr.payFrozen ?? false, - ...(err ? { error: err } : {}), - }); + const err = payRetryRecord !== undefined ? payRetryRecord : refundQueryRetryRecord + + transactions.push(buildTransactionForPurchase(pr, refunds, err)); }); tx.tips.iter().forEachAsync(async (tipRecord) => { @@ -509,20 +798,7 @@ export async function getTransactions( } const opId = RetryTags.forTipPickup(tipRecord); const retryRecord = await tx.operationRetries.get(opId); - transactions.push({ - type: TransactionType.Tip, - amountEffective: Amounts.stringify(tipRecord.tipAmountEffective), - amountRaw: Amounts.stringify(tipRecord.tipAmountRaw), - pending: !tipRecord.pickedUpTimestamp, - frozen: false, - timestamp: tipRecord.acceptedTimestamp, - transactionId: makeEventId( - TransactionType.Tip, - tipRecord.walletTipId, - ), - merchantBaseUrl: tipRecord.merchantBaseUrl, - error: retryRecord?.lastError, - }); + transactions.push(buildTransactionForTip(tipRecord, retryRecord)); }); }); diff --git a/packages/taler-wallet-core/src/operations/withdraw.ts b/packages/taler-wallet-core/src/operations/withdraw.ts index 47252a7e4..f2152ccbc 100644 --- a/packages/taler-wallet-core/src/operations/withdraw.ts +++ b/packages/taler-wallet-core/src/operations/withdraw.ts @@ -50,6 +50,7 @@ import { TalerErrorCode, TalerErrorDetail, TalerProtocolTimestamp, + TransactionType, UnblindedSignature, URL, VersionMatchResult, @@ -104,6 +105,7 @@ import { getExchangeTrust, updateExchangeFromUrl, } from "./exchanges.js"; +import { makeEventId } from "./transactions.js"; /** * Logger for this file. @@ -256,7 +258,7 @@ export function selectWithdrawalDenominations( DenominationRecord.getValue(d), d.fees.feeWithdraw, ).amount; - for (;;) { + for (; ;) { if (Amounts.cmp(remaining, cost) < 0) { break; } @@ -890,8 +892,7 @@ export async function updateWithdrawalDenoms( denom.verificationStatus === DenominationVerificationStatus.Unverified ) { logger.trace( - `Validating denomination (${current + 1}/${ - denominations.length + `Validating denomination (${current + 1}/${denominations.length }) signature of ${denom.denomPubHash}`, ); let valid = false; @@ -974,7 +975,7 @@ async function queryReserve( if ( resp.status === 404 && result.talerErrorResponse.code === - TalerErrorCode.EXCHANGE_RESERVES_STATUS_UNKNOWN + TalerErrorCode.EXCHANGE_RESERVES_STATUS_UNKNOWN ) { ws.notify({ type: NotificationType.ReserveNotYetFound, @@ -1003,10 +1004,16 @@ async function queryReserve( return { ready: true }; } +enum BankStatusResultCode { + Done = "done", + Waiting = "waiting", + Aborted = "aborted", +} + export async function processWithdrawalGroup( ws: InternalWalletState, withdrawalGroupId: string, - options: {} = {}, + options: object = {}, ): Promise<OperationAttemptResult> { logger.trace("processing withdrawal group", withdrawalGroupId); const withdrawalGroup = await ws.db @@ -1053,13 +1060,15 @@ export async function processWithdrawalGroup( }; } } + break; } - case ReserveRecordStatus.BankAborted: + case ReserveRecordStatus.BankAborted: { // FIXME return { type: OperationAttemptResultType.Pending, result: undefined, }; + } case ReserveRecordStatus.Dormant: // We can try to withdraw, nothing needs to be done with the reserve. break; @@ -1288,7 +1297,7 @@ export async function getExchangeWithdrawalInfo( ) { logger.warn( `wallet's support for exchange protocol version ${WALLET_EXCHANGE_PROTOCOL_VERSION} might be outdated ` + - `(exchange has ${exchangeDetails.protocolVersion}), checking for updates`, + `(exchange has ${exchangeDetails.protocolVersion}), checking for updates`, ); } } @@ -1540,12 +1549,6 @@ async function registerReserveWithBank( ws.notify({ type: NotificationType.ReserveRegisteredWithBank }); } -enum BankStatusResultCode { - Done = "done", - Waiting = "waiting", - Aborted = "aborted", -} - interface BankStatusResult { status: BankStatusResultCode; } @@ -1790,6 +1793,10 @@ export async function acceptWithdrawalFromUri( return { reservePub: existingWithdrawalGroup.reservePub, confirmTransferUrl: url, + transactionId: makeEventId( + TransactionType.Withdrawal, + existingWithdrawalGroup.withdrawalGroupId, + ) }; } @@ -1847,6 +1854,10 @@ export async function acceptWithdrawalFromUri( return { reservePub: withdrawalGroup.reservePub, confirmTransferUrl: withdrawInfo.confirmTransferUrl, + transactionId: makeEventId( + TransactionType.Withdrawal, + withdrawalGroupId, + ) }; } @@ -1901,5 +1912,9 @@ export async function createManualWithdrawal( return { reservePub: withdrawalGroup.reservePub, exchangePaytoUris: exchangePaytoUris, + transactionId: makeEventId( + TransactionType.Withdrawal, + withdrawalGroupId, + ) }; } diff --git a/packages/taler-wallet-core/src/wallet.ts b/packages/taler-wallet-core/src/wallet.ts index 02ed8a61b..49c7f77cf 100644 --- a/packages/taler-wallet-core/src/wallet.ts +++ b/packages/taler-wallet-core/src/wallet.ts @@ -91,6 +91,7 @@ import { OperationMap, FeeDescription, TalerErrorDetail, + codecForTransactionByIdRequest, } from "@gnu-taler/taler-util"; import { TalerCryptoInterface } from "./crypto/cryptoImplementation.js"; import { @@ -198,6 +199,7 @@ import { import { acceptTip, prepareTip, processTip } from "./operations/tip.js"; import { deleteTransaction, + getTransactionById, getTransactions, retryTransaction, } from "./operations/transactions.js"; @@ -1080,6 +1082,10 @@ async function dispatchRequestInternal( const req = codecForTransactionsRequest().decode(payload); return await getTransactions(ws, req); } + case "getTransactionById": { + const req = codecForTransactionByIdRequest().decode(payload); + return await getTransactionById(ws, req) + } case "addExchange": { const req = codecForAddExchangeRequest().decode(payload); await updateExchangeFromUrl(ws, req.exchangeBaseUrl, { @@ -1227,8 +1233,7 @@ async function dispatchRequestInternal( } case "acceptTip": { const req = codecForAcceptTipRequest().decode(payload); - await acceptTip(ws, req.walletTipId); - return {}; + return await acceptTip(ws, req.walletTipId); } case "exportBackupPlain": { return exportBackup(ws); diff --git a/packages/taler-wallet-webextension/src/components/BankDetailsByPaytoType.tsx b/packages/taler-wallet-webextension/src/components/BankDetailsByPaytoType.tsx index 3a15cf1fb..9ab5212f2 100644 --- a/packages/taler-wallet-webextension/src/components/BankDetailsByPaytoType.tsx +++ b/packages/taler-wallet-webextension/src/components/BankDetailsByPaytoType.tsx @@ -73,10 +73,17 @@ export function BankDetailsByPaytoType({ </p> <table> <tr> - <td>{payto.targetPath}</td> <td> - <Amount value={amount} hideCurrency /> BTC + <div> + {payto.targetPath} <Amount value={amount} hideCurrency /> BTC + </div> + {payto.segwitAddrs.map((addr, i) => ( + <div key={i}> + {addr} <Amount value={min} hideCurrency /> BTC + </div> + ))} </td> + <td></td> <td> <CopyButton getContent={() => @@ -85,21 +92,6 @@ export function BankDetailsByPaytoType({ /> </td> </tr> - {payto.segwitAddrs.map((addr, i) => ( - <tr key={i}> - <td>{addr}</td> - <td> - <Amount value={min} hideCurrency /> BTC - </td> - <td> - <CopyButton - getContent={() => - `${addr} ${Amounts.stringifyValue(min)} BTC` - } - /> - </td> - </tr> - ))} </table> <p> <i18n.Translate> diff --git a/packages/taler-wallet-webextension/src/cta/Payment/stories.tsx b/packages/taler-wallet-webextension/src/cta/Payment/stories.tsx index 877c1996a..559e5c5d4 100644 --- a/packages/taler-wallet-webextension/src/cta/Payment/stories.tsx +++ b/packages/taler-wallet-webextension/src/cta/Payment/stories.tsx @@ -348,6 +348,7 @@ export const AlreadyPaidWithoutFulfillment = createExample(BaseView, { payResult: { type: ConfirmPayResultType.Done, contractTerms: {} as any, + transactionId: "", }, payStatus: { status: PreparePayResultType.AlreadyConfirmed, @@ -386,6 +387,7 @@ export const AlreadyPaidWithFulfillment = createExample(BaseView, { fulfillment_message: "thanks for buying!", fulfillment_url: "https://demo.taler.net", } as Partial<ContractTerms> as any, + transactionId: "", }, payStatus: { status: PreparePayResultType.AlreadyConfirmed, diff --git a/packages/taler-wallet-webextension/src/wallet/DeveloperPage.stories.tsx b/packages/taler-wallet-webextension/src/wallet/DeveloperPage.stories.tsx index 1a5d72337..ae1581009 100644 --- a/packages/taler-wallet-webextension/src/wallet/DeveloperPage.stories.tsx +++ b/packages/taler-wallet-webextension/src/wallet/DeveloperPage.stories.tsx @@ -35,6 +35,7 @@ export const AllOff = createExample(TestedComponent, { onDownloadDatabase: async () => "this is the content of the database", operations: [ { + id: "", type: PendingTaskType.ExchangeUpdate, exchangeBaseUrl: "http://exchange.url.", givesLifeness: false, diff --git a/packages/taler-wallet-webextension/src/wallet/Transaction.tsx b/packages/taler-wallet-webextension/src/wallet/Transaction.tsx index 3d2a16e8f..21cee8789 100644 --- a/packages/taler-wallet-webextension/src/wallet/Transaction.tsx +++ b/packages/taler-wallet-webextension/src/wallet/Transaction.tsx @@ -70,13 +70,8 @@ interface Props { } async function getTransaction(tid: string): Promise<Transaction> { - const res = await wxApi.getTransactions(); - const ts = res.transactions.filter((t) => t.transactionId === tid); - if (ts.length > 1) throw Error("more than one transaction with this id"); - if (ts.length === 1) { - return ts[0]; - } - throw Error("no transaction found"); + const res = await wxApi.getTransactionById(tid); + return res; } export function TransactionPage({ tid, goToWalletHistory }: Props): VNode { diff --git a/packages/taler-wallet-webextension/src/wxApi.ts b/packages/taler-wallet-webextension/src/wxApi.ts index b797179c3..e9d26853d 100644 --- a/packages/taler-wallet-webextension/src/wxApi.ts +++ b/packages/taler-wallet-webextension/src/wxApi.ts @@ -68,6 +68,10 @@ import { WalletCoreVersion, WithdrawUriInfoResponse, ExchangeFullDetails, + Transaction, + AcceptTipResponse, + AcceptPeerPullPaymentResponse, + AcceptPeerPushPaymentResponse, } from "@gnu-taler/taler-util"; import { AddBackupProviderRequest, @@ -476,7 +480,7 @@ export function prepareTip(req: PrepareTipRequest): Promise<PrepareTipResult> { return callBackend("prepareTip", req); } -export function acceptTip(req: AcceptTipRequest): Promise<void> { +export function acceptTip(req: AcceptTipRequest): Promise<AcceptTipResponse> { return callBackend("acceptTip", req); } @@ -513,7 +517,7 @@ export function checkPeerPushPayment( } export function acceptPeerPushPayment( req: AcceptPeerPushPaymentRequest, -): Promise<void> { +): Promise<AcceptPeerPushPaymentResponse> { return callBackend("acceptPeerPushPayment", req); } export function initiatePeerPullPayment( @@ -528,6 +532,12 @@ export function checkPeerPullPayment( } export function acceptPeerPullPayment( req: AcceptPeerPullPaymentRequest, -): Promise<void> { +): Promise<AcceptPeerPullPaymentResponse> { return callBackend("acceptPeerPullPayment", req); } + +export function getTransactionById(tid: string): Promise<Transaction> { + return callBackend("getTransactionById", { + transactionId: tid + }) +}
\ No newline at end of file |