show wire details when the deposit has been wired
This commit is contained in:
parent
329b766ae7
commit
74dba9506d
@ -535,7 +535,7 @@ export interface TransactionRefresh extends TransactionCommon {
|
|||||||
/**
|
/**
|
||||||
* Fees, i.e. the effective, negative effect of the refresh
|
* Fees, i.e. the effective, negative effect of the refresh
|
||||||
* on the balance.
|
* on the balance.
|
||||||
*
|
*
|
||||||
* Only applicable for stand-alone refreshes, and zero for
|
* Only applicable for stand-alone refreshes, and zero for
|
||||||
* other refreshes where the transaction itself accounts for the
|
* other refreshes where the transaction itself accounts for the
|
||||||
* refresh fee.
|
* refresh fee.
|
||||||
@ -578,6 +578,17 @@ export interface TransactionDeposit extends TransactionCommon {
|
|||||||
* Did all the deposit requests succeed?
|
* Did all the deposit requests succeed?
|
||||||
*/
|
*/
|
||||||
deposited: boolean;
|
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 {
|
export interface TransactionByIdRequest {
|
||||||
|
@ -1671,6 +1671,20 @@ export interface DepositGroupRecord {
|
|||||||
operationStatus: OperationStatus;
|
operationStatus: OperationStatus;
|
||||||
|
|
||||||
transactionPerCoin: TransactionStatus[];
|
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;
|
||||||
|
};
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -53,12 +53,15 @@ import {
|
|||||||
TrackDepositGroupRequest,
|
TrackDepositGroupRequest,
|
||||||
TrackDepositGroupResponse,
|
TrackDepositGroupResponse,
|
||||||
TrackTransaction,
|
TrackTransaction,
|
||||||
|
TrackTransactionWired,
|
||||||
TransactionType,
|
TransactionType,
|
||||||
URL,
|
URL,
|
||||||
|
WireFee,
|
||||||
} from "@gnu-taler/taler-util";
|
} from "@gnu-taler/taler-util";
|
||||||
import {
|
import {
|
||||||
DenominationRecord,
|
DenominationRecord,
|
||||||
DepositGroupRecord,
|
DepositGroupRecord,
|
||||||
|
ExchangeDetailsRecord,
|
||||||
OperationStatus,
|
OperationStatus,
|
||||||
TransactionStatus,
|
TransactionStatus,
|
||||||
} from "../db.js";
|
} from "../db.js";
|
||||||
@ -157,7 +160,6 @@ export async function processDepositGroup(
|
|||||||
const perm = depositPermissions[i];
|
const perm = depositPermissions[i];
|
||||||
|
|
||||||
let updatedDeposit: boolean | undefined = undefined;
|
let updatedDeposit: boolean | undefined = undefined;
|
||||||
let updatedTxStatus: TransactionStatus | undefined = undefined;
|
|
||||||
|
|
||||||
if (!depositGroup.depositedPerCoin[i]) {
|
if (!depositGroup.depositedPerCoin[i]) {
|
||||||
const requestBody: ExchangeDepositRequest = {
|
const requestBody: ExchangeDepositRequest = {
|
||||||
@ -186,6 +188,17 @@ export async function processDepositGroup(
|
|||||||
updatedDeposit = true;
|
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) {
|
if (depositGroup.transactionPerCoin[i] !== TransactionStatus.Wired) {
|
||||||
const track = await trackDepositPermission(ws, depositGroup, perm);
|
const track = await trackDepositPermission(ws, depositGroup, perm);
|
||||||
|
|
||||||
@ -207,6 +220,32 @@ export async function processDepositGroup(
|
|||||||
}
|
}
|
||||||
} else if (track.type === "wired") {
|
} else if (track.type === "wired") {
|
||||||
updatedTxStatus = TransactionStatus.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 {
|
} else {
|
||||||
updatedTxStatus = TransactionStatus.Unknown;
|
updatedTxStatus = TransactionStatus.Unknown;
|
||||||
}
|
}
|
||||||
@ -226,6 +265,14 @@ export async function processDepositGroup(
|
|||||||
if (updatedTxStatus !== undefined) {
|
if (updatedTxStatus !== undefined) {
|
||||||
dg.transactionPerCoin[i] = updatedTxStatus;
|
dg.transactionPerCoin[i] = updatedTxStatus;
|
||||||
}
|
}
|
||||||
|
if (newWiredTransaction) {
|
||||||
|
if (!dg.trackingState) {
|
||||||
|
dg.trackingState = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
dg.trackingState[newWiredTransaction.id] =
|
||||||
|
newWiredTransaction.value;
|
||||||
|
}
|
||||||
await tx.depositGroups.put(dg);
|
await tx.depositGroups.put(dg);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -257,6 +304,50 @@ export async function processDepositGroup(
|
|||||||
return OperationAttemptResult.finishedEmpty();
|
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(
|
export async function trackDepositGroup(
|
||||||
ws: InternalWalletState,
|
ws: InternalWalletState,
|
||||||
req: TrackDepositGroupRequest,
|
req: TrackDepositGroupRequest,
|
||||||
|
@ -764,6 +764,7 @@ function buildTransactionForDeposit(
|
|||||||
deposited = false;
|
deposited = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
type: TransactionType.Deposit,
|
type: TransactionType.Deposit,
|
||||||
amountRaw: Amounts.stringify(dg.effectiveDepositAmount),
|
amountRaw: Amounts.stringify(dg.effectiveDepositAmount),
|
||||||
@ -788,6 +789,7 @@ function buildTransactionForDeposit(
|
|||||||
)) /
|
)) /
|
||||||
dg.transactionPerCoin.length,
|
dg.transactionPerCoin.length,
|
||||||
depositGroupId: dg.depositGroupId,
|
depositGroupId: dg.depositGroupId,
|
||||||
|
trackingState: Object.values(dg.trackingState ?? {}),
|
||||||
deposited,
|
deposited,
|
||||||
...(ort?.lastError ? { error: ort.lastError } : {}),
|
...(ort?.lastError ? { error: ort.lastError } : {}),
|
||||||
};
|
};
|
||||||
|
@ -76,7 +76,11 @@ export function TransactionItem(props: { tx: Transaction }): VNode {
|
|||||||
subtitle={tx.info.summary}
|
subtitle={tx.info.summary}
|
||||||
timestamp={AbsoluteTime.fromTimestamp(tx.timestamp)}
|
timestamp={AbsoluteTime.fromTimestamp(tx.timestamp)}
|
||||||
iconPath={"P"}
|
iconPath={"P"}
|
||||||
// pending={tx.pending}
|
pending={
|
||||||
|
tx.extendedStatus === ExtendedStatus.Pending
|
||||||
|
? i18n.str`Payment in progress`
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
case TransactionType.Refund:
|
case TransactionType.Refund:
|
||||||
@ -89,7 +93,11 @@ export function TransactionItem(props: { tx: Transaction }): VNode {
|
|||||||
title={tx.info.merchant.name}
|
title={tx.info.merchant.name}
|
||||||
timestamp={AbsoluteTime.fromTimestamp(tx.timestamp)}
|
timestamp={AbsoluteTime.fromTimestamp(tx.timestamp)}
|
||||||
iconPath={"R"}
|
iconPath={"R"}
|
||||||
// pending={tx.pending}
|
pending={
|
||||||
|
tx.extendedStatus === ExtendedStatus.Pending
|
||||||
|
? i18n.str`Executing refund...`
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
case TransactionType.Tip:
|
case TransactionType.Tip:
|
||||||
@ -101,7 +109,11 @@ export function TransactionItem(props: { tx: Transaction }): VNode {
|
|||||||
title={new URL(tx.merchantBaseUrl).hostname}
|
title={new URL(tx.merchantBaseUrl).hostname}
|
||||||
timestamp={AbsoluteTime.fromTimestamp(tx.timestamp)}
|
timestamp={AbsoluteTime.fromTimestamp(tx.timestamp)}
|
||||||
iconPath={"T"}
|
iconPath={"T"}
|
||||||
// pending={tx.pending}
|
pending={
|
||||||
|
tx.extendedStatus === ExtendedStatus.Pending
|
||||||
|
? i18n.str`Grabbing the tipping...`
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
case TransactionType.Refresh:
|
case TransactionType.Refresh:
|
||||||
@ -113,7 +125,11 @@ export function TransactionItem(props: { tx: Transaction }): VNode {
|
|||||||
title={"Refresh"}
|
title={"Refresh"}
|
||||||
timestamp={AbsoluteTime.fromTimestamp(tx.timestamp)}
|
timestamp={AbsoluteTime.fromTimestamp(tx.timestamp)}
|
||||||
iconPath={"R"}
|
iconPath={"R"}
|
||||||
// pending={tx.pending}
|
pending={
|
||||||
|
tx.extendedStatus === ExtendedStatus.Pending
|
||||||
|
? i18n.str`Refreshing coins...`
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
case TransactionType.Deposit:
|
case TransactionType.Deposit:
|
||||||
@ -125,7 +141,11 @@ export function TransactionItem(props: { tx: Transaction }): VNode {
|
|||||||
title={tx.targetPaytoUri}
|
title={tx.targetPaytoUri}
|
||||||
timestamp={AbsoluteTime.fromTimestamp(tx.timestamp)}
|
timestamp={AbsoluteTime.fromTimestamp(tx.timestamp)}
|
||||||
iconPath={"D"}
|
iconPath={"D"}
|
||||||
// pending={tx.pending}
|
pending={
|
||||||
|
tx.extendedStatus === ExtendedStatus.Pending
|
||||||
|
? i18n.str`Deposit in progress`
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
case TransactionType.PeerPullCredit:
|
case TransactionType.PeerPullCredit:
|
||||||
@ -137,7 +157,11 @@ export function TransactionItem(props: { tx: Transaction }): VNode {
|
|||||||
title={tx.info.summary || "Invoice"}
|
title={tx.info.summary || "Invoice"}
|
||||||
timestamp={AbsoluteTime.fromTimestamp(tx.timestamp)}
|
timestamp={AbsoluteTime.fromTimestamp(tx.timestamp)}
|
||||||
iconPath={"I"}
|
iconPath={"I"}
|
||||||
// pending={tx.pending}
|
pending={
|
||||||
|
tx.extendedStatus === ExtendedStatus.Pending
|
||||||
|
? i18n.str`Waiting to be paid`
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
case TransactionType.PeerPullDebit:
|
case TransactionType.PeerPullDebit:
|
||||||
@ -149,7 +173,11 @@ export function TransactionItem(props: { tx: Transaction }): VNode {
|
|||||||
title={tx.info.summary || "Invoice"}
|
title={tx.info.summary || "Invoice"}
|
||||||
timestamp={AbsoluteTime.fromTimestamp(tx.timestamp)}
|
timestamp={AbsoluteTime.fromTimestamp(tx.timestamp)}
|
||||||
iconPath={"I"}
|
iconPath={"I"}
|
||||||
// pending={tx.pending}
|
pending={
|
||||||
|
tx.extendedStatus === ExtendedStatus.Pending
|
||||||
|
? i18n.str`Payment in progress`
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
case TransactionType.PeerPushCredit:
|
case TransactionType.PeerPushCredit:
|
||||||
@ -161,7 +189,11 @@ export function TransactionItem(props: { tx: Transaction }): VNode {
|
|||||||
title={tx.info.summary || "Transfer"}
|
title={tx.info.summary || "Transfer"}
|
||||||
timestamp={AbsoluteTime.fromTimestamp(tx.timestamp)}
|
timestamp={AbsoluteTime.fromTimestamp(tx.timestamp)}
|
||||||
iconPath={"T"}
|
iconPath={"T"}
|
||||||
// pending={tx.pending}
|
pending={
|
||||||
|
tx.extendedStatus === ExtendedStatus.Pending
|
||||||
|
? i18n.str`Receiving the transfer`
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
case TransactionType.PeerPushDebit:
|
case TransactionType.PeerPushDebit:
|
||||||
@ -173,7 +205,11 @@ export function TransactionItem(props: { tx: Transaction }): VNode {
|
|||||||
title={tx.info.summary || "Transfer"}
|
title={tx.info.summary || "Transfer"}
|
||||||
timestamp={AbsoluteTime.fromTimestamp(tx.timestamp)}
|
timestamp={AbsoluteTime.fromTimestamp(tx.timestamp)}
|
||||||
iconPath={"T"}
|
iconPath={"T"}
|
||||||
// pending={tx.pending}
|
pending={
|
||||||
|
tx.extendedStatus === ExtendedStatus.Pending
|
||||||
|
? i18n.str`Waiting to be received`
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
default: {
|
default: {
|
||||||
|
@ -28,6 +28,7 @@ import {
|
|||||||
stringifyPaytoUri,
|
stringifyPaytoUri,
|
||||||
TalerProtocolTimestamp,
|
TalerProtocolTimestamp,
|
||||||
Transaction,
|
Transaction,
|
||||||
|
TransactionDeposit,
|
||||||
TransactionType,
|
TransactionType,
|
||||||
TranslatedString,
|
TranslatedString,
|
||||||
WithdrawalType,
|
WithdrawalType,
|
||||||
@ -714,13 +715,24 @@ export function TransactionView({
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
) : transaction.wireTransferProgress === 100 ? (
|
) : transaction.wireTransferProgress === 100 ? (
|
||||||
<AlertView
|
<Fragment>
|
||||||
alert={{
|
<AlertView
|
||||||
type: "success",
|
alert={{
|
||||||
message: i18n.str`Wire transfer completed`,
|
type: "success",
|
||||||
description: i18n.str` `,
|
message: i18n.str`Wire transfer completed`,
|
||||||
}}
|
description: i18n.str` `,
|
||||||
/>
|
}}
|
||||||
|
/>
|
||||||
|
<Part
|
||||||
|
title={i18n.str`Transfer details`}
|
||||||
|
text={
|
||||||
|
<TrackingDepositDetails
|
||||||
|
trackingState={transaction.trackingState}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
kind="neutral"
|
||||||
|
/>
|
||||||
|
</Fragment>
|
||||||
) : (
|
) : (
|
||||||
<AlertView
|
<AlertView
|
||||||
alert={{
|
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 {
|
function DepositDetails({ amount }: { amount: AmountWithFee }): VNode {
|
||||||
const { i18n } = useTranslationContext();
|
const { i18n } = useTranslationContext();
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user