aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--packages/taler-wallet-core/src/operations/backup.ts502
-rw-r--r--packages/taler-wallet-core/src/operations/pay.ts47
-rw-r--r--packages/taler-wallet-core/src/types/backupTypes.ts61
-rw-r--r--packages/taler-wallet-core/src/types/dbTypes.ts17
-rw-r--r--packages/taler-wallet-core/src/types/pendingTypes.ts276
-rw-r--r--packages/taler-wallet-core/src/types/transactionsTypes.ts337
6 files changed, 1183 insertions, 57 deletions
diff --git a/packages/taler-wallet-core/src/operations/backup.ts b/packages/taler-wallet-core/src/operations/backup.ts
index f071b6d08..fdccd23c1 100644
--- a/packages/taler-wallet-core/src/operations/backup.ts
+++ b/packages/taler-wallet-core/src/operations/backup.ts
@@ -31,6 +31,7 @@ import {
BackupCoinSource,
BackupCoinSourceType,
BackupDenomination,
+ BackupDenomSel,
BackupExchange,
BackupExchangeWireFee,
BackupProposal,
@@ -39,6 +40,7 @@ import {
BackupRecoupGroup,
BackupRefreshGroup,
BackupRefreshOldCoin,
+ BackupRefreshReason,
BackupRefreshSession,
BackupRefundItem,
BackupRefundState,
@@ -50,15 +52,24 @@ import {
import { TransactionHandle } from "../util/query";
import {
AbortStatus,
+ CoinSource,
CoinSourceType,
CoinStatus,
ConfigRecord,
+ DenominationStatus,
+ DenomSelectionState,
+ ExchangeUpdateStatus,
+ ExchangeWireInfo,
+ ProposalDownload,
ProposalStatus,
+ RefreshSessionRecord,
RefundState,
+ ReserveBankInfo,
+ ReserveRecordStatus,
Stores,
} from "../types/dbTypes";
-import { checkDbInvariant } from "../util/invariants";
-import { Amounts, codecForAmountString } from "../util/amounts";
+import { checkDbInvariant, checkLogicInvariant } from "../util/invariants";
+import { AmountJson, Amounts, codecForAmountString } from "../util/amounts";
import {
decodeCrock,
eddsaGetPublic,
@@ -71,7 +82,11 @@ import {
import { canonicalizeBaseUrl, canonicalJson, j2s } from "../util/helpers";
import { getTimestampNow, Timestamp } from "../util/time";
import { URL } from "../util/url";
-import { AmountString, TipResponse } from "../types/talerTypes";
+import {
+ AmountString,
+ codecForContractTerms,
+ ContractTerms,
+} from "../types/talerTypes";
import {
buildCodecForObject,
Codec,
@@ -85,6 +100,8 @@ import {
import { Logger } from "../util/logging";
import { gzipSync } from "fflate";
import { kdf } from "../crypto/primitives/kdf";
+import { initRetryInfo } from "../util/retries";
+import { RefreshReason } from "../types/walletTypes";
interface WalletBackupConfState {
deviceId: string;
@@ -207,7 +224,7 @@ export async function exportBackup(
timestamp_start: wg.timestampStart,
timestamp_finish: wg.timestampFinish,
withdrawal_group_id: wg.withdrawalGroupId,
- secret_seed: wg.secretSeed
+ secret_seed: wg.secretSeed,
});
});
@@ -425,7 +442,7 @@ export async function exportBackup(
backupPurchases.push({
clock_created: 1,
- contract_terms_raw: purch.contractTermsRaw,
+ contract_terms_raw: purch.download.contractTermsRaw,
auto_refund_deadline: purch.autoRefundDeadline,
merchant_pay_sig: purch.merchantPaySig,
pay_coins: purch.payCoinSelection.coinPubs.map((x, i) => ({
@@ -478,6 +495,9 @@ export async function exportBackup(
timestamp: prop.timestamp,
contract_terms_raw: prop.download?.contractTermsRaw,
download_session_id: prop.downloadSessionId,
+ merchant_base_url: prop.merchantBaseUrl,
+ order_id: prop.orderId,
+ merchant_sig: prop.download?.contractData.merchantSig,
});
});
@@ -572,9 +592,47 @@ export async function encryptBackup(
throw Error("not implemented");
}
+interface CompletedCoin {
+ coinPub: string;
+ coinEvHash: string;
+}
+
+/**
+ * Precomputed cryptographic material for a backup import.
+ *
+ * We separate this data from the backup blob as we want the backup
+ * blob to be small, and we can't compute it during the database transaction,
+ * as the async crypto worker communication would auto-close the database transaction.
+ */
+interface BackupCryptoPrecomputedData {
+ denomPubToHash: Record<string, string>;
+ coinPrivToCompletedCoin: Record<string, CompletedCoin>;
+ proposalNoncePrivToProposalPub: { [priv: string]: string };
+ proposalIdToContractTermsHash: { [proposalId: string]: string };
+ reservePrivToPub: Record<string, string>;
+}
+
+function checkBackupInvariant(b: boolean, m?: string): asserts b {
+ if (!b) {
+ if (m) {
+ throw Error(`BUG: backup invariant failed (${m})`);
+ } else {
+ throw Error("BUG: backup invariant failed");
+ }
+ }
+}
+
+function getDenomSelStateFromBackup(
+ tx: TransactionHandle<typeof Stores.denominations>,
+ sel: BackupDenomSel,
+): Promise<DenomSelectionState> {
+ throw Error("not implemented");
+}
+
export async function importBackup(
ws: InternalWalletState,
backupRequest: BackupRequest,
+ cryptoComp: BackupCryptoPrecomputedData,
): Promise<void> {
await provideBackupState(ws);
return ws.db.runWithWriteTransaction(
@@ -593,8 +651,439 @@ export async function importBackup(
Stores.withdrawalGroups,
],
async (tx) => {
+ // FIXME: validate schema!
+ const backupBlob = backupRequest.backupBlob as WalletBackupContentV1;
- });
+ // FIXME: validate version
+
+ for (const backupExchange of backupBlob.exchanges) {
+ const existingExchange = await tx.get(
+ Stores.exchanges,
+ backupExchange.base_url,
+ );
+
+ if (!existingExchange) {
+ const wireInfo: ExchangeWireInfo = {
+ accounts: backupExchange.accounts.map((x) => ({
+ master_sig: x.master_sig,
+ payto_uri: x.payto_uri,
+ })),
+ feesForType: {},
+ };
+ for (const fee of backupExchange.wire_fees) {
+ const w = (wireInfo.feesForType[fee.wire_type] ??= []);
+ w.push({
+ closingFee: Amounts.parseOrThrow(fee.closing_fee),
+ endStamp: fee.end_stamp,
+ sig: fee.sig,
+ startStamp: fee.start_stamp,
+ wireFee: Amounts.parseOrThrow(fee.wire_fee),
+ });
+ }
+ await tx.put(Stores.exchanges, {
+ addComplete: true,
+ baseUrl: backupExchange.base_url,
+ builtIn: false,
+ updateReason: undefined,
+ permanent: true,
+ retryInfo: initRetryInfo(),
+ termsOfServiceAcceptedEtag: backupExchange.tos_etag_accepted,
+ termsOfServiceText: undefined,
+ termsOfServiceLastEtag: backupExchange.tos_etag_last,
+ updateStarted: getTimestampNow(),
+ updateStatus: ExchangeUpdateStatus.FetchKeys,
+ wireInfo,
+ details: {
+ currency: backupExchange.currency,
+ auditors: backupExchange.auditors.map((x) => ({
+ auditor_pub: x.auditor_pub,
+ auditor_url: x.auditor_url,
+ denomination_keys: x.denomination_keys,
+ })),
+ lastUpdateTime: { t_ms: "never" },
+ masterPublicKey: backupExchange.master_public_key,
+ nextUpdateTime: { t_ms: "never" },
+ protocolVersion: backupExchange.protocol_version,
+ signingKeys: backupExchange.signing_keys.map((x) => ({
+ key: x.key,
+ master_sig: x.master_sig,
+ stamp_end: x.stamp_end,
+ stamp_expire: x.stamp_expire,
+ stamp_start: x.stamp_start,
+ })),
+ },
+ });
+ }
+
+ for (const backupDenomination of backupExchange.denominations) {
+ const denomPubHash =
+ cryptoComp.denomPubToHash[backupDenomination.denom_pub];
+ checkLogicInvariant(!!denomPubHash);
+ const existingDenom = await tx.get(Stores.denominations, [
+ backupExchange.base_url,
+ denomPubHash,
+ ]);
+ if (!existingDenom) {
+ await tx.put(Stores.denominations, {
+ denomPub: backupDenomination.denom_pub,
+ denomPubHash: denomPubHash,
+ exchangeBaseUrl: backupExchange.base_url,
+ feeDeposit: Amounts.parseOrThrow(backupDenomination.fee_deposit),
+ feeRefresh: Amounts.parseOrThrow(backupDenomination.fee_refresh),
+ feeRefund: Amounts.parseOrThrow(backupDenomination.fee_refund),
+ feeWithdraw: Amounts.parseOrThrow(
+ backupDenomination.fee_withdraw,
+ ),
+ isOffered: backupDenomination.is_offered,
+ isRevoked: backupDenomination.is_revoked,
+ masterSig: backupDenomination.master_sig,
+ stampExpireDeposit: backupDenomination.stamp_expire_deposit,
+ stampExpireLegal: backupDenomination.stamp_expire_legal,
+ stampExpireWithdraw: backupDenomination.stamp_expire_withdraw,
+ stampStart: backupDenomination.stamp_start,
+ status: DenominationStatus.VerifiedGood,
+ value: Amounts.parseOrThrow(backupDenomination.value),
+ });
+ }
+ for (const backupCoin of backupDenomination.coins) {
+ const compCoin =
+ cryptoComp.coinPrivToCompletedCoin[backupCoin.coin_priv];
+ checkLogicInvariant(!!compCoin);
+ const existingCoin = await tx.get(Stores.coins, compCoin.coinPub);
+ if (!existingCoin) {
+ let coinSource: CoinSource;
+ switch (backupCoin.coin_source.type) {
+ case BackupCoinSourceType.Refresh:
+ coinSource = {
+ type: CoinSourceType.Refresh,
+ oldCoinPub: backupCoin.coin_source.old_coin_pub,
+ };
+ break;
+ case BackupCoinSourceType.Tip:
+ coinSource = {
+ type: CoinSourceType.Tip,
+ coinIndex: backupCoin.coin_source.coin_index,
+ walletTipId: backupCoin.coin_source.wallet_tip_id,
+ };
+ break;
+ case BackupCoinSourceType.Withdraw:
+ coinSource = {
+ type: CoinSourceType.Withdraw,
+ coinIndex: backupCoin.coin_source.coin_index,
+ reservePub: backupCoin.coin_source.reserve_pub,
+ withdrawalGroupId:
+ backupCoin.coin_source.withdrawal_group_id,
+ };
+ break;
+ }
+ await tx.put(Stores.coins, {
+ blindingKey: backupCoin.blinding_key,
+ coinEvHash: compCoin.coinEvHash,
+ coinPriv: backupCoin.coin_priv,
+ currentAmount: Amounts.parseOrThrow(backupCoin.current_amount),
+ denomSig: backupCoin.denom_sig,
+ coinPub: compCoin.coinPub,
+ suspended: false,
+ exchangeBaseUrl: backupExchange.base_url,
+ denomPub: backupDenomination.denom_pub,
+ denomPubHash,
+ status: backupCoin.fresh
+ ? CoinStatus.Fresh
+ : CoinStatus.Dormant,
+ coinSource,
+ });
+ }
+ }
+ }
+
+ for (const backupReserve of backupExchange.reserves) {
+ const reservePub =
+ cryptoComp.reservePrivToPub[backupReserve.reserve_priv];
+ checkLogicInvariant(!!reservePub);
+ const existingReserve = await tx.get(Stores.reserves, reservePub);
+ const instructedAmount = Amounts.parseOrThrow(
+ backupReserve.instructed_amount,
+ );
+ if (!existingReserve) {
+ let bankInfo: ReserveBankInfo | undefined;
+ if (backupReserve.bank_info) {
+ bankInfo = {
+ exchangePaytoUri: backupReserve.bank_info.exchange_payto_uri,
+ statusUrl: backupReserve.bank_info.status_url,
+ confirmUrl: backupReserve.bank_info.confirm_url,
+ };
+ }
+ await tx.put(Stores.reserves, {
+ currency: instructedAmount.currency,
+ instructedAmount,
+ exchangeBaseUrl: backupExchange.base_url,
+ reservePub,
+ reservePriv: backupReserve.reserve_priv,
+ requestedQuery: false,
+ bankInfo,
+ timestampCreated: backupReserve.timestamp_created,
+ timestampBankConfirmed:
+ backupReserve.bank_info?.timestamp_bank_confirmed,
+ timestampReserveInfoPosted:
+ backupReserve.bank_info?.timestamp_reserve_info_posted,
+ senderWire: backupReserve.sender_wire,
+ retryInfo: initRetryInfo(false),
+ lastError: undefined,
+ lastSuccessfulStatusQuery: { t_ms: "never" },
+ initialWithdrawalGroupId:
+ backupReserve.initial_withdrawal_group_id,
+ initialWithdrawalStarted:
+ backupReserve.withdrawal_groups.length > 0,
+ // FIXME!
+ reserveStatus: ReserveRecordStatus.QUERYING_STATUS,
+ initialDenomSel: await getDenomSelStateFromBackup(
+ tx,
+ backupReserve.initial_selected_denoms,
+ ),
+ });
+ }
+ for (const backupWg of backupReserve.withdrawal_groups) {
+ const existingWg = await tx.get(
+ Stores.withdrawalGroups,
+ backupWg.withdrawal_group_id,
+ );
+ if (!existingWg) {
+ await tx.put(Stores.withdrawalGroups, {
+ denomsSel: await getDenomSelStateFromBackup(
+ tx,
+ backupWg.selected_denoms,
+ ),
+ exchangeBaseUrl: backupExchange.base_url,
+ lastError: undefined,
+ rawWithdrawalAmount: Amounts.parseOrThrow(
+ backupWg.raw_withdrawal_amount,
+ ),
+ reservePub,
+ retryInfo: initRetryInfo(false),
+ secretSeed: backupWg.secret_seed,
+ timestampStart: backupWg.timestamp_start,
+ timestampFinish: backupWg.timestamp_finish,
+ withdrawalGroupId: backupWg.withdrawal_group_id,
+ });
+ }
+ }
+ }
+ }
+
+ for (const backupProposal of backupBlob.proposals) {
+ const existingProposal = await tx.get(
+ Stores.proposals,
+ backupProposal.proposal_id,
+ );
+ if (!existingProposal) {
+ let download: ProposalDownload | undefined;
+ let proposalStatus: ProposalStatus;
+ switch (backupProposal.proposal_status) {
+ case BackupProposalStatus.Proposed:
+ if (backupProposal.contract_terms_raw) {
+ proposalStatus = ProposalStatus.PROPOSED;
+ } else {
+ proposalStatus = ProposalStatus.DOWNLOADING;
+ }
+ break;
+ case BackupProposalStatus.Refused:
+ proposalStatus = ProposalStatus.REFUSED;
+ break;
+ case BackupProposalStatus.Repurchase:
+ proposalStatus = ProposalStatus.REPURCHASE;
+ break;
+ case BackupProposalStatus.PermanentlyFailed:
+ proposalStatus = ProposalStatus.PERMANENTLY_FAILED;
+ break;
+ }
+ if (backupProposal.contract_terms_raw) {
+ checkDbInvariant(!!backupProposal.merchant_sig);
+ const parsedContractTerms = codecForContractTerms().decode(
+ backupProposal.contract_terms_raw,
+ );
+ const amount = Amounts.parseOrThrow(parsedContractTerms.amount);
+ const contractTermsHash =
+ cryptoComp.proposalIdToContractTermsHash[
+ backupProposal.proposal_id
+ ];
+ let maxWireFee: AmountJson;
+ if (parsedContractTerms.max_wire_fee) {
+ maxWireFee = Amounts.parseOrThrow(
+ parsedContractTerms.max_wire_fee,
+ );
+ } else {
+ maxWireFee = Amounts.getZero(amount.currency);
+ }
+ download = {
+ contractData: {
+ amount,
+ contractTermsHash: contractTermsHash,
+ fulfillmentUrl: parsedContractTerms.fulfillment_url ?? "",
+ merchantBaseUrl: parsedContractTerms.merchant_base_url,
+ merchantPub: parsedContractTerms.merchant_pub,
+ merchantSig: backupProposal.merchant_sig,
+ orderId: parsedContractTerms.order_id,
+ summary: parsedContractTerms.summary,
+ autoRefund: parsedContractTerms.auto_refund,
+ maxWireFee,
+ payDeadline: parsedContractTerms.pay_deadline,
+ refundDeadline: parsedContractTerms.refund_deadline,
+ wireFeeAmortization:
+ parsedContractTerms.wire_fee_amortization || 1,
+ allowedAuditors: parsedContractTerms.auditors.map((x) => ({
+ auditorBaseUrl: x.url,
+ auditorPub: x.master_pub,
+ })),
+ allowedExchanges: parsedContractTerms.exchanges.map((x) => ({
+ exchangeBaseUrl: x.url,
+ exchangePub: x.master_pub,
+ })),
+ timestamp: parsedContractTerms.timestamp,
+ wireMethod: parsedContractTerms.wire_method,
+ wireInfoHash: parsedContractTerms.h_wire,
+ maxDepositFee: Amounts.parseOrThrow(
+ parsedContractTerms.max_fee,
+ ),
+ merchant: parsedContractTerms.merchant,
+ products: parsedContractTerms.products,
+ summaryI18n: parsedContractTerms.summary_i18n,
+ },
+ contractTermsRaw: backupProposal.contract_terms_raw,
+ };
+ }
+ await tx.put(Stores.proposals, {
+ claimToken: backupProposal.claim_token,
+ lastError: undefined,
+ merchantBaseUrl: backupProposal.merchant_base_url,
+ timestamp: backupProposal.timestamp,
+ orderId: backupProposal.order_id,
+ noncePriv: backupProposal.nonce_priv,
+ noncePub:
+ cryptoComp.proposalNoncePrivToProposalPub[
+ backupProposal.nonce_priv
+ ],
+ proposalId: backupProposal.proposal_id,
+ repurchaseProposalId: backupProposal.repurchase_proposal_id,
+ retryInfo: initRetryInfo(false),
+ download,
+ proposalStatus,
+ });
+ }
+ }
+
+ for (const backupPurchase of backupBlob.purchases) {
+ const existingPurchase = await tx.get(
+ Stores.purchases,
+ backupPurchase.proposal_id,
+ );
+ if (!existingPurchase) {
+ await tx.put(Stores.purchases, {});
+ }
+ }
+
+ for (const backupRefreshGroup of backupBlob.refresh_groups) {
+ const existingRg = await tx.get(
+ Stores.refreshGroups,
+ backupRefreshGroup.refresh_group_id,
+ );
+ if (!existingRg) {
+ let reason: RefreshReason;
+ switch (backupRefreshGroup.reason) {
+ case BackupRefreshReason.AbortPay:
+ reason = RefreshReason.AbortPay;
+ break;
+ case BackupRefreshReason.BackupRestored:
+ reason = RefreshReason.BackupRestored;
+ break;
+ case BackupRefreshReason.Manual:
+ reason = RefreshReason.Manual;
+ break;
+ case BackupRefreshReason.Pay:
+ reason = RefreshReason.Pay;
+ break;
+ case BackupRefreshReason.Recoup:
+ reason = RefreshReason.Recoup;
+ break;
+ case BackupRefreshReason.Refund:
+ reason = RefreshReason.Refund;
+ break;
+ case BackupRefreshReason.Scheduled:
+ reason = RefreshReason.Scheduled;
+ break;
+ }
+ const refreshSessionPerCoin: (
+ | RefreshSessionRecord
+ | undefined
+ )[] = [];
+ for (const oldCoin of backupRefreshGroup.old_coins) {
+ if (oldCoin.refresh_session) {
+ const denomSel = await getDenomSelStateFromBackup(
+ tx,
+ oldCoin.refresh_session.new_denoms,
+ );
+ refreshSessionPerCoin.push({
+ sessionSecretSeed: oldCoin.refresh_session.session_secret_seed,
+ norevealIndex: oldCoin.refresh_session.noreveal_index,
+ newDenoms: oldCoin.refresh_session.new_denoms.map((x) => ({
+ count: x.count,
+ denomPubHash: x.denom_pub_hash,
+ })),
+ amountRefreshOutput: denomSel.totalCoinValue,
+ });
+ } else {
+ refreshSessionPerCoin.push(undefined);
+ }
+ }
+ await tx.put(Stores.refreshGroups, {
+ timestampFinished: backupRefreshGroup.timestamp_finished,
+ timestampCreated: backupRefreshGroup.timestamp_started,
+ refreshGroupId: backupRefreshGroup.refresh_group_id,
+ reason,
+ lastError: undefined,
+ lastErrorPerCoin: {},
+ oldCoinPubs: backupRefreshGroup.old_coins.map((x) => x.coin_pub),
+ finishedPerCoin: backupRefreshGroup.old_coins.map(
+ (x) => x.finished,
+ ),
+ inputPerCoin: backupRefreshGroup.old_coins.map((x) =>
+ Amounts.parseOrThrow(x.input_amount),
+ ),
+ estimatedOutputPerCoin: backupRefreshGroup.old_coins.map((x) =>
+ Amounts.parseOrThrow(x.estimated_output_amount),
+ ),
+ refreshSessionPerCoin,
+ retryInfo: initRetryInfo(false),
+ });
+ }
+ }
+
+ for (const backupTip of backupBlob.tips) {
+ const existingTip = await tx.get(Stores.tips, backupTip.wallet_tip_id);
+ if (!existingTip) {
+ const denomsSel = await getDenomSelStateFromBackup(
+ tx,
+ backupTip.selected_denoms,
+ );
+ await tx.put(Stores.tips, {
+ acceptedTimestamp: backupTip.timestamp_accepted,
+ createdTimestamp: backupTip.timestamp_created,
+ denomsSel,
+ exchangeBaseUrl: backupTip.exchange_base_url,
+ lastError: undefined,
+ merchantBaseUrl: backupTip.exchange_base_url,
+ merchantTipId: backupTip.merchant_tip_id,
+ pickedUpTimestamp: backupTip.timestam_picked_up,
+ retryInfo: initRetryInfo(false),
+ secretSeed: backupTip.secret_seed,
+ tipAmountEffective: denomsSel.totalCoinValue,
+ tipAmountRaw: Amounts.parseOrThrow(backupTip.tip_amount_raw),
+ tipExpiration: backupTip.timestamp_expiration,
+ walletTipId: backupTip.wallet_tip_id,
+ });
+ }
+ }
+ },
+ );
}
function deriveAccountKeyPair(
@@ -607,7 +1096,6 @@ function deriveAccountKeyPair(
stringToBytes("taler-sync-account-key-salt"),
stringToBytes(providerUrl),
);
-
return {
eddsaPriv: privateKey,
eddsaPub: eddsaGetPublic(privateKey),
diff --git a/packages/taler-wallet-core/src/operations/pay.ts b/packages/taler-wallet-core/src/operations/pay.ts
index c374cfe4a..ecbe37a64 100644
--- a/packages/taler-wallet-core/src/operations/pay.ts
+++ b/packages/taler-wallet-core/src/operations/pay.ts
@@ -441,8 +441,7 @@ async function recordConfirmPay(
const payCostInfo = await getTotalPaymentCost(ws, coinSelection);
const t: PurchaseRecord = {
abortStatus: AbortStatus.None,
- contractTermsRaw: d.contractTermsRaw,
- contractData: d.contractData,
+ download: d,
lastSessionId: sessionId,
payCoinSelection: coinSelection,
totalPayCost: payCostInfo,
@@ -763,7 +762,7 @@ async function processDownloadProposalImpl(
products: parsedContractTerms.products,
summaryI18n: parsedContractTerms.summary_i18n,
},
- contractTermsRaw: JSON.stringify(proposalResp.contract_terms),
+ contractTermsRaw: proposalResp.contract_terms,
};
if (
fulfillmentUrl &&
@@ -877,7 +876,7 @@ async function storeFirstPaySuccess(
purchase.payRetryInfo = initRetryInfo(false);
purchase.merchantPaySig = paySig;
if (isFirst) {
- const ar = purchase.contractData.autoRefund;
+ const ar = purchase.download.contractData.autoRefund;
if (ar) {
logger.info("auto_refund present");
purchase.refundQueryRequested = true;
@@ -938,8 +937,8 @@ async function submitPay(
if (!purchase.merchantPaySig) {
const payUrl = new URL(
- `orders/${purchase.contractData.orderId}/pay`,
- purchase.contractData.merchantBaseUrl,
+ `orders/${purchase.download.contractData.orderId}/pay`,
+ purchase.download.contractData.merchantBaseUrl,
).href;
const reqBody = {
@@ -986,10 +985,10 @@ async function submitPay(
logger.trace("got success from pay URL", merchantResp);
- const merchantPub = purchase.contractData.merchantPub;
+ const merchantPub = purchase.download.contractData.merchantPub;
const valid: boolean = await ws.cryptoApi.isValidPaymentSignature(
merchantResp.sig,
- purchase.contractData.contractTermsHash,
+ purchase.download.contractData.contractTermsHash,
merchantPub,
);
@@ -1002,12 +1001,12 @@ async function submitPay(
await storeFirstPaySuccess(ws, proposalId, sessionId, merchantResp.sig);
} else {
const payAgainUrl = new URL(
- `orders/${purchase.contractData.orderId}/paid`,
- purchase.contractData.merchantBaseUrl,
+ `orders/${purchase.download.contractData.orderId}/paid`,
+ purchase.download.contractData.merchantBaseUrl,
).href;
const reqBody = {
sig: purchase.merchantPaySig,
- h_contract: purchase.contractData.contractTermsHash,
+ h_contract: purchase.download.contractData.contractTermsHash,
session_id: sessionId ?? "",
};
const resp = await ws.runSequentialized([EXCHANGE_COINS_LOCK], () =>
@@ -1047,7 +1046,7 @@ async function submitPay(
return {
type: ConfirmPayResultType.Done,
- contractTerms: JSON.parse(purchase.contractTermsRaw),
+ contractTerms: purchase.download.contractTermsRaw,
};
}
@@ -1120,7 +1119,7 @@ export async function preparePayForUri(
logger.info("not confirming payment, insufficient coins");
return {
status: PreparePayResultType.InsufficientBalance,
- contractTerms: JSON.parse(d.contractTermsRaw),
+ contractTerms: d.contractTermsRaw,
proposalId: proposal.proposalId,
amountRaw: Amounts.stringify(d.contractData.amount),
};
@@ -1132,7 +1131,7 @@ export async function preparePayForUri(
return {
status: PreparePayResultType.PaymentPossible,
- contractTerms: JSON.parse(d.contractTermsRaw),
+ contractTerms: d.contractTermsRaw,
proposalId: proposal.proposalId,
amountEffective: Amounts.stringify(totalCost),
amountRaw: Amounts.stringify(res.paymentAmount),
@@ -1161,20 +1160,20 @@ export async function preparePayForUri(
}
return {
status: PreparePayResultType.AlreadyConfirmed,
- contractTerms: JSON.parse(purchase.contractTermsRaw),
- contractTermsHash: purchase.contractData.contractTermsHash,
+ contractTerms: purchase.download.contractTermsRaw,
+ contractTermsHash: purchase.download.contractData.contractTermsHash,
paid: true,
- amountRaw: Amounts.stringify(purchase.contractData.amount),
+ amountRaw: Amounts.stringify(purchase.download.contractData.amount),
amountEffective: Amounts.stringify(purchase.totalPayCost),
proposalId,
};
} else if (!purchase.timestampFirstSuccessfulPay) {
return {
status: PreparePayResultType.AlreadyConfirmed,
- contractTerms: JSON.parse(purchase.contractTermsRaw),
- contractTermsHash: purchase.contractData.contractTermsHash,
+ contractTerms: purchase.download.contractTermsRaw,
+ contractTermsHash: purchase.download.contractData.contractTermsHash,
paid: false,
- amountRaw: Amounts.stringify(purchase.contractData.amount),
+ amountRaw: Amounts.stringify(purchase.download.contractData.amount),
amountEffective: Amounts.stringify(purchase.totalPayCost),
proposalId,
};
@@ -1182,12 +1181,12 @@ export async function preparePayForUri(
const paid = !purchase.paymentSubmitPending;
return {
status: PreparePayResultType.AlreadyConfirmed,
- contractTerms: JSON.parse(purchase.contractTermsRaw),
- contractTermsHash: purchase.contractData.contractTermsHash,
+ contractTerms: purchase.download.contractTermsRaw,
+ contractTermsHash: purchase.download.contractData.contractTermsHash,
paid,
- amountRaw: Amounts.stringify(purchase.contractData.amount),
+ amountRaw: Amounts.stringify(purchase.download.contractData.amount),
amountEffective: Amounts.stringify(purchase.totalPayCost),
- ...(paid ? { nextUrl: purchase.contractData.orderId } : {}),
+ ...(paid ? { nextUrl: purchase.download.contractData.orderId } : {}),
proposalId,
};
}
diff --git a/packages/taler-wallet-core/src/types/backupTypes.ts b/packages/taler-wallet-core/src/types/backupTypes.ts
index d40d4fa6c..0b7f93c69 100644
--- a/packages/taler-wallet-core/src/types/backupTypes.ts
+++ b/packages/taler-wallet-core/src/types/backupTypes.ts
@@ -33,11 +33,15 @@
* aren't exported yet (and not even implemented in wallet-core).
* 6. Returning money to own bank account isn't supported/exported yet.
* 7. Peer-to-peer payments aren't supported yet.
+ * 8. Next update time / next refresh time isn't backed up yet.
*
* Questions:
* 1. What happens when two backups are merged that have
* the same coin in different refresh groups?
* => Both are added, one will eventually fail
+ * 2. Should we make more information forgettable? I.e. is
+ * the coin selection still relevant for a purchase after the coins
+ * are legally expired?
*
* General considerations / decisions:
* 1. Information about previously occurring errors and
@@ -74,6 +78,8 @@ type DeviceIdString = string;
*/
type ClockValue = number;
+type RawContractTerms = any;
+
/**
* Content of the backup.
*
@@ -544,10 +550,7 @@ export interface BackupRefreshSession {
/**
* Hased denominations of the newly requested coins.
*/
- new_denoms: {
- count: number;
- denom_pub_hash: string;
- }[];
+ new_denoms: BackupDenomSel;
/**
* Seed used to derive the planchets and
@@ -654,10 +657,7 @@ export interface BackupWithdrawalGroup {
/**
* Multiset of denominations selected for withdrawal.
*/
- selected_denoms: {
- denom_pub_hash: string;
- count: number;
- }[];
+ selected_denoms: BackupDenomSel;
}
export enum BackupRefundState {
@@ -747,7 +747,14 @@ export interface BackupPurchase {
/**
* Contract terms we got from the merchant.
*/
- contract_terms_raw: string;
+ contract_terms_raw: RawContractTerms;
+
+ /**
+ * Signature on the contract terms.
+ *
+ * Must be present if contract_terms_raw is present.
+ */
+ merchant_sig?: string;
/**
* Private key for the nonce. Might eventually be used
@@ -889,6 +896,14 @@ export interface BackupDenomination {
coins: BackupCoin[];
}
+/**
+ * Denomination selection.
+ */
+export type BackupDenomSel = {
+ denom_pub_hash: string;
+ count: number;
+}[];
+
export interface BackupReserve {
/**
* The reserve private key.
@@ -961,10 +976,7 @@ export interface BackupReserve {
* Denominations selected for the initial withdrawal.
* Stored here to show costs before withdrawal has begun.
*/
- initial_selected_denoms: {
- denom_pub_hash: string;
- count: number;
- }[];
+ initial_selected_denoms: BackupDenomSel;
/**
* Groups of withdrawal operations for this reserve. Typically just one.
@@ -1127,10 +1139,6 @@ export enum BackupProposalStatus {
*/
Proposed = "proposed",
/**
- * The user has accepted the proposal.
- */
- Accepted = "accepted",
- /**
* The user has rejected the proposal.
*/
Refused = "refused",
@@ -1151,9 +1159,21 @@ export enum BackupProposalStatus {
*/
export interface BackupProposal {
/**
+ * Base URL of the merchant that proposed the purchase.
+ */
+ merchant_base_url: string;
+
+ /**
* Downloaded data from the merchant.
*/
- contract_terms_raw?: string;
+ contract_terms_raw?: RawContractTerms;
+
+ /**
+ * Signature on the contract terms.
+ *
+ * Must be present if contract_terms_raw is present.
+ */
+ merchant_sig?: string;
/**
* Unique ID when the order is stored in the wallet DB.
@@ -1161,6 +1181,11 @@ export interface BackupProposal {
proposal_id: string;
/**
+ * Merchant-assigned order ID of the proposal.
+ */
+ order_id: string;
+
+ /**
* Timestamp of when the record
* was created.
*/
diff --git a/packages/taler-wallet-core/src/types/dbTypes.ts b/packages/taler-wallet-core/src/types/dbTypes.ts
index 7ba3b8604..5b05e2874 100644
--- a/packages/taler-wallet-core/src/types/dbTypes.ts
+++ b/packages/taler-wallet-core/src/types/dbTypes.ts
@@ -753,7 +753,7 @@ export interface ProposalDownload {
/**
* The contract that was offered by the merchant.
*/
- contractTermsRaw: string;
+ contractTermsRaw: any;
contractData: WalletContractData;
}
@@ -1200,14 +1200,9 @@ export interface PurchaseRecord {
noncePub: string;
/**
- * Contract terms we got from the merchant.
+ * Downloaded and parsed proposal data.
*/
- contractTermsRaw: string;
-
- /**
- * Parsed contract terms.
- */
- contractData: WalletContractData;
+ download: ProposalDownload;
/**
* Deposit permissions, available once the user has accepted the payment.
@@ -1291,6 +1286,9 @@ export interface ConfigRecord<T> {
value: T;
}
+/**
+ * FIXME: Eliminate this in favor of DenomSelectionState.
+ */
export interface DenominationSelectionInfo {
totalCoinValue: AmountJson;
totalWithdrawCost: AmountJson;
@@ -1303,6 +1301,9 @@ export interface DenominationSelectionInfo {
}[];
}
+/**
+ * Selected denominations withn some extra info.
+ */
export interface DenomSelectionState {
totalCoinValue: AmountJson;
totalWithdrawCost: AmountJson;
diff --git a/packages/taler-wallet-core/src/types/pendingTypes.ts b/packages/taler-wallet-core/src/types/pendingTypes.ts
new file mode 100644
index 000000000..18d9a2fa4
--- /dev/null
+++ b/packages/taler-wallet-core/src/types/pendingTypes.ts
@@ -0,0 +1,276 @@
+/*
+ This file is part of GNU Taler
+ (C) 2019 GNUnet e.V.
+
+ 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 <http://www.gnu.org/licenses/>
+ */
+
+/**
+ * Type and schema definitions for pending operations in the wallet.
+ */
+
+/**
+ * Imports.
+ */
+import { TalerErrorDetails, BalancesResponse } from "./walletTypes";
+import { ReserveRecordStatus } from "./dbTypes";
+import { Timestamp, Duration } from "../util/time";
+import { RetryInfo } from "../util/retries";
+
+export enum PendingOperationType {
+ Bug = "bug",
+ ExchangeUpdate = "exchange-update",
+ ExchangeCheckRefresh = "exchange-check-refresh",
+ Pay = "pay",
+ ProposalChoice = "proposal-choice",
+ ProposalDownload = "proposal-download",
+ Refresh = "refresh",
+ Reserve = "reserve",
+ Recoup = "recoup",
+ RefundQuery = "refund-query",
+ TipChoice = "tip-choice",
+ TipPickup = "tip-pickup",
+ Withdraw = "withdraw",
+}
+
+/**
+ * Information about a pending operation.
+ */
+export type PendingOperationInfo = PendingOperationInfoCommon &
+ (
+ | PendingBugOperation
+ | PendingExchangeUpdateOperation
+ | PendingExchangeCheckRefreshOperation
+ | PendingPayOperation
+ | PendingProposalChoiceOperation
+ | PendingProposalDownloadOperation
+ | PendingRefreshOperation
+ | PendingRefundQueryOperation
+ | PendingReserveOperation
+ | PendingTipChoiceOperation
+ | PendingTipPickupOperation
+ | PendingWithdrawOperation
+ | PendingRecoupOperation
+ );
+
+/**
+ * The wallet is currently updating information about an exchange.
+ */
+export interface PendingExchangeUpdateOperation {
+ type: PendingOperationType.ExchangeUpdate;
+ stage: ExchangeUpdateOperationStage;
+ reason: string;
+ exchangeBaseUrl: string;
+ lastError: TalerErrorDetails | undefined;
+}
+
+/**
+ * The wallet should check whether coins from this exchange
+ * need to be auto-refreshed.
+ */
+export interface PendingExchangeCheckRefreshOperation {
+ type: PendingOperationType.ExchangeCheckRefresh;
+ exchangeBaseUrl: string;
+}
+
+/**
+ * Some interal error happened in the wallet. This pending operation
+ * should *only* be reported for problems in the wallet, not when
+ * a problem with a merchant/exchange/etc. occurs.
+ */
+export interface PendingBugOperation {
+ type: PendingOperationType.Bug;
+ message: string;
+ details: any;
+}
+
+/**
+ * Current state of an exchange update operation.
+ */
+export enum ExchangeUpdateOperationStage {
+ FetchKeys = "fetch-keys",
+ FetchWire = "fetch-wire",
+ FinalizeUpdate = "finalize-update",
+}
+
+export enum ReserveType {
+ /**
+ * Manually created.
+ */
+ Manual = "manual",
+ /**
+ * Withdrawn from a bank that has "tight" Taler integration
+ */
+ TalerBankWithdraw = "taler-bank-withdraw",
+}
+
+/**
+ * Status of processing a reserve.
+ *
+ * Does *not* include the withdrawal operation that might result
+ * from this.
+ */
+export interface PendingReserveOperation {
+ type: PendingOperationType.Reserve;
+ retryInfo: RetryInfo | undefined;
+ stage: ReserveRecordStatus;
+ timestampCreated: Timestamp;
+ reserveType: ReserveType;
+ reservePub: string;
+ bankWithdrawConfirmUrl?: string;
+}
+
+/**
+ * Status of an ongoing withdrawal operation.
+ */
+export interface PendingRefreshOperation {
+ type: PendingOperationType.Refresh;
+ lastError?: TalerErrorDetails;
+ refreshGroupId: string;
+ finishedPerCoin: boolean[];
+ retryInfo: RetryInfo;
+}
+
+/**
+ * Status of downloading signed contract terms from a merchant.
+ */
+export interface PendingProposalDownloadOperation {
+ type: PendingOperationType.ProposalDownload;
+ merchantBaseUrl: string;
+ proposalTimestamp: Timestamp;
+ proposalId: string;
+ orderId: string;
+ lastError?: TalerErrorDetails;
+ retryInfo: RetryInfo;
+}
+
+/**
+ * User must choose whether to accept or reject the merchant's
+ * proposed contract terms.
+ */
+export interface PendingProposalChoiceOperation {
+ type: PendingOperationType.ProposalChoice;
+ merchantBaseUrl: string;
+ proposalTimestamp: Timestamp;
+ proposalId: string;
+}
+
+/**
+ * The wallet is picking up a tip that the user has accepted.
+ */
+export interface PendingTipPickupOperation {
+ type: PendingOperationType.TipPickup;
+ tipId: string;
+ merchantBaseUrl: string;
+ merchantTipId: string;
+}
+
+/**
+ * The wallet has been offered a tip, and the user now needs to
+ * decide whether to accept or reject the tip.
+ */
+export interface PendingTipChoiceOperation {
+ type: PendingOperationType.TipChoice;
+ tipId: string;
+ merchantBaseUrl: string;
+ merchantTipId: string;
+}
+
+/**
+ * The wallet is signing coins and then sending them to
+ * the merchant.
+ */
+export interface PendingPayOperation {
+ type: PendingOperationType.Pay;
+ proposalId: string;
+ isReplay: boolean;
+ retryInfo: RetryInfo;
+ lastError: TalerErrorDetails | undefined;
+}
+
+/**
+ * The wallet is querying the merchant about whether any refund
+ * permissions are available for a purchase.
+ */
+export interface PendingRefundQueryOperation {
+ type: PendingOperationType.RefundQuery;
+ proposalId: string;
+ retryInfo: RetryInfo;
+ lastError: TalerErrorDetails | undefined;
+}
+
+export interface PendingRecoupOperation {
+ type: PendingOperationType.Recoup;
+ recoupGroupId: string;
+ retryInfo: RetryInfo;
+ lastError: TalerErrorDetails | undefined;
+}
+
+/**
+ * Status of an ongoing withdrawal operation.
+ */
+export interface PendingWithdrawOperation {
+ type: PendingOperationType.Withdraw;
+ lastError: TalerErrorDetails | undefined;
+ retryInfo: RetryInfo;
+ withdrawalGroupId: string;
+ numCoinsWithdrawn: number;
+ numCoinsTotal: number;
+}
+
+/**
+ * Fields that are present in every pending operation.
+ */
+export interface PendingOperationInfoCommon {
+ /**
+ * Type of the pending operation.
+ */
+ type: PendingOperationType;
+
+ /**
+ * Set to true if the operation indicates that something is really in progress,
+ * as opposed to some regular scheduled operation or a permanent failure.
+ */
+ givesLifeness: boolean;
+
+ /**
+ * Retry info, not available on all pending operations.
+ * If it is available, it must have the same name.
+ */
+ retryInfo?: RetryInfo;
+}
+
+/**
+ * Response returned from the pending operations API.
+ */
+export interface PendingOperationsResponse {
+ /**
+ * List of pending operations.
+ */
+ pendingOperations: PendingOperationInfo[];
+
+ /**
+ * Current wallet balance, including pending balances.
+ */
+ walletBalance: BalancesResponse;
+
+ /**
+ * When is the next pending operation due to be re-tried?
+ */
+ nextRetryDelay: Duration;
+
+ /**
+ * Does this response only include pending operations that
+ * are due to be executed right now?
+ */
+ onlyDue: boolean;
+}
diff --git a/packages/taler-wallet-core/src/types/transactionsTypes.ts b/packages/taler-wallet-core/src/types/transactionsTypes.ts
new file mode 100644
index 000000000..0a683f298
--- /dev/null
+++ b/packages/taler-wallet-core/src/types/transactionsTypes.ts
@@ -0,0 +1,337 @@
+/*
+ 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 <http://www.gnu.org/licenses/>
+ */
+
+/**
+ * Type and schema definitions for the wallet's transaction list.
+ *
+ * @author Florian Dold
+ * @author Torsten Grote
+ */
+
+/**
+ * Imports.
+ */
+import { Timestamp } from "../util/time";
+import {
+ AmountString,
+ Product,
+ InternationalizedString,
+ MerchantInfo,
+ codecForInternationalizedString,
+ codecForMerchantInfo,
+ codecForProduct,
+} from "./talerTypes";
+import {
+ Codec,
+ buildCodecForObject,
+ codecOptional,
+ codecForString,
+ codecForList,
+ codecForAny,
+} from "../util/codec";
+import { TalerErrorDetails } from "./walletTypes";
+
+export interface TransactionsRequest {
+ /**
+ * return only transactions in the given currency
+ */
+ currency?: string;
+
+ /**
+ * if present, results will be limited to transactions related to the given search string
+ */
+ search?: string;
+}
+
+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: string;
+
+ // the type of the transaction; different types might provide additional information
+ type: TransactionType;
+
+ // main timestamp of the transaction
+ timestamp: Timestamp;
+
+ // true if the transaction is still pending, false otherwise
+ // If a transaction is not longer pending, its timestamp will be updated,
+ // but its transactionId will remain unchanged
+ pending: boolean;
+
+ // 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?: TalerErrorDetails;
+}
+
+export type Transaction =
+ | TransactionWithdrawal
+ | TransactionPayment
+ | TransactionRefund
+ | TransactionTip
+ | TransactionRefresh;
+
+export enum TransactionType {
+ Withdrawal = "withdrawal",
+ Payment = "payment",
+ Refund = "refund",
+ Refresh = "refresh",
+ Tip = "tip",
+}
+
+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.
+ */
+ exchangePaytoUris: string[];
+}
+
+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;
+}
+
+// This should only be used for actual withdrawals
+// and not for tips that have their own transactions type.
+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;
+}
+
+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;
+
+ /**
+ * How far did the wallet get with processing the payment?
+ */
+ status: PaymentStatus;
+
+ /**
+ * Amount that must be paid for the contract
+ */
+ amountRaw: AmountString;
+
+ /**
+ * Amount that was paid, including deposit, wire and refresh fees.
+ */
+ amountEffective: AmountString;
+}
+
+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;
+
+ /**
+ * List of products that are part of the order
+ */
+ products: Product[] | undefined;
+
+ /**
+ * 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;
+}
+
+interface TransactionRefund extends TransactionCommon {
+ type: TransactionType.Refund;
+
+ // ID for the transaction that is refunded
+ refundedTransactionId: string;
+
+ // Additional information about the refunded payment
+ info: OrderShortInfo;
+
+ // 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;
+}
+
+interface TransactionTip extends TransactionCommon {
+ type: TransactionType.Tip;
+
+ // Raw amount of the tip, without extra fees that apply
+ amountRaw: AmountString;
+
+ // Amount will be (or was) added to the wallet's balance after fees and refreshing
+ amountEffective: AmountString;
+
+ merchantBaseUrl: string;
+}
+
+// A transaction shown for refreshes that are not associated to other transactions
+// such as a refresh necessary before coin expiration.
+// It should only be returned by the API if the effective amount is different from zero.
+interface TransactionRefresh extends TransactionCommon {
+ type: TransactionType.Refresh;
+
+ // Exchange that the coins are refreshed with
+ exchangeBaseUrl: string;
+
+ // Raw amount that is refreshed
+ amountRaw: AmountString;
+
+ // Amount that will be paid as fees for the refresh
+ amountEffective: AmountString;
+}
+
+export const codecForTransactionsRequest = (): Codec<TransactionsRequest> =>
+ buildCodecForObject<TransactionsRequest>()
+ .property("currency", codecOptional(codecForString()))
+ .property("search", codecOptional(codecForString()))
+ .build("TransactionsRequest");
+
+// FIXME: do full validation here!
+export const codecForTransactionsResponse = (): Codec<TransactionsResponse> =>
+ buildCodecForObject<TransactionsResponse>()
+ .property("transactions", codecForList(codecForAny()))
+ .build("TransactionsResponse");
+
+export const codecForOrderShortInfo = (): Codec<OrderShortInfo> =>
+ buildCodecForObject<OrderShortInfo>()
+ .property("contractTermsHash", codecForString())
+ .property("fulfillmentMessage", codecOptional(codecForString()))
+ .property(
+ "fulfillmentMessage_i18n",
+ codecOptional(codecForInternationalizedString()),
+ )
+ .property("fulfillmentUrl", codecOptional(codecForString()))
+ .property("merchant", codecForMerchantInfo())
+ .property("orderId", codecForString())
+ .property("products", codecOptional(codecForList(codecForProduct())))
+ .property("summary", codecForString())
+ .property("summary_i18n", codecOptional(codecForInternationalizedString()))
+ .build("OrderShortInfo");