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:
Sebastian 2022-09-16 11:06:55 -03:00
parent a66b636dee
commit 5d08379139
No known key found for this signature in database
GPG Key ID: BE4FF68352439FC1
16 changed files with 703 additions and 327 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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
)
}
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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
})
}