/* This file is part of GNU Taler (C) 2019 Taler Systems S.A. GNU Taler is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3, or (at your option) any later version. GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with GNU Taler; see the file COPYING. If not, see */ /** * Type and schema definitions for the wallet's transaction list. * * @author Florian Dold * @author Torsten Grote */ /** * Imports. */ import { Codec, buildCodecForObject, codecForAny, codecForBoolean, codecForConstString, codecForEither, codecForList, codecForString, codecOptional, } from "./codec.js"; import { AmountString, InternationalizedString, MerchantInfo, codecForInternationalizedString, codecForMerchantInfo, } from "./taler-types.js"; import { TalerPreciseTimestamp, TalerProtocolTimestamp } from "./time.js"; import { RefreshReason, ScopeInfo, TalerErrorDetail, TransactionIdStr, TransactionStateFilter, WithdrawalExchangeAccountDetails, codecForScopeInfo, } from "./wallet-types.js"; export interface TransactionsRequest { /** * return only transactions in the given currency * * it will be removed in next release * * @deprecated use scopeInfo */ currency?: string; /** * return only transactions in the given scopeInfo */ scopeInfo?: ScopeInfo; /** * if present, results will be limited to transactions related to the given search string */ search?: string; /** * Sort order of the transaction items. * By default, items are sorted ascending by their * main timestamp. * * ascending: ascending by timestamp, but pending transactions first * descending: ascending by timestamp, but pending transactions first * stable-ascending: ascending by timestamp, with pending transactions amidst other transactions * (stable in the sense of: pending transactions don't jump around) */ sort?: "ascending" | "descending" | "stable-ascending"; /** * If true, include all refreshes in the transactions list. */ includeRefreshes?: boolean; filterByState?: TransactionStateFilter; } export interface TransactionState { major: TransactionMajorState; minor?: TransactionMinorState; } export enum TransactionMajorState { // No state, only used when reporting transitions into the initial state None = "none", Pending = "pending", Done = "done", Aborting = "aborting", Aborted = "aborted", Suspended = "suspended", Dialog = "dialog", SuspendedAborting = "suspended-aborting", Failed = "failed", Expired = "expired", // Only used for the notification, never in the transaction history Deleted = "deleted", } export enum TransactionMinorState { // Placeholder until D37 is fully implemented Unknown = "unknown", Deposit = "deposit", KycRequired = "kyc", AmlRequired = "aml", MergeKycRequired = "merge-kyc", Track = "track", SubmitPayment = "submit-payment", RebindSession = "rebind-session", Refresh = "refresh", Pickup = "pickup", AutoRefund = "auto-refund", User = "user", Bank = "bank", Exchange = "exchange", ClaimProposal = "claim-proposal", CheckRefund = "check-refund", CreatePurse = "create-purse", DeletePurse = "delete-purse", RefreshExpired = "refresh-expired", Ready = "ready", Merge = "merge", Repurchase = "repurchase", BankRegisterReserve = "bank-register-reserve", BankConfirmTransfer = "bank-confirm-transfer", WithdrawCoins = "withdraw-coins", ExchangeWaitReserve = "exchange-wait-reserve", AbortingBank = "aborting-bank", Aborting = "aborting", Refused = "refused", Withdraw = "withdraw", MerchantOrderProposed = "merchant-order-proposed", Proposed = "proposed", RefundAvailable = "refund-available", AcceptRefund = "accept-refund", PaidByOther = "paid-by-other", CompletedByOtherWallet = "completed-by-other-wallet", } export enum TransactionAction { Delete = "delete", Suspend = "suspend", Resume = "resume", Abort = "abort", Fail = "fail", Retry = "retry", } export interface TransactionsResponse { // a list of past and pending transactions sorted by pending, timestamp and transactionId. // In case two events are both pending and have the same timestamp, // they are sorted by the transactionId // (lexically ascending and locale-independent comparison). transactions: Transaction[]; } export interface TransactionCommon { // opaque unique ID for the transaction, used as a starting point for paginating queries // and for invoking actions on the transaction (e.g. deleting/hiding it from the history) transactionId: TransactionIdStr; // the type of the transaction; different types might provide additional information type: TransactionType; // main timestamp of the transaction timestamp: TalerPreciseTimestamp; /** * Transaction state, as per DD37. */ txState: TransactionState; /** * Possible transitions based on the current state. */ txActions: TransactionAction[]; /** * Raw amount of the transaction (exclusive of fees or other extra costs). */ amountRaw: AmountString; /** * Amount added or removed from the wallet's balance (including all fees and other costs). */ amountEffective: AmountString; error?: TalerErrorDetail; /** * If the transaction minor state is in KycRequired this field is going to * have the location where the user need to go to complete KYC information. */ kycUrl?: string; } export type Transaction = | TransactionWithdrawal | TransactionPayment | TransactionRefund | TransactionRefresh | TransactionDeposit | TransactionPeerPullCredit | TransactionPeerPullDebit | TransactionPeerPushCredit | TransactionPeerPushDebit | TransactionInternalWithdrawal | TransactionRecoup | TransactionDenomLoss; export enum TransactionType { Withdrawal = "withdrawal", InternalWithdrawal = "internal-withdrawal", Payment = "payment", Refund = "refund", Refresh = "refresh", Deposit = "deposit", PeerPushDebit = "peer-push-debit", PeerPushCredit = "peer-push-credit", PeerPullDebit = "peer-pull-debit", PeerPullCredit = "peer-pull-credit", Recoup = "recoup", DenomLoss = "denom-loss", } export enum WithdrawalType { TalerBankIntegrationApi = "taler-bank-integration-api", ManualTransfer = "manual-transfer", } export type WithdrawalDetails = | WithdrawalDetailsForManualTransfer | WithdrawalDetailsForTalerBankIntegrationApi; interface WithdrawalDetailsForManualTransfer { type: WithdrawalType.ManualTransfer; /** * Payto URIs that the exchange supports. * * Already contains the amount and message. * * @deprecated in favor of exchangeCreditAccounts */ exchangePaytoUris: string[]; exchangeCreditAccountDetails?: WithdrawalExchangeAccountDetails[]; // Public key of the reserve reservePub: string; /** * Is the reserve ready for withdrawal? */ reserveIsReady: boolean; } interface WithdrawalDetailsForTalerBankIntegrationApi { type: WithdrawalType.TalerBankIntegrationApi; /** * Set to true if the bank has confirmed the withdrawal, false if not. * An unconfirmed withdrawal usually requires user-input and should be highlighted in the UI. * See also bankConfirmationUrl below. */ confirmed: boolean; /** * If the withdrawal is unconfirmed, this can include a URL for user * initiated confirmation. */ bankConfirmationUrl?: string; // Public key of the reserve reservePub: string; /** * Is the reserve ready for withdrawal? */ reserveIsReady: boolean; exchangeCreditAccountDetails?: WithdrawalExchangeAccountDetails[]; } export enum DenomLossEventType { DenomExpired = "denom-expired", DenomVanished = "denom-vanished", DenomUnoffered = "denom-unoffered", } /** * A transaction to indicate financial loss due to denominations * that became unusable for deposits. */ export interface TransactionDenomLoss extends TransactionCommon { type: TransactionType.DenomLoss; lossEventType: DenomLossEventType; exchangeBaseUrl: string; } /** * A withdrawal transaction (either bank-integrated or manual). */ export interface TransactionWithdrawal extends TransactionCommon { type: TransactionType.Withdrawal; /** * Exchange of the withdrawal. */ exchangeBaseUrl: string; /** * Amount that got subtracted from the reserve balance. */ amountRaw: AmountString; /** * Amount that actually was (or will be) added to the wallet's balance. */ amountEffective: AmountString; withdrawalDetails: WithdrawalDetails; } /** * Internal withdrawal operation, only reported on request. * * Some transactions (peer-*-credit) internally do a withdrawal, * but only the peer-*-credit transaction is reported. * * The internal withdrawal transaction allows to access the details of * the underlying withdrawal for testing/debugging. * * It is usually not reported, so that amounts of transactions properly * add up, since the amountEffecive of the withdrawal is already reported * in the peer-*-credit transaction. */ export interface TransactionInternalWithdrawal extends TransactionCommon { type: TransactionType.InternalWithdrawal; /** * Exchange of the withdrawal. */ exchangeBaseUrl: string; /** * Amount that got subtracted from the reserve balance. */ amountRaw: AmountString; /** * Amount that actually was (or will be) added to the wallet's balance. */ amountEffective: AmountString; withdrawalDetails: WithdrawalDetails; } export interface PeerInfoShort { expiration: TalerProtocolTimestamp | undefined; summary: string | undefined; } /** * Credit because we were paid for a P2P invoice we created. */ export interface TransactionPeerPullCredit extends TransactionCommon { type: TransactionType.PeerPullCredit; info: PeerInfoShort; /** * Exchange used. */ exchangeBaseUrl: string; /** * Amount that got subtracted from the reserve balance. */ amountRaw: AmountString; /** * Amount that actually was (or will be) added to the wallet's balance. */ amountEffective: AmountString; /** * URI to send to the other party. * * Only available in the right state. */ talerUri: string | undefined; } /** * Debit because we paid someone's invoice. */ export interface TransactionPeerPullDebit extends TransactionCommon { type: TransactionType.PeerPullDebit; info: PeerInfoShort; /** * Exchange used. */ exchangeBaseUrl: string; amountRaw: AmountString; amountEffective: AmountString; } /** * We sent money via a P2P payment. */ export interface TransactionPeerPushDebit extends TransactionCommon { type: TransactionType.PeerPushDebit; info: PeerInfoShort; /** * Exchange used. */ exchangeBaseUrl: string; /** * Amount that got subtracted from the reserve balance. */ amountRaw: AmountString; /** * Amount that actually was (or will be) added to the wallet's balance. */ amountEffective: AmountString; /** * URI to accept the payment. * * Only present if the transaction is in a state where the other party can * accept the payment. */ talerUri?: string; } /** * We received money via a P2P payment. */ export interface TransactionPeerPushCredit extends TransactionCommon { type: TransactionType.PeerPushCredit; info: PeerInfoShort; /** * Exchange used. */ exchangeBaseUrl: string; /** * Amount that got subtracted from the reserve balance. */ amountRaw: AmountString; /** * Amount that actually was (or will be) added to the wallet's balance. */ amountEffective: AmountString; } /** * The exchange revoked a key and the wallet recoups funds. */ export interface TransactionRecoup extends TransactionCommon { type: TransactionType.Recoup; } export enum PaymentStatus { /** * Explicitly aborted after timeout / failure */ Aborted = "aborted", /** * Payment failed, wallet will auto-retry. * User should be given the option to retry now / abort. */ Failed = "failed", /** * Paid successfully */ Paid = "paid", /** * User accepted, payment is processing. */ Accepted = "accepted", } export interface TransactionPayment extends TransactionCommon { type: TransactionType.Payment; /** * Additional information about the payment. */ info: OrderShortInfo; /** * Wallet-internal end-to-end identifier for the payment. */ proposalId: string; /** * Amount that must be paid for the contract */ amountRaw: AmountString; /** * Amount that was paid, including deposit, wire and refresh fees. */ amountEffective: AmountString; /** * Amount that has been refunded by the merchant */ totalRefundRaw: AmountString; /** * Amount will be added to the wallet's balance after fees and refreshing */ totalRefundEffective: AmountString; /** * Amount pending to be picked up */ refundPending: AmountString | undefined; /** * Reference to applied refunds */ refunds: RefundInfoShort[]; /** * Is the wallet currently checking for a refund? */ refundQueryActive: boolean; /** * Does this purchase has an pos validation */ posConfirmation: string | undefined; } export interface OrderShortInfo { /** * Order ID, uniquely identifies the order within a merchant instance */ orderId: string; /** * Hash of the contract terms. */ contractTermsHash: string; /** * More information about the merchant */ merchant: MerchantInfo; /** * Summary of the order, given by the merchant */ summary: string; /** * Map from IETF BCP 47 language tags to localized summaries */ summary_i18n?: InternationalizedString; /** * URL of the fulfillment, given by the merchant */ fulfillmentUrl?: string; /** * Plain text message that should be shown to the user * when the payment is complete. */ fulfillmentMessage?: string; /** * Translations of fulfillmentMessage. */ fulfillmentMessage_i18n?: InternationalizedString; } export interface RefundInfoShort { transactionId: string; timestamp: TalerProtocolTimestamp; amountEffective: AmountString; amountRaw: AmountString; } /** * Summary information about the payment that we got a refund for. */ export interface RefundPaymentInfo { summary: string; summary_i18n?: InternationalizedString; /** * More information about the merchant */ merchant: MerchantInfo; } export interface TransactionRefund extends TransactionCommon { type: TransactionType.Refund; // Amount that has been refunded by the merchant amountRaw: AmountString; // Amount will be added to the wallet's balance after fees and refreshing amountEffective: AmountString; // ID for the transaction that is refunded refundedTransactionId: string; paymentInfo: RefundPaymentInfo | undefined; } /** * A transaction shown for refreshes. * Only shown for (1) refreshes not associated with other transactions * and (2) refreshes in an error state. */ export interface TransactionRefresh extends TransactionCommon { type: TransactionType.Refresh; refreshReason: RefreshReason; /** * Transaction ID that caused this refresh. */ originatingTransactionId?: string; /** * Always zero for refreshes */ amountRaw: AmountString; /** * Fees, i.e. the effective, negative effect of the refresh * on the balance. * * Only applicable for stand-alone refreshes, and zero for * other refreshes where the transaction itself accounts for the * refresh fee. */ amountEffective: AmountString; refreshInputAmount: AmountString; refreshOutputAmount: AmountString; } export interface DepositTransactionTrackingState { // Raw wire transfer identifier of the deposit. wireTransferId: string; // When was the wire transfer given to the bank. timestampExecuted: TalerProtocolTimestamp; // Total amount transfer for this wtid (including fees) amountRaw: AmountString; // Wire fee amount for this exchange wireFee: AmountString; } /** * Deposit transaction, which effectively sends * money from this wallet somewhere else. */ export interface TransactionDeposit extends TransactionCommon { type: TransactionType.Deposit; depositGroupId: string; /** * Target for the deposit. */ targetPaytoUri: string; /** * Raw amount that is being deposited */ amountRaw: AmountString; /** * Effective amount that is being deposited */ amountEffective: AmountString; wireTransferDeadline: TalerProtocolTimestamp; wireTransferProgress: number; /** * Did all the deposit requests succeed? */ deposited: boolean; trackingState: Array; } export interface TransactionByIdRequest { transactionId: string; } export const codecForTransactionByIdRequest = (): Codec => buildCodecForObject() .property("transactionId", codecForString()) .build("TransactionByIdRequest"); export interface WithdrawalTransactionByURIRequest { talerWithdrawUri: string; } export const codecForWithdrawalTransactionByURIRequest = (): Codec => buildCodecForObject() .property("talerWithdrawUri", codecForString()) .build("WithdrawalTransactionByURIRequest"); export const codecForTransactionsRequest = (): Codec => buildCodecForObject() .property("currency", codecOptional(codecForString())) .property("scopeInfo", codecOptional(codecForScopeInfo())) .property("search", codecOptional(codecForString())) .property( "sort", codecOptional( codecForEither( codecForConstString("ascending"), codecForConstString("descending"), codecForConstString("stable-ascending"), ), ), ) .property("includeRefreshes", codecOptional(codecForBoolean())) .build("TransactionsRequest"); // FIXME: do full validation here! export const codecForTransactionsResponse = (): Codec => buildCodecForObject() .property("transactions", codecForList(codecForAny())) .build("TransactionsResponse"); export const codecForOrderShortInfo = (): Codec => buildCodecForObject() .property("contractTermsHash", codecForString()) .property("fulfillmentMessage", codecOptional(codecForString())) .property( "fulfillmentMessage_i18n", codecOptional(codecForInternationalizedString()), ) .property("fulfillmentUrl", codecOptional(codecForString())) .property("merchant", codecForMerchantInfo()) .property("orderId", codecForString()) .property("summary", codecForString()) .property("summary_i18n", codecOptional(codecForInternationalizedString())) .build("OrderShortInfo"); export interface ListAssociatedRefreshesRequest { transactionId: string; } export const codecForListAssociatedRefreshesRequest = (): Codec => buildCodecForObject() .property("transactionId", codecForString()) .build("ListAssociatedRefreshesRequest"); export interface ListAssociatedRefreshesResponse { transactionIds: string[]; }