working on #7357
getTransactionById is introduced: with that we move all transaction information building into a function transactionId was added in every response that creates a tx
This commit is contained in:
parent
a66b636dee
commit
5d08379139
@ -56,6 +56,16 @@ export namespace TalerProtocolTimestamp {
|
|||||||
t_s: s,
|
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 {
|
export interface Duration {
|
||||||
|
@ -505,6 +505,15 @@ export interface TransactionDeposit extends TransactionCommon {
|
|||||||
amountEffective: AmountString;
|
amountEffective: AmountString;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface TransactionByIdRequest {
|
||||||
|
transactionId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const codecForTransactionByIdRequest = (): Codec<TransactionByIdRequest> =>
|
||||||
|
buildCodecForObject<TransactionByIdRequest>()
|
||||||
|
.property("transactionId", codecForString())
|
||||||
|
.build("TransactionByIdRequest");
|
||||||
|
|
||||||
export const codecForTransactionsRequest = (): Codec<TransactionsRequest> =>
|
export const codecForTransactionsRequest = (): Codec<TransactionsRequest> =>
|
||||||
buildCodecForObject<TransactionsRequest>()
|
buildCodecForObject<TransactionsRequest>()
|
||||||
.property("currency", codecOptional(codecForString()))
|
.property("currency", codecOptional(codecForString()))
|
||||||
|
@ -138,11 +138,12 @@ export enum ConfirmPayResultType {
|
|||||||
export interface ConfirmPayResultDone {
|
export interface ConfirmPayResultDone {
|
||||||
type: ConfirmPayResultType.Done;
|
type: ConfirmPayResultType.Done;
|
||||||
contractTerms: ContractTerms;
|
contractTerms: ContractTerms;
|
||||||
|
transactionId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ConfirmPayResultPending {
|
export interface ConfirmPayResultPending {
|
||||||
type: ConfirmPayResultType.Pending;
|
type: ConfirmPayResultType.Pending;
|
||||||
|
transactionId: string;
|
||||||
lastError: TalerErrorDetail | undefined;
|
lastError: TalerErrorDetail | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -152,12 +153,14 @@ export const codecForConfirmPayResultPending =
|
|||||||
(): Codec<ConfirmPayResultPending> =>
|
(): Codec<ConfirmPayResultPending> =>
|
||||||
buildCodecForObject<ConfirmPayResultPending>()
|
buildCodecForObject<ConfirmPayResultPending>()
|
||||||
.property("lastError", codecForAny())
|
.property("lastError", codecForAny())
|
||||||
|
.property("transactionId", codecForString())
|
||||||
.property("type", codecForConstString(ConfirmPayResultType.Pending))
|
.property("type", codecForConstString(ConfirmPayResultType.Pending))
|
||||||
.build("ConfirmPayResultPending");
|
.build("ConfirmPayResultPending");
|
||||||
|
|
||||||
export const codecForConfirmPayResultDone = (): Codec<ConfirmPayResultDone> =>
|
export const codecForConfirmPayResultDone = (): Codec<ConfirmPayResultDone> =>
|
||||||
buildCodecForObject<ConfirmPayResultDone>()
|
buildCodecForObject<ConfirmPayResultDone>()
|
||||||
.property("type", codecForConstString(ConfirmPayResultType.Done))
|
.property("type", codecForConstString(ConfirmPayResultType.Done))
|
||||||
|
.property("transactionId", codecForString())
|
||||||
.property("contractTerms", codecForContractTerms())
|
.property("contractTerms", codecForContractTerms())
|
||||||
.build("ConfirmPayResultDone");
|
.build("ConfirmPayResultDone");
|
||||||
|
|
||||||
@ -334,6 +337,10 @@ export interface PrepareTipResult {
|
|||||||
expirationTimestamp: TalerProtocolTimestamp;
|
expirationTimestamp: TalerProtocolTimestamp;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface AcceptTipResponse {
|
||||||
|
transactionId: string;
|
||||||
|
}
|
||||||
|
|
||||||
export const codecForPrepareTipResult = (): Codec<PrepareTipResult> =>
|
export const codecForPrepareTipResult = (): Codec<PrepareTipResult> =>
|
||||||
buildCodecForObject<PrepareTipResult>()
|
buildCodecForObject<PrepareTipResult>()
|
||||||
.property("accepted", codecForBoolean())
|
.property("accepted", codecForBoolean())
|
||||||
@ -462,6 +469,7 @@ export interface BankWithdrawDetails {
|
|||||||
export interface AcceptWithdrawalResponse {
|
export interface AcceptWithdrawalResponse {
|
||||||
reservePub: string;
|
reservePub: string;
|
||||||
confirmTransferUrl?: string;
|
confirmTransferUrl?: string;
|
||||||
|
transactionId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -864,6 +872,8 @@ export interface AcceptManualWithdrawalResult {
|
|||||||
* Public key of the newly created reserve.
|
* Public key of the newly created reserve.
|
||||||
*/
|
*/
|
||||||
reservePub: string;
|
reservePub: string;
|
||||||
|
|
||||||
|
transactionId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ManualWithdrawalDetails {
|
export interface ManualWithdrawalDetails {
|
||||||
@ -1252,6 +1262,8 @@ export const codecForWithdrawTestBalance =
|
|||||||
export interface ApplyRefundResponse {
|
export interface ApplyRefundResponse {
|
||||||
contractTermsHash: string;
|
contractTermsHash: string;
|
||||||
|
|
||||||
|
transactionId: string;
|
||||||
|
|
||||||
proposalId: string;
|
proposalId: string;
|
||||||
|
|
||||||
amountEffectivePaid: AmountString;
|
amountEffectivePaid: AmountString;
|
||||||
@ -1273,6 +1285,7 @@ export const codecForApplyRefundResponse = (): Codec<ApplyRefundResponse> =>
|
|||||||
.property("contractTermsHash", codecForString())
|
.property("contractTermsHash", codecForString())
|
||||||
.property("pendingAtExchange", codecForBoolean())
|
.property("pendingAtExchange", codecForBoolean())
|
||||||
.property("proposalId", codecForString())
|
.property("proposalId", codecForString())
|
||||||
|
.property("transactionId", codecForString())
|
||||||
.property("info", codecForOrderShortInfo())
|
.property("info", codecForOrderShortInfo())
|
||||||
.build("ApplyRefundResponse");
|
.build("ApplyRefundResponse");
|
||||||
|
|
||||||
@ -1374,6 +1387,7 @@ export const codecForCreateDepositGroupRequest =
|
|||||||
|
|
||||||
export interface CreateDepositGroupResponse {
|
export interface CreateDepositGroupResponse {
|
||||||
depositGroupId: string;
|
depositGroupId: string;
|
||||||
|
transactionId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TrackDepositGroupRequest {
|
export interface TrackDepositGroupRequest {
|
||||||
@ -1539,6 +1553,7 @@ export interface InitiatePeerPushPaymentResponse {
|
|||||||
mergePriv: string;
|
mergePriv: string;
|
||||||
contractPriv: string;
|
contractPriv: string;
|
||||||
talerUri: string;
|
talerUri: string;
|
||||||
|
transactionId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const codecForInitiatePeerPushPaymentRequest =
|
export const codecForInitiatePeerPushPaymentRequest =
|
||||||
@ -1586,6 +1601,13 @@ export interface AcceptPeerPushPaymentRequest {
|
|||||||
*/
|
*/
|
||||||
peerPushPaymentIncomingId: string;
|
peerPushPaymentIncomingId: string;
|
||||||
}
|
}
|
||||||
|
export interface AcceptPeerPushPaymentResponse {
|
||||||
|
transactionId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AcceptPeerPullPaymentResponse {
|
||||||
|
transactionId: string;
|
||||||
|
}
|
||||||
|
|
||||||
export const codecForAcceptPeerPushPaymentRequest =
|
export const codecForAcceptPeerPushPaymentRequest =
|
||||||
(): Codec<AcceptPeerPushPaymentRequest> =>
|
(): Codec<AcceptPeerPushPaymentRequest> =>
|
||||||
@ -1629,4 +1651,6 @@ export interface InitiatePeerPullPaymentResponse {
|
|||||||
* that was requested.
|
* that was requested.
|
||||||
*/
|
*/
|
||||||
talerUri: string;
|
talerUri: string;
|
||||||
|
|
||||||
|
transactionId: string;
|
||||||
}
|
}
|
||||||
|
@ -41,6 +41,7 @@ import {
|
|||||||
TalerProtocolTimestamp,
|
TalerProtocolTimestamp,
|
||||||
TrackDepositGroupRequest,
|
TrackDepositGroupRequest,
|
||||||
TrackDepositGroupResponse,
|
TrackDepositGroupResponse,
|
||||||
|
TransactionType,
|
||||||
URL,
|
URL,
|
||||||
} from "@gnu-taler/taler-util";
|
} from "@gnu-taler/taler-util";
|
||||||
import {
|
import {
|
||||||
@ -62,6 +63,7 @@ import {
|
|||||||
getTotalPaymentCost,
|
getTotalPaymentCost,
|
||||||
} from "./pay.js";
|
} from "./pay.js";
|
||||||
import { getTotalRefreshCost } from "./refresh.js";
|
import { getTotalRefreshCost } from "./refresh.js";
|
||||||
|
import { makeEventId } from "./transactions.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Logger.
|
* Logger.
|
||||||
@ -531,7 +533,10 @@ export async function createDepositGroup(
|
|||||||
await tx.depositGroups.put(depositGroup);
|
await tx.depositGroups.put(depositGroup);
|
||||||
});
|
});
|
||||||
|
|
||||||
return { depositGroupId };
|
return {
|
||||||
|
depositGroupId: depositGroupId,
|
||||||
|
transactionId: makeEventId(TransactionType.Deposit, depositGroupId)
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -103,6 +103,7 @@ import { RetryInfo, RetryTags, scheduleRetry } from "../util/retries.js";
|
|||||||
import { spendCoins } from "../wallet.js";
|
import { spendCoins } from "../wallet.js";
|
||||||
import { getExchangeDetails } from "./exchanges.js";
|
import { getExchangeDetails } from "./exchanges.js";
|
||||||
import { createRefreshGroup, getTotalRefreshCost } from "./refresh.js";
|
import { createRefreshGroup, getTotalRefreshCost } from "./refresh.js";
|
||||||
|
import { makeEventId } from "./transactions.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Logger.
|
* Logger.
|
||||||
@ -511,7 +512,7 @@ export function extractContractData(
|
|||||||
export async function processDownloadProposal(
|
export async function processDownloadProposal(
|
||||||
ws: InternalWalletState,
|
ws: InternalWalletState,
|
||||||
proposalId: string,
|
proposalId: string,
|
||||||
options: {} = {},
|
options: object = {},
|
||||||
): Promise<OperationAttemptResult> {
|
): Promise<OperationAttemptResult> {
|
||||||
const proposal = await ws.db
|
const proposal = await ws.db
|
||||||
.mktx((x) => [x.proposals])
|
.mktx((x) => [x.proposals])
|
||||||
@ -1312,6 +1313,7 @@ export async function runPayForConfirmPay(
|
|||||||
return {
|
return {
|
||||||
type: ConfirmPayResultType.Done,
|
type: ConfirmPayResultType.Done,
|
||||||
contractTerms: purchase.download.contractTermsRaw,
|
contractTerms: purchase.download.contractTermsRaw,
|
||||||
|
transactionId: makeEventId(TransactionType.Payment, proposalId)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
case OperationAttemptResultType.Error:
|
case OperationAttemptResultType.Error:
|
||||||
@ -1320,6 +1322,7 @@ export async function runPayForConfirmPay(
|
|||||||
case OperationAttemptResultType.Pending:
|
case OperationAttemptResultType.Pending:
|
||||||
return {
|
return {
|
||||||
type: ConfirmPayResultType.Pending,
|
type: ConfirmPayResultType.Pending,
|
||||||
|
transactionId: makeEventId(TransactionType.Payment, proposalId),
|
||||||
lastError: undefined,
|
lastError: undefined,
|
||||||
};
|
};
|
||||||
case OperationAttemptResultType.Longpoll:
|
case OperationAttemptResultType.Longpoll:
|
||||||
|
@ -20,11 +20,11 @@
|
|||||||
import {
|
import {
|
||||||
AbsoluteTime,
|
AbsoluteTime,
|
||||||
AcceptPeerPullPaymentRequest,
|
AcceptPeerPullPaymentRequest,
|
||||||
|
AcceptPeerPullPaymentResponse,
|
||||||
AcceptPeerPushPaymentRequest,
|
AcceptPeerPushPaymentRequest,
|
||||||
|
AcceptPeerPushPaymentResponse,
|
||||||
AgeCommitmentProof,
|
AgeCommitmentProof,
|
||||||
AmountJson,
|
AmountJson, Amounts,
|
||||||
AmountLike,
|
|
||||||
Amounts,
|
|
||||||
AmountString,
|
AmountString,
|
||||||
buildCodecForObject,
|
buildCodecForObject,
|
||||||
CheckPeerPullPaymentRequest,
|
CheckPeerPullPaymentRequest,
|
||||||
@ -34,9 +34,7 @@ import {
|
|||||||
Codec,
|
Codec,
|
||||||
codecForAmountString,
|
codecForAmountString,
|
||||||
codecForAny,
|
codecForAny,
|
||||||
codecForExchangeGetContractResponse,
|
codecForExchangeGetContractResponse, constructPayPullUri,
|
||||||
CoinPublicKey,
|
|
||||||
constructPayPullUri,
|
|
||||||
constructPayPushUri,
|
constructPayPushUri,
|
||||||
ContractTermsUtil,
|
ContractTermsUtil,
|
||||||
decodeCrock,
|
decodeCrock,
|
||||||
@ -58,25 +56,25 @@ import {
|
|||||||
RefreshReason,
|
RefreshReason,
|
||||||
strcmp,
|
strcmp,
|
||||||
TalerProtocolTimestamp,
|
TalerProtocolTimestamp,
|
||||||
|
TransactionType,
|
||||||
UnblindedSignature,
|
UnblindedSignature,
|
||||||
WalletAccountMergeFlags,
|
WalletAccountMergeFlags
|
||||||
} from "@gnu-taler/taler-util";
|
} from "@gnu-taler/taler-util";
|
||||||
import {
|
import {
|
||||||
CoinStatus,
|
CoinStatus,
|
||||||
MergeReserveInfo,
|
MergeReserveInfo,
|
||||||
ReserveRecordStatus,
|
ReserveRecordStatus,
|
||||||
WalletStoresV1,
|
WalletStoresV1,
|
||||||
WithdrawalRecordType,
|
WithdrawalRecordType
|
||||||
} from "../db.js";
|
} from "../db.js";
|
||||||
import { readSuccessResponseJsonOrThrow } from "../util/http.js";
|
|
||||||
import { InternalWalletState } from "../internal-wallet-state.js";
|
import { InternalWalletState } from "../internal-wallet-state.js";
|
||||||
|
import { readSuccessResponseJsonOrThrow } from "../util/http.js";
|
||||||
import { checkDbInvariant } from "../util/invariants.js";
|
import { checkDbInvariant } from "../util/invariants.js";
|
||||||
import { internalCreateWithdrawalGroup } from "./withdraw.js";
|
|
||||||
import { GetReadOnlyAccess } from "../util/query.js";
|
import { GetReadOnlyAccess } from "../util/query.js";
|
||||||
import { createRefreshGroup } from "./refresh.js";
|
|
||||||
import { updateExchangeFromUrl } from "./exchanges.js";
|
|
||||||
import { spendCoins } from "../wallet.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");
|
const logger = new Logger("operations/peer-to-peer.ts");
|
||||||
|
|
||||||
@ -338,6 +336,7 @@ export async function initiatePeerToPeerPush(
|
|||||||
exchangeBaseUrl: coinSelRes.exchangeBaseUrl,
|
exchangeBaseUrl: coinSelRes.exchangeBaseUrl,
|
||||||
contractPriv: econtractResp.contractPriv,
|
contractPriv: econtractResp.contractPriv,
|
||||||
}),
|
}),
|
||||||
|
transactionId: makeEventId(TransactionType.PeerPushDebit, pursePair.pub)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -472,7 +471,7 @@ async function getMergeReserveInfo(
|
|||||||
export async function acceptPeerPushPayment(
|
export async function acceptPeerPushPayment(
|
||||||
ws: InternalWalletState,
|
ws: InternalWalletState,
|
||||||
req: AcceptPeerPushPaymentRequest,
|
req: AcceptPeerPushPaymentRequest,
|
||||||
): Promise<void> {
|
): Promise<AcceptPeerPushPaymentResponse> {
|
||||||
const peerInc = await ws.db
|
const peerInc = await ws.db
|
||||||
.mktx((x) => [x.peerPushPaymentIncoming])
|
.mktx((x) => [x.peerPushPaymentIncoming])
|
||||||
.runReadOnly(async (tx) => {
|
.runReadOnly(async (tx) => {
|
||||||
@ -533,7 +532,7 @@ export async function acceptPeerPushPayment(
|
|||||||
const res = await readSuccessResponseJsonOrThrow(mergeHttpReq, codecForAny());
|
const res = await readSuccessResponseJsonOrThrow(mergeHttpReq, codecForAny());
|
||||||
logger.info(`merge response: ${j2s(res)}`);
|
logger.info(`merge response: ${j2s(res)}`);
|
||||||
|
|
||||||
await internalCreateWithdrawalGroup(ws, {
|
const wg = await internalCreateWithdrawalGroup(ws, {
|
||||||
amount,
|
amount,
|
||||||
wgInfo: {
|
wgInfo: {
|
||||||
withdrawalType: WithdrawalRecordType.PeerPushCredit,
|
withdrawalType: WithdrawalRecordType.PeerPushCredit,
|
||||||
@ -546,6 +545,13 @@ export async function acceptPeerPushPayment(
|
|||||||
pub: mergeReserveInfo.reservePub,
|
pub: mergeReserveInfo.reservePub,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
transactionId: makeEventId(
|
||||||
|
TransactionType.PeerPushCredit,
|
||||||
|
wg.withdrawalGroupId
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -554,7 +560,7 @@ export async function acceptPeerPushPayment(
|
|||||||
export async function acceptPeerPullPayment(
|
export async function acceptPeerPullPayment(
|
||||||
ws: InternalWalletState,
|
ws: InternalWalletState,
|
||||||
req: AcceptPeerPullPaymentRequest,
|
req: AcceptPeerPullPaymentRequest,
|
||||||
): Promise<void> {
|
): Promise<AcceptPeerPullPaymentResponse> {
|
||||||
const peerPullInc = await ws.db
|
const peerPullInc = await ws.db
|
||||||
.mktx((x) => [x.peerPullPaymentIncoming])
|
.mktx((x) => [x.peerPullPaymentIncoming])
|
||||||
.runReadOnly(async (tx) => {
|
.runReadOnly(async (tx) => {
|
||||||
@ -630,6 +636,13 @@ export async function acceptPeerPullPayment(
|
|||||||
const httpResp = await ws.http.postJson(purseDepositUrl.href, depositPayload);
|
const httpResp = await ws.http.postJson(purseDepositUrl.href, depositPayload);
|
||||||
const resp = await readSuccessResponseJsonOrThrow(httpResp, codecForAny());
|
const resp = await readSuccessResponseJsonOrThrow(httpResp, codecForAny());
|
||||||
logger.trace(`purse deposit response: ${j2s(resp)}`);
|
logger.trace(`purse deposit response: ${j2s(resp)}`);
|
||||||
|
|
||||||
|
return {
|
||||||
|
transactionId: makeEventId(
|
||||||
|
TransactionType.PeerPullDebit,
|
||||||
|
req.peerPullPaymentIncomingId,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function checkPeerPullPayment(
|
export async function checkPeerPullPayment(
|
||||||
@ -801,7 +814,7 @@ export async function initiatePeerRequestForPay(
|
|||||||
|
|
||||||
logger.info(`reserve merge response: ${j2s(resp)}`);
|
logger.info(`reserve merge response: ${j2s(resp)}`);
|
||||||
|
|
||||||
await internalCreateWithdrawalGroup(ws, {
|
const wg = await internalCreateWithdrawalGroup(ws, {
|
||||||
amount: Amounts.parseOrThrow(req.amount),
|
amount: Amounts.parseOrThrow(req.amount),
|
||||||
wgInfo: {
|
wgInfo: {
|
||||||
withdrawalType: WithdrawalRecordType.PeerPullCredit,
|
withdrawalType: WithdrawalRecordType.PeerPullCredit,
|
||||||
@ -821,5 +834,9 @@ export async function initiatePeerRequestForPay(
|
|||||||
exchangeBaseUrl: req.exchangeBaseUrl,
|
exchangeBaseUrl: req.exchangeBaseUrl,
|
||||||
contractPriv: econtractResp.contractPriv,
|
contractPriv: econtractResp.contractPriv,
|
||||||
}),
|
}),
|
||||||
|
transactionId: makeEventId(
|
||||||
|
TransactionType.PeerPullCredit,
|
||||||
|
wg.withdrawalGroupId
|
||||||
|
)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -46,6 +46,7 @@ import {
|
|||||||
TalerErrorCode,
|
TalerErrorCode,
|
||||||
TalerErrorDetail,
|
TalerErrorDetail,
|
||||||
TalerProtocolTimestamp,
|
TalerProtocolTimestamp,
|
||||||
|
TransactionType,
|
||||||
URL,
|
URL,
|
||||||
} from "@gnu-taler/taler-util";
|
} from "@gnu-taler/taler-util";
|
||||||
import {
|
import {
|
||||||
@ -63,6 +64,7 @@ import { readSuccessResponseJsonOrThrow } from "../util/http.js";
|
|||||||
import { checkDbInvariant } from "../util/invariants.js";
|
import { checkDbInvariant } from "../util/invariants.js";
|
||||||
import { GetReadWriteAccess } from "../util/query.js";
|
import { GetReadWriteAccess } from "../util/query.js";
|
||||||
import { createRefreshGroup, getTotalRefreshCost } from "./refresh.js";
|
import { createRefreshGroup, getTotalRefreshCost } from "./refresh.js";
|
||||||
|
import { makeEventId } from "./transactions.js";
|
||||||
|
|
||||||
const logger = new Logger("refund.ts");
|
const logger = new Logger("refund.ts");
|
||||||
|
|
||||||
@ -573,6 +575,7 @@ export async function applyRefundFromPurchaseId(
|
|||||||
return {
|
return {
|
||||||
contractTermsHash: purchase.download.contractData.contractTermsHash,
|
contractTermsHash: purchase.download.contractData.contractTermsHash,
|
||||||
proposalId: purchase.proposalId,
|
proposalId: purchase.proposalId,
|
||||||
|
transactionId: makeEventId(TransactionType.Payment, proposalId), //FIXME: can we have the tx id of the refund
|
||||||
amountEffectivePaid: Amounts.stringify(summary.amountEffectivePaid),
|
amountEffectivePaid: Amounts.stringify(summary.amountEffectivePaid),
|
||||||
amountRefundGone: Amounts.stringify(summary.amountRefundGone),
|
amountRefundGone: Amounts.stringify(summary.amountRefundGone),
|
||||||
amountRefundGranted: Amounts.stringify(summary.amountRefundGranted),
|
amountRefundGranted: Amounts.stringify(summary.amountRefundGranted),
|
||||||
|
@ -18,6 +18,7 @@
|
|||||||
* Imports.
|
* Imports.
|
||||||
*/
|
*/
|
||||||
import {
|
import {
|
||||||
|
AcceptTipResponse,
|
||||||
Amounts,
|
Amounts,
|
||||||
BlindedDenominationSignature,
|
BlindedDenominationSignature,
|
||||||
codecForMerchantTipResponseV2,
|
codecForMerchantTipResponseV2,
|
||||||
@ -32,6 +33,7 @@ import {
|
|||||||
TalerErrorCode,
|
TalerErrorCode,
|
||||||
TalerProtocolTimestamp,
|
TalerProtocolTimestamp,
|
||||||
TipPlanchetDetail,
|
TipPlanchetDetail,
|
||||||
|
TransactionType,
|
||||||
URL,
|
URL,
|
||||||
} from "@gnu-taler/taler-util";
|
} from "@gnu-taler/taler-util";
|
||||||
import { DerivedTipPlanchet } from "../crypto/cryptoTypes.js";
|
import { DerivedTipPlanchet } from "../crypto/cryptoTypes.js";
|
||||||
@ -53,6 +55,7 @@ import {
|
|||||||
import { checkDbInvariant, checkLogicInvariant } from "../util/invariants.js";
|
import { checkDbInvariant, checkLogicInvariant } from "../util/invariants.js";
|
||||||
import { makeCoinAvailable } from "../wallet.js";
|
import { makeCoinAvailable } from "../wallet.js";
|
||||||
import { updateExchangeFromUrl } from "./exchanges.js";
|
import { updateExchangeFromUrl } from "./exchanges.js";
|
||||||
|
import { makeEventId } from "./transactions.js";
|
||||||
import {
|
import {
|
||||||
getCandidateWithdrawalDenoms,
|
getCandidateWithdrawalDenoms,
|
||||||
getExchangeWithdrawalInfo,
|
getExchangeWithdrawalInfo,
|
||||||
@ -341,7 +344,7 @@ export async function processTip(
|
|||||||
export async function acceptTip(
|
export async function acceptTip(
|
||||||
ws: InternalWalletState,
|
ws: InternalWalletState,
|
||||||
tipId: string,
|
tipId: string,
|
||||||
): Promise<void> {
|
): Promise<AcceptTipResponse> {
|
||||||
const found = await ws.db
|
const found = await ws.db
|
||||||
.mktx((x) => [x.tips])
|
.mktx((x) => [x.tips])
|
||||||
.runReadWrite(async (tx) => {
|
.runReadWrite(async (tx) => {
|
||||||
@ -357,4 +360,10 @@ export async function acceptTip(
|
|||||||
if (found) {
|
if (found) {
|
||||||
await processTip(ws, tipId);
|
await processTip(ws, tipId);
|
||||||
}
|
}
|
||||||
|
return {
|
||||||
|
transactionId: makeEventId(
|
||||||
|
TransactionType.Tip,
|
||||||
|
tipId
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -19,13 +19,16 @@
|
|||||||
*/
|
*/
|
||||||
import {
|
import {
|
||||||
AbsoluteTime,
|
AbsoluteTime,
|
||||||
addPaytoQueryParams, Amounts,
|
addPaytoQueryParams, AmountJson, Amounts,
|
||||||
constructPayPullUri,
|
constructPayPullUri,
|
||||||
constructPayPushUri,
|
constructPayPushUri,
|
||||||
Logger,
|
Logger,
|
||||||
OrderShortInfo, PaymentStatus,
|
OrderShortInfo, PaymentStatus,
|
||||||
RefundInfoShort,
|
RefundInfoShort,
|
||||||
|
TalerProtocolTimestamp,
|
||||||
Transaction,
|
Transaction,
|
||||||
|
TransactionByIdRequest,
|
||||||
|
TransactionRefund,
|
||||||
TransactionsRequest,
|
TransactionsRequest,
|
||||||
TransactionsResponse,
|
TransactionsResponse,
|
||||||
TransactionType,
|
TransactionType,
|
||||||
@ -34,8 +37,16 @@ import {
|
|||||||
} from "@gnu-taler/taler-util";
|
} from "@gnu-taler/taler-util";
|
||||||
import {
|
import {
|
||||||
AbortStatus,
|
AbortStatus,
|
||||||
|
DepositGroupRecord,
|
||||||
|
ExchangeDetailsRecord,
|
||||||
|
OperationRetryRecord,
|
||||||
|
PeerPullPaymentIncomingRecord,
|
||||||
|
PeerPushPaymentInitiationRecord,
|
||||||
|
PurchaseRecord,
|
||||||
RefundState,
|
RefundState,
|
||||||
|
TipRecord,
|
||||||
WalletRefundItem,
|
WalletRefundItem,
|
||||||
|
WithdrawalGroupRecord,
|
||||||
WithdrawalRecordType
|
WithdrawalRecordType
|
||||||
} from "../db.js";
|
} from "../db.js";
|
||||||
import { InternalWalletState } from "../internal-wallet-state.js";
|
import { InternalWalletState } from "../internal-wallet-state.js";
|
||||||
@ -44,6 +55,7 @@ import { processDepositGroup } from "./deposits.js";
|
|||||||
import { getExchangeDetails } from "./exchanges.js";
|
import { getExchangeDetails } from "./exchanges.js";
|
||||||
import { processPurchasePay } from "./pay.js";
|
import { processPurchasePay } from "./pay.js";
|
||||||
import { processRefreshGroup } from "./refresh.js";
|
import { processRefreshGroup } from "./refresh.js";
|
||||||
|
import { applyRefundFromPurchaseId } from "./refund.js";
|
||||||
import { processTip } from "./tip.js";
|
import { processTip } from "./tip.js";
|
||||||
import { processWithdrawalGroup } from "./withdraw.js";
|
import { processWithdrawalGroup } from "./withdraw.js";
|
||||||
|
|
||||||
@ -114,6 +126,500 @@ const txOrder: { [t in TransactionType]: number } = {
|
|||||||
[TransactionType.Tip]: 11,
|
[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.
|
* Retrieve the full event history for this wallet.
|
||||||
*/
|
*/
|
||||||
@ -137,7 +643,6 @@ export async function getTransactions(
|
|||||||
x.proposals,
|
x.proposals,
|
||||||
x.purchases,
|
x.purchases,
|
||||||
x.recoupGroups,
|
x.recoupGroups,
|
||||||
x.recoupGroups,
|
|
||||||
x.tips,
|
x.tips,
|
||||||
x.tombstones,
|
x.tombstones,
|
||||||
x.withdrawalGroups,
|
x.withdrawalGroups,
|
||||||
@ -152,27 +657,7 @@ export async function getTransactions(
|
|||||||
if (shouldSkipSearch(transactionsRequest, [])) {
|
if (shouldSkipSearch(transactionsRequest, [])) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
transactions.push({
|
transactions.push(buildTransactionForPushPaymentDebit(pi));
|
||||||
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,
|
|
||||||
),
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
tx.peerPullPaymentIncoming.iter().forEachAsync(async (pi) => {
|
tx.peerPullPaymentIncoming.iter().forEachAsync(async (pi) => {
|
||||||
@ -187,23 +672,7 @@ export async function getTransactions(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
transactions.push({
|
transactions.push(buildTransactionForPullPaymentDebit(pi));
|
||||||
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,
|
|
||||||
),
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
tx.withdrawalGroups.iter().forEachAsync(async (wsr) => {
|
tx.withdrawalGroups.iter().forEachAsync(async (wsr) => {
|
||||||
@ -223,64 +692,18 @@ export async function getTransactions(
|
|||||||
const opId = RetryTags.forWithdrawal(wsr);
|
const opId = RetryTags.forWithdrawal(wsr);
|
||||||
const ort = await tx.operationRetries.get(opId);
|
const ort = await tx.operationRetries.get(opId);
|
||||||
|
|
||||||
let withdrawalDetails: WithdrawalDetails;
|
|
||||||
if (wsr.wgInfo.withdrawalType === WithdrawalRecordType.PeerPullCredit) {
|
if (wsr.wgInfo.withdrawalType === WithdrawalRecordType.PeerPullCredit) {
|
||||||
transactions.push({
|
transactions.push(buildTransactionForPullPaymentCredit(wsr, ort));
|
||||||
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 } : {}),
|
|
||||||
});
|
|
||||||
return;
|
return;
|
||||||
} else if (
|
} else if (
|
||||||
wsr.wgInfo.withdrawalType === WithdrawalRecordType.PeerPushCredit
|
wsr.wgInfo.withdrawalType === WithdrawalRecordType.PeerPushCredit
|
||||||
) {
|
) {
|
||||||
transactions.push({
|
transactions.push(buildTransactionForPushPaymentCredit(wsr, ort));
|
||||||
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 } : {}),
|
|
||||||
});
|
|
||||||
return;
|
return;
|
||||||
} else if (
|
} else if (
|
||||||
wsr.wgInfo.withdrawalType === WithdrawalRecordType.BankIntegrated
|
wsr.wgInfo.withdrawalType === WithdrawalRecordType.BankIntegrated
|
||||||
) {
|
) {
|
||||||
withdrawalDetails = {
|
transactions.push(buildTransactionForBankIntegratedWithdraw(wsr, ort));
|
||||||
type: WithdrawalType.TalerBankIntegrationApi,
|
|
||||||
confirmed: wsr.wgInfo.bankInfo.timestampBankConfirmed
|
|
||||||
? true
|
|
||||||
: false,
|
|
||||||
reservePub: wsr.reservePub,
|
|
||||||
bankConfirmationUrl: wsr.wgInfo.bankInfo.confirmUrl,
|
|
||||||
};
|
|
||||||
} else {
|
} else {
|
||||||
const exchangeDetails = await getExchangeDetails(
|
const exchangeDetails = await getExchangeDetails(
|
||||||
tx,
|
tx,
|
||||||
@ -290,31 +713,9 @@ export async function getTransactions(
|
|||||||
// FIXME: report somehow
|
// FIXME: report somehow
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
withdrawalDetails = {
|
|
||||||
type: WithdrawalType.ManualTransfer,
|
|
||||||
reservePub: wsr.reservePub,
|
|
||||||
exchangePaytoUris:
|
|
||||||
exchangeDetails.wireInfo?.accounts.map(
|
|
||||||
(x) => addPaytoQueryParams(x.payto_uri, { subject: wsr.reservePub }),
|
|
||||||
) ?? [],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
transactions.push({
|
transactions.push(buildTransactionForManualWithdraw(wsr, exchangeDetails, ort));
|
||||||
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 } : {}),
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
tx.depositGroups.iter().forEachAsync(async (dg) => {
|
tx.depositGroups.iter().forEachAsync(async (dg) => {
|
||||||
@ -324,21 +725,8 @@ export async function getTransactions(
|
|||||||
}
|
}
|
||||||
const opId = RetryTags.forDeposit(dg);
|
const opId = RetryTags.forDeposit(dg);
|
||||||
const retryRecord = await tx.operationRetries.get(opId);
|
const retryRecord = await tx.operationRetries.get(opId);
|
||||||
transactions.push({
|
|
||||||
type: TransactionType.Deposit,
|
transactions.push(buildTransactionForDeposit(dg, retryRecord));
|
||||||
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 } : {}),
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
tx.purchases.iter().forEachAsync(async (pr) => {
|
tx.purchases.iter().forEachAsync(async (pr) => {
|
||||||
@ -358,107 +746,31 @@ export async function getTransactions(
|
|||||||
if (!proposal) {
|
if (!proposal) {
|
||||||
return;
|
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 filteredRefunds = await Promise.all(Object.values(pr.refunds).map(async r => {
|
||||||
const refund = pr.refunds[rk];
|
const t = await tx.tombstones.get(makeEventId(
|
||||||
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,
|
TombstoneTag.DeleteRefund,
|
||||||
pr.proposalId,
|
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,
|
||||||
);
|
);
|
||||||
const tombstone = await tx.tombstones.get(refundTombstoneId);
|
|
||||||
if (tombstone) {
|
|
||||||
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) {
|
transactions.push(
|
||||||
amountRaw = Amounts.add(amountRaw, refund.refundAmount).amount;
|
buildTransactionForRefund(pr, refundInfo, refundQueryRetryRecord)
|
||||||
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,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const payOpId = RetryTags.forPay(pr);
|
const payOpId = RetryTags.forPay(pr);
|
||||||
const refundQueryOpId = RetryTags.forRefundQuery(pr);
|
const refundQueryOpId = RetryTags.forRefundQuery(pr);
|
||||||
@ -467,32 +779,9 @@ export async function getTransactions(
|
|||||||
refundQueryOpId,
|
refundQueryOpId,
|
||||||
);
|
);
|
||||||
|
|
||||||
const err =
|
const err = payRetryRecord !== undefined ? payRetryRecord : refundQueryRetryRecord
|
||||||
refundQueryRetryRecord?.lastError ?? payRetryRecord?.lastError;
|
|
||||||
transactions.push({
|
transactions.push(buildTransactionForPurchase(pr, refunds, err));
|
||||||
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) => {
|
tx.tips.iter().forEachAsync(async (tipRecord) => {
|
||||||
@ -509,20 +798,7 @@ export async function getTransactions(
|
|||||||
}
|
}
|
||||||
const opId = RetryTags.forTipPickup(tipRecord);
|
const opId = RetryTags.forTipPickup(tipRecord);
|
||||||
const retryRecord = await tx.operationRetries.get(opId);
|
const retryRecord = await tx.operationRetries.get(opId);
|
||||||
transactions.push({
|
transactions.push(buildTransactionForTip(tipRecord, retryRecord));
|
||||||
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,
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -50,6 +50,7 @@ import {
|
|||||||
TalerErrorCode,
|
TalerErrorCode,
|
||||||
TalerErrorDetail,
|
TalerErrorDetail,
|
||||||
TalerProtocolTimestamp,
|
TalerProtocolTimestamp,
|
||||||
|
TransactionType,
|
||||||
UnblindedSignature,
|
UnblindedSignature,
|
||||||
URL,
|
URL,
|
||||||
VersionMatchResult,
|
VersionMatchResult,
|
||||||
@ -104,6 +105,7 @@ import {
|
|||||||
getExchangeTrust,
|
getExchangeTrust,
|
||||||
updateExchangeFromUrl,
|
updateExchangeFromUrl,
|
||||||
} from "./exchanges.js";
|
} from "./exchanges.js";
|
||||||
|
import { makeEventId } from "./transactions.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Logger for this file.
|
* Logger for this file.
|
||||||
@ -256,7 +258,7 @@ export function selectWithdrawalDenominations(
|
|||||||
DenominationRecord.getValue(d),
|
DenominationRecord.getValue(d),
|
||||||
d.fees.feeWithdraw,
|
d.fees.feeWithdraw,
|
||||||
).amount;
|
).amount;
|
||||||
for (;;) {
|
for (; ;) {
|
||||||
if (Amounts.cmp(remaining, cost) < 0) {
|
if (Amounts.cmp(remaining, cost) < 0) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -890,8 +892,7 @@ export async function updateWithdrawalDenoms(
|
|||||||
denom.verificationStatus === DenominationVerificationStatus.Unverified
|
denom.verificationStatus === DenominationVerificationStatus.Unverified
|
||||||
) {
|
) {
|
||||||
logger.trace(
|
logger.trace(
|
||||||
`Validating denomination (${current + 1}/${
|
`Validating denomination (${current + 1}/${denominations.length
|
||||||
denominations.length
|
|
||||||
}) signature of ${denom.denomPubHash}`,
|
}) signature of ${denom.denomPubHash}`,
|
||||||
);
|
);
|
||||||
let valid = false;
|
let valid = false;
|
||||||
@ -974,7 +975,7 @@ async function queryReserve(
|
|||||||
if (
|
if (
|
||||||
resp.status === 404 &&
|
resp.status === 404 &&
|
||||||
result.talerErrorResponse.code ===
|
result.talerErrorResponse.code ===
|
||||||
TalerErrorCode.EXCHANGE_RESERVES_STATUS_UNKNOWN
|
TalerErrorCode.EXCHANGE_RESERVES_STATUS_UNKNOWN
|
||||||
) {
|
) {
|
||||||
ws.notify({
|
ws.notify({
|
||||||
type: NotificationType.ReserveNotYetFound,
|
type: NotificationType.ReserveNotYetFound,
|
||||||
@ -1003,10 +1004,16 @@ async function queryReserve(
|
|||||||
return { ready: true };
|
return { ready: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum BankStatusResultCode {
|
||||||
|
Done = "done",
|
||||||
|
Waiting = "waiting",
|
||||||
|
Aborted = "aborted",
|
||||||
|
}
|
||||||
|
|
||||||
export async function processWithdrawalGroup(
|
export async function processWithdrawalGroup(
|
||||||
ws: InternalWalletState,
|
ws: InternalWalletState,
|
||||||
withdrawalGroupId: string,
|
withdrawalGroupId: string,
|
||||||
options: {} = {},
|
options: object = {},
|
||||||
): Promise<OperationAttemptResult> {
|
): Promise<OperationAttemptResult> {
|
||||||
logger.trace("processing withdrawal group", withdrawalGroupId);
|
logger.trace("processing withdrawal group", withdrawalGroupId);
|
||||||
const withdrawalGroup = await ws.db
|
const withdrawalGroup = await ws.db
|
||||||
@ -1053,13 +1060,15 @@ export async function processWithdrawalGroup(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
case ReserveRecordStatus.BankAborted:
|
case ReserveRecordStatus.BankAborted: {
|
||||||
// FIXME
|
// FIXME
|
||||||
return {
|
return {
|
||||||
type: OperationAttemptResultType.Pending,
|
type: OperationAttemptResultType.Pending,
|
||||||
result: undefined,
|
result: undefined,
|
||||||
};
|
};
|
||||||
|
}
|
||||||
case ReserveRecordStatus.Dormant:
|
case ReserveRecordStatus.Dormant:
|
||||||
// We can try to withdraw, nothing needs to be done with the reserve.
|
// We can try to withdraw, nothing needs to be done with the reserve.
|
||||||
break;
|
break;
|
||||||
@ -1288,7 +1297,7 @@ export async function getExchangeWithdrawalInfo(
|
|||||||
) {
|
) {
|
||||||
logger.warn(
|
logger.warn(
|
||||||
`wallet's support for exchange protocol version ${WALLET_EXCHANGE_PROTOCOL_VERSION} might be outdated ` +
|
`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 });
|
ws.notify({ type: NotificationType.ReserveRegisteredWithBank });
|
||||||
}
|
}
|
||||||
|
|
||||||
enum BankStatusResultCode {
|
|
||||||
Done = "done",
|
|
||||||
Waiting = "waiting",
|
|
||||||
Aborted = "aborted",
|
|
||||||
}
|
|
||||||
|
|
||||||
interface BankStatusResult {
|
interface BankStatusResult {
|
||||||
status: BankStatusResultCode;
|
status: BankStatusResultCode;
|
||||||
}
|
}
|
||||||
@ -1790,6 +1793,10 @@ export async function acceptWithdrawalFromUri(
|
|||||||
return {
|
return {
|
||||||
reservePub: existingWithdrawalGroup.reservePub,
|
reservePub: existingWithdrawalGroup.reservePub,
|
||||||
confirmTransferUrl: url,
|
confirmTransferUrl: url,
|
||||||
|
transactionId: makeEventId(
|
||||||
|
TransactionType.Withdrawal,
|
||||||
|
existingWithdrawalGroup.withdrawalGroupId,
|
||||||
|
)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1847,6 +1854,10 @@ export async function acceptWithdrawalFromUri(
|
|||||||
return {
|
return {
|
||||||
reservePub: withdrawalGroup.reservePub,
|
reservePub: withdrawalGroup.reservePub,
|
||||||
confirmTransferUrl: withdrawInfo.confirmTransferUrl,
|
confirmTransferUrl: withdrawInfo.confirmTransferUrl,
|
||||||
|
transactionId: makeEventId(
|
||||||
|
TransactionType.Withdrawal,
|
||||||
|
withdrawalGroupId,
|
||||||
|
)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1901,5 +1912,9 @@ export async function createManualWithdrawal(
|
|||||||
return {
|
return {
|
||||||
reservePub: withdrawalGroup.reservePub,
|
reservePub: withdrawalGroup.reservePub,
|
||||||
exchangePaytoUris: exchangePaytoUris,
|
exchangePaytoUris: exchangePaytoUris,
|
||||||
|
transactionId: makeEventId(
|
||||||
|
TransactionType.Withdrawal,
|
||||||
|
withdrawalGroupId,
|
||||||
|
)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -91,6 +91,7 @@ import {
|
|||||||
OperationMap,
|
OperationMap,
|
||||||
FeeDescription,
|
FeeDescription,
|
||||||
TalerErrorDetail,
|
TalerErrorDetail,
|
||||||
|
codecForTransactionByIdRequest,
|
||||||
} from "@gnu-taler/taler-util";
|
} from "@gnu-taler/taler-util";
|
||||||
import { TalerCryptoInterface } from "./crypto/cryptoImplementation.js";
|
import { TalerCryptoInterface } from "./crypto/cryptoImplementation.js";
|
||||||
import {
|
import {
|
||||||
@ -198,6 +199,7 @@ import {
|
|||||||
import { acceptTip, prepareTip, processTip } from "./operations/tip.js";
|
import { acceptTip, prepareTip, processTip } from "./operations/tip.js";
|
||||||
import {
|
import {
|
||||||
deleteTransaction,
|
deleteTransaction,
|
||||||
|
getTransactionById,
|
||||||
getTransactions,
|
getTransactions,
|
||||||
retryTransaction,
|
retryTransaction,
|
||||||
} from "./operations/transactions.js";
|
} from "./operations/transactions.js";
|
||||||
@ -1080,6 +1082,10 @@ async function dispatchRequestInternal(
|
|||||||
const req = codecForTransactionsRequest().decode(payload);
|
const req = codecForTransactionsRequest().decode(payload);
|
||||||
return await getTransactions(ws, req);
|
return await getTransactions(ws, req);
|
||||||
}
|
}
|
||||||
|
case "getTransactionById": {
|
||||||
|
const req = codecForTransactionByIdRequest().decode(payload);
|
||||||
|
return await getTransactionById(ws, req)
|
||||||
|
}
|
||||||
case "addExchange": {
|
case "addExchange": {
|
||||||
const req = codecForAddExchangeRequest().decode(payload);
|
const req = codecForAddExchangeRequest().decode(payload);
|
||||||
await updateExchangeFromUrl(ws, req.exchangeBaseUrl, {
|
await updateExchangeFromUrl(ws, req.exchangeBaseUrl, {
|
||||||
@ -1227,8 +1233,7 @@ async function dispatchRequestInternal(
|
|||||||
}
|
}
|
||||||
case "acceptTip": {
|
case "acceptTip": {
|
||||||
const req = codecForAcceptTipRequest().decode(payload);
|
const req = codecForAcceptTipRequest().decode(payload);
|
||||||
await acceptTip(ws, req.walletTipId);
|
return await acceptTip(ws, req.walletTipId);
|
||||||
return {};
|
|
||||||
}
|
}
|
||||||
case "exportBackupPlain": {
|
case "exportBackupPlain": {
|
||||||
return exportBackup(ws);
|
return exportBackup(ws);
|
||||||
|
@ -73,10 +73,17 @@ export function BankDetailsByPaytoType({
|
|||||||
</p>
|
</p>
|
||||||
<table>
|
<table>
|
||||||
<tr>
|
<tr>
|
||||||
<td>{payto.targetPath}</td>
|
|
||||||
<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>
|
||||||
<td>
|
<td>
|
||||||
<CopyButton
|
<CopyButton
|
||||||
getContent={() =>
|
getContent={() =>
|
||||||
@ -85,21 +92,6 @@ export function BankDetailsByPaytoType({
|
|||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</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>
|
</table>
|
||||||
<p>
|
<p>
|
||||||
<i18n.Translate>
|
<i18n.Translate>
|
||||||
|
@ -348,6 +348,7 @@ export const AlreadyPaidWithoutFulfillment = createExample(BaseView, {
|
|||||||
payResult: {
|
payResult: {
|
||||||
type: ConfirmPayResultType.Done,
|
type: ConfirmPayResultType.Done,
|
||||||
contractTerms: {} as any,
|
contractTerms: {} as any,
|
||||||
|
transactionId: "",
|
||||||
},
|
},
|
||||||
payStatus: {
|
payStatus: {
|
||||||
status: PreparePayResultType.AlreadyConfirmed,
|
status: PreparePayResultType.AlreadyConfirmed,
|
||||||
@ -386,6 +387,7 @@ export const AlreadyPaidWithFulfillment = createExample(BaseView, {
|
|||||||
fulfillment_message: "thanks for buying!",
|
fulfillment_message: "thanks for buying!",
|
||||||
fulfillment_url: "https://demo.taler.net",
|
fulfillment_url: "https://demo.taler.net",
|
||||||
} as Partial<ContractTerms> as any,
|
} as Partial<ContractTerms> as any,
|
||||||
|
transactionId: "",
|
||||||
},
|
},
|
||||||
payStatus: {
|
payStatus: {
|
||||||
status: PreparePayResultType.AlreadyConfirmed,
|
status: PreparePayResultType.AlreadyConfirmed,
|
||||||
|
@ -35,6 +35,7 @@ export const AllOff = createExample(TestedComponent, {
|
|||||||
onDownloadDatabase: async () => "this is the content of the database",
|
onDownloadDatabase: async () => "this is the content of the database",
|
||||||
operations: [
|
operations: [
|
||||||
{
|
{
|
||||||
|
id: "",
|
||||||
type: PendingTaskType.ExchangeUpdate,
|
type: PendingTaskType.ExchangeUpdate,
|
||||||
exchangeBaseUrl: "http://exchange.url.",
|
exchangeBaseUrl: "http://exchange.url.",
|
||||||
givesLifeness: false,
|
givesLifeness: false,
|
||||||
|
@ -70,13 +70,8 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function getTransaction(tid: string): Promise<Transaction> {
|
async function getTransaction(tid: string): Promise<Transaction> {
|
||||||
const res = await wxApi.getTransactions();
|
const res = await wxApi.getTransactionById(tid);
|
||||||
const ts = res.transactions.filter((t) => t.transactionId === tid);
|
return res;
|
||||||
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");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function TransactionPage({ tid, goToWalletHistory }: Props): VNode {
|
export function TransactionPage({ tid, goToWalletHistory }: Props): VNode {
|
||||||
|
@ -68,6 +68,10 @@ import {
|
|||||||
WalletCoreVersion,
|
WalletCoreVersion,
|
||||||
WithdrawUriInfoResponse,
|
WithdrawUriInfoResponse,
|
||||||
ExchangeFullDetails,
|
ExchangeFullDetails,
|
||||||
|
Transaction,
|
||||||
|
AcceptTipResponse,
|
||||||
|
AcceptPeerPullPaymentResponse,
|
||||||
|
AcceptPeerPushPaymentResponse,
|
||||||
} from "@gnu-taler/taler-util";
|
} from "@gnu-taler/taler-util";
|
||||||
import {
|
import {
|
||||||
AddBackupProviderRequest,
|
AddBackupProviderRequest,
|
||||||
@ -476,7 +480,7 @@ export function prepareTip(req: PrepareTipRequest): Promise<PrepareTipResult> {
|
|||||||
return callBackend("prepareTip", req);
|
return callBackend("prepareTip", req);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function acceptTip(req: AcceptTipRequest): Promise<void> {
|
export function acceptTip(req: AcceptTipRequest): Promise<AcceptTipResponse> {
|
||||||
return callBackend("acceptTip", req);
|
return callBackend("acceptTip", req);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -513,7 +517,7 @@ export function checkPeerPushPayment(
|
|||||||
}
|
}
|
||||||
export function acceptPeerPushPayment(
|
export function acceptPeerPushPayment(
|
||||||
req: AcceptPeerPushPaymentRequest,
|
req: AcceptPeerPushPaymentRequest,
|
||||||
): Promise<void> {
|
): Promise<AcceptPeerPushPaymentResponse> {
|
||||||
return callBackend("acceptPeerPushPayment", req);
|
return callBackend("acceptPeerPushPayment", req);
|
||||||
}
|
}
|
||||||
export function initiatePeerPullPayment(
|
export function initiatePeerPullPayment(
|
||||||
@ -528,6 +532,12 @@ export function checkPeerPullPayment(
|
|||||||
}
|
}
|
||||||
export function acceptPeerPullPayment(
|
export function acceptPeerPullPayment(
|
||||||
req: AcceptPeerPullPaymentRequest,
|
req: AcceptPeerPullPaymentRequest,
|
||||||
): Promise<void> {
|
): Promise<AcceptPeerPullPaymentResponse> {
|
||||||
return callBackend("acceptPeerPullPayment", req);
|
return callBackend("acceptPeerPullPayment", req);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getTransactionById(tid: string): Promise<Transaction> {
|
||||||
|
return callBackend("getTransactionById", {
|
||||||
|
transactionId: tid
|
||||||
|
})
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user