show wire details when the deposit has been wired

This commit is contained in:
Sebastian 2023-03-29 15:14:02 -03:00
parent 329b766ae7
commit 74dba9506d
No known key found for this signature in database
GPG Key ID: 173909D1A5F66069
6 changed files with 226 additions and 18 deletions

View File

@ -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 {

View File

@ -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;
};
};
}
/**

View File

@ -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,

View File

@ -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 } : {}),
};

View File

@ -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: {

View File

@ -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();