diff options
author | Sebastian <sebasjm@gmail.com> | 2023-03-29 15:14:02 -0300 |
---|---|---|
committer | Sebastian <sebasjm@gmail.com> | 2023-03-29 15:14:24 -0300 |
commit | 74dba9506dba104d918c5386e67146f71f07436c (patch) | |
tree | 2d6434f66e2a14a56f0a748733462a0089bdee44 /packages | |
parent | 329b766ae78405e086e7b6f078168bc0c136d317 (diff) | |
download | wallet-core-74dba9506dba104d918c5386e67146f71f07436c.tar.xz |
show wire details when the deposit has been wired
Diffstat (limited to 'packages')
6 files changed, 226 insertions, 18 deletions
diff --git a/packages/taler-util/src/transactions-types.ts b/packages/taler-util/src/transactions-types.ts index 9623e25a9..1d7e6ef30 100644 --- a/packages/taler-util/src/transactions-types.ts +++ b/packages/taler-util/src/transactions-types.ts @@ -535,7 +535,7 @@ export interface TransactionRefresh extends TransactionCommon { /** * Fees, i.e. the effective, negative effect of the refresh * on the balance. - * + * * Only applicable for stand-alone refreshes, and zero for * other refreshes where the transaction itself accounts for the * refresh fee. @@ -578,6 +578,17 @@ export interface TransactionDeposit extends TransactionCommon { * Did all the deposit requests succeed? */ deposited: boolean; + + trackingState: Array<{ + // Raw wire transfer identifier of the deposit. + wireTransferId: string; + // When was the wire transfer given to the bank. + timestampExecuted: TalerProtocolTimestamp; + // Total amount transfer for this wtid (including fees) + amountRaw: AmountString; + // Total amount received for this wtid (without fees) + amountEffective: AmountString; + }>; } export interface TransactionByIdRequest { diff --git a/packages/taler-wallet-core/src/db.ts b/packages/taler-wallet-core/src/db.ts index cd676b7ca..fb5ea025a 100644 --- a/packages/taler-wallet-core/src/db.ts +++ b/packages/taler-wallet-core/src/db.ts @@ -1671,6 +1671,20 @@ export interface DepositGroupRecord { operationStatus: OperationStatus; transactionPerCoin: TransactionStatus[]; + + trackingState?: { + [signature: string]: { + // Raw wire transfer identifier of the deposit. + wireTransferId: string; + // When was the wire transfer given to the bank. + timestampExecuted: TalerProtocolTimestamp; + // Total amount transfer for this wtid (including fees) + amountRaw: AmountString; + // Total amount received for this wtid (without fees) + amountEffective: AmountString; + exchangePub: string; + }; + }; } /** diff --git a/packages/taler-wallet-core/src/operations/deposits.ts b/packages/taler-wallet-core/src/operations/deposits.ts index 22283b7a8..c6cd4732c 100644 --- a/packages/taler-wallet-core/src/operations/deposits.ts +++ b/packages/taler-wallet-core/src/operations/deposits.ts @@ -53,12 +53,15 @@ import { TrackDepositGroupRequest, TrackDepositGroupResponse, TrackTransaction, + TrackTransactionWired, TransactionType, URL, + WireFee, } from "@gnu-taler/taler-util"; import { DenominationRecord, DepositGroupRecord, + ExchangeDetailsRecord, OperationStatus, TransactionStatus, } from "../db.js"; @@ -157,7 +160,6 @@ export async function processDepositGroup( const perm = depositPermissions[i]; let updatedDeposit: boolean | undefined = undefined; - let updatedTxStatus: TransactionStatus | undefined = undefined; if (!depositGroup.depositedPerCoin[i]) { const requestBody: ExchangeDepositRequest = { @@ -186,6 +188,17 @@ export async function processDepositGroup( updatedDeposit = true; } + let updatedTxStatus: TransactionStatus | undefined = undefined; + type ValueOf<T> = T[keyof T]; + + let newWiredTransaction: + | { + id: string; + value: ValueOf<NonNullable<DepositGroupRecord["trackingState"]>>; + } + | undefined; + + let signature: string | undefined; if (depositGroup.transactionPerCoin[i] !== TransactionStatus.Wired) { const track = await trackDepositPermission(ws, depositGroup, perm); @@ -207,6 +220,32 @@ export async function processDepositGroup( } } else if (track.type === "wired") { updatedTxStatus = TransactionStatus.Wired; + + const payto = parsePaytoUri(depositGroup.wire.payto_uri); + if (!payto) { + throw Error(`unparsable payto: ${depositGroup.wire.payto_uri}`); + } + + const fee = await getExchangeWireFee( + ws, + payto.targetType, + perm.exchange_url, + track.execution_time, + ); + const raw = Amounts.parseOrThrow(track.coin_contribution); + const wireFee = Amounts.parseOrThrow(fee.wireFee); + const effective = Amounts.sub(raw, wireFee).amount; + + newWiredTransaction = { + value: { + amountRaw: Amounts.stringify(raw), + amountEffective: Amounts.stringify(effective), + exchangePub: track.exchange_pub, + timestampExecuted: track.execution_time, + wireTransferId: track.wtid, + }, + id: track.exchange_sig, + }; } else { updatedTxStatus = TransactionStatus.Unknown; } @@ -226,6 +265,14 @@ export async function processDepositGroup( if (updatedTxStatus !== undefined) { dg.transactionPerCoin[i] = updatedTxStatus; } + if (newWiredTransaction) { + if (!dg.trackingState) { + dg.trackingState = {}; + } + + dg.trackingState[newWiredTransaction.id] = + newWiredTransaction.value; + } await tx.depositGroups.put(dg); }); } @@ -257,6 +304,50 @@ export async function processDepositGroup( return OperationAttemptResult.finishedEmpty(); } +async function getExchangeWireFee( + ws: InternalWalletState, + wireType: string, + baseUrl: string, + time: TalerProtocolTimestamp, +): Promise<WireFee> { + const exchangeDetails = await ws.db + .mktx((x) => [x.exchanges, x.exchangeDetails]) + .runReadOnly(async (tx) => { + const ex = await tx.exchanges.get(baseUrl); + if (!ex || !ex.detailsPointer) return undefined; + return await tx.exchangeDetails.indexes.byPointer.get([ + baseUrl, + ex.detailsPointer.currency, + ex.detailsPointer.masterPublicKey, + ]); + }); + + if (!exchangeDetails) { + throw Error(`exchange missing: ${baseUrl}`); + } + + const fees = exchangeDetails.wireInfo.feesForType[wireType]; + if (!fees || fees.length === 0) { + throw Error( + `exchange ${baseUrl} doesn't have fees for wire type ${wireType}`, + ); + } + const fee = fees.find((x) => { + return AbsoluteTime.isBetween( + AbsoluteTime.fromTimestamp(time), + AbsoluteTime.fromTimestamp(x.startStamp), + AbsoluteTime.fromTimestamp(x.endStamp), + ); + }); + if (!fee) { + throw Error( + `exchange ${exchangeDetails.exchangeBaseUrl} doesn't have fees for wire type ${wireType} at ${time.t_s}`, + ); + } + + return fee; +} + export async function trackDepositGroup( ws: InternalWalletState, req: TrackDepositGroupRequest, diff --git a/packages/taler-wallet-core/src/operations/transactions.ts b/packages/taler-wallet-core/src/operations/transactions.ts index 3e396098e..c0045aced 100644 --- a/packages/taler-wallet-core/src/operations/transactions.ts +++ b/packages/taler-wallet-core/src/operations/transactions.ts @@ -764,6 +764,7 @@ function buildTransactionForDeposit( deposited = false; } } + return { type: TransactionType.Deposit, amountRaw: Amounts.stringify(dg.effectiveDepositAmount), @@ -788,6 +789,7 @@ function buildTransactionForDeposit( )) / dg.transactionPerCoin.length, depositGroupId: dg.depositGroupId, + trackingState: Object.values(dg.trackingState ?? {}), deposited, ...(ort?.lastError ? { error: ort.lastError } : {}), }; diff --git a/packages/taler-wallet-webextension/src/components/TransactionItem.tsx b/packages/taler-wallet-webextension/src/components/TransactionItem.tsx index 934a0fe52..7ddaee9f3 100644 --- a/packages/taler-wallet-webextension/src/components/TransactionItem.tsx +++ b/packages/taler-wallet-webextension/src/components/TransactionItem.tsx @@ -76,7 +76,11 @@ export function TransactionItem(props: { tx: Transaction }): VNode { subtitle={tx.info.summary} timestamp={AbsoluteTime.fromTimestamp(tx.timestamp)} iconPath={"P"} - // pending={tx.pending} + pending={ + tx.extendedStatus === ExtendedStatus.Pending + ? i18n.str`Payment in progress` + : undefined + } /> ); case TransactionType.Refund: @@ -89,7 +93,11 @@ export function TransactionItem(props: { tx: Transaction }): VNode { title={tx.info.merchant.name} timestamp={AbsoluteTime.fromTimestamp(tx.timestamp)} iconPath={"R"} - // pending={tx.pending} + pending={ + tx.extendedStatus === ExtendedStatus.Pending + ? i18n.str`Executing refund...` + : undefined + } /> ); case TransactionType.Tip: @@ -101,7 +109,11 @@ export function TransactionItem(props: { tx: Transaction }): VNode { title={new URL(tx.merchantBaseUrl).hostname} timestamp={AbsoluteTime.fromTimestamp(tx.timestamp)} iconPath={"T"} - // pending={tx.pending} + pending={ + tx.extendedStatus === ExtendedStatus.Pending + ? i18n.str`Grabbing the tipping...` + : undefined + } /> ); case TransactionType.Refresh: @@ -113,7 +125,11 @@ export function TransactionItem(props: { tx: Transaction }): VNode { title={"Refresh"} timestamp={AbsoluteTime.fromTimestamp(tx.timestamp)} iconPath={"R"} - // pending={tx.pending} + pending={ + tx.extendedStatus === ExtendedStatus.Pending + ? i18n.str`Refreshing coins...` + : undefined + } /> ); case TransactionType.Deposit: @@ -125,7 +141,11 @@ export function TransactionItem(props: { tx: Transaction }): VNode { title={tx.targetPaytoUri} timestamp={AbsoluteTime.fromTimestamp(tx.timestamp)} iconPath={"D"} - // pending={tx.pending} + pending={ + tx.extendedStatus === ExtendedStatus.Pending + ? i18n.str`Deposit in progress` + : undefined + } /> ); case TransactionType.PeerPullCredit: @@ -137,7 +157,11 @@ export function TransactionItem(props: { tx: Transaction }): VNode { title={tx.info.summary || "Invoice"} timestamp={AbsoluteTime.fromTimestamp(tx.timestamp)} iconPath={"I"} - // pending={tx.pending} + pending={ + tx.extendedStatus === ExtendedStatus.Pending + ? i18n.str`Waiting to be paid` + : undefined + } /> ); case TransactionType.PeerPullDebit: @@ -149,7 +173,11 @@ export function TransactionItem(props: { tx: Transaction }): VNode { title={tx.info.summary || "Invoice"} timestamp={AbsoluteTime.fromTimestamp(tx.timestamp)} iconPath={"I"} - // pending={tx.pending} + pending={ + tx.extendedStatus === ExtendedStatus.Pending + ? i18n.str`Payment in progress` + : undefined + } /> ); case TransactionType.PeerPushCredit: @@ -161,7 +189,11 @@ export function TransactionItem(props: { tx: Transaction }): VNode { title={tx.info.summary || "Transfer"} timestamp={AbsoluteTime.fromTimestamp(tx.timestamp)} iconPath={"T"} - // pending={tx.pending} + pending={ + tx.extendedStatus === ExtendedStatus.Pending + ? i18n.str`Receiving the transfer` + : undefined + } /> ); case TransactionType.PeerPushDebit: @@ -173,7 +205,11 @@ export function TransactionItem(props: { tx: Transaction }): VNode { title={tx.info.summary || "Transfer"} timestamp={AbsoluteTime.fromTimestamp(tx.timestamp)} iconPath={"T"} - // pending={tx.pending} + pending={ + tx.extendedStatus === ExtendedStatus.Pending + ? i18n.str`Waiting to be received` + : undefined + } /> ); default: { diff --git a/packages/taler-wallet-webextension/src/wallet/Transaction.tsx b/packages/taler-wallet-webextension/src/wallet/Transaction.tsx index 217a77575..a9683f680 100644 --- a/packages/taler-wallet-webextension/src/wallet/Transaction.tsx +++ b/packages/taler-wallet-webextension/src/wallet/Transaction.tsx @@ -28,6 +28,7 @@ import { stringifyPaytoUri, TalerProtocolTimestamp, Transaction, + TransactionDeposit, TransactionType, TranslatedString, WithdrawalType, @@ -714,13 +715,24 @@ export function TransactionView({ }} /> ) : transaction.wireTransferProgress === 100 ? ( - <AlertView - alert={{ - type: "success", - message: i18n.str`Wire transfer completed`, - description: i18n.str` `, - }} - /> + <Fragment> + <AlertView + alert={{ + type: "success", + message: i18n.str`Wire transfer completed`, + description: i18n.str` `, + }} + /> + <Part + title={i18n.str`Transfer details`} + text={ + <TrackingDepositDetails + trackingState={transaction.trackingState} + /> + } + kind="neutral" + /> + </Fragment> ) : ( <AlertView alert={{ @@ -1559,6 +1571,48 @@ function RefundDetails({ amount }: { amount: AmountWithFee }): VNode { ); } +function TrackingDepositDetails({ + trackingState, +}: { + trackingState: TransactionDeposit["trackingState"]; +}): VNode { + const { i18n } = useTranslationContext(); + + const trackByWtid = Object.values(trackingState ?? {}).reduce((prev, cur) => { + const am = Amounts.parseOrThrow(cur.amountEffective); + const sum = !prev[cur.wireTransferId] + ? am + : Amounts.add(prev[cur.wireTransferId], am).amount; + prev[cur.wireTransferId] = sum; + return prev; + }, {} as Record<string, AmountJson>); + const wireTransfers = Object.entries(trackByWtid).map(([id, amountJson]) => ({ + id, + amount: Amounts.stringify(amountJson), + })); + + return ( + <PurchaseDetailsTable> + <tr> + <td> + <i18n.Translate>Transfer identification</i18n.Translate> + </td> + <td> + <i18n.Translate>Amount</i18n.Translate> + </td> + </tr> + + {wireTransfers.map((wire) => ( + <tr> + <td>{wire.id}</td> + <td> + <Amount value={wire.amount} /> + </td> + </tr> + ))} + </PurchaseDetailsTable> + ); +} function DepositDetails({ amount }: { amount: AmountWithFee }): VNode { const { i18n } = useTranslationContext(); |