address first batch of transaction list issues

This commit is contained in:
Florian Dold 2020-05-15 16:03:52 +05:30
parent 35c83414f9
commit 3eb88574bc
No known key found for this signature in database
GPG Key ID: D2E4F00F29D02A4B
9 changed files with 257 additions and 141 deletions

View File

@ -7,7 +7,7 @@ import { openDatabase, Database, Store, Index } from "./util/query";
* with each major change. When incrementing the major version, * with each major change. When incrementing the major version,
* the wallet should import data from the previous version. * the wallet should import data from the previous version.
*/ */
const TALER_DB_NAME = "taler-walletdb-v3"; const TALER_DB_NAME = "taler-walletdb-v4";
/** /**
* Current database minor version, should be incremented * Current database minor version, should be incremented

View File

@ -137,11 +137,7 @@ export async function getTotalPaymentCost(
ws: InternalWalletState, ws: InternalWalletState,
pcs: PayCoinSelection, pcs: PayCoinSelection,
): Promise<PayCostInfo> { ): Promise<PayCostInfo> {
const costs = [ const costs = [];
pcs.paymentAmount,
pcs.customerDepositFees,
pcs.customerWireFees,
];
for (let i = 0; i < pcs.coinPubs.length; i++) { for (let i = 0; i < pcs.coinPubs.length; i++) {
const coin = await ws.db.get(Stores.coins, pcs.coinPubs[i]); const coin = await ws.db.get(Stores.coins, pcs.coinPubs[i]);
if (!coin) { if (!coin) {
@ -165,6 +161,7 @@ export async function getTotalPaymentCost(
const amountLeft = Amounts.sub(denom.value, pcs.coinContributions[i]) const amountLeft = Amounts.sub(denom.value, pcs.coinContributions[i])
.amount; .amount;
const refreshCost = getTotalRefreshCost(allDenoms, denom, amountLeft); const refreshCost = getTotalRefreshCost(allDenoms, denom, amountLeft);
costs.push(pcs.coinContributions[i]);
costs.push(refreshCost); costs.push(refreshCost);
} }
return { return {
@ -670,6 +667,9 @@ async function processDownloadProposalImpl(
wireMethod: parsedContractTerms.wire_method, wireMethod: parsedContractTerms.wire_method,
wireInfoHash: parsedContractTerms.h_wire, wireInfoHash: parsedContractTerms.h_wire,
maxDepositFee: Amounts.parseOrThrow(parsedContractTerms.max_fee), maxDepositFee: Amounts.parseOrThrow(parsedContractTerms.max_fee),
merchant: parsedContractTerms.merchant,
products: parsedContractTerms.products,
summaryI18n: parsedContractTerms.summary_i18n,
}, },
contractTermsRaw: JSON.stringify(proposalResp.contract_terms), contractTermsRaw: JSON.stringify(proposalResp.contract_terms),
}; };

View File

@ -35,6 +35,7 @@ import {
WalletReserveHistoryItemType, WalletReserveHistoryItemType,
WithdrawalSourceType, WithdrawalSourceType,
ReserveHistoryRecord, ReserveHistoryRecord,
ReserveBankInfo,
} from "../types/dbTypes"; } from "../types/dbTypes";
import { Logger } from "../util/logging"; import { Logger } from "../util/logging";
import { Amounts } from "../util/amounts"; import { Amounts } from "../util/amounts";
@ -48,9 +49,11 @@ import { assertUnreachable } from "../util/assertUnreachable";
import { encodeCrock, getRandomBytes } from "../crypto/talerCrypto"; import { encodeCrock, getRandomBytes } from "../crypto/talerCrypto";
import { randomBytes } from "../crypto/primitives/nacl-fast"; import { randomBytes } from "../crypto/primitives/nacl-fast";
import { import {
getVerifiedWithdrawDenomList, selectWithdrawalDenoms,
processWithdrawGroup, processWithdrawGroup,
getBankWithdrawalInfo, getBankWithdrawalInfo,
denomSelectionInfoToState,
getWithdrawDenomList,
} from "./withdraw"; } from "./withdraw";
import { import {
guardOperationException, guardOperationException,
@ -100,6 +103,20 @@ export async function createReserve(
reserveStatus = ReserveRecordStatus.UNCONFIRMED; reserveStatus = ReserveRecordStatus.UNCONFIRMED;
} }
let bankInfo: ReserveBankInfo | undefined;
if (req.bankWithdrawStatusUrl) {
const denomSelInfo = await selectWithdrawalDenoms(ws, canonExchange, req.amount);
const denomSel = denomSelectionInfoToState(denomSelInfo);
bankInfo = {
statusUrl: req.bankWithdrawStatusUrl,
amount: req.amount,
bankWithdrawalGroupId: encodeCrock(getRandomBytes(32)),
withdrawalStarted: false,
denomSel,
};
}
const reserveRecord: ReserveRecord = { const reserveRecord: ReserveRecord = {
timestampCreated: now, timestampCreated: now,
exchangeBaseUrl: canonExchange, exchangeBaseUrl: canonExchange,
@ -108,14 +125,7 @@ export async function createReserve(
senderWire: req.senderWire, senderWire: req.senderWire,
timestampConfirmed: undefined, timestampConfirmed: undefined,
timestampReserveInfoPosted: undefined, timestampReserveInfoPosted: undefined,
bankInfo: req.bankWithdrawStatusUrl bankInfo,
? {
statusUrl: req.bankWithdrawStatusUrl,
amount: req.amount,
bankWithdrawalGroupId: encodeCrock(getRandomBytes(32)),
withdrawalStarted: false,
}
: undefined,
exchangeWire: req.exchangeWire, exchangeWire: req.exchangeWire,
reserveStatus, reserveStatus,
lastSuccessfulStatusQuery: undefined, lastSuccessfulStatusQuery: undefined,
@ -286,10 +296,11 @@ async function registerReserveWithBank(
default: default:
return; return;
} }
const bankStatusUrl = reserve.bankInfo?.statusUrl; const bankInfo = reserve.bankInfo;
if (!bankStatusUrl) { if (!bankInfo) {
return; return;
} }
const bankStatusUrl = bankInfo.statusUrl;
console.log("making selection"); console.log("making selection");
if (reserve.timestampReserveInfoPosted) { if (reserve.timestampReserveInfoPosted) {
throw Error("bank claims that reserve info selection is not done"); throw Error("bank claims that reserve info selection is not done");
@ -309,6 +320,9 @@ async function registerReserveWithBank(
} }
r.timestampReserveInfoPosted = getTimestampNow(); r.timestampReserveInfoPosted = getTimestampNow();
r.reserveStatus = ReserveRecordStatus.WAIT_CONFIRM_BANK; r.reserveStatus = ReserveRecordStatus.WAIT_CONFIRM_BANK;
if (!r.bankInfo) {
throw Error("invariant failed");
}
r.retryInfo = initRetryInfo(); r.retryInfo = initRetryInfo();
return r; return r;
}); });
@ -657,7 +671,7 @@ async function depleteReserve(
logger.trace(`getting denom list`); logger.trace(`getting denom list`);
const denomsForWithdraw = await getVerifiedWithdrawDenomList( const denomsForWithdraw = await selectWithdrawalDenoms(
ws, ws,
reserve.exchangeBaseUrl, reserve.exchangeBaseUrl,
withdrawAmount, withdrawAmount,
@ -752,17 +766,8 @@ async function depleteReserve(
retryInfo: initRetryInfo(), retryInfo: initRetryInfo(),
lastErrorPerCoin: {}, lastErrorPerCoin: {},
lastError: undefined, lastError: undefined,
denomsSel: { denomsSel: denomSelectionInfoToState(denomsForWithdraw),
totalCoinValue: denomsForWithdraw.totalCoinValue, };
totalWithdrawCost: denomsForWithdraw.totalWithdrawCost,
selectedDenoms: denomsForWithdraw.selectedDenoms.map((x) => {
return {
count: x.count,
denomPubHash: x.denom.denomPubHash,
};
}),
},
};
await tx.put(Stores.reserves, newReserve); await tx.put(Stores.reserves, newReserve);
await tx.put(Stores.reserveHistory, newHist); await tx.put(Stores.reserveHistory, newHist);

View File

@ -34,8 +34,9 @@ import {
} from "../types/dbTypes"; } from "../types/dbTypes";
import { import {
getExchangeWithdrawalInfo, getExchangeWithdrawalInfo,
getVerifiedWithdrawDenomList, selectWithdrawalDenoms,
processWithdrawGroup, processWithdrawGroup,
denomSelectionInfoToState,
} from "./withdraw"; } from "./withdraw";
import { updateExchangeFromUrl } from "./exchanges"; import { updateExchangeFromUrl } from "./exchanges";
import { getRandomBytes, encodeCrock } from "../crypto/talerCrypto"; import { getRandomBytes, encodeCrock } from "../crypto/talerCrypto";
@ -81,7 +82,7 @@ export async function getTipStatus(
); );
const tipId = encodeCrock(getRandomBytes(32)); const tipId = encodeCrock(getRandomBytes(32));
const selectedDenoms = await getVerifiedWithdrawDenomList( const selectedDenoms = await selectWithdrawalDenoms(
ws, ws,
tipPickupStatus.exchange_url, tipPickupStatus.exchange_url,
amount, amount,
@ -107,16 +108,7 @@ export async function getTipStatus(
).amount, ).amount,
retryInfo: initRetryInfo(), retryInfo: initRetryInfo(),
lastError: undefined, lastError: undefined,
denomsSel: { denomsSel: denomSelectionInfoToState(selectedDenoms),
totalCoinValue: selectedDenoms.totalCoinValue,
totalWithdrawCost: selectedDenoms.totalWithdrawCost,
selectedDenoms: selectedDenoms.selectedDenoms.map((x) => {
return {
count: x.count,
denomPubHash: x.denom.denomPubHash,
};
}),
},
}; };
await ws.db.put(Stores.tips, tipRecord); await ws.db.put(Stores.tips, tipRecord);
} }

View File

@ -18,7 +18,7 @@
* Imports. * Imports.
*/ */
import { InternalWalletState } from "./state"; import { InternalWalletState } from "./state";
import { Stores, ReserveRecordStatus, PurchaseRecord, ProposalStatus } from "../types/dbTypes"; import { Stores, ReserveRecordStatus, PurchaseRecord } from "../types/dbTypes";
import { Amounts, AmountJson } from "../util/amounts"; import { Amounts, AmountJson } from "../util/amounts";
import { timestampCmp } from "../util/time"; import { timestampCmp } from "../util/time";
import { import {
@ -131,10 +131,8 @@ export async function getTransactions(
if (wsr.timestampFinish) { if (wsr.timestampFinish) {
transactions.push({ transactions.push({
type: TransactionType.Withdrawal, type: TransactionType.Withdrawal,
amountEffective: Amounts.stringify( amountEffective: Amounts.stringify(wsr.denomsSel.totalCoinValue),
wsr.denomsSel.totalWithdrawCost, amountRaw: Amounts.stringify(wsr.denomsSel.totalWithdrawCost),
),
amountRaw: Amounts.stringify(wsr.denomsSel.totalCoinValue),
confirmed: true, confirmed: true,
exchangeBaseUrl: wsr.exchangeBaseUrl, exchangeBaseUrl: wsr.exchangeBaseUrl,
pending: !wsr.timestampFinish, pending: !wsr.timestampFinish,
@ -163,9 +161,9 @@ export async function getTransactions(
transactions.push({ transactions.push({
type: TransactionType.Withdrawal, type: TransactionType.Withdrawal,
confirmed: false, confirmed: false,
amountRaw: Amounts.stringify(r.bankInfo.amount), amountRaw: Amounts.stringify(r.bankInfo.denomSel.totalWithdrawCost),
amountEffective: undefined, amountEffective: Amounts.stringify(r.bankInfo.denomSel.totalCoinValue),
exchangeBaseUrl: undefined, exchangeBaseUrl: r.exchangeBaseUrl,
pending: true, pending: true,
timestamp: r.timestampCreated, timestamp: r.timestampCreated,
bankConfirmationUrl: r.bankInfo.confirmUrl, bankConfirmationUrl: r.bankInfo.confirmUrl,
@ -176,38 +174,6 @@ export async function getTransactions(
}); });
}); });
tx.iter(Stores.proposals).forEachAsync(async (proposal) => {
if (!proposal.download) {
return;
}
if (proposal.proposalStatus !== ProposalStatus.PROPOSED) {
return;
}
const dl = proposal.download;
const purchase = await tx.get(Stores.purchases, proposal.proposalId);
if (purchase) {
return;
}
transactions.push({
type: TransactionType.Payment,
amountRaw: Amounts.stringify(dl.contractData.amount),
amountEffective: undefined,
status: PaymentStatus.Offered,
pending: true,
timestamp: proposal.timestamp,
transactionId: makeEventId(TransactionType.Payment, proposal.proposalId),
info: {
fulfillmentUrl: dl.contractData.fulfillmentUrl,
merchant: {},
orderId: dl.contractData.orderId,
products: [],
summary: dl.contractData.summary,
summary_i18n: {},
},
});
});
tx.iter(Stores.purchases).forEachAsync(async (pr) => { tx.iter(Stores.purchases).forEachAsync(async (pr) => {
if ( if (
transactionsRequest?.currency && transactionsRequest?.currency &&
@ -231,11 +197,11 @@ export async function getTransactions(
transactionId: makeEventId(TransactionType.Payment, pr.proposalId), transactionId: makeEventId(TransactionType.Payment, pr.proposalId),
info: { info: {
fulfillmentUrl: pr.contractData.fulfillmentUrl, fulfillmentUrl: pr.contractData.fulfillmentUrl,
merchant: {}, merchant: pr.contractData.merchant,
orderId: pr.contractData.orderId, orderId: pr.contractData.orderId,
products: [], products: pr.contractData.products,
summary: pr.contractData.summary, summary: pr.contractData.summary,
summary_i18n: {}, summary_i18n: pr.contractData.summaryI18n,
}, },
}); });
@ -258,7 +224,8 @@ export async function getTransactions(
timestamp: rg.timestampQueried, timestamp: rg.timestampQueried,
transactionId: makeEventId( transactionId: makeEventId(
TransactionType.Refund, TransactionType.Refund,
`{rg.timestampQueried.t_ms}`, pr.proposalId,
`${rg.timestampQueried.t_ms}`,
), ),
refundedTransactionId: makeEventId( refundedTransactionId: makeEventId(
TransactionType.Payment, TransactionType.Payment,

View File

@ -1,6 +1,6 @@
/* /*
This file is part of GNU Taler This file is part of GNU Taler
(C) 2019-2029 Taler Systems SA (C) 2019-2020 Taler Systems SA
GNU Taler is free software; you can redistribute it and/or modify it under the 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 terms of the GNU General Public License as published by the Free Software
@ -27,6 +27,7 @@ import {
DenominationSelectionInfo, DenominationSelectionInfo,
PlanchetRecord, PlanchetRecord,
WithdrawalSourceType, WithdrawalSourceType,
DenomSelectionState,
} from "../types/dbTypes"; } from "../types/dbTypes";
import { import {
BankWithdrawDetails, BankWithdrawDetails,
@ -419,6 +420,19 @@ async function processPlanchet(
} }
} }
export function denomSelectionInfoToState(dsi: DenominationSelectionInfo): DenomSelectionState {
return {
selectedDenoms: dsi.selectedDenoms.map((x) => {
return {
count: x.count,
denomPubHash: x.denom.denomPubHash
};
}),
totalCoinValue: dsi.totalCoinValue,
totalWithdrawCost: dsi.totalWithdrawCost,
}
}
/** /**
* Get a list of denominations to withdraw from the given exchange for the * Get a list of denominations to withdraw from the given exchange for the
* given amount, making sure that all denominations' signatures are verified. * given amount, making sure that all denominations' signatures are verified.
@ -426,7 +440,7 @@ async function processPlanchet(
* Writes to the DB in order to record the result from verifying * Writes to the DB in order to record the result from verifying
* denominations. * denominations.
*/ */
export async function getVerifiedWithdrawDenomList( export async function selectWithdrawalDenoms(
ws: InternalWalletState, ws: InternalWalletState,
exchangeBaseUrl: string, exchangeBaseUrl: string,
amount: AmountJson, amount: AmountJson,
@ -603,7 +617,7 @@ export async function getExchangeWithdrawalInfo(
throw Error(`exchange ${exchangeInfo.baseUrl} wire details not available`); throw Error(`exchange ${exchangeInfo.baseUrl} wire details not available`);
} }
const selectedDenoms = await getVerifiedWithdrawDenomList( const selectedDenoms = await selectWithdrawalDenoms(
ws, ws,
baseUrl, baseUrl,
amount, amount,

View File

@ -31,6 +31,8 @@ import {
PayReq, PayReq,
TipResponse, TipResponse,
ExchangeSignKeyJson, ExchangeSignKeyJson,
MerchantInfo,
Product,
} from "./talerTypes"; } from "./talerTypes";
import { Index, Store } from "../util/query"; import { Index, Store } from "../util/query";
@ -216,6 +218,15 @@ export interface ReserveHistoryRecord {
reserveTransactions: WalletReserveHistoryItem[]; reserveTransactions: WalletReserveHistoryItem[];
} }
export interface ReserveBankInfo {
statusUrl: string;
confirmUrl?: string;
amount: AmountJson;
bankWithdrawalGroupId: string;
withdrawalStarted: boolean;
denomSel: DenomSelectionState;
}
/** /**
* A reserve record as stored in the wallet's database. * A reserve record as stored in the wallet's database.
*/ */
@ -278,13 +289,7 @@ export interface ReserveRecord {
* Extra state for when this is a withdrawal involving * Extra state for when this is a withdrawal involving
* a Taler-integrated bank. * a Taler-integrated bank.
*/ */
bankInfo?: { bankInfo?: ReserveBankInfo;
statusUrl: string;
confirmUrl?: string;
amount: AmountJson;
bankWithdrawalGroupId: string;
withdrawalStarted: boolean;
};
reserveStatus: ReserveRecordStatus; reserveStatus: ReserveRecordStatus;
@ -1179,10 +1184,13 @@ export interface AllowedExchangeInfo {
* processing in the wallet. * processing in the wallet.
*/ */
export interface WalletContractData { export interface WalletContractData {
products?: Product[];
summaryI18n: { [lang_tag: string]: string } | undefined;
fulfillmentUrl: string; fulfillmentUrl: string;
contractTermsHash: string; contractTermsHash: string;
merchantSig: string; merchantSig: string;
merchantPub: string; merchantPub: string;
merchant: MerchantInfo;
amount: AmountJson; amount: AmountJson;
orderId: string; orderId: string;
merchantBaseUrl: string; merchantBaseUrl: string;

View File

@ -260,6 +260,55 @@ export class AuditorHandle {
url: string; url: string;
} }
export interface MerchantInfo {
name: string;
jurisdiction: string | undefined;
address: string | undefined;
}
export interface Tax {
// the name of the tax
name: string;
// amount paid in tax
tax: AmountString;
}
export interface Product {
// merchant-internal identifier for the product.
product_id?: string;
// Human-readable product description.
description: string;
// Map from IETF BCP 47 language tags to localized descriptions
description_i18n?: { [lang_tag: string]: string };
// The number of units of the product to deliver to the customer.
quantity?: number;
// The unit in which the product is measured (liters, kilograms, packages, etc.)
unit?: string;
// The price of the product; this is the total price for quantity times unit of this product.
price?: AmountString;
// An optional base64-encoded product image
image?: string;
// a list of taxes paid by the merchant for this product. Can be empty.
taxes?: Tax[];
// time indicating when this product should be delivered
delivery_date?: Timestamp;
// where to deliver this product. This may be an URL for online delivery
// (i.e. 'http://example.com/download' or 'mailto:customer@example.com'),
// or a location label defined inside the proposition's 'locations'.
// The presence of a colon (':') indicates the use of an URL.
delivery_location?: string;
}
/** /**
* Contract terms from a merchant. * Contract terms from a merchant.
*/ */
@ -284,6 +333,8 @@ export class ContractTerms {
*/ */
summary: string; summary: string;
summary_i18n?: { [lang_tag: string]: string };
/** /**
* Nonce used to ensure freshness. * Nonce used to ensure freshness.
*/ */
@ -317,7 +368,7 @@ export class ContractTerms {
/** /**
* Information about the merchant. * Information about the merchant.
*/ */
merchant: any; merchant: MerchantInfo;
/** /**
* Public key of the merchant. * Public key of the merchant.
@ -332,7 +383,7 @@ export class ContractTerms {
/** /**
* Products that are sold in this contract. * Products that are sold in this contract.
*/ */
products?: any[]; products?: Product[];
/** /**
* Deadline for refunds. * Deadline for refunds.
@ -805,6 +856,35 @@ export const codecForAuditorHandle = (): Codec<AuditorHandle> =>
.property("url", codecForString) .property("url", codecForString)
.build("AuditorHandle"); .build("AuditorHandle");
export const codecForMerchantInfo = (): Codec<MerchantInfo> =>
makeCodecForObject<MerchantInfo>()
.property("name", codecForString)
.property("address", makeCodecOptional(codecForString))
.property("jurisdiction", makeCodecOptional(codecForString))
.build("MerchantInfo");
export const codecForTax = (): Codec<Tax> =>
makeCodecForObject<Tax>()
.property("name", codecForString)
.property("tax", codecForString)
.build("Tax");
export const codecForI18n = (): Codec<{ [lang_tag: string]: string }> =>
makeCodecForMap(codecForString)
export const codecForProduct = (): Codec<Product> =>
makeCodecForObject<Product>()
.property("product_id", makeCodecOptional(codecForString))
.property("description", codecForString)
.property("description_i18n", makeCodecOptional(codecForI18n()))
.property("quantity", makeCodecOptional(codecForNumber))
.property("unit", makeCodecOptional(codecForString))
.property("price", makeCodecOptional(codecForString))
.property("delivery_date", makeCodecOptional(codecForTimestamp))
.property("delivery_location", makeCodecOptional(codecForString))
.build("Tax");
export const codecForContractTerms = (): Codec<ContractTerms> => export const codecForContractTerms = (): Codec<ContractTerms> =>
makeCodecForObject<ContractTerms>() makeCodecForObject<ContractTerms>()
.property("order_id", codecForString) .property("order_id", codecForString)
@ -814,6 +894,7 @@ export const codecForContractTerms = (): Codec<ContractTerms> =>
.property("auto_refund", makeCodecOptional(codecForDuration)) .property("auto_refund", makeCodecOptional(codecForDuration))
.property("wire_method", codecForString) .property("wire_method", codecForString)
.property("summary", codecForString) .property("summary", codecForString)
.property("summary_i18n", makeCodecOptional(codecForI18n()))
.property("nonce", codecForString) .property("nonce", codecForString)
.property("amount", codecForString) .property("amount", codecForString)
.property("auditors", makeCodecForList(codecForAuditorHandle())) .property("auditors", makeCodecForList(codecForAuditorHandle()))
@ -824,10 +905,10 @@ export const codecForContractTerms = (): Codec<ContractTerms> =>
.property("locations", codecForAny) .property("locations", codecForAny)
.property("max_fee", codecForString) .property("max_fee", codecForString)
.property("max_wire_fee", makeCodecOptional(codecForString)) .property("max_wire_fee", makeCodecOptional(codecForString))
.property("merchant", codecForAny) .property("merchant", codecForMerchantInfo())
.property("merchant_pub", codecForString) .property("merchant_pub", codecForString)
.property("exchanges", makeCodecForList(codecForExchangeHandle())) .property("exchanges", makeCodecForList(codecForExchangeHandle()))
.property("products", makeCodecOptional(makeCodecForList(codecForAny))) .property("products", makeCodecOptional(makeCodecForList(codecForProduct())))
.property("extra", codecForAny) .property("extra", codecForAny)
.build("ContractTerms"); .build("ContractTerms");
@ -852,10 +933,7 @@ export const codecForMerchantRefundResponse = (): Codec<
makeCodecForObject<MerchantRefundResponse>() makeCodecForObject<MerchantRefundResponse>()
.property("merchant_pub", codecForString) .property("merchant_pub", codecForString)
.property("h_contract_terms", codecForString) .property("h_contract_terms", codecForString)
.property( .property("refunds", makeCodecForList(codecForMerchantRefundPermission()))
"refunds",
makeCodecForList(codecForMerchantRefundPermission()),
)
.build("MerchantRefundResponse"); .build("MerchantRefundResponse");
export const codecForReserveSigSingleton = (): Codec<ReserveSigSingleton> => export const codecForReserveSigSingleton = (): Codec<ReserveSigSingleton> =>

View File

@ -16,13 +16,16 @@
/** /**
* Type and schema definitions for the wallet's transaction list. * Type and schema definitions for the wallet's transaction list.
*
* @author Florian Dold
* @author Torsten Grote
*/ */
/** /**
* Imports. * Imports.
*/ */
import { Timestamp } from "../util/time"; import { Timestamp } from "../util/time";
import { AmountString } from "./talerTypes"; import { AmountString, Product } from "./talerTypes";
export interface TransactionsRequest { export interface TransactionsRequest {
/** /**
@ -44,6 +47,24 @@ export interface TransactionsResponse {
transactions: Transaction[]; transactions: Transaction[];
} }
interface TransactionError {
/**
* TALER_EC_* unique error code.
* The action(s) offered and message displayed on the transaction item depend on this code.
*/
ec: number;
/**
* English-only error hint, if available.
*/
hint?: string;
/**
* Error details specific to "ec", if applicable/available
*/
details?: any;
}
export interface TransactionCommon { export interface TransactionCommon {
// opaque unique ID for the transaction, used as a starting point for paginating queries // 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) // and for invoking actions on the transaction (e.g. deleting/hiding it from the history)
@ -64,16 +85,17 @@ export interface TransactionCommon {
amountRaw: AmountString; amountRaw: AmountString;
// Amount added or removed from the wallet's balance (including all fees and other costs) // Amount added or removed from the wallet's balance (including all fees and other costs)
amountEffective?: AmountString; amountEffective: AmountString;
error?: TransactionError;
} }
export type Transaction = ( export type Transaction =
TransactionWithdrawal | | TransactionWithdrawal
TransactionPayment | | TransactionPayment
TransactionRefund | | TransactionRefund
TransactionTip | | TransactionTip
TransactionRefresh | TransactionRefresh;
)
export const enum TransactionType { export const enum TransactionType {
Withdrawal = "withdrawal", Withdrawal = "withdrawal",
@ -93,79 +115,109 @@ interface TransactionWithdrawal extends TransactionCommon {
*/ */
exchangeBaseUrl?: string; exchangeBaseUrl?: string;
// 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. * true if the bank has confirmed the withdrawal, false if not.
// See also bankConfirmationUrl below. * An unconfirmed withdrawal usually requires user-input and should be highlighted in the UI.
* See also bankConfirmationUrl below.
*/
confirmed: boolean; confirmed: boolean;
// If the withdrawal is unconfirmed, this can include a URL for user initiated confirmation. /**
* If the withdrawal is unconfirmed, this can include a URL for user
* initiated confirmation.
*/
bankConfirmationUrl?: string; bankConfirmationUrl?: string;
// Amount that has been subtracted from the reserve's balance for this withdrawal. /**
* Amount that got subtracted from the reserve balance.
*/
amountRaw: AmountString; amountRaw: AmountString;
/** /**
* Amount that actually was (or will be) added to the wallet's balance. * Amount that actually was (or will be) added to the wallet's balance.
* Only present if an exchange has already been selected.
*/ */
amountEffective?: AmountString; amountEffective: AmountString;
} }
export const enum PaymentStatus { export const enum PaymentStatus {
// Explicitly aborted after timeout / failure /**
* Explicitly aborted after timeout / failure
*/
Aborted = "aborted", Aborted = "aborted",
// Payment failed, wallet will auto-retry. /**
// User should be given the option to retry now / abort. * Payment failed, wallet will auto-retry.
* User should be given the option to retry now / abort.
*/
Failed = "failed", Failed = "failed",
// Paid successfully /**
* Paid successfully
*/
Paid = "paid", Paid = "paid",
// Only offered, user must accept / decline /**
Offered = "offered", * User accepted, payment is processing.
*/
// User accepted, payment is processing.
Accepted = "accepted", Accepted = "accepted",
} }
export interface TransactionPayment extends TransactionCommon { export interface TransactionPayment extends TransactionCommon {
type: TransactionType.Payment; type: TransactionType.Payment;
// Additional information about the payment. /**
* Additional information about the payment.
*/
info: PaymentShortInfo; info: PaymentShortInfo;
/**
* How far did the wallet get with processing the payment?
*/
status: PaymentStatus; status: PaymentStatus;
// Amount that must be paid for the contract /**
* Amount that must be paid for the contract
*/
amountRaw: AmountString; amountRaw: AmountString;
// Amount that was paid, including deposit, wire and refresh fees. /**
amountEffective?: AmountString; * Amount that was paid, including deposit, wire and refresh fees.
*/
amountEffective: AmountString;
} }
interface PaymentShortInfo { interface PaymentShortInfo {
// Order ID, uniquely identifies the order within a merchant instance /**
* Order ID, uniquely identifies the order within a merchant instance
*/
orderId: string; orderId: string;
// More information about the merchant /**
* More information about the merchant
*/
merchant: any; merchant: any;
// Summary of the order, given by the merchant /**
* Summary of the order, given by the merchant
*/
summary: string; summary: string;
// Map from IETF BCP 47 language tags to localized summaries /**
* Map from IETF BCP 47 language tags to localized summaries
*/
summary_i18n?: { [lang_tag: string]: string }; summary_i18n?: { [lang_tag: string]: string };
// List of products that are part of the order /**
products: any[]; * List of products that are part of the order
*/
products: Product[] | undefined;
// URL of the fulfillment, given by the merchant /**
* URL of the fulfillment, given by the merchant
*/
fulfillmentUrl: string; fulfillmentUrl: string;
} }
interface TransactionRefund extends TransactionCommon { interface TransactionRefund extends TransactionCommon {
type: TransactionType.Refund; type: TransactionType.Refund;
@ -221,4 +273,4 @@ interface TransactionRefresh extends TransactionCommon {
// Amount that will be paid as fees for the refresh // Amount that will be paid as fees for the refresh
amountEffective: AmountString; amountEffective: AmountString;
} }