From a11ac57535b0375f152ce115ee541cb8aca98e84 Mon Sep 17 00:00:00 2001 From: Florian Dold Date: Wed, 24 Aug 2022 22:17:19 +0200 Subject: wallet-core: p2p support for transactions list --- packages/taler-util/src/transactionsTypes.ts | 88 +++- .../src/integrationtests/test-peer-to-peer-pull.ts | 44 +- .../src/integrationtests/test-peer-to-peer-push.ts | 13 + packages/taler-wallet-core/src/db.ts | 30 +- .../src/operations/peer-to-peer.ts | 87 ++-- .../src/operations/transactions.ts | 530 ++++++++++++--------- .../taler-wallet-core/src/operations/withdraw.ts | 5 + 7 files changed, 524 insertions(+), 273 deletions(-) diff --git a/packages/taler-util/src/transactionsTypes.ts b/packages/taler-util/src/transactionsTypes.ts index 645f0120b..c4bd3d464 100644 --- a/packages/taler-util/src/transactionsTypes.ts +++ b/packages/taler-util/src/transactionsTypes.ts @@ -102,7 +102,11 @@ export type Transaction = | TransactionRefund | TransactionTip | TransactionRefresh - | TransactionDeposit; + | TransactionDeposit + | TransactionPeerPullCredit + | TransactionPeerPullDebit + | TransactionPeerPushCredit + | TransactionPeerPushDebit; export enum TransactionType { Withdrawal = "withdrawal", @@ -111,6 +115,10 @@ export enum TransactionType { Refresh = "refresh", Tip = "tip", Deposit = "deposit", + PeerPushDebit = "peer-push-debit", + PeerPushCredit = "peer-push-credit", + PeerPullDebit = "peer-pull-debit", + PeerPullCredit = "peer-pull-credit", } export enum WithdrawalType { @@ -179,6 +187,76 @@ export interface TransactionWithdrawal extends TransactionCommon { withdrawalDetails: WithdrawalDetails; } +export interface TransactionPeerPullCredit extends TransactionCommon { + type: TransactionType.PeerPullCredit; + + /** + * Exchange used. + */ + exchangeBaseUrl: string; + + /** + * Amount that got subtracted from the reserve balance. + */ + amountRaw: AmountString; + + /** + * Amount that actually was (or will be) added to the wallet's balance. + */ + amountEffective: AmountString; +} + +export interface TransactionPeerPullDebit extends TransactionCommon { + type: TransactionType.PeerPullDebit; + + /** + * Exchange used. + */ + exchangeBaseUrl: string; + + amountRaw: AmountString; + + amountEffective: AmountString; +} + +export interface TransactionPeerPushDebit extends TransactionCommon { + type: TransactionType.PeerPushDebit; + + /** + * Exchange used. + */ + exchangeBaseUrl: string; + + /** + * Amount that got subtracted from the reserve balance. + */ + amountRaw: AmountString; + + /** + * Amount that actually was (or will be) added to the wallet's balance. + */ + amountEffective: AmountString; +} + +export interface TransactionPeerPushCredit extends TransactionCommon { + type: TransactionType.PeerPushCredit; + + /** + * Exchange used. + */ + exchangeBaseUrl: string; + + /** + * Amount that got subtracted from the reserve balance. + */ + amountRaw: AmountString; + + /** + * Amount that actually was (or will be) added to the wallet's balance. + */ + amountEffective: AmountString; +} + export enum PaymentStatus { /** * Explicitly aborted after timeout / failure @@ -311,10 +389,10 @@ export interface OrderShortInfo { } export interface RefundInfoShort { - transactionId: string, - timestamp: TalerProtocolTimestamp, - amountEffective: AmountString, - amountRaw: AmountString, + transactionId: string; + timestamp: TalerProtocolTimestamp; + amountEffective: AmountString; + amountRaw: AmountString; } export interface TransactionRefund extends TransactionCommon { diff --git a/packages/taler-wallet-cli/src/integrationtests/test-peer-to-peer-pull.ts b/packages/taler-wallet-cli/src/integrationtests/test-peer-to-peer-pull.ts index 1be1563ce..0c149d63a 100644 --- a/packages/taler-wallet-cli/src/integrationtests/test-peer-to-peer-pull.ts +++ b/packages/taler-wallet-cli/src/integrationtests/test-peer-to-peer-pull.ts @@ -19,7 +19,7 @@ */ import { j2s } from "@gnu-taler/taler-util"; import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; -import { GlobalTestState } from "../harness/harness.js"; +import { GlobalTestState, WalletCli } from "../harness/harness.js"; import { createSimpleTestkudosEnvironment, withdrawViaBank, @@ -31,16 +31,23 @@ import { export async function runPeerToPeerPullTest(t: GlobalTestState) { // Set up test environment - const { wallet, bank, exchange, merchant } = - await createSimpleTestkudosEnvironment(t); + const { bank, exchange, merchant } = await createSimpleTestkudosEnvironment( + t, + ); // Withdraw digital cash into the wallet. - - await withdrawViaBank(t, { wallet, bank, exchange, amount: "TESTKUDOS:20" }); - - await wallet.runUntilDone(); - - const resp = await wallet.client.call( + const wallet1 = new WalletCli(t, "w1"); + const wallet2 = new WalletCli(t, "w2"); + await withdrawViaBank(t, { + wallet: wallet2, + bank, + exchange, + amount: "TESTKUDOS:20", + }); + + await wallet1.runUntilDone(); + + const resp = await wallet1.client.call( WalletApiOperation.InitiatePeerPullPayment, { exchangeBaseUrl: exchange.baseUrl, @@ -51,7 +58,7 @@ export async function runPeerToPeerPullTest(t: GlobalTestState) { }, ); - const checkResp = await wallet.client.call( + const checkResp = await wallet2.client.call( WalletApiOperation.CheckPeerPullPayment, { talerUri: resp.talerUri, @@ -60,18 +67,27 @@ export async function runPeerToPeerPullTest(t: GlobalTestState) { console.log(`checkResp: ${j2s(checkResp)}`); - const acceptResp = await wallet.client.call( + const acceptResp = await wallet2.client.call( WalletApiOperation.AcceptPeerPullPayment, { peerPullPaymentIncomingId: checkResp.peerPullPaymentIncomingId, }, ); - const txs = await wallet.client.call(WalletApiOperation.GetTransactions, {}); + await wallet1.runUntilDone(); + await wallet2.runUntilDone(); - console.log(`transactions: ${j2s(txs)}`); + const txn1 = await wallet1.client.call( + WalletApiOperation.GetTransactions, + {}, + ); + const txn2 = await wallet2.client.call( + WalletApiOperation.GetTransactions, + {}, + ); - await wallet.runUntilDone(); + console.log(`txn1: ${j2s(txn1)}`); + console.log(`txn2: ${j2s(txn2)}`); } runPeerToPeerPullTest.suites = ["wallet"]; diff --git a/packages/taler-wallet-cli/src/integrationtests/test-peer-to-peer-push.ts b/packages/taler-wallet-cli/src/integrationtests/test-peer-to-peer-push.ts index bf65731d2..ebbe87ae8 100644 --- a/packages/taler-wallet-cli/src/integrationtests/test-peer-to-peer-push.ts +++ b/packages/taler-wallet-cli/src/integrationtests/test-peer-to-peer-push.ts @@ -17,6 +17,7 @@ /** * Imports. */ +import { j2s } from "@gnu-taler/taler-util"; import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; import { GlobalTestState, WalletCli } from "../harness/harness.js"; import { @@ -78,6 +79,18 @@ export async function runPeerToPeerPushTest(t: GlobalTestState) { await wallet1.runUntilDone(); await wallet2.runUntilDone(); + + const txn1 = await wallet1.client.call( + WalletApiOperation.GetTransactions, + {}, + ); + const txn2 = await wallet2.client.call( + WalletApiOperation.GetTransactions, + {}, + ); + + console.log(`txn1: ${j2s(txn1)}`); + console.log(`txn2: ${j2s(txn2)}`); } runPeerToPeerPushTest.suites = ["wallet"]; diff --git a/packages/taler-wallet-core/src/db.ts b/packages/taler-wallet-core/src/db.ts index 3d59ce0a7..e6b4854db 100644 --- a/packages/taler-wallet-core/src/db.ts +++ b/packages/taler-wallet-core/src/db.ts @@ -1219,6 +1219,13 @@ export interface DenomSelectionState { }[]; } +export const enum WithdrawalRecordType { + BankManual = "bank-manual", + BankIntegrated = "bank-integrated", + PeerPullCredit = "peer-pull-credit", + PeerPushCredit = "peer-push-credit", +} + /** * Group of withdrawal operations that need to be executed. * (Either for a normal withdrawal or from a tip.) @@ -1232,6 +1239,8 @@ export interface WithdrawalGroupRecord { */ withdrawalGroupId: string; + withdrawalType: WithdrawalRecordType; + /** * Secret seed used to derive planchets. * Stored since planchets are created lazily. @@ -1607,8 +1616,6 @@ export interface PeerPushPaymentInitiationRecord { contractPriv: string; - contractPub: string; - purseExpiration: TalerProtocolTimestamp; /** @@ -1681,7 +1688,11 @@ export interface PeerPullPaymentIncomingRecord { contractTerms: PeerContractTerms; - timestamp: TalerProtocolTimestamp; + timestampCreated: TalerProtocolTimestamp; + + paid: boolean; + + accepted: boolean; contractPriv: string; } @@ -1878,9 +1889,18 @@ export const WalletStoresV1 = { ]), }, ), - peerPullPaymentInitiation: describeStore( + peerPullPaymentInitiations: describeStore( describeContents( - "peerPushPaymentInitiation", + "peerPullPaymentInitiations", + { + keyPath: "pursePub", + }, + ), + {}, + ), + peerPushPaymentInitiations: describeStore( + describeContents( + "peerPushPaymentInitiations", { keyPath: "pursePub", }, 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 ddfaa0827..d6d71720c 100644 --- a/packages/taler-wallet-core/src/operations/peer-to-peer.ts +++ b/packages/taler-wallet-core/src/operations/peer-to-peer.ts @@ -65,6 +65,7 @@ import { MergeReserveInfo, ReserveRecordStatus, WalletStoresV1, + WithdrawalRecordType, } from "../db.js"; import { readSuccessResponseJsonOrThrow } from "../util/http.js"; import { InternalWalletState } from "../internal-wallet-state.js"; @@ -208,12 +209,39 @@ export async function initiatePeerToPeerPush( ): Promise { // FIXME: actually create a record for retries here! const instructedAmount = Amounts.parseOrThrow(req.amount); + + const pursePair = await ws.cryptoApi.createEddsaKeypair({}); + const mergePair = await ws.cryptoApi.createEddsaKeypair({}); + + const purseExpiration: TalerProtocolTimestamp = AbsoluteTime.toTimestamp( + AbsoluteTime.addDuration( + AbsoluteTime.now(), + Duration.fromSpec({ days: 2 }), + ), + ); + + const contractTerms = { + ...req.partialContractTerms, + purse_expiration: purseExpiration, + amount: req.amount, + }; + + const hContractTerms = ContractTermsUtil.hashContractTerms(contractTerms); + + const econtractResp = await ws.cryptoApi.encryptContractForMerge({ + contractTerms, + mergePriv: mergePair.priv, + pursePriv: pursePair.priv, + pursePub: pursePair.pub, + }); + const coinSelRes: PeerCoinSelection | undefined = await ws.db .mktx((x) => ({ exchanges: x.exchanges, coins: x.coins, denominations: x.denominations, refreshGroups: x.refreshGroups, + peerPushPaymentInitiations: x.peerPushPaymentInitiations, })) .runReadWrite(async (tx) => { const sel = await selectPeerCoins(ws, tx, instructedAmount); @@ -232,6 +260,20 @@ export async function initiatePeerToPeerPush( await tx.coins.put(coin); } + await tx.peerPushPaymentInitiations.add({ + amount: Amounts.stringify(instructedAmount), + contractPriv: econtractResp.contractPriv, + exchangeBaseUrl: sel.exchangeBaseUrl, + mergePriv: mergePair.priv, + mergePub: mergePair.pub, + // FIXME: only set this later! + purseCreated: true, + purseExpiration: purseExpiration, + pursePriv: pursePair.priv, + pursePub: pursePair.pub, + timestampCreated: TalerProtocolTimestamp.now(), + }); + await createRefreshGroup(ws, tx, pubs, RefreshReason.Pay); return sel; @@ -242,24 +284,6 @@ export async function initiatePeerToPeerPush( throw Error("insufficient balance"); } - const pursePair = await ws.cryptoApi.createEddsaKeypair({}); - const mergePair = await ws.cryptoApi.createEddsaKeypair({}); - - const purseExpiration: TalerProtocolTimestamp = AbsoluteTime.toTimestamp( - AbsoluteTime.addDuration( - AbsoluteTime.now(), - Duration.fromSpec({ days: 2 }), - ), - ); - - const contractTerms = { - ...req.partialContractTerms, - purse_expiration: purseExpiration, - amount: req.amount, - }; - - const hContractTerms = ContractTermsUtil.hashContractTerms(contractTerms); - const purseSigResp = await ws.cryptoApi.signPurseCreation({ hContractTerms, mergePub: mergePair.pub, @@ -280,13 +304,6 @@ export async function initiatePeerToPeerPush( coinSelRes.exchangeBaseUrl, ); - const econtractResp = await ws.cryptoApi.encryptContractForMerge({ - contractTerms, - mergePriv: mergePair.priv, - pursePriv: pursePair.priv, - pursePub: pursePair.pub, - }); - const httpResp = await ws.http.postJson(createPurseUrl.href, { amount: Amounts.stringify(instructedAmount), merge_pub: mergePair.pub, @@ -517,6 +534,7 @@ export async function acceptPeerPushPayment( await internalCreateWithdrawalGroup(ws, { amount, + withdrawalType: WithdrawalRecordType.PeerPushCredit, exchangeBaseUrl: peerInc.exchangeBaseUrl, reserveStatus: ReserveRecordStatus.QueryingStatus, reserveKeyPair: { @@ -554,6 +572,7 @@ export async function acceptPeerPullPayment( coins: x.coins, denominations: x.denominations, refreshGroups: x.refreshGroups, + peerPullPaymentIncoming: x.peerPullPaymentIncoming, })) .runReadWrite(async (tx) => { const sel = await selectPeerCoins(ws, tx, instructedAmount); @@ -574,6 +593,15 @@ export async function acceptPeerPullPayment( await createRefreshGroup(ws, tx, pubs, RefreshReason.Pay); + const pi = await tx.peerPullPaymentIncoming.get( + req.peerPullPaymentIncomingId, + ); + if (!pi) { + throw Error(); + } + pi.accepted = true; + await tx.peerPullPaymentIncoming.put(pi); + return sel; }); logger.info(`selected p2p coins: ${j2s(coinSelRes)}`); @@ -656,8 +684,10 @@ export async function checkPeerPullPayment( contractPriv: contractPriv, exchangeBaseUrl: exchangeBaseUrl, pursePub: pursePub, - timestamp: TalerProtocolTimestamp.now(), + timestampCreated: TalerProtocolTimestamp.now(), contractTerms: dec.contractTerms, + paid: false, + accepted: false, }); }); @@ -672,6 +702,8 @@ export async function initiatePeerRequestForPay( ws: InternalWalletState, req: InitiatePeerPullPaymentRequest, ): Promise { + await updateExchangeFromUrl(ws, req.exchangeBaseUrl); + const mergeReserveInfo = await getMergeReserveInfo(ws, { exchangeBaseUrl: req.exchangeBaseUrl, }); @@ -727,7 +759,7 @@ export async function initiatePeerRequestForPay( await ws.db .mktx((x) => ({ - peerPullPaymentInitiation: x.peerPullPaymentInitiation, + peerPullPaymentInitiation: x.peerPullPaymentInitiations, })) .runReadWrite(async (tx) => { await tx.peerPullPaymentInitiation.put({ @@ -772,6 +804,7 @@ export async function initiatePeerRequestForPay( await internalCreateWithdrawalGroup(ws, { amount: Amounts.parseOrThrow(req.amount), + withdrawalType: WithdrawalRecordType.PeerPullCredit, exchangeBaseUrl: req.exchangeBaseUrl, reserveStatus: ReserveRecordStatus.QueryingStatus, reserveKeyPair: { diff --git a/packages/taler-wallet-core/src/operations/transactions.ts b/packages/taler-wallet-core/src/operations/transactions.ts index ec499420f..62df996c3 100644 --- a/packages/taler-wallet-core/src/operations/transactions.ts +++ b/packages/taler-wallet-core/src/operations/transactions.ts @@ -38,6 +38,7 @@ import { RefundState, ReserveRecordStatus, WalletRefundItem, + WithdrawalRecordType, } from "../db.js"; import { processDepositGroup } from "./deposits.js"; import { getExchangeDetails } from "./exchanges.js"; @@ -101,10 +102,14 @@ const txOrder: { [t in TransactionType]: number } = { [TransactionType.Withdrawal]: 1, [TransactionType.Tip]: 2, [TransactionType.Payment]: 3, - [TransactionType.Refund]: 4, - [TransactionType.Deposit]: 5, - [TransactionType.Refresh]: 6, - [TransactionType.Tip]: 7, + [TransactionType.PeerPullCredit]: 4, + [TransactionType.PeerPullDebit]: 5, + [TransactionType.PeerPushCredit]: 6, + [TransactionType.PeerPushDebit]: 7, + [TransactionType.Refund]: 8, + [TransactionType.Deposit]: 9, + [TransactionType.Refresh]: 10, + [TransactionType.Tip]: 11, }; /** @@ -131,267 +136,348 @@ export async function getTransactions( recoupGroups: x.recoupGroups, depositGroups: x.depositGroups, tombstones: x.tombstones, + peerPushPaymentInitiations: x.peerPushPaymentInitiations, + peerPullPaymentIncoming: x.peerPullPaymentIncoming, })) - .runReadOnly( - // Report withdrawals that are currently in progress. - async (tx) => { - tx.withdrawalGroups.iter().forEachAsync(async (wsr) => { - if ( - shouldSkipCurrency( - transactionsRequest, - wsr.rawWithdrawalAmount.currency, - ) - ) { - return; - } + .runReadOnly(async (tx) => { + tx.peerPushPaymentInitiations.iter().forEachAsync(async (pi) => { + const amount = Amounts.parseOrThrow(pi.amount); + if (shouldSkipCurrency(transactionsRequest, amount.currency)) { + return; + } + if (shouldSkipSearch(transactionsRequest, [])) { + return; + } + transactions.push({ + type: TransactionType.PeerPushDebit, + amountEffective: pi.amount, + amountRaw: pi.amount, + exchangeBaseUrl: pi.exchangeBaseUrl, + frozen: false, + pending: !pi.purseCreated, + timestamp: pi.timestampCreated, + transactionId: makeEventId( + TransactionType.PeerPushDebit, + pi.pursePub, + ), + }); + }); - if (shouldSkipSearch(transactionsRequest, [])) { - return; - } - let withdrawalDetails: WithdrawalDetails; - if (wsr.bankInfo) { - withdrawalDetails = { - type: WithdrawalType.TalerBankIntegrationApi, - confirmed: wsr.bankInfo.timestampBankConfirmed ? true : false, - reservePub: wsr.reservePub, - bankConfirmationUrl: wsr.bankInfo.confirmUrl, - }; - } else { - const exchangeDetails = await getExchangeDetails( - tx, - wsr.exchangeBaseUrl, - ); - if (!exchangeDetails) { - // FIXME: report somehow - return; - } - withdrawalDetails = { - type: WithdrawalType.ManualTransfer, - reservePub: wsr.reservePub, - exchangePaytoUris: - exchangeDetails.wireInfo?.accounts.map((x) => `${x.payto_uri}?subject=${wsr.reservePub}`) ?? - [], - }; - } + tx.peerPullPaymentIncoming.iter().forEachAsync(async (pi) => { + const amount = Amounts.parseOrThrow(pi.contractTerms.amount); + if (shouldSkipCurrency(transactionsRequest, amount.currency)) { + return; + } + if (shouldSkipSearch(transactionsRequest, [])) { + return; + } + if (!pi.accepted) { + return; + } + transactions.push({ + type: TransactionType.PeerPullDebit, + amountEffective: Amounts.stringify(amount), + amountRaw: Amounts.stringify(amount), + exchangeBaseUrl: pi.exchangeBaseUrl, + frozen: false, + pending: false, + timestamp: pi.timestampCreated, + transactionId: makeEventId( + TransactionType.PeerPullDebit, + pi.pursePub, + ), + }); + }); + tx.withdrawalGroups.iter().forEachAsync(async (wsr) => { + if ( + shouldSkipCurrency( + transactionsRequest, + wsr.rawWithdrawalAmount.currency, + ) + ) { + return; + } + + if (shouldSkipSearch(transactionsRequest, [])) { + return; + } + let withdrawalDetails: WithdrawalDetails; + if (wsr.withdrawalType === WithdrawalRecordType.PeerPullCredit) { transactions.push({ - type: TransactionType.Withdrawal, + type: TransactionType.PeerPullCredit, 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, + TransactionType.PeerPullCredit, wsr.withdrawalGroupId, ), frozen: false, ...(wsr.lastError ? { error: wsr.lastError } : {}), }); - }); - - tx.depositGroups.iter().forEachAsync(async (dg) => { - const amount = Amounts.parseOrThrow(dg.contractTermsRaw.amount); - if (shouldSkipCurrency(transactionsRequest, amount.currency)) { - return; - } - + return; + } else if (wsr.withdrawalType === WithdrawalRecordType.PeerPushCredit) { 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, + type: TransactionType.PeerPushCredit, + amountEffective: Amounts.stringify(wsr.denomsSel.totalCoinValue), + amountRaw: Amounts.stringify(wsr.rawWithdrawalAmount), + exchangeBaseUrl: wsr.exchangeBaseUrl, + pending: !wsr.timestampFinish, + timestamp: wsr.timestampStart, transactionId: makeEventId( - TransactionType.Deposit, - dg.depositGroupId, + TransactionType.PeerPushCredit, + wsr.withdrawalGroupId, ), - depositGroupId: dg.depositGroupId, - ...(dg.lastError ? { error: dg.lastError } : {}), + frozen: false, + ...(wsr.lastError ? { error: wsr.lastError } : {}), }); - }); - - tx.purchases.iter().forEachAsync(async (pr) => { - if ( - shouldSkipCurrency( - transactionsRequest, - pr.download.contractData.amount.currency, - ) - ) { - return; - } - const contractData = pr.download.contractData; - if (shouldSkipSearch(transactionsRequest, [contractData.summary])) { - return; - } - const proposal = await tx.proposals.get(pr.proposalId); - if (!proposal) { + return; + } else if (wsr.bankInfo) { + withdrawalDetails = { + type: WithdrawalType.TalerBankIntegrationApi, + confirmed: wsr.bankInfo.timestampBankConfirmed ? true : false, + reservePub: wsr.reservePub, + bankConfirmationUrl: wsr.bankInfo.confirmUrl, + }; + } else { + const exchangeDetails = await getExchangeDetails( + tx, + wsr.exchangeBaseUrl, + ); + if (!exchangeDetails) { + // FIXME: report somehow return; } - const info: OrderShortInfo = { - merchant: contractData.merchant, - orderId: contractData.orderId, - products: contractData.products, - summary: contractData.summary, - summary_i18n: contractData.summaryI18n, - contractTermsHash: contractData.contractTermsHash, + withdrawalDetails = { + type: WithdrawalType.ManualTransfer, + reservePub: wsr.reservePub, + exchangePaytoUris: + exchangeDetails.wireInfo?.accounts.map( + (x) => `${x.payto_uri}?subject=${wsr.reservePub}`, + ) ?? [], }; - if (contractData.fulfillmentUrl !== "") { - info.fulfillmentUrl = contractData.fulfillmentUrl; + } + + 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, + ...(wsr.lastError ? { error: wsr.lastError } : {}), + }); + }); + + tx.depositGroups.iter().forEachAsync(async (dg) => { + const amount = Amounts.parseOrThrow(dg.contractTermsRaw.amount); + if (shouldSkipCurrency(transactionsRequest, amount.currency)) { + return; + } + + 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, + ...(dg.lastError ? { error: dg.lastError } : {}), + }); + }); + + tx.purchases.iter().forEachAsync(async (pr) => { + if ( + shouldSkipCurrency( + transactionsRequest, + pr.download.contractData.amount.currency, + ) + ) { + return; + } + const contractData = pr.download.contractData; + if (shouldSkipSearch(transactionsRequest, [contractData.summary])) { + return; + } + const proposal = await tx.proposals.get(pr.proposalId); + 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(); + + 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( + TombstoneTag.DeleteRefund, + pr.proposalId, + groupKey, + ); + const tombstone = await tx.tombstones.get(refundTombstoneId); + if (tombstone) { + continue; } - const paymentTransactionId = makeEventId( - TransactionType.Payment, + const refundTransactionId = makeEventId( + TransactionType.Refund, pr.proposalId, + groupKey, ); - const refundGroupKeys = new Set(); - + 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 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( - TombstoneTag.DeleteRefund, - pr.proposalId, - groupKey, - ); - const tombstone = await tx.tombstones.get(refundTombstoneId); - if (tombstone) { + const myGroupKey = `${refund.executionTime.t_s}`; + if (myGroupKey !== groupKey) { continue; } - const refundTransactionId = makeEventId( - TransactionType.Refund, - pr.proposalId, - groupKey, - ); - 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"); + r0 = refund; } - 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, - }); + 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"); } - const err = pr.lastPayError ?? pr.lastRefundStatusError; + totalRefundRaw = Amounts.add(totalRefundRaw, amountRaw).amount; + totalRefundEffective = Amounts.add( + totalRefundEffective, + amountEffective, + ).amount; transactions.push({ - type: TransactionType.Payment, - amountRaw: Amounts.stringify(contractData.amount), - amountEffective: Amounts.stringify(pr.totalPayCost), - totalRefundRaw: Amounts.stringify(totalRefundRaw), - totalRefundEffective: Amounts.stringify(totalRefundEffective), + 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), - 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 } : {}), + pending: false, + frozen: false, }); + } + + const err = pr.lastPayError ?? pr.lastRefundStatusError; + 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 } : {}), }); + }); - tx.tips.iter().forEachAsync(async (tipRecord) => { - if ( - shouldSkipCurrency( - transactionsRequest, - tipRecord.tipAmountRaw.currency, - ) - ) { - return; - } - if (!tipRecord.acceptedTimestamp) { - return; - } - 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, - // merchant: { - // name: tipRecord.merchantBaseUrl, - // }, - error: tipRecord.lastError, - }); + tx.tips.iter().forEachAsync(async (tipRecord) => { + if ( + shouldSkipCurrency( + transactionsRequest, + tipRecord.tipAmountRaw.currency, + ) + ) { + return; + } + if (!tipRecord.acceptedTimestamp) { + return; + } + 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, + // merchant: { + // name: tipRecord.merchantBaseUrl, + // }, + error: tipRecord.lastError, }); - }, - ); + }); + }); const txPending = transactions.filter((x) => x.pending); const txNotPending = transactions.filter((x) => !x.pending); diff --git a/packages/taler-wallet-core/src/operations/withdraw.ts b/packages/taler-wallet-core/src/operations/withdraw.ts index 3c4e2d98c..4e350670d 100644 --- a/packages/taler-wallet-core/src/operations/withdraw.ts +++ b/packages/taler-wallet-core/src/operations/withdraw.ts @@ -74,6 +74,7 @@ import { ReserveRecordStatus, WalletStoresV1, WithdrawalGroupRecord, + WithdrawalRecordType, } from "../db.js"; import { getErrorDetailFromException, @@ -1700,6 +1701,7 @@ export async function internalCreateWithdrawalGroup( forcedDenomSel?: ForcedDenomSel; reserveKeyPair?: EddsaKeypair; restrictAge?: number; + withdrawalType: WithdrawalRecordType; }, ): Promise { const reserveKeyPair = @@ -1745,6 +1747,7 @@ export async function internalCreateWithdrawalGroup( restrictAge: args.restrictAge, senderWire: undefined, timestampFinish: undefined, + withdrawalType: args.withdrawalType, }; const exchangeInfo = await updateExchangeFromUrl(ws, canonExchange); @@ -1819,6 +1822,7 @@ export async function acceptWithdrawalFromUri( const withdrawalGroup = await internalCreateWithdrawalGroup(ws, { amount: withdrawInfo.amount, exchangeBaseUrl: req.selectedExchange, + withdrawalType: WithdrawalRecordType.BankIntegrated, forcedDenomSel: req.forcedDenomSel, reserveStatus: ReserveRecordStatus.RegisteringBank, bankInfo: { @@ -1877,6 +1881,7 @@ export async function createManualWithdrawal( ): Promise { const withdrawalGroup = await internalCreateWithdrawalGroup(ws, { amount: Amounts.jsonifyAmount(req.amount), + withdrawalType: WithdrawalRecordType.BankManual, exchangeBaseUrl: req.exchangeBaseUrl, bankInfo: undefined, forcedDenomSel: req.forcedDenomSel, -- cgit v1.2.3