commit eec6695be0409669fcad36c6cc7ea01f48d41c97
parent a57fcb144d8de40fe50b825d34a27e415ef3fec3
Author: Florian Dold <florian@dold.me>
Date: Fri, 14 Oct 2022 22:38:40 +0200
wallet-core: DB tweaks, consistent file naming convention
Diffstat:
27 files changed, 7915 insertions(+), 7917 deletions(-)
diff --git a/packages/taler-util/src/ReserveStatus.ts b/packages/taler-util/src/ReserveStatus.ts
@@ -27,7 +27,7 @@ import {
codecForList,
Codec,
} from "./codec.js";
-import { AmountString } from "./talerTypes.js";
+import { AmountString } from "./taler-types.js";
import {
ReserveTransaction,
codecForReserveTransaction,
diff --git a/packages/taler-util/src/ReserveTransaction.ts b/packages/taler-util/src/ReserveTransaction.ts
@@ -37,7 +37,7 @@ import {
EddsaSignatureString,
EddsaPublicKeyString,
CoinPublicKeyString,
-} from "./talerTypes.js";
+} from "./taler-types.js";
import {
AbsoluteTime,
codecForTimestamp,
diff --git a/packages/taler-util/src/amounts.ts b/packages/taler-util/src/amounts.ts
@@ -27,7 +27,7 @@ import {
codecForNumber,
Codec,
} from "./codec.js";
-import { AmountString } from "./talerTypes.js";
+import { AmountString } from "./taler-types.js";
/**
* Number of fractional units that one value unit represents.
diff --git a/packages/taler-util/src/backup-types.ts b/packages/taler-util/src/backup-types.ts
@@ -0,0 +1,1279 @@
+/*
+ This file is part of GNU Taler
+ (C) 2020 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 declarations for the backup content format.
+ *
+ * Contains some redundancy with the other type declarations,
+ * as the backup schema must remain very stable and should be self-contained.
+ *
+ * Future:
+ * 1. Ghost spends (coin unexpectedly spent by a wallet with shared data)
+ * 2. Ghost withdrawals (reserve unexpectedly emptied by another wallet with shared data)
+ * 3. Track losses through re-denomination of payments/refreshes
+ * 4. (Feature:) Payments to own bank account and P2P-payments need to be backed up
+ * 5. Track last/next update time, so on restore we need to do less work
+ * 6. Currency render preferences?
+ *
+ * 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?
+ * => Yes, still needs to be implemented
+ * 3. What about re-denominations / re-selection of payment coins?
+ * Is it enough to store a clock value for the selection?
+ * => Coin derivation should also consider denom pub hash
+ *
+ * General considerations / decisions:
+ * 1. Information about previously occurring errors and
+ * retries is never backed up.
+ * 2. The ToS text of an exchange is never backed up.
+ * 3. Derived information is never backed up (hashed values, public keys
+ * when we know the private key).
+ *
+ * Problems:
+ *
+ * Withdrawal group fork/merging loses money:
+ * - Before the withdrawal happens, wallet forks into two backups.
+ * - Both wallets need to re-denominate the withdrawal (unlikely but possible).
+ * - Because the backup doesn't store planchets where a withdrawal was attempted,
+ * after merging some money will be list.
+ * - Fix: backup withdrawal objects also store planchets where withdrawal has been attempted
+ *
+ * @author Florian Dold <dold@taler.net>
+ */
+
+/**
+ * Imports.
+ */
+import { DenominationPubKey, UnblindedSignature } from "./taler-types.js";
+import { TalerProtocolDuration, TalerProtocolTimestamp } from "./time.js";
+
+export const BACKUP_TAG = "gnu-taler-wallet-backup-content" as const;
+/**
+ * Major version. Each increment means a backwards-incompatible change.
+ * Typically this means that a custom converter needs to be written.
+ */
+export const BACKUP_VERSION_MAJOR = 1 as const;
+
+/**
+ * Minor version. Each increment means that information is added to the backup
+ * in a backwards-compatible way.
+ *
+ * Wallets can always import a smaller minor version than their own backup code version.
+ * When importing a bigger version, data loss is possible and the user should be urged to
+ * upgrade their wallet first.
+ */
+export const BACKUP_VERSION_MINOR = 1 as const;
+
+/**
+ * Type alias for strings that are to be treated like amounts.
+ */
+type BackupAmountString = string;
+
+/**
+ * A human-recognizable identifier here that is
+ * reasonable unique and assigned the first time the wallet is
+ * started/installed, such as:
+ *
+ * `${wallet-implementation} ${os} ${hostname} (${short-uid})`
+ * => e.g. "GNU Taler Android iceking ABC123"
+ */
+type DeviceIdString = string;
+
+/**
+ * Contract terms JSON.
+ */
+type RawContractTerms = any;
+
+/**
+ * Unique identifier for an operation, used to either (a) reference
+ * the operation in a tombstone (b) disambiguate conflicting writes.
+ */
+type OperationUid = string;
+
+/**
+ * Content of the backup.
+ *
+ * The contents of the wallet must be serialized in a deterministic
+ * way across implementations, so that the normalized backup content
+ * JSON is identical when the wallet's content is identical.
+ */
+export interface WalletBackupContentV1 {
+ /**
+ * Magic constant to identify that this is a backup content JSON.
+ */
+ schema_id: typeof BACKUP_TAG;
+
+ /**
+ * Version of the schema.
+ */
+ schema_version: typeof BACKUP_VERSION_MAJOR;
+
+ minor_version: number;
+
+ /**
+ * Root public key of the wallet. This field is present as
+ * a sanity check if the backup content JSON is loaded from file.
+ */
+ wallet_root_pub: string;
+
+ /**
+ * Current device identifier that "owns" the backup.
+ *
+ * This identifier allows one wallet to notice when another
+ * wallet is "alive" and connected to the same sync provider.
+ */
+ current_device_id: DeviceIdString;
+
+ /**
+ * Timestamp of the backup.
+ *
+ * This timestamp should only be advanced if the content
+ * of the backup changes.
+ */
+ timestamp: TalerProtocolTimestamp;
+
+ /**
+ * Per-exchange data sorted by exchange master public key.
+ *
+ * Sorted by the exchange public key.
+ */
+ exchanges: BackupExchange[];
+
+ exchange_details: BackupExchangeDetails[];
+
+ /**
+ * Withdrawal groups.
+ *
+ * Sorted by the withdrawal group ID.
+ */
+ withdrawal_groups: BackupWithdrawalGroup[];
+
+ /**
+ * Grouped refresh sessions.
+ *
+ * Sorted by the refresh group ID.
+ */
+ refresh_groups: BackupRefreshGroup[];
+
+ /**
+ * Tips.
+ *
+ * Sorted by the wallet tip ID.
+ */
+ tips: BackupTip[];
+
+ /**
+ * Accepted purchases.
+ *
+ * Sorted by the proposal ID.
+ */
+ purchases: BackupPurchase[];
+
+ /**
+ * All backup providers. Backup providers
+ * in this list should be considered "active".
+ *
+ * Sorted by the provider base URL.
+ */
+ backup_providers: BackupBackupProvider[];
+
+ /**
+ * Recoup groups.
+ */
+ recoup_groups: BackupRecoupGroup[];
+
+ /**
+ * Trusted auditors, either for official (3 letter) or local (4-12 letter)
+ * currencies.
+ *
+ * Auditors are sorted by their canonicalized base URL.
+ */
+ trusted_auditors: { [currency: string]: BackupTrustAuditor[] };
+
+ /**
+ * Trusted exchange. Only applicable for local currencies (4-12 letter currency code).
+ *
+ * Exchanges are sorted by their canonicalized base URL.
+ */
+ trusted_exchanges: { [currency: string]: BackupTrustExchange[] };
+
+ /**
+ * Interning table for forgettable values of contract terms.
+ *
+ * Used to reduce storage space, as many forgettable items (product image,
+ * addresses, etc.) might be shared among many contract terms.
+ */
+ intern_table: { [hash: string]: any };
+
+ /**
+ * Permanent error reports.
+ */
+ error_reports: BackupErrorReport[];
+
+ /**
+ * Deletion tombstones. Lexically sorted.
+ */
+ tombstones: Tombstone[];
+}
+
+export enum BackupOperationStatus {
+ Cancelled = "cancelled",
+ Finished = "finished",
+ Pending = "pending",
+}
+
+export enum BackupWgType {
+ BankManual = "bank-manual",
+ BankIntegrated = "bank-integrated",
+ PeerPullCredit = "peer-pull-credit",
+ PeerPushCredit = "peer-push-credit",
+ Recoup = "recoup",
+}
+
+export type BackupWgInfo =
+ | {
+ type: BackupWgType.BankManual;
+ }
+ | {
+ type: BackupWgType.BankIntegrated;
+ taler_withdraw_uri: string;
+
+ /**
+ * URL that the user can be redirected to, and allows
+ * them to confirm (or abort) the bank-integrated withdrawal.
+ */
+ confirm_url?: string;
+
+ /**
+ * Exchange payto URI that the bank will use to fund the reserve.
+ */
+ exchange_payto_uri: string;
+
+ /**
+ * Time when the information about this reserve was posted to the bank.
+ *
+ * Only applies if bankWithdrawStatusUrl is defined.
+ *
+ * Set to undefined if that hasn't happened yet.
+ */
+ timestamp_reserve_info_posted?: TalerProtocolTimestamp;
+
+ /**
+ * Time when the reserve was confirmed by the bank.
+ *
+ * Set to undefined if not confirmed yet.
+ */
+ timestamp_bank_confirmed?: TalerProtocolTimestamp;
+ }
+ | {
+ type: BackupWgType.PeerPullCredit;
+ contract_terms: any;
+ contract_priv: string;
+ }
+ | {
+ type: BackupWgType.PeerPushCredit;
+ contract_terms: any;
+ }
+ | {
+ type: BackupWgType.Recoup;
+ };
+
+/**
+ * FIXME: Open questions:
+ * - Do we have to store the denomination selection? Why?
+ * (If deterministic, amount shouldn't change. Not storing it is simpler.)
+ */
+export interface BackupWithdrawalGroup {
+ withdrawal_group_id: string;
+
+ /**
+ * Detailed info based on the type of withdrawal group.
+ */
+ info: BackupWgInfo;
+
+ secret_seed: string;
+
+ reserve_priv: string;
+
+ exchange_base_url: string;
+
+ timestamp_created: TalerProtocolTimestamp;
+
+ timestamp_finish?: TalerProtocolTimestamp;
+
+ operation_status: BackupOperationStatus;
+
+ instructed_amount: BackupAmountString;
+
+ /**
+ * Amount including fees (i.e. the amount subtracted from the
+ * reserve to withdraw all coins in this withdrawal session).
+ *
+ * Note that this *includes* the amount remaining in the reserve
+ * that is too small to be withdrawn, and thus can't be derived
+ * from selectedDenoms.
+ */
+ raw_withdrawal_amount: BackupAmountString;
+
+ /**
+ * Restrict withdrawals from this reserve to this age.
+ */
+ restrict_age?: number;
+
+ /**
+ * Multiset of denominations selected for withdrawal.
+ */
+ selected_denoms: BackupDenomSel;
+
+ selected_denoms_uid: OperationUid;
+}
+
+/**
+ * Tombstone in the format "<type>:<key>"
+ */
+export type Tombstone = string;
+
+/**
+ * Detailed error report.
+ *
+ * For auditor-relevant reports with attached cryptographic proof,
+ * the error report also should contain the submission status to
+ * the auditor(s).
+ */
+interface BackupErrorReport {
+ // FIXME: specify!
+}
+
+/**
+ * Trust declaration for an auditor.
+ *
+ * The trust applies based on the public key of
+ * the auditor, irrespective of what base URL the exchange
+ * is referencing.
+ */
+export interface BackupTrustAuditor {
+ /**
+ * Base URL of the auditor.
+ */
+ auditor_base_url: string;
+
+ /**
+ * Public key of the auditor.
+ */
+ auditor_pub: string;
+
+ /**
+ * UIDs for the operation of adding this auditor
+ * as a trusted auditor.
+ */
+ uids: OperationUid;
+}
+
+/**
+ * Trust declaration for an exchange.
+ *
+ * The trust only applies for the combination of base URL
+ * and public key. If the master public key changes while the base
+ * URL stays the same, the exchange has to be re-added by a wallet update
+ * or by the user.
+ */
+export interface BackupTrustExchange {
+ /**
+ * Canonicalized exchange base URL.
+ */
+ exchange_base_url: string;
+
+ /**
+ * Master public key of the exchange.
+ */
+ exchange_master_pub: string;
+
+ /**
+ * UIDs for the operation of adding this exchange
+ * as trusted.
+ */
+ uids: OperationUid;
+}
+
+export class BackupBackupProviderTerms {
+ /**
+ * Last known supported protocol version.
+ */
+ supported_protocol_version: string;
+
+ /**
+ * Last known annual fee.
+ */
+ annual_fee: BackupAmountString;
+
+ /**
+ * Last known storage limit.
+ */
+ storage_limit_in_megabytes: number;
+}
+
+/**
+ * Backup information about one backup storage provider.
+ */
+export class BackupBackupProvider {
+ /**
+ * Canonicalized base URL of the provider.
+ */
+ base_url: string;
+
+ /**
+ * Last known terms. Might be unavailable in some situations, such
+ * as directly after restoring form a backup recovery document.
+ */
+ terms?: BackupBackupProviderTerms;
+
+ /**
+ * Proposal IDs for payments to this provider.
+ */
+ pay_proposal_ids: string[];
+
+ /**
+ * UIDs for adding this backup provider.
+ */
+ uids: OperationUid[];
+}
+
+/**
+ * Status of recoup operations that were grouped together.
+ *
+ * The remaining amount of the corresponding coins must be set to
+ * zero when the recoup group is created/imported.
+ */
+export interface BackupRecoupGroup {
+ /**
+ * Unique identifier for the recoup group record.
+ */
+ recoup_group_id: string;
+
+ /**
+ * Timestamp when the recoup was started.
+ */
+ timestamp_created: TalerProtocolTimestamp;
+
+ timestamp_finish?: TalerProtocolTimestamp;
+ finish_clock?: TalerProtocolTimestamp;
+ finish_is_failure?: boolean;
+
+ /**
+ * Information about each coin being recouped.
+ */
+ coins: {
+ coin_pub: string;
+ recoup_finished: boolean;
+ old_amount: BackupAmountString;
+ }[];
+}
+
+/**
+ * Types of coin sources.
+ */
+export enum BackupCoinSourceType {
+ Withdraw = "withdraw",
+ Refresh = "refresh",
+ Tip = "tip",
+}
+
+/**
+ * Metadata about a coin obtained via withdrawing.
+ */
+export interface BackupWithdrawCoinSource {
+ type: BackupCoinSourceType.Withdraw;
+
+ /**
+ * Can be the empty string for orphaned coins.
+ */
+ withdrawal_group_id: string;
+
+ /**
+ * Index of the coin in the withdrawal session.
+ */
+ coin_index: number;
+
+ /**
+ * Reserve public key for the reserve we got this coin from.
+ */
+ reserve_pub: string;
+}
+
+/**
+ * Metadata about a coin obtained from refreshing.
+ *
+ * FIXME: Currently does not link to the refreshGroupId because
+ * the wallet DB doesn't do this. Not really necessary,
+ * but would be more consistent.
+ */
+export interface BackupRefreshCoinSource {
+ type: BackupCoinSourceType.Refresh;
+
+ /**
+ * Public key of the coin that was refreshed into this coin.
+ */
+ old_coin_pub: string;
+}
+
+/**
+ * Metadata about a coin obtained from a tip.
+ */
+export interface BackupTipCoinSource {
+ type: BackupCoinSourceType.Tip;
+
+ /**
+ * Wallet's identifier for the tip that this coin
+ * originates from.
+ */
+ wallet_tip_id: string;
+
+ /**
+ * Index in the tip planchets of the tip.
+ */
+ coin_index: number;
+}
+
+/**
+ * Metadata about a coin depending on the origin.
+ */
+export type BackupCoinSource =
+ | BackupWithdrawCoinSource
+ | BackupRefreshCoinSource
+ | BackupTipCoinSource;
+
+/**
+ * Backup information about a coin.
+ *
+ * (Always part of a BackupExchange/BackupDenom)
+ */
+export interface BackupCoin {
+ /**
+ * Where did the coin come from? Used for recouping coins.
+ */
+ coin_source: BackupCoinSource;
+
+ /**
+ * Private key to authorize operations on the coin.
+ */
+ coin_priv: string;
+
+ /**
+ * Unblinded signature by the exchange.
+ */
+ denom_sig: UnblindedSignature;
+
+ /**
+ * Amount that's left on the coin.
+ */
+ current_amount: BackupAmountString;
+
+ /**
+ * Blinding key used when withdrawing the coin.
+ * Potentionally used again during payback.
+ */
+ blinding_key: string;
+
+ /**
+ * Does the wallet think that the coin is still fresh?
+ *
+ * Note that even if a fresh coin is imported, it should still
+ * be refreshed in most situations.
+ */
+ fresh: boolean;
+}
+
+/**
+ * Status of a tip we got from a merchant.
+ */
+export interface BackupTip {
+ /**
+ * Tip ID chosen by the wallet.
+ */
+ wallet_tip_id: string;
+
+ /**
+ * The merchant's identifier for this tip.
+ */
+ merchant_tip_id: string;
+
+ /**
+ * Secret seed used for the tipping planchets.
+ */
+ secret_seed: string;
+
+ /**
+ * Has the user accepted the tip? Only after the tip has been accepted coins
+ * withdrawn from the tip may be used.
+ */
+ timestamp_accepted: TalerProtocolTimestamp | undefined;
+
+ /**
+ * When was the tip first scanned by the wallet?
+ */
+ timestamp_created: TalerProtocolTimestamp;
+
+ timestamp_finished?: TalerProtocolTimestamp;
+ finish_is_failure?: boolean;
+
+ /**
+ * The tipped amount.
+ */
+ tip_amount_raw: BackupAmountString;
+
+ /**
+ * Timestamp, the tip can't be picked up anymore after this deadline.
+ */
+ timestamp_expiration: TalerProtocolTimestamp;
+
+ /**
+ * The exchange that will sign our coins, chosen by the merchant.
+ */
+ exchange_base_url: string;
+
+ /**
+ * Base URL of the merchant that is giving us the tip.
+ */
+ merchant_base_url: string;
+
+ /**
+ * Selected denominations. Determines the effective tip amount.
+ */
+ selected_denoms: BackupDenomSel;
+
+ /**
+ * UID for the denomination selection.
+ * Used to disambiguate when merging.
+ */
+ selected_denoms_uid: OperationUid;
+}
+
+/**
+ * Reasons for why a coin is being refreshed.
+ */
+export enum BackupRefreshReason {
+ Manual = "manual",
+ Pay = "pay",
+ Refund = "refund",
+ AbortPay = "abort-pay",
+ Recoup = "recoup",
+ BackupRestored = "backup-restored",
+ Scheduled = "scheduled",
+}
+
+/**
+ * Information about one refresh session, always part
+ * of a refresh group.
+ *
+ * (Public key of the old coin is stored in the refresh group.)
+ */
+export interface BackupRefreshSession {
+ /**
+ * Hashed denominations of the newly requested coins.
+ */
+ new_denoms: BackupDenomSel;
+
+ /**
+ * Seed used to derive the planchets and
+ * transfer private keys for this refresh session.
+ */
+ session_secret_seed: string;
+
+ /**
+ * The no-reveal-index after we've done the melting.
+ */
+ noreveal_index?: number;
+}
+
+/**
+ * Refresh session for one coin inside a refresh group.
+ */
+export interface BackupRefreshOldCoin {
+ /**
+ * Public key of the old coin,
+ */
+ coin_pub: string;
+
+ /**
+ * Requested amount to refresh. Must be subtracted from the coin's remaining
+ * amount as soon as the coin is added to the refresh group.
+ */
+ input_amount: BackupAmountString;
+
+ /**
+ * Estimated output (may change if it takes a long time to create the
+ * actual session).
+ */
+ estimated_output_amount: BackupAmountString;
+
+ /**
+ * Did the refresh session finish (or was it unnecessary/impossible to create
+ * one)
+ */
+ finished: boolean;
+
+ /**
+ * Refresh session (if created) or undefined it not created yet.
+ */
+ refresh_session: BackupRefreshSession | undefined;
+}
+
+/**
+ * Information about one refresh group.
+ *
+ * May span more than one exchange, but typically doesn't
+ */
+export interface BackupRefreshGroup {
+ refresh_group_id: string;
+
+ reason: BackupRefreshReason;
+
+ /**
+ * Details per old coin.
+ */
+ old_coins: BackupRefreshOldCoin[];
+
+ timestamp_created: TalerProtocolTimestamp;
+
+ timestamp_finish?: TalerProtocolTimestamp;
+ finish_is_failure?: boolean;
+}
+
+export enum BackupRefundState {
+ Failed = "failed",
+ Applied = "applied",
+ Pending = "pending",
+}
+
+/**
+ * Common information about a refund.
+ */
+export interface BackupRefundItemCommon {
+ /**
+ * Execution time as claimed by the merchant
+ */
+ execution_time: TalerProtocolTimestamp;
+
+ /**
+ * Time when the wallet became aware of the refund.
+ */
+ obtained_time: TalerProtocolTimestamp;
+
+ /**
+ * Amount refunded for the coin.
+ */
+ refund_amount: BackupAmountString;
+
+ /**
+ * Coin being refunded.
+ */
+ coin_pub: string;
+
+ /**
+ * The refund transaction ID for the refund.
+ */
+ rtransaction_id: number;
+
+ /**
+ * Upper bound on the refresh cost incurred by
+ * applying this refund.
+ *
+ * Might be lower in practice when two refunds on the same
+ * coin are refreshed in the same refresh operation.
+ *
+ * Used to display fees, and stored since it's expensive to recompute
+ * accurately.
+ */
+ total_refresh_cost_bound: BackupAmountString;
+}
+
+/**
+ * Failed refund, either because the merchant did
+ * something wrong or it expired.
+ */
+export interface BackupRefundFailedItem extends BackupRefundItemCommon {
+ type: BackupRefundState.Failed;
+}
+
+export interface BackupRefundPendingItem extends BackupRefundItemCommon {
+ type: BackupRefundState.Pending;
+}
+
+export interface BackupRefundAppliedItem extends BackupRefundItemCommon {
+ type: BackupRefundState.Applied;
+}
+
+/**
+ * State of one refund from the merchant, maintained by the wallet.
+ */
+export type BackupRefundItem =
+ | BackupRefundFailedItem
+ | BackupRefundPendingItem
+ | BackupRefundAppliedItem;
+
+/**
+ * Data we store when the payment was accepted.
+ */
+export interface BackupPayInfo {
+ pay_coins: {
+ /**
+ * Public keys of the coins that were selected.
+ */
+ coin_pub: string;
+
+ /**
+ * Amount that each coin contributes.
+ */
+ contribution: BackupAmountString;
+ }[];
+
+ /**
+ * Unique ID to disambiguate pay coin selection on merge.
+ */
+ pay_coins_uid: OperationUid;
+
+ /**
+ * Total cost initially shown to the user.
+ *
+ * This includes the amount taken by the merchant, fees (wire/deposit) contributed
+ * by the customer, refreshing fees, fees for withdraw-after-refresh and "trimmings"
+ * of coins that are too small to spend.
+ *
+ * Note that in rare situations, this cost might not be accurate (e.g.
+ * when the payment or refresh gets re-denominated).
+ * We might show adjustments to this later, but currently we don't do so.
+ */
+ total_pay_cost: BackupAmountString;
+}
+
+export interface BackupPurchase {
+ /**
+ * Proposal ID for this purchase. Uniquely identifies the
+ * purchase and the proposal.
+ */
+ proposal_id: string;
+
+ /**
+ * Status of the proposal.
+ */
+ proposal_status: BackupProposalStatus;
+
+ /**
+ * Proposal that this one got "redirected" to as part of
+ * the repurchase detection.
+ */
+ repurchase_proposal_id: string | undefined;
+
+ /**
+ * Session ID we got when downloading the contract.
+ */
+ download_session_id?: string;
+
+ /**
+ * Merchant-assigned order ID of the proposal.
+ */
+ order_id: string;
+
+ /**
+ * Base URL of the merchant that proposed the purchase.
+ */
+ merchant_base_url: string;
+
+ /**
+ * Claim token initially given by the merchant.
+ */
+ claim_token: string | undefined;
+
+ /**
+ * Contract terms we got from the merchant.
+ */
+ contract_terms_raw?: RawContractTerms;
+
+ /**
+ * Signature on the contract terms.
+ *
+ * FIXME: Better name needed.
+ */
+ merchant_sig?: string;
+
+ /**
+ * Private key for the nonce. Might eventually be used
+ * to prove ownership of the contract.
+ */
+ nonce_priv: string;
+
+ pay_info: BackupPayInfo | undefined;
+
+ /**
+ * Timestamp of the first time that sending a payment to the merchant
+ * for this purchase was successful.
+ */
+ timestamp_first_successful_pay: TalerProtocolTimestamp | undefined;
+
+ /**
+ * Signature by the merchant confirming the payment.
+ */
+ merchant_pay_sig: string | undefined;
+
+ timestamp_proposed: TalerProtocolTimestamp;
+
+ /**
+ * When was the purchase made?
+ * Refers to the time that the user accepted.
+ */
+ timestamp_accepted: TalerProtocolTimestamp | undefined;
+
+ /**
+ * Pending refunds for the purchase. A refund is pending
+ * when the merchant reports a transient error from the exchange.
+ */
+ refunds: BackupRefundItem[];
+
+ /**
+ * Continue querying the refund status until this deadline has expired.
+ */
+ auto_refund_deadline: TalerProtocolTimestamp | undefined;
+}
+
+/**
+ * Info about one denomination in the backup.
+ *
+ * Note that the wallet only backs up validated denominations.
+ */
+export interface BackupDenomination {
+ /**
+ * Value of one coin of the denomination.
+ */
+ value: BackupAmountString;
+
+ /**
+ * The denomination public key.
+ */
+ denom_pub: DenominationPubKey;
+
+ /**
+ * Fee for withdrawing.
+ */
+ fee_withdraw: BackupAmountString;
+
+ /**
+ * Fee for depositing.
+ */
+ fee_deposit: BackupAmountString;
+
+ /**
+ * Fee for refreshing.
+ */
+ fee_refresh: BackupAmountString;
+
+ /**
+ * Fee for refunding.
+ */
+ fee_refund: BackupAmountString;
+
+ /**
+ * Validity start date of the denomination.
+ */
+ stamp_start: TalerProtocolTimestamp;
+
+ /**
+ * Date after which the currency can't be withdrawn anymore.
+ */
+ stamp_expire_withdraw: TalerProtocolTimestamp;
+
+ /**
+ * Date after the denomination officially doesn't exist anymore.
+ */
+ stamp_expire_legal: TalerProtocolTimestamp;
+
+ /**
+ * Data after which coins of this denomination can't be deposited anymore.
+ */
+ stamp_expire_deposit: TalerProtocolTimestamp;
+
+ /**
+ * Signature by the exchange's master key over the denomination
+ * information.
+ */
+ master_sig: string;
+
+ /**
+ * Was this denomination still offered by the exchange the last time
+ * we checked?
+ * Only false when the exchange redacts a previously published denomination.
+ */
+ is_offered: boolean;
+
+ /**
+ * Did the exchange revoke the denomination?
+ * When this field is set to true in the database, the same transaction
+ * should also mark all affected coins as revoked.
+ */
+ is_revoked: boolean;
+
+ /**
+ * Coins of this denomination.
+ */
+ coins: BackupCoin[];
+
+ /**
+ * The list issue date of the exchange "/keys" response
+ * that this denomination was last seen in.
+ */
+ list_issue_date: TalerProtocolTimestamp;
+}
+
+/**
+ * Denomination selection.
+ */
+export type BackupDenomSel = {
+ denom_pub_hash: string;
+ count: number;
+}[];
+
+/**
+ * Wire fee for one wire payment target type as stored in the
+ * wallet's database.
+ *
+ * (Flattened to a list to make the declaration simpler).
+ */
+export interface BackupExchangeWireFee {
+ wire_type: string;
+
+ /**
+ * Fee for wire transfers.
+ */
+ wire_fee: string;
+
+ wad_fee: string;
+
+ /**
+ * Fees to close and refund a reserve.
+ */
+ closing_fee: string;
+
+ /**
+ * Start date of the fee.
+ */
+ start_stamp: TalerProtocolTimestamp;
+
+ /**
+ * End date of the fee.
+ */
+ end_stamp: TalerProtocolTimestamp;
+
+ /**
+ * Signature made by the exchange master key.
+ */
+ sig: string;
+}
+
+/**
+ * Global fee as stored in the wallet's database.
+ *
+ */
+export interface BackupExchangeGlobalFees {
+ startDate: TalerProtocolTimestamp;
+ endDate: TalerProtocolTimestamp;
+
+ kycFee: BackupAmountString;
+ historyFee: BackupAmountString;
+ accountFee: BackupAmountString;
+ purseFee: BackupAmountString;
+
+ historyTimeout: TalerProtocolDuration;
+ kycTimeout: TalerProtocolDuration;
+ purseTimeout: TalerProtocolDuration;
+
+ purseLimit: number;
+
+ signature: string;
+}
+/**
+ * Structure of one exchange signing key in the /keys response.
+ */
+export class BackupExchangeSignKey {
+ stamp_start: TalerProtocolTimestamp;
+ stamp_expire: TalerProtocolTimestamp;
+ stamp_end: TalerProtocolTimestamp;
+ key: string;
+ master_sig: string;
+}
+
+/**
+ * Signature by the auditor that a particular denomination key is audited.
+ */
+export class BackupAuditorDenomSig {
+ /**
+ * Denomination public key's hash.
+ */
+ denom_pub_h: string;
+
+ /**
+ * The signature.
+ */
+ auditor_sig: string;
+}
+
+/**
+ * Auditor information as given by the exchange in /keys.
+ */
+export class BackupExchangeAuditor {
+ /**
+ * Auditor's public key.
+ */
+ auditor_pub: string;
+
+ /**
+ * Base URL of the auditor.
+ */
+ auditor_url: string;
+
+ /**
+ * List of signatures for denominations by the auditor.
+ */
+ denomination_keys: BackupAuditorDenomSig[];
+}
+
+/**
+ * Backup information for an exchange. Serves effectively
+ * as a pointer to the exchange details identified by
+ * the base URL, master public key and currency.
+ */
+export interface BackupExchange {
+ base_url: string;
+
+ master_public_key: string;
+
+ currency: string;
+
+ /**
+ * Time when the pointer to the exchange details
+ * was last updated.
+ *
+ * Used to facilitate automatic merging.
+ */
+ update_clock: TalerProtocolTimestamp;
+}
+
+/**
+ * Backup information about an exchange's details.
+ *
+ * Note that one base URL can have multiple exchange
+ * details. The BackupExchange stores a pointer
+ * to the current exchange details.
+ */
+export interface BackupExchangeDetails {
+ /**
+ * Canonicalized base url of the exchange.
+ */
+ base_url: string;
+
+ /**
+ * Master public key of the exchange.
+ */
+ master_public_key: string;
+
+ /**
+ * Auditors (partially) auditing the exchange.
+ */
+ auditors: BackupExchangeAuditor[];
+
+ /**
+ * Currency that the exchange offers.
+ */
+ currency: string;
+
+ /**
+ * Denominations offered by the exchange.
+ */
+ denominations: BackupDenomination[];
+
+ /**
+ * Last observed protocol version.
+ */
+ protocol_version: string;
+
+ /**
+ * Closing delay of reserves.
+ */
+ reserve_closing_delay: TalerProtocolDuration;
+
+ /**
+ * Signing keys we got from the exchange, can also contain
+ * older signing keys that are not returned by /keys anymore.
+ */
+ signing_keys: BackupExchangeSignKey[];
+
+ wire_fees: BackupExchangeWireFee[];
+
+ global_fees: BackupExchangeGlobalFees[];
+
+ /**
+ * Bank accounts offered by the exchange;
+ */
+ accounts: {
+ payto_uri: string;
+ master_sig: string;
+ }[];
+
+ /**
+ * ETag for last terms of service download.
+ */
+ tos_accepted_etag: string | undefined;
+
+ /**
+ * Timestamp when the ToS has been accepted.
+ */
+ tos_accepted_timestamp: TalerProtocolTimestamp | undefined;
+}
+
+export enum BackupProposalStatus {
+ /**
+ * Proposed (and either downloaded or not,
+ * depending on whether contract terms are present),
+ * but the user needs to accept/reject it.
+ */
+ Proposed = "proposed",
+ /**
+ * The user has rejected the proposal.
+ */
+ Refused = "refused",
+ /**
+ * Downloading or processing the proposal has failed permanently.
+ *
+ * FIXME: Should this be modeled as a "misbehavior report" instead?
+ */
+ PermanentlyFailed = "permanently-failed",
+ /**
+ * Downloaded proposal was detected as a re-purchase.
+ */
+ Repurchase = "repurchase",
+
+ Paid = "paid",
+}
+
+export interface BackupRecovery {
+ walletRootPriv: string;
+ providers: {
+ url: string;
+ }[];
+}
diff --git a/packages/taler-util/src/backupTypes.ts b/packages/taler-util/src/backupTypes.ts
@@ -1,1279 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2020 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 declarations for the backup content format.
- *
- * Contains some redundancy with the other type declarations,
- * as the backup schema must remain very stable and should be self-contained.
- *
- * Future:
- * 1. Ghost spends (coin unexpectedly spent by a wallet with shared data)
- * 2. Ghost withdrawals (reserve unexpectedly emptied by another wallet with shared data)
- * 3. Track losses through re-denomination of payments/refreshes
- * 4. (Feature:) Payments to own bank account and P2P-payments need to be backed up
- * 5. Track last/next update time, so on restore we need to do less work
- * 6. Currency render preferences?
- *
- * 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?
- * => Yes, still needs to be implemented
- * 3. What about re-denominations / re-selection of payment coins?
- * Is it enough to store a clock value for the selection?
- * => Coin derivation should also consider denom pub hash
- *
- * General considerations / decisions:
- * 1. Information about previously occurring errors and
- * retries is never backed up.
- * 2. The ToS text of an exchange is never backed up.
- * 3. Derived information is never backed up (hashed values, public keys
- * when we know the private key).
- *
- * Problems:
- *
- * Withdrawal group fork/merging loses money:
- * - Before the withdrawal happens, wallet forks into two backups.
- * - Both wallets need to re-denominate the withdrawal (unlikely but possible).
- * - Because the backup doesn't store planchets where a withdrawal was attempted,
- * after merging some money will be list.
- * - Fix: backup withdrawal objects also store planchets where withdrawal has been attempted
- *
- * @author Florian Dold <dold@taler.net>
- */
-
-/**
- * Imports.
- */
-import { DenominationPubKey, UnblindedSignature } from "./talerTypes.js";
-import { TalerProtocolDuration, TalerProtocolTimestamp } from "./time.js";
-
-export const BACKUP_TAG = "gnu-taler-wallet-backup-content" as const;
-/**
- * Major version. Each increment means a backwards-incompatible change.
- * Typically this means that a custom converter needs to be written.
- */
-export const BACKUP_VERSION_MAJOR = 1 as const;
-
-/**
- * Minor version. Each increment means that information is added to the backup
- * in a backwards-compatible way.
- *
- * Wallets can always import a smaller minor version than their own backup code version.
- * When importing a bigger version, data loss is possible and the user should be urged to
- * upgrade their wallet first.
- */
-export const BACKUP_VERSION_MINOR = 1 as const;
-
-/**
- * Type alias for strings that are to be treated like amounts.
- */
-type BackupAmountString = string;
-
-/**
- * A human-recognizable identifier here that is
- * reasonable unique and assigned the first time the wallet is
- * started/installed, such as:
- *
- * `${wallet-implementation} ${os} ${hostname} (${short-uid})`
- * => e.g. "GNU Taler Android iceking ABC123"
- */
-type DeviceIdString = string;
-
-/**
- * Contract terms JSON.
- */
-type RawContractTerms = any;
-
-/**
- * Unique identifier for an operation, used to either (a) reference
- * the operation in a tombstone (b) disambiguate conflicting writes.
- */
-type OperationUid = string;
-
-/**
- * Content of the backup.
- *
- * The contents of the wallet must be serialized in a deterministic
- * way across implementations, so that the normalized backup content
- * JSON is identical when the wallet's content is identical.
- */
-export interface WalletBackupContentV1 {
- /**
- * Magic constant to identify that this is a backup content JSON.
- */
- schema_id: typeof BACKUP_TAG;
-
- /**
- * Version of the schema.
- */
- schema_version: typeof BACKUP_VERSION_MAJOR;
-
- minor_version: number;
-
- /**
- * Root public key of the wallet. This field is present as
- * a sanity check if the backup content JSON is loaded from file.
- */
- wallet_root_pub: string;
-
- /**
- * Current device identifier that "owns" the backup.
- *
- * This identifier allows one wallet to notice when another
- * wallet is "alive" and connected to the same sync provider.
- */
- current_device_id: DeviceIdString;
-
- /**
- * Timestamp of the backup.
- *
- * This timestamp should only be advanced if the content
- * of the backup changes.
- */
- timestamp: TalerProtocolTimestamp;
-
- /**
- * Per-exchange data sorted by exchange master public key.
- *
- * Sorted by the exchange public key.
- */
- exchanges: BackupExchange[];
-
- exchange_details: BackupExchangeDetails[];
-
- /**
- * Withdrawal groups.
- *
- * Sorted by the withdrawal group ID.
- */
- withdrawal_groups: BackupWithdrawalGroup[];
-
- /**
- * Grouped refresh sessions.
- *
- * Sorted by the refresh group ID.
- */
- refresh_groups: BackupRefreshGroup[];
-
- /**
- * Tips.
- *
- * Sorted by the wallet tip ID.
- */
- tips: BackupTip[];
-
- /**
- * Accepted purchases.
- *
- * Sorted by the proposal ID.
- */
- purchases: BackupPurchase[];
-
- /**
- * All backup providers. Backup providers
- * in this list should be considered "active".
- *
- * Sorted by the provider base URL.
- */
- backup_providers: BackupBackupProvider[];
-
- /**
- * Recoup groups.
- */
- recoup_groups: BackupRecoupGroup[];
-
- /**
- * Trusted auditors, either for official (3 letter) or local (4-12 letter)
- * currencies.
- *
- * Auditors are sorted by their canonicalized base URL.
- */
- trusted_auditors: { [currency: string]: BackupTrustAuditor[] };
-
- /**
- * Trusted exchange. Only applicable for local currencies (4-12 letter currency code).
- *
- * Exchanges are sorted by their canonicalized base URL.
- */
- trusted_exchanges: { [currency: string]: BackupTrustExchange[] };
-
- /**
- * Interning table for forgettable values of contract terms.
- *
- * Used to reduce storage space, as many forgettable items (product image,
- * addresses, etc.) might be shared among many contract terms.
- */
- intern_table: { [hash: string]: any };
-
- /**
- * Permanent error reports.
- */
- error_reports: BackupErrorReport[];
-
- /**
- * Deletion tombstones. Lexically sorted.
- */
- tombstones: Tombstone[];
-}
-
-export enum BackupOperationStatus {
- Cancelled = "cancelled",
- Finished = "finished",
- Pending = "pending",
-}
-
-export enum BackupWgType {
- BankManual = "bank-manual",
- BankIntegrated = "bank-integrated",
- PeerPullCredit = "peer-pull-credit",
- PeerPushCredit = "peer-push-credit",
- Recoup = "recoup",
-}
-
-export type BackupWgInfo =
- | {
- type: BackupWgType.BankManual;
- }
- | {
- type: BackupWgType.BankIntegrated;
- taler_withdraw_uri: string;
-
- /**
- * URL that the user can be redirected to, and allows
- * them to confirm (or abort) the bank-integrated withdrawal.
- */
- confirm_url?: string;
-
- /**
- * Exchange payto URI that the bank will use to fund the reserve.
- */
- exchange_payto_uri: string;
-
- /**
- * Time when the information about this reserve was posted to the bank.
- *
- * Only applies if bankWithdrawStatusUrl is defined.
- *
- * Set to undefined if that hasn't happened yet.
- */
- timestamp_reserve_info_posted?: TalerProtocolTimestamp;
-
- /**
- * Time when the reserve was confirmed by the bank.
- *
- * Set to undefined if not confirmed yet.
- */
- timestamp_bank_confirmed?: TalerProtocolTimestamp;
- }
- | {
- type: BackupWgType.PeerPullCredit;
- contract_terms: any;
- contract_priv: string;
- }
- | {
- type: BackupWgType.PeerPushCredit;
- contract_terms: any;
- }
- | {
- type: BackupWgType.Recoup;
- };
-
-/**
- * FIXME: Open questions:
- * - Do we have to store the denomination selection? Why?
- * (If deterministic, amount shouldn't change. Not storing it is simpler.)
- */
-export interface BackupWithdrawalGroup {
- withdrawal_group_id: string;
-
- /**
- * Detailed info based on the type of withdrawal group.
- */
- info: BackupWgInfo;
-
- secret_seed: string;
-
- reserve_priv: string;
-
- exchange_base_url: string;
-
- timestamp_created: TalerProtocolTimestamp;
-
- timestamp_finish?: TalerProtocolTimestamp;
-
- operation_status: BackupOperationStatus;
-
- instructed_amount: BackupAmountString;
-
- /**
- * Amount including fees (i.e. the amount subtracted from the
- * reserve to withdraw all coins in this withdrawal session).
- *
- * Note that this *includes* the amount remaining in the reserve
- * that is too small to be withdrawn, and thus can't be derived
- * from selectedDenoms.
- */
- raw_withdrawal_amount: BackupAmountString;
-
- /**
- * Restrict withdrawals from this reserve to this age.
- */
- restrict_age?: number;
-
- /**
- * Multiset of denominations selected for withdrawal.
- */
- selected_denoms: BackupDenomSel;
-
- selected_denoms_uid: OperationUid;
-}
-
-/**
- * Tombstone in the format "<type>:<key>"
- */
-export type Tombstone = string;
-
-/**
- * Detailed error report.
- *
- * For auditor-relevant reports with attached cryptographic proof,
- * the error report also should contain the submission status to
- * the auditor(s).
- */
-interface BackupErrorReport {
- // FIXME: specify!
-}
-
-/**
- * Trust declaration for an auditor.
- *
- * The trust applies based on the public key of
- * the auditor, irrespective of what base URL the exchange
- * is referencing.
- */
-export interface BackupTrustAuditor {
- /**
- * Base URL of the auditor.
- */
- auditor_base_url: string;
-
- /**
- * Public key of the auditor.
- */
- auditor_pub: string;
-
- /**
- * UIDs for the operation of adding this auditor
- * as a trusted auditor.
- */
- uids: OperationUid;
-}
-
-/**
- * Trust declaration for an exchange.
- *
- * The trust only applies for the combination of base URL
- * and public key. If the master public key changes while the base
- * URL stays the same, the exchange has to be re-added by a wallet update
- * or by the user.
- */
-export interface BackupTrustExchange {
- /**
- * Canonicalized exchange base URL.
- */
- exchange_base_url: string;
-
- /**
- * Master public key of the exchange.
- */
- exchange_master_pub: string;
-
- /**
- * UIDs for the operation of adding this exchange
- * as trusted.
- */
- uids: OperationUid;
-}
-
-export class BackupBackupProviderTerms {
- /**
- * Last known supported protocol version.
- */
- supported_protocol_version: string;
-
- /**
- * Last known annual fee.
- */
- annual_fee: BackupAmountString;
-
- /**
- * Last known storage limit.
- */
- storage_limit_in_megabytes: number;
-}
-
-/**
- * Backup information about one backup storage provider.
- */
-export class BackupBackupProvider {
- /**
- * Canonicalized base URL of the provider.
- */
- base_url: string;
-
- /**
- * Last known terms. Might be unavailable in some situations, such
- * as directly after restoring form a backup recovery document.
- */
- terms?: BackupBackupProviderTerms;
-
- /**
- * Proposal IDs for payments to this provider.
- */
- pay_proposal_ids: string[];
-
- /**
- * UIDs for adding this backup provider.
- */
- uids: OperationUid[];
-}
-
-/**
- * Status of recoup operations that were grouped together.
- *
- * The remaining amount of the corresponding coins must be set to
- * zero when the recoup group is created/imported.
- */
-export interface BackupRecoupGroup {
- /**
- * Unique identifier for the recoup group record.
- */
- recoup_group_id: string;
-
- /**
- * Timestamp when the recoup was started.
- */
- timestamp_created: TalerProtocolTimestamp;
-
- timestamp_finish?: TalerProtocolTimestamp;
- finish_clock?: TalerProtocolTimestamp;
- finish_is_failure?: boolean;
-
- /**
- * Information about each coin being recouped.
- */
- coins: {
- coin_pub: string;
- recoup_finished: boolean;
- old_amount: BackupAmountString;
- }[];
-}
-
-/**
- * Types of coin sources.
- */
-export enum BackupCoinSourceType {
- Withdraw = "withdraw",
- Refresh = "refresh",
- Tip = "tip",
-}
-
-/**
- * Metadata about a coin obtained via withdrawing.
- */
-export interface BackupWithdrawCoinSource {
- type: BackupCoinSourceType.Withdraw;
-
- /**
- * Can be the empty string for orphaned coins.
- */
- withdrawal_group_id: string;
-
- /**
- * Index of the coin in the withdrawal session.
- */
- coin_index: number;
-
- /**
- * Reserve public key for the reserve we got this coin from.
- */
- reserve_pub: string;
-}
-
-/**
- * Metadata about a coin obtained from refreshing.
- *
- * FIXME: Currently does not link to the refreshGroupId because
- * the wallet DB doesn't do this. Not really necessary,
- * but would be more consistent.
- */
-export interface BackupRefreshCoinSource {
- type: BackupCoinSourceType.Refresh;
-
- /**
- * Public key of the coin that was refreshed into this coin.
- */
- old_coin_pub: string;
-}
-
-/**
- * Metadata about a coin obtained from a tip.
- */
-export interface BackupTipCoinSource {
- type: BackupCoinSourceType.Tip;
-
- /**
- * Wallet's identifier for the tip that this coin
- * originates from.
- */
- wallet_tip_id: string;
-
- /**
- * Index in the tip planchets of the tip.
- */
- coin_index: number;
-}
-
-/**
- * Metadata about a coin depending on the origin.
- */
-export type BackupCoinSource =
- | BackupWithdrawCoinSource
- | BackupRefreshCoinSource
- | BackupTipCoinSource;
-
-/**
- * Backup information about a coin.
- *
- * (Always part of a BackupExchange/BackupDenom)
- */
-export interface BackupCoin {
- /**
- * Where did the coin come from? Used for recouping coins.
- */
- coin_source: BackupCoinSource;
-
- /**
- * Private key to authorize operations on the coin.
- */
- coin_priv: string;
-
- /**
- * Unblinded signature by the exchange.
- */
- denom_sig: UnblindedSignature;
-
- /**
- * Amount that's left on the coin.
- */
- current_amount: BackupAmountString;
-
- /**
- * Blinding key used when withdrawing the coin.
- * Potentionally used again during payback.
- */
- blinding_key: string;
-
- /**
- * Does the wallet think that the coin is still fresh?
- *
- * Note that even if a fresh coin is imported, it should still
- * be refreshed in most situations.
- */
- fresh: boolean;
-}
-
-/**
- * Status of a tip we got from a merchant.
- */
-export interface BackupTip {
- /**
- * Tip ID chosen by the wallet.
- */
- wallet_tip_id: string;
-
- /**
- * The merchant's identifier for this tip.
- */
- merchant_tip_id: string;
-
- /**
- * Secret seed used for the tipping planchets.
- */
- secret_seed: string;
-
- /**
- * Has the user accepted the tip? Only after the tip has been accepted coins
- * withdrawn from the tip may be used.
- */
- timestamp_accepted: TalerProtocolTimestamp | undefined;
-
- /**
- * When was the tip first scanned by the wallet?
- */
- timestamp_created: TalerProtocolTimestamp;
-
- timestamp_finished?: TalerProtocolTimestamp;
- finish_is_failure?: boolean;
-
- /**
- * The tipped amount.
- */
- tip_amount_raw: BackupAmountString;
-
- /**
- * Timestamp, the tip can't be picked up anymore after this deadline.
- */
- timestamp_expiration: TalerProtocolTimestamp;
-
- /**
- * The exchange that will sign our coins, chosen by the merchant.
- */
- exchange_base_url: string;
-
- /**
- * Base URL of the merchant that is giving us the tip.
- */
- merchant_base_url: string;
-
- /**
- * Selected denominations. Determines the effective tip amount.
- */
- selected_denoms: BackupDenomSel;
-
- /**
- * UID for the denomination selection.
- * Used to disambiguate when merging.
- */
- selected_denoms_uid: OperationUid;
-}
-
-/**
- * Reasons for why a coin is being refreshed.
- */
-export enum BackupRefreshReason {
- Manual = "manual",
- Pay = "pay",
- Refund = "refund",
- AbortPay = "abort-pay",
- Recoup = "recoup",
- BackupRestored = "backup-restored",
- Scheduled = "scheduled",
-}
-
-/**
- * Information about one refresh session, always part
- * of a refresh group.
- *
- * (Public key of the old coin is stored in the refresh group.)
- */
-export interface BackupRefreshSession {
- /**
- * Hashed denominations of the newly requested coins.
- */
- new_denoms: BackupDenomSel;
-
- /**
- * Seed used to derive the planchets and
- * transfer private keys for this refresh session.
- */
- session_secret_seed: string;
-
- /**
- * The no-reveal-index after we've done the melting.
- */
- noreveal_index?: number;
-}
-
-/**
- * Refresh session for one coin inside a refresh group.
- */
-export interface BackupRefreshOldCoin {
- /**
- * Public key of the old coin,
- */
- coin_pub: string;
-
- /**
- * Requested amount to refresh. Must be subtracted from the coin's remaining
- * amount as soon as the coin is added to the refresh group.
- */
- input_amount: BackupAmountString;
-
- /**
- * Estimated output (may change if it takes a long time to create the
- * actual session).
- */
- estimated_output_amount: BackupAmountString;
-
- /**
- * Did the refresh session finish (or was it unnecessary/impossible to create
- * one)
- */
- finished: boolean;
-
- /**
- * Refresh session (if created) or undefined it not created yet.
- */
- refresh_session: BackupRefreshSession | undefined;
-}
-
-/**
- * Information about one refresh group.
- *
- * May span more than one exchange, but typically doesn't
- */
-export interface BackupRefreshGroup {
- refresh_group_id: string;
-
- reason: BackupRefreshReason;
-
- /**
- * Details per old coin.
- */
- old_coins: BackupRefreshOldCoin[];
-
- timestamp_created: TalerProtocolTimestamp;
-
- timestamp_finish?: TalerProtocolTimestamp;
- finish_is_failure?: boolean;
-}
-
-export enum BackupRefundState {
- Failed = "failed",
- Applied = "applied",
- Pending = "pending",
-}
-
-/**
- * Common information about a refund.
- */
-export interface BackupRefundItemCommon {
- /**
- * Execution time as claimed by the merchant
- */
- execution_time: TalerProtocolTimestamp;
-
- /**
- * Time when the wallet became aware of the refund.
- */
- obtained_time: TalerProtocolTimestamp;
-
- /**
- * Amount refunded for the coin.
- */
- refund_amount: BackupAmountString;
-
- /**
- * Coin being refunded.
- */
- coin_pub: string;
-
- /**
- * The refund transaction ID for the refund.
- */
- rtransaction_id: number;
-
- /**
- * Upper bound on the refresh cost incurred by
- * applying this refund.
- *
- * Might be lower in practice when two refunds on the same
- * coin are refreshed in the same refresh operation.
- *
- * Used to display fees, and stored since it's expensive to recompute
- * accurately.
- */
- total_refresh_cost_bound: BackupAmountString;
-}
-
-/**
- * Failed refund, either because the merchant did
- * something wrong or it expired.
- */
-export interface BackupRefundFailedItem extends BackupRefundItemCommon {
- type: BackupRefundState.Failed;
-}
-
-export interface BackupRefundPendingItem extends BackupRefundItemCommon {
- type: BackupRefundState.Pending;
-}
-
-export interface BackupRefundAppliedItem extends BackupRefundItemCommon {
- type: BackupRefundState.Applied;
-}
-
-/**
- * State of one refund from the merchant, maintained by the wallet.
- */
-export type BackupRefundItem =
- | BackupRefundFailedItem
- | BackupRefundPendingItem
- | BackupRefundAppliedItem;
-
-/**
- * Data we store when the payment was accepted.
- */
-export interface BackupPayInfo {
- pay_coins: {
- /**
- * Public keys of the coins that were selected.
- */
- coin_pub: string;
-
- /**
- * Amount that each coin contributes.
- */
- contribution: BackupAmountString;
- }[];
-
- /**
- * Unique ID to disambiguate pay coin selection on merge.
- */
- pay_coins_uid: OperationUid;
-
- /**
- * Total cost initially shown to the user.
- *
- * This includes the amount taken by the merchant, fees (wire/deposit) contributed
- * by the customer, refreshing fees, fees for withdraw-after-refresh and "trimmings"
- * of coins that are too small to spend.
- *
- * Note that in rare situations, this cost might not be accurate (e.g.
- * when the payment or refresh gets re-denominated).
- * We might show adjustments to this later, but currently we don't do so.
- */
- total_pay_cost: BackupAmountString;
-}
-
-export interface BackupPurchase {
- /**
- * Proposal ID for this purchase. Uniquely identifies the
- * purchase and the proposal.
- */
- proposal_id: string;
-
- /**
- * Status of the proposal.
- */
- proposal_status: BackupProposalStatus;
-
- /**
- * Proposal that this one got "redirected" to as part of
- * the repurchase detection.
- */
- repurchase_proposal_id: string | undefined;
-
- /**
- * Session ID we got when downloading the contract.
- */
- download_session_id?: string;
-
- /**
- * Merchant-assigned order ID of the proposal.
- */
- order_id: string;
-
- /**
- * Base URL of the merchant that proposed the purchase.
- */
- merchant_base_url: string;
-
- /**
- * Claim token initially given by the merchant.
- */
- claim_token: string | undefined;
-
- /**
- * Contract terms we got from the merchant.
- */
- contract_terms_raw?: RawContractTerms;
-
- /**
- * Signature on the contract terms.
- *
- * FIXME: Better name needed.
- */
- merchant_sig?: string;
-
- /**
- * Private key for the nonce. Might eventually be used
- * to prove ownership of the contract.
- */
- nonce_priv: string;
-
- pay_info: BackupPayInfo | undefined;
-
- /**
- * Timestamp of the first time that sending a payment to the merchant
- * for this purchase was successful.
- */
- timestamp_first_successful_pay: TalerProtocolTimestamp | undefined;
-
- /**
- * Signature by the merchant confirming the payment.
- */
- merchant_pay_sig: string | undefined;
-
- timestamp_proposed: TalerProtocolTimestamp;
-
- /**
- * When was the purchase made?
- * Refers to the time that the user accepted.
- */
- timestamp_accepted: TalerProtocolTimestamp | undefined;
-
- /**
- * Pending refunds for the purchase. A refund is pending
- * when the merchant reports a transient error from the exchange.
- */
- refunds: BackupRefundItem[];
-
- /**
- * Continue querying the refund status until this deadline has expired.
- */
- auto_refund_deadline: TalerProtocolTimestamp | undefined;
-}
-
-/**
- * Info about one denomination in the backup.
- *
- * Note that the wallet only backs up validated denominations.
- */
-export interface BackupDenomination {
- /**
- * Value of one coin of the denomination.
- */
- value: BackupAmountString;
-
- /**
- * The denomination public key.
- */
- denom_pub: DenominationPubKey;
-
- /**
- * Fee for withdrawing.
- */
- fee_withdraw: BackupAmountString;
-
- /**
- * Fee for depositing.
- */
- fee_deposit: BackupAmountString;
-
- /**
- * Fee for refreshing.
- */
- fee_refresh: BackupAmountString;
-
- /**
- * Fee for refunding.
- */
- fee_refund: BackupAmountString;
-
- /**
- * Validity start date of the denomination.
- */
- stamp_start: TalerProtocolTimestamp;
-
- /**
- * Date after which the currency can't be withdrawn anymore.
- */
- stamp_expire_withdraw: TalerProtocolTimestamp;
-
- /**
- * Date after the denomination officially doesn't exist anymore.
- */
- stamp_expire_legal: TalerProtocolTimestamp;
-
- /**
- * Data after which coins of this denomination can't be deposited anymore.
- */
- stamp_expire_deposit: TalerProtocolTimestamp;
-
- /**
- * Signature by the exchange's master key over the denomination
- * information.
- */
- master_sig: string;
-
- /**
- * Was this denomination still offered by the exchange the last time
- * we checked?
- * Only false when the exchange redacts a previously published denomination.
- */
- is_offered: boolean;
-
- /**
- * Did the exchange revoke the denomination?
- * When this field is set to true in the database, the same transaction
- * should also mark all affected coins as revoked.
- */
- is_revoked: boolean;
-
- /**
- * Coins of this denomination.
- */
- coins: BackupCoin[];
-
- /**
- * The list issue date of the exchange "/keys" response
- * that this denomination was last seen in.
- */
- list_issue_date: TalerProtocolTimestamp;
-}
-
-/**
- * Denomination selection.
- */
-export type BackupDenomSel = {
- denom_pub_hash: string;
- count: number;
-}[];
-
-/**
- * Wire fee for one wire payment target type as stored in the
- * wallet's database.
- *
- * (Flattened to a list to make the declaration simpler).
- */
-export interface BackupExchangeWireFee {
- wire_type: string;
-
- /**
- * Fee for wire transfers.
- */
- wire_fee: string;
-
- wad_fee: string;
-
- /**
- * Fees to close and refund a reserve.
- */
- closing_fee: string;
-
- /**
- * Start date of the fee.
- */
- start_stamp: TalerProtocolTimestamp;
-
- /**
- * End date of the fee.
- */
- end_stamp: TalerProtocolTimestamp;
-
- /**
- * Signature made by the exchange master key.
- */
- sig: string;
-}
-
-/**
- * Global fee as stored in the wallet's database.
- *
- */
-export interface BackupExchangeGlobalFees {
- startDate: TalerProtocolTimestamp;
- endDate: TalerProtocolTimestamp;
-
- kycFee: BackupAmountString;
- historyFee: BackupAmountString;
- accountFee: BackupAmountString;
- purseFee: BackupAmountString;
-
- historyTimeout: TalerProtocolDuration;
- kycTimeout: TalerProtocolDuration;
- purseTimeout: TalerProtocolDuration;
-
- purseLimit: number;
-
- signature: string;
-}
-/**
- * Structure of one exchange signing key in the /keys response.
- */
-export class BackupExchangeSignKey {
- stamp_start: TalerProtocolTimestamp;
- stamp_expire: TalerProtocolTimestamp;
- stamp_end: TalerProtocolTimestamp;
- key: string;
- master_sig: string;
-}
-
-/**
- * Signature by the auditor that a particular denomination key is audited.
- */
-export class BackupAuditorDenomSig {
- /**
- * Denomination public key's hash.
- */
- denom_pub_h: string;
-
- /**
- * The signature.
- */
- auditor_sig: string;
-}
-
-/**
- * Auditor information as given by the exchange in /keys.
- */
-export class BackupExchangeAuditor {
- /**
- * Auditor's public key.
- */
- auditor_pub: string;
-
- /**
- * Base URL of the auditor.
- */
- auditor_url: string;
-
- /**
- * List of signatures for denominations by the auditor.
- */
- denomination_keys: BackupAuditorDenomSig[];
-}
-
-/**
- * Backup information for an exchange. Serves effectively
- * as a pointer to the exchange details identified by
- * the base URL, master public key and currency.
- */
-export interface BackupExchange {
- base_url: string;
-
- master_public_key: string;
-
- currency: string;
-
- /**
- * Time when the pointer to the exchange details
- * was last updated.
- *
- * Used to facilitate automatic merging.
- */
- update_clock: TalerProtocolTimestamp;
-}
-
-/**
- * Backup information about an exchange's details.
- *
- * Note that one base URL can have multiple exchange
- * details. The BackupExchange stores a pointer
- * to the current exchange details.
- */
-export interface BackupExchangeDetails {
- /**
- * Canonicalized base url of the exchange.
- */
- base_url: string;
-
- /**
- * Master public key of the exchange.
- */
- master_public_key: string;
-
- /**
- * Auditors (partially) auditing the exchange.
- */
- auditors: BackupExchangeAuditor[];
-
- /**
- * Currency that the exchange offers.
- */
- currency: string;
-
- /**
- * Denominations offered by the exchange.
- */
- denominations: BackupDenomination[];
-
- /**
- * Last observed protocol version.
- */
- protocol_version: string;
-
- /**
- * Closing delay of reserves.
- */
- reserve_closing_delay: TalerProtocolDuration;
-
- /**
- * Signing keys we got from the exchange, can also contain
- * older signing keys that are not returned by /keys anymore.
- */
- signing_keys: BackupExchangeSignKey[];
-
- wire_fees: BackupExchangeWireFee[];
-
- global_fees: BackupExchangeGlobalFees[];
-
- /**
- * Bank accounts offered by the exchange;
- */
- accounts: {
- payto_uri: string;
- master_sig: string;
- }[];
-
- /**
- * ETag for last terms of service download.
- */
- tos_accepted_etag: string | undefined;
-
- /**
- * Timestamp when the ToS has been accepted.
- */
- tos_accepted_timestamp: TalerProtocolTimestamp | undefined;
-}
-
-export enum BackupProposalStatus {
- /**
- * Proposed (and either downloaded or not,
- * depending on whether contract terms are present),
- * but the user needs to accept/reject it.
- */
- Proposed = "proposed",
- /**
- * The user has rejected the proposal.
- */
- Refused = "refused",
- /**
- * Downloading or processing the proposal has failed permanently.
- *
- * FIXME: Should this be modeled as a "misbehavior report" instead?
- */
- PermanentlyFailed = "permanently-failed",
- /**
- * Downloaded proposal was detected as a re-purchase.
- */
- Repurchase = "repurchase",
-
- Paid = "paid",
-}
-
-export interface BackupRecovery {
- walletRootPriv: string;
- providers: {
- url: string;
- }[];
-}
diff --git a/packages/taler-util/src/bitcoin.ts b/packages/taler-util/src/bitcoin.ts
@@ -23,7 +23,7 @@
* Imports.
*/
import { AmountJson, Amounts } from "./amounts.js";
-import { decodeCrock } from "./talerCrypto.js";
+import { decodeCrock } from "./taler-crypto.js";
import * as segwit from "./segwit_addr.js";
function buf2hex(buffer: Uint8Array) {
diff --git a/packages/taler-util/src/contract-terms.test.ts b/packages/taler-util/src/contract-terms.test.ts
@@ -0,0 +1,127 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021 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/>
+ */
+
+/**
+ * Imports.
+ */
+import test from "ava";
+import { initNodePrng } from "./prng-node.js";
+import { ContractTermsUtil } from "./contract-terms.js";
+
+// Since we import nacl-fast directly (and not via index.node.ts), we need to
+// init the PRNG manually.
+initNodePrng();
+
+test("contract terms canon hashing", (t) => {
+ const cReq = {
+ foo: 42,
+ bar: "hello",
+ $forgettable: {
+ foo: true,
+ },
+ };
+
+ const c1 = ContractTermsUtil.saltForgettable(cReq);
+ const c2 = ContractTermsUtil.saltForgettable(cReq);
+ t.assert(typeof cReq.$forgettable.foo === "boolean");
+ t.assert(typeof c1.$forgettable.foo === "string");
+ t.assert(c1.$forgettable.foo !== c2.$forgettable.foo);
+
+ const h1 = ContractTermsUtil.hashContractTerms(c1);
+
+ const c3 = ContractTermsUtil.scrub(JSON.parse(JSON.stringify(c1)));
+
+ t.assert(c3.foo === undefined);
+ t.assert(c3.bar === cReq.bar);
+
+ const h2 = ContractTermsUtil.hashContractTerms(c3);
+
+ t.deepEqual(h1, h2);
+});
+
+test("contract terms canon hashing (nested)", (t) => {
+ const cReq = {
+ foo: 42,
+ bar: {
+ prop1: "hello, world",
+ $forgettable: {
+ prop1: true,
+ },
+ },
+ $forgettable: {
+ bar: true,
+ },
+ };
+
+ const c1 = ContractTermsUtil.saltForgettable(cReq);
+
+ t.is(typeof c1.$forgettable.bar, "string");
+ t.is(typeof c1.bar.$forgettable.prop1, "string");
+
+ const forgetPath = (x: any, s: string) =>
+ ContractTermsUtil.forgetAll(x, (p) => p.join(".") === s);
+
+ // Forget bar first
+ const c2 = forgetPath(c1, "bar");
+
+ // Forget bar.prop1 first
+ const c3 = forgetPath(forgetPath(c1, "bar.prop1"), "bar");
+
+ // Forget everything
+ const c4 = ContractTermsUtil.scrub(c1);
+
+ const h1 = ContractTermsUtil.hashContractTerms(c1);
+ const h2 = ContractTermsUtil.hashContractTerms(c2);
+ const h3 = ContractTermsUtil.hashContractTerms(c3);
+ const h4 = ContractTermsUtil.hashContractTerms(c4);
+
+ t.is(h1, h2);
+ t.is(h1, h3);
+ t.is(h1, h4);
+
+ // Doesn't contain salt
+ t.false(ContractTermsUtil.validateForgettable(cReq));
+
+ t.true(ContractTermsUtil.validateForgettable(c1));
+ t.true(ContractTermsUtil.validateForgettable(c2));
+ t.true(ContractTermsUtil.validateForgettable(c3));
+ t.true(ContractTermsUtil.validateForgettable(c4));
+});
+
+test("contract terms reference vector", (t) => {
+ const j = {
+ k1: 1,
+ $forgettable: {
+ k1: "SALT",
+ },
+ k2: {
+ n1: true,
+ $forgettable: {
+ n1: "salt",
+ },
+ },
+ k3: {
+ n1: "string",
+ },
+ };
+
+ const h = ContractTermsUtil.hashContractTerms(j);
+
+ t.deepEqual(
+ h,
+ "VDE8JPX0AEEE3EX1K8E11RYEWSZQKGGZCV6BWTE4ST1C8711P7H850Z7F2Q2HSSYETX87ERC2JNHWB7GTDWTDWMM716VKPSRBXD7SRR",
+ );
+});
diff --git a/packages/taler-util/src/contract-terms.ts b/packages/taler-util/src/contract-terms.ts
@@ -0,0 +1,231 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021 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/>
+ */
+
+import { canonicalJson } from "./helpers.js";
+import { Logger } from "./logging.js";
+import { kdf } from "./kdf.js";
+import {
+ decodeCrock,
+ encodeCrock,
+ getRandomBytes,
+ hash,
+ stringToBytes,
+} from "./taler-crypto.js";
+
+const logger = new Logger("contractTerms.ts");
+
+export namespace ContractTermsUtil {
+ export function forgetAllImpl(
+ anyJson: any,
+ path: string[],
+ pred: PathPredicate,
+ ): any {
+ const dup = JSON.parse(JSON.stringify(anyJson));
+ if (Array.isArray(dup)) {
+ for (let i = 0; i < dup.length; i++) {
+ dup[i] = forgetAllImpl(dup[i], [...path, `${i}`], pred);
+ }
+ } else if (typeof dup === "object" && dup != null) {
+ if (typeof dup.$forgettable === "object") {
+ for (const x of Object.keys(dup.$forgettable)) {
+ if (!pred([...path, x])) {
+ continue;
+ }
+ if (!dup.$forgotten) {
+ dup.$forgotten = {};
+ }
+ if (!dup.$forgotten[x]) {
+ const membValCanon = stringToBytes(
+ canonicalJson(scrub(dup[x])) + "\0",
+ );
+ const membSalt = stringToBytes(dup.$forgettable[x] + "\0");
+ const h = kdf(64, membValCanon, membSalt, new Uint8Array([]));
+ dup.$forgotten[x] = encodeCrock(h);
+ }
+ delete dup[x];
+ delete dup.$forgettable[x];
+ }
+ if (Object.keys(dup.$forgettable).length === 0) {
+ delete dup.$forgettable;
+ }
+ }
+ for (const x of Object.keys(dup)) {
+ if (x.startsWith("$")) {
+ continue;
+ }
+ dup[x] = forgetAllImpl(dup[x], [...path, x], pred);
+ }
+ }
+ return dup;
+ }
+
+ export type PathPredicate = (path: string[]) => boolean;
+
+ /**
+ * Scrub all forgettable members from an object.
+ */
+ export function scrub(anyJson: any): any {
+ return forgetAllImpl(anyJson, [], () => true);
+ }
+
+ /**
+ * Recursively forget all forgettable members of an object,
+ * where the path matches a predicate.
+ */
+ export function forgetAll(anyJson: any, pred: PathPredicate): any {
+ return forgetAllImpl(anyJson, [], pred);
+ }
+
+ /**
+ * Generate a salt for all members marked as forgettable,
+ * but which don't have an actual salt yet.
+ */
+ export function saltForgettable(anyJson: any): any {
+ const dup = JSON.parse(JSON.stringify(anyJson));
+ if (Array.isArray(dup)) {
+ for (let i = 0; i < dup.length; i++) {
+ dup[i] = saltForgettable(dup[i]);
+ }
+ } else if (typeof dup === "object" && dup !== null) {
+ if (typeof dup.$forgettable === "object") {
+ for (const k of Object.keys(dup.$forgettable)) {
+ if (dup.$forgettable[k] === true) {
+ dup.$forgettable[k] = encodeCrock(getRandomBytes(32));
+ }
+ }
+ }
+ for (const x of Object.keys(dup)) {
+ if (x.startsWith("$")) {
+ continue;
+ }
+ dup[x] = saltForgettable(dup[x]);
+ }
+ }
+ return dup;
+ }
+
+ const nameRegex = /^[0-9A-Za-z_]+$/;
+
+ /**
+ * Check that the given JSON object is well-formed with regards
+ * to forgettable fields and other restrictions for forgettable JSON.
+ */
+ export function validateForgettable(anyJson: any): boolean {
+ if (typeof anyJson === "string") {
+ return true;
+ }
+ if (typeof anyJson === "number") {
+ return (
+ Number.isInteger(anyJson) &&
+ anyJson >= Number.MIN_SAFE_INTEGER &&
+ anyJson <= Number.MAX_SAFE_INTEGER
+ );
+ }
+ if (typeof anyJson === "boolean") {
+ return true;
+ }
+ if (anyJson === null) {
+ return true;
+ }
+ if (Array.isArray(anyJson)) {
+ return anyJson.every((x) => validateForgettable(x));
+ }
+ if (typeof anyJson === "object") {
+ for (const k of Object.keys(anyJson)) {
+ if (k.match(nameRegex)) {
+ if (validateForgettable(anyJson[k])) {
+ continue;
+ } else {
+ return false;
+ }
+ }
+ if (k === "$forgettable") {
+ const fga = anyJson.$forgettable;
+ if (!fga || typeof fga !== "object") {
+ return false;
+ }
+ for (const fk of Object.keys(fga)) {
+ if (!fk.match(nameRegex)) {
+ return false;
+ }
+ if (!(fk in anyJson)) {
+ return false;
+ }
+ const fv = anyJson.$forgettable[fk];
+ if (typeof fv !== "string") {
+ return false;
+ }
+ }
+ } else if (k === "$forgotten") {
+ const fgo = anyJson.$forgotten;
+ if (!fgo || typeof fgo !== "object") {
+ return false;
+ }
+ for (const fk of Object.keys(fgo)) {
+ if (!fk.match(nameRegex)) {
+ return false;
+ }
+ // Check that the value has actually been forgotten.
+ if (fk in anyJson) {
+ return false;
+ }
+ const fv = anyJson.$forgotten[fk];
+ if (typeof fv !== "string") {
+ return false;
+ }
+ try {
+ const decFv = decodeCrock(fv);
+ if (decFv.length != 64) {
+ return false;
+ }
+ } catch (e) {
+ return false;
+ }
+ // Check that salt has been deleted after forgetting.
+ if (anyJson.$forgettable?.[k] !== undefined) {
+ return false;
+ }
+ }
+ } else {
+ return false;
+ }
+ }
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Check that no forgettable information has been forgotten.
+ *
+ * Must only be called on an object already validated with validateForgettable.
+ */
+ export function validateNothingForgotten(contractTerms: any): boolean {
+ throw Error("not implemented yet");
+ }
+
+ /**
+ * Hash a contract terms object. Forgettable fields
+ * are scrubbed and JSON canonicalization is applied
+ * before hashing.
+ */
+ export function hashContractTerms(contractTerms: unknown): string {
+ const cleaned = scrub(contractTerms);
+ const canon = canonicalJson(cleaned) + "\0";
+ const bytes = stringToBytes(canon);
+ return encodeCrock(hash(bytes));
+ }
+}
diff --git a/packages/taler-util/src/contractTerms.test.ts b/packages/taler-util/src/contractTerms.test.ts
@@ -1,127 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2021 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/>
- */
-
-/**
- * Imports.
- */
-import test from "ava";
-import { initNodePrng } from "./prng-node.js";
-import { ContractTermsUtil } from "./contractTerms.js";
-
-// Since we import nacl-fast directly (and not via index.node.ts), we need to
-// init the PRNG manually.
-initNodePrng();
-
-test("contract terms canon hashing", (t) => {
- const cReq = {
- foo: 42,
- bar: "hello",
- $forgettable: {
- foo: true,
- },
- };
-
- const c1 = ContractTermsUtil.saltForgettable(cReq);
- const c2 = ContractTermsUtil.saltForgettable(cReq);
- t.assert(typeof cReq.$forgettable.foo === "boolean");
- t.assert(typeof c1.$forgettable.foo === "string");
- t.assert(c1.$forgettable.foo !== c2.$forgettable.foo);
-
- const h1 = ContractTermsUtil.hashContractTerms(c1);
-
- const c3 = ContractTermsUtil.scrub(JSON.parse(JSON.stringify(c1)));
-
- t.assert(c3.foo === undefined);
- t.assert(c3.bar === cReq.bar);
-
- const h2 = ContractTermsUtil.hashContractTerms(c3);
-
- t.deepEqual(h1, h2);
-});
-
-test("contract terms canon hashing (nested)", (t) => {
- const cReq = {
- foo: 42,
- bar: {
- prop1: "hello, world",
- $forgettable: {
- prop1: true,
- },
- },
- $forgettable: {
- bar: true,
- },
- };
-
- const c1 = ContractTermsUtil.saltForgettable(cReq);
-
- t.is(typeof c1.$forgettable.bar, "string");
- t.is(typeof c1.bar.$forgettable.prop1, "string");
-
- const forgetPath = (x: any, s: string) =>
- ContractTermsUtil.forgetAll(x, (p) => p.join(".") === s);
-
- // Forget bar first
- const c2 = forgetPath(c1, "bar");
-
- // Forget bar.prop1 first
- const c3 = forgetPath(forgetPath(c1, "bar.prop1"), "bar");
-
- // Forget everything
- const c4 = ContractTermsUtil.scrub(c1);
-
- const h1 = ContractTermsUtil.hashContractTerms(c1);
- const h2 = ContractTermsUtil.hashContractTerms(c2);
- const h3 = ContractTermsUtil.hashContractTerms(c3);
- const h4 = ContractTermsUtil.hashContractTerms(c4);
-
- t.is(h1, h2);
- t.is(h1, h3);
- t.is(h1, h4);
-
- // Doesn't contain salt
- t.false(ContractTermsUtil.validateForgettable(cReq));
-
- t.true(ContractTermsUtil.validateForgettable(c1));
- t.true(ContractTermsUtil.validateForgettable(c2));
- t.true(ContractTermsUtil.validateForgettable(c3));
- t.true(ContractTermsUtil.validateForgettable(c4));
-});
-
-test("contract terms reference vector", (t) => {
- const j = {
- k1: 1,
- $forgettable: {
- k1: "SALT",
- },
- k2: {
- n1: true,
- $forgettable: {
- n1: "salt",
- },
- },
- k3: {
- n1: "string",
- },
- };
-
- const h = ContractTermsUtil.hashContractTerms(j);
-
- t.deepEqual(
- h,
- "VDE8JPX0AEEE3EX1K8E11RYEWSZQKGGZCV6BWTE4ST1C8711P7H850Z7F2Q2HSSYETX87ERC2JNHWB7GTDWTDWMM716VKPSRBXD7SRR",
- );
-});
diff --git a/packages/taler-util/src/contractTerms.ts b/packages/taler-util/src/contractTerms.ts
@@ -1,231 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2021 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/>
- */
-
-import { canonicalJson } from "./helpers.js";
-import { Logger } from "./logging.js";
-import { kdf } from "./kdf.js";
-import {
- decodeCrock,
- encodeCrock,
- getRandomBytes,
- hash,
- stringToBytes,
-} from "./talerCrypto.js";
-
-const logger = new Logger("contractTerms.ts");
-
-export namespace ContractTermsUtil {
- export function forgetAllImpl(
- anyJson: any,
- path: string[],
- pred: PathPredicate,
- ): any {
- const dup = JSON.parse(JSON.stringify(anyJson));
- if (Array.isArray(dup)) {
- for (let i = 0; i < dup.length; i++) {
- dup[i] = forgetAllImpl(dup[i], [...path, `${i}`], pred);
- }
- } else if (typeof dup === "object" && dup != null) {
- if (typeof dup.$forgettable === "object") {
- for (const x of Object.keys(dup.$forgettable)) {
- if (!pred([...path, x])) {
- continue;
- }
- if (!dup.$forgotten) {
- dup.$forgotten = {};
- }
- if (!dup.$forgotten[x]) {
- const membValCanon = stringToBytes(
- canonicalJson(scrub(dup[x])) + "\0",
- );
- const membSalt = stringToBytes(dup.$forgettable[x] + "\0");
- const h = kdf(64, membValCanon, membSalt, new Uint8Array([]));
- dup.$forgotten[x] = encodeCrock(h);
- }
- delete dup[x];
- delete dup.$forgettable[x];
- }
- if (Object.keys(dup.$forgettable).length === 0) {
- delete dup.$forgettable;
- }
- }
- for (const x of Object.keys(dup)) {
- if (x.startsWith("$")) {
- continue;
- }
- dup[x] = forgetAllImpl(dup[x], [...path, x], pred);
- }
- }
- return dup;
- }
-
- export type PathPredicate = (path: string[]) => boolean;
-
- /**
- * Scrub all forgettable members from an object.
- */
- export function scrub(anyJson: any): any {
- return forgetAllImpl(anyJson, [], () => true);
- }
-
- /**
- * Recursively forget all forgettable members of an object,
- * where the path matches a predicate.
- */
- export function forgetAll(anyJson: any, pred: PathPredicate): any {
- return forgetAllImpl(anyJson, [], pred);
- }
-
- /**
- * Generate a salt for all members marked as forgettable,
- * but which don't have an actual salt yet.
- */
- export function saltForgettable(anyJson: any): any {
- const dup = JSON.parse(JSON.stringify(anyJson));
- if (Array.isArray(dup)) {
- for (let i = 0; i < dup.length; i++) {
- dup[i] = saltForgettable(dup[i]);
- }
- } else if (typeof dup === "object" && dup !== null) {
- if (typeof dup.$forgettable === "object") {
- for (const k of Object.keys(dup.$forgettable)) {
- if (dup.$forgettable[k] === true) {
- dup.$forgettable[k] = encodeCrock(getRandomBytes(32));
- }
- }
- }
- for (const x of Object.keys(dup)) {
- if (x.startsWith("$")) {
- continue;
- }
- dup[x] = saltForgettable(dup[x]);
- }
- }
- return dup;
- }
-
- const nameRegex = /^[0-9A-Za-z_]+$/;
-
- /**
- * Check that the given JSON object is well-formed with regards
- * to forgettable fields and other restrictions for forgettable JSON.
- */
- export function validateForgettable(anyJson: any): boolean {
- if (typeof anyJson === "string") {
- return true;
- }
- if (typeof anyJson === "number") {
- return (
- Number.isInteger(anyJson) &&
- anyJson >= Number.MIN_SAFE_INTEGER &&
- anyJson <= Number.MAX_SAFE_INTEGER
- );
- }
- if (typeof anyJson === "boolean") {
- return true;
- }
- if (anyJson === null) {
- return true;
- }
- if (Array.isArray(anyJson)) {
- return anyJson.every((x) => validateForgettable(x));
- }
- if (typeof anyJson === "object") {
- for (const k of Object.keys(anyJson)) {
- if (k.match(nameRegex)) {
- if (validateForgettable(anyJson[k])) {
- continue;
- } else {
- return false;
- }
- }
- if (k === "$forgettable") {
- const fga = anyJson.$forgettable;
- if (!fga || typeof fga !== "object") {
- return false;
- }
- for (const fk of Object.keys(fga)) {
- if (!fk.match(nameRegex)) {
- return false;
- }
- if (!(fk in anyJson)) {
- return false;
- }
- const fv = anyJson.$forgettable[fk];
- if (typeof fv !== "string") {
- return false;
- }
- }
- } else if (k === "$forgotten") {
- const fgo = anyJson.$forgotten;
- if (!fgo || typeof fgo !== "object") {
- return false;
- }
- for (const fk of Object.keys(fgo)) {
- if (!fk.match(nameRegex)) {
- return false;
- }
- // Check that the value has actually been forgotten.
- if (fk in anyJson) {
- return false;
- }
- const fv = anyJson.$forgotten[fk];
- if (typeof fv !== "string") {
- return false;
- }
- try {
- const decFv = decodeCrock(fv);
- if (decFv.length != 64) {
- return false;
- }
- } catch (e) {
- return false;
- }
- // Check that salt has been deleted after forgetting.
- if (anyJson.$forgettable?.[k] !== undefined) {
- return false;
- }
- }
- } else {
- return false;
- }
- }
- return true;
- }
- return false;
- }
-
- /**
- * Check that no forgettable information has been forgotten.
- *
- * Must only be called on an object already validated with validateForgettable.
- */
- export function validateNothingForgotten(contractTerms: any): boolean {
- throw Error("not implemented yet");
- }
-
- /**
- * Hash a contract terms object. Forgettable fields
- * are scrubbed and JSON canonicalization is applied
- * before hashing.
- */
- export function hashContractTerms(contractTerms: unknown): string {
- const cleaned = scrub(contractTerms);
- const canon = canonicalJson(cleaned) + "\0";
- const bytes = stringToBytes(canon);
- return encodeCrock(hash(bytes));
- }
-}
diff --git a/packages/taler-util/src/index.ts b/packages/taler-util/src/index.ts
@@ -3,7 +3,7 @@ import { TalerErrorCode } from "./taler-error-codes.js";
export { TalerErrorCode };
export * from "./amounts.js";
-export * from "./backupTypes.js";
+export * from "./backup-types.js";
export * from "./codec.js";
export * from "./helpers.js";
export * from "./libtool-version.js";
@@ -11,17 +11,17 @@ export * from "./notifications.js";
export * from "./payto.js";
export * from "./ReserveStatus.js";
export * from "./ReserveTransaction.js";
-export * from "./talerTypes.js";
+export * from "./taler-types.js";
export * from "./taleruri.js";
export * from "./time.js";
-export * from "./transactionsTypes.js";
-export * from "./walletTypes.js";
+export * from "./transactions-types.js";
+export * from "./wallet-types.js";
export * from "./i18n.js";
export * from "./logging.js";
export * from "./url.js";
export { fnutil } from "./fnutils.js";
export * from "./kdf.js";
-export * from "./talerCrypto.js";
+export * from "./taler-crypto.js";
export * from "./http-status-codes.js";
export * from "./bitcoin.js";
export {
@@ -32,4 +32,4 @@ export {
} from "./nacl-fast.js";
export { RequestThrottler } from "./RequestThrottler.js";
export * from "./CancellationToken.js";
-export * from "./contractTerms.js";
+export * from "./contract-terms.js";
diff --git a/packages/taler-util/src/notifications.ts b/packages/taler-util/src/notifications.ts
@@ -22,7 +22,7 @@
/**
* Imports.
*/
-import { TalerErrorDetail } from "./walletTypes.js";
+import { TalerErrorDetail } from "./wallet-types.js";
export enum NotificationType {
CoinWithdrawn = "coin-withdrawn",
diff --git a/packages/taler-util/src/taler-crypto.test.ts b/packages/taler-util/src/taler-crypto.test.ts
@@ -0,0 +1,431 @@
+/*
+ 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/>
+ */
+
+/**
+ * Imports
+ */
+import test from "ava";
+import {
+ encodeCrock,
+ decodeCrock,
+ ecdheGetPublic,
+ eddsaGetPublic,
+ keyExchangeEddsaEcdhe,
+ keyExchangeEcdheEddsa,
+ stringToBytes,
+ bytesToString,
+ deriveBSeed,
+ csBlind,
+ csUnblind,
+ csVerify,
+ scalarMultBase25519,
+ deriveSecrets,
+ calcRBlind,
+ Edx25519,
+ getRandomBytes,
+ bigintToNaclArr,
+ bigintFromNaclArr,
+} from "./taler-crypto.js";
+import { sha512, kdf } from "./kdf.js";
+import * as nacl from "./nacl-fast.js";
+import { initNodePrng } from "./prng-node.js";
+
+// Since we import nacl-fast directly (and not via index.node.ts), we need to
+// init the PRNG manually.
+initNodePrng();
+import bigint from "big-integer";
+import { AssertionError } from "assert";
+import BigInteger from "big-integer";
+
+test("encoding", (t) => {
+ const s = "Hello, World";
+ const encStr = encodeCrock(stringToBytes(s));
+ const outBuf = decodeCrock(encStr);
+ const sOut = bytesToString(outBuf);
+ t.deepEqual(s, sOut);
+});
+
+test("taler-exchange-tvg hash code", (t) => {
+ const input = "91JPRV3F5GG4EKJN41A62V35E8";
+ const output =
+ "CW96WR74JS8T53EC8GKSGD49QKH4ZNFTZXDAWMMV5GJ1E4BM6B8GPN5NVHDJ8ZVXNCW7Q4WBYCV61HCA3PZC2YJD850DT29RHHN7ESR";
+
+ const myOutput = encodeCrock(sha512(decodeCrock(input)));
+
+ t.deepEqual(myOutput, output);
+});
+
+test("taler-exchange-tvg ecdhe key", (t) => {
+ const priv1 = "X4T4N0M8PVQXQEBW2BA7049KFSM7J437NSDFC6GDNM3N5J9367A0";
+ const pub1 = "M997P494MS6A95G1P0QYWW2VNPSHSX5Q6JBY5B9YMNYWP0B50X3G";
+ const priv2 = "14A0MMQ64DCV8HE0CS3WBC9DHFJAHXRGV7NEARFJPC5R5E1697E0";
+ const skm =
+ "NXRY2YCY7H9B6KM928ZD55WG964G59YR0CPX041DYXKBZZ85SAWNPQ8B30QRM5FMHYCXJAN0EAADJYWEF1X3PAC2AJN28626TR5A6AR";
+
+ const myPub1 = nacl.scalarMult_base(decodeCrock(priv1));
+ t.deepEqual(encodeCrock(myPub1), pub1);
+
+ const mySkm = nacl.hash(
+ nacl.scalarMult(decodeCrock(priv2), decodeCrock(pub1)),
+ );
+ t.deepEqual(encodeCrock(mySkm), skm);
+});
+
+test("taler-exchange-tvg eddsa key", (t) => {
+ const priv = "9TM70AKDTS57AWY9JK2J4TMBTMW6K62WHHGZWYDG0VM5ABPZKD40";
+ const pub = "8GSJZ649T2PXMKZC01Y4ANNBE7MF14QVK9SQEC4E46ZHKCVG8AS0";
+
+ const pair = nacl.crypto_sign_keyPair_fromSeed(decodeCrock(priv));
+ t.deepEqual(encodeCrock(pair.publicKey), pub);
+});
+
+test("taler-exchange-tvg kdf", (t) => {
+ const salt = "94KPT83PCNS7J83KC5P78Y8";
+ const ikm = "94KPT83MD1JJ0WV5CDS6AX10D5Q70XBM41NPAY90DNGQ8SBJD5GPR";
+ const ctx =
+ "94KPT83141HPYVKMCNW78833D1TPWTSC41GPRWVF41NPWVVQDRG62WS04XMPWSKF4WG6JVH0EHM6A82J8S1G";
+ const outLen = 64;
+ const out =
+ "GTMR4QT05Z9WF5HKVG0WK9RPXGHSMHJNW377G9GJXCA8B0FEKPF4D27RJMSJZYWSQNTBJ5EYVV7ZW18B48Z0JVJJ80RHB706Y96Q358";
+
+ const myOut = kdf(
+ outLen,
+ decodeCrock(ikm),
+ decodeCrock(salt),
+ decodeCrock(ctx),
+ );
+
+ t.deepEqual(encodeCrock(myOut), out);
+});
+
+test("taler-exchange-tvg eddsa_ecdh", (t) => {
+ const priv_ecdhe = "4AFZWMSGTVCHZPQ0R81NWXDCK4N58G7SDBBE5KXE080Y50370JJG";
+ const pub_ecdhe = "FXFN5GPAFTKVPWJDPVXQ87167S8T82T5ZV8CDYC0NH2AE14X0M30";
+ const priv_eddsa = "1KG54M8T3X8BSFSZXCR3SQBSR7Y9P53NX61M864S7TEVMJ2XVPF0";
+ const pub_eddsa = "7BXWKG6N224C57RTDV8XEAHR108HG78NMA995BE8QAT5GC1S7E80";
+ const key_material =
+ "PKZ42Z56SVK2796HG1QYBRJ6ZQM2T9QGA3JA4AAZ8G7CWK9FPX175Q9JE5P0ZAX3HWWPHAQV4DPCK10R9X3SAXHRV0WF06BHEC2ZTKR";
+
+ const myEcdhePub = ecdheGetPublic(decodeCrock(priv_ecdhe));
+ t.deepEqual(encodeCrock(myEcdhePub), pub_ecdhe);
+
+ const myEddsaPub = eddsaGetPublic(decodeCrock(priv_eddsa));
+ t.deepEqual(encodeCrock(myEddsaPub), pub_eddsa);
+
+ const myKm1 = keyExchangeEddsaEcdhe(
+ decodeCrock(priv_eddsa),
+ decodeCrock(pub_ecdhe),
+ );
+ t.deepEqual(encodeCrock(myKm1), key_material);
+
+ const myKm2 = keyExchangeEcdheEddsa(
+ decodeCrock(priv_ecdhe),
+ decodeCrock(pub_eddsa),
+ );
+ t.deepEqual(encodeCrock(myKm2), key_material);
+});
+
+test("incremental hashing #1", (t) => {
+ const n = 1024;
+ const d = nacl.randomBytes(n);
+
+ const h1 = nacl.hash(d);
+ const h2 = new nacl.HashState().update(d).finish();
+
+ const s = new nacl.HashState();
+ for (let i = 0; i < n; i++) {
+ const b = new Uint8Array(1);
+ b[0] = d[i];
+ s.update(b);
+ }
+
+ const h3 = s.finish();
+
+ t.deepEqual(encodeCrock(h1), encodeCrock(h2));
+ t.deepEqual(encodeCrock(h1), encodeCrock(h3));
+});
+
+test("incremental hashing #2", (t) => {
+ const n = 10;
+ const d = nacl.randomBytes(n);
+
+ const h1 = nacl.hash(d);
+ const h2 = new nacl.HashState().update(d).finish();
+ const s = new nacl.HashState();
+ for (let i = 0; i < n; i++) {
+ const b = new Uint8Array(1);
+ b[0] = d[i];
+ s.update(b);
+ }
+
+ const h3 = s.finish();
+
+ t.deepEqual(encodeCrock(h1), encodeCrock(h3));
+ t.deepEqual(encodeCrock(h1), encodeCrock(h2));
+});
+
+test("taler-exchange-tvg eddsa_ecdh #2", (t) => {
+ const priv_ecdhe = "W5FH9CFS3YPGSCV200GE8TH6MAACPKKGEG2A5JTFSD1HZ5RYT7Q0";
+ const pub_ecdhe = "FER9CRS2T8783TAANPZ134R704773XT0ZT1XPFXZJ9D4QX67ZN00";
+ const priv_eddsa = "MSZ1TBKC6YQ19ZFP3NTJVKWNVGFP35BBRW8FTAQJ9Z2B96VC9P4G";
+ const pub_eddsa = "Y7MKG85PBT8ZEGHF08JBVZXEV70TS0PY5Y2CMEN1WXEDN63KP1A0";
+ const key_material =
+ "G6RA58N61K7MT3WA13Q7VRTE1FQS6H43RX9HK8Z5TGAB61601GEGX51JRHHQMNKNM2R9AVC1STSGQDRHGKWVYP584YGBCTVMMJYQF30";
+
+ const myEcdhePub = ecdheGetPublic(decodeCrock(priv_ecdhe));
+ t.deepEqual(encodeCrock(myEcdhePub), pub_ecdhe);
+
+ const myEddsaPub = eddsaGetPublic(decodeCrock(priv_eddsa));
+ t.deepEqual(encodeCrock(myEddsaPub), pub_eddsa);
+
+ const myKm1 = keyExchangeEddsaEcdhe(
+ decodeCrock(priv_eddsa),
+ decodeCrock(pub_ecdhe),
+ );
+ t.deepEqual(encodeCrock(myKm1), key_material);
+
+ const myKm2 = keyExchangeEcdheEddsa(
+ decodeCrock(priv_ecdhe),
+ decodeCrock(pub_eddsa),
+ );
+ t.deepEqual(encodeCrock(myKm2), key_material);
+});
+
+test("taler CS blind c", async (t) => {
+ /**$
+ * Test Vectors:
+ {
+ "operation": "cs_blind_signing",
+ "message_hash": "KZ7540050MWFPPPJ6C0910TC15AWD6KN6GMK4YH8PY5Z2RKP7NQMHZ1NDD7JHD9CA2CZXDKYN7XRX521YERAF6N50VJZMHWPH18TCFG",
+ "cs_public_key": "1903SZ7QE1K8T4BHTJ32KDJ153SBXT22DGNQDY5NKJE535J72H2G",
+ "cs_private_key": "K43QAMEPE9KJJTX6AJZD6N4SN1N3ARVAXZ2MRNPT85FHD4QD2C60",
+ "cs_nonce": "GWPVFP9160XNADYQZ4T6S7RACB2482KG1JCY0X2Z5R52W74YXY3G",
+ "cs_r_priv_0": "B01FJCRCST8JM10K17SJXY7S7HH7T65JMFQ03H6PNYY9Z167Q1T0",
+ "cs_r_priv_1": "N3GW5X6VYSB8PY83CYNHJ3PN6TCA5N5BCS4WT2WEEQH7MTK915P0",
+ "cs_r_pub_0": "J5XFBKFP9T6BM02H6ZV6Y568PQ2K398MD339036F25XTSP1A7T3G",
+ "cs_r_pub_1": "GA2CZKJ6CWFS81ZN1T5R4GQFHF7XJV6HWHDR1JA9VATKKXQN89J0",
+ "cs_bs_alpha_0": "R06FWJ4XEK4JKKKA03JARGD0PD5JAX8DK2N6J0K8CAZZMVQEJ1T0",
+ "cs_bs_alpha_1": "13NXE2FEHJS0Q5XCWNRF4V1NC3BSAHN6BW02WZ07PG6967156HYG",
+ "cs_bs_beta_0": "T3EZP42RJQXRTJ4FTDWF18Z422VX7KFGN8GJ3QCCM1QV3N456HD0",
+ "cs_bs_beta_1": "P3MECYGCCR58QVEDSW443699CDXVT8C8W5ZT22PPNRJ363M72H6G",
+ "cs_r_pub_blind_0": "CHK7JC4SXZ4Y9RDA3881S82F7BP99H35Q361WR6RBXN5YN2ZM1M0",
+ "cs_r_pub_blind_1": "4C65R74GA9PPDX4DC2B948W96T3Z6QEENK2NDJQPNB9QBTKCT590",
+ "cs_c_0": "F288QXT67TR36E6DHE399G8J24RM6C3DP16HGMH74B6WZ1DETR10",
+ "cs_c_1": "EFK5WTN01NCVS3DZCG20MQDHRHBATRG8589BA0XSZDZ6D0HFR470",
+ "cs_blind_s": "6KZF904YZA8KK4C8X5JV57E7B84SR8TDDN9GDC8QTRRSNTHJTM4G",
+ "cs_b": "0000000",
+ "cs_sig_s": "F4ZKMFW3Q7DFN0N94KAMG2JFFHAC362T0QZ6ZCVZ73RS8P91CR70",
+ "cs_sig_R": "CHK7JC4SXZ4Y9RDA3881S82F7BP99H35Q361WR6RBXN5YN2ZM1M0",
+ "cs_c_blind_0": "6TN5454DZCHBDXFAGQFXQY37FNX6YRKW0MPFEX4TG5EHXC98M840",
+ "cs_c_blind_1": "EX6MYRZX6EC93YB4EE3M7AR3PQDYYG4092917YF29HD36X58NG0G",
+ "cs_prehash_0": "D29BBP762HEN6ZHZ5T2T6S4VMV400K9Y659M1QQZYZ0WJS3V3EJSF0FVXSCD1E99JJJMW295EY8TEE97YEGSGEQ0Q0A9DDMS2NCAG9R",
+ "cs_prehash_1": "9BYD02BC29ZF26BG88DWFCCENCS8CD8VZN76XP8JPWKTN9JS73MBCD0F36N0JSM223MRNJZACNYPMW23SGRHYVSP6BTT79GSSK5R228"
+ }
+ */
+
+ type CsBlindSignature = {
+ sBlind: Uint8Array;
+ rPubBlind: Uint8Array;
+ };
+ /**
+ * CS denomination keypair
+ */
+ const priv = "K43QAMEPE9KJJTX6AJZD6N4SN1N3ARVAXZ2MRNPT85FHD4QD2C60";
+ const pub_cmp = "1903SZ7QE1K8T4BHTJ32KDJ153SBXT22DGNQDY5NKJE535J72H2G";
+ const pub = await scalarMultBase25519(decodeCrock(priv));
+ t.deepEqual(decodeCrock(pub_cmp), pub);
+
+ const nonce = "GWPVFP9160XNADYQZ4T6S7RACB2482KG1JCY0X2Z5R52W74YXY3G";
+ const msg_hash =
+ "KZ7540050MWFPPPJ6C0910TC15AWD6KN6GMK4YH8PY5Z2RKP7NQMHZ1NDD7JHD9CA2CZXDKYN7XRX521YERAF6N50VJZMHWPH18TCFG";
+
+ /**
+ * rPub is returned from the exchange's new /csr API
+ */
+ const rPriv0 = "B01FJCRCST8JM10K17SJXY7S7HH7T65JMFQ03H6PNYY9Z167Q1T0";
+ const rPriv1 = "N3GW5X6VYSB8PY83CYNHJ3PN6TCA5N5BCS4WT2WEEQH7MTK915P0";
+ const rPub0 = await scalarMultBase25519(decodeCrock(rPriv0));
+ const rPub1 = await scalarMultBase25519(decodeCrock(rPriv1));
+
+ const rPub: [Uint8Array, Uint8Array] = [rPub0, rPub1];
+
+ t.deepEqual(
+ rPub[0],
+ decodeCrock("J5XFBKFP9T6BM02H6ZV6Y568PQ2K398MD339036F25XTSP1A7T3G"),
+ );
+ t.deepEqual(
+ rPub[1],
+ decodeCrock("GA2CZKJ6CWFS81ZN1T5R4GQFHF7XJV6HWHDR1JA9VATKKXQN89J0"),
+ );
+
+ /**
+ * Test if blinding seed derivation is deterministic
+ * In the wallet the b-seed MUST be different from the Withdraw-Nonce or Refresh Nonce!
+ * (Eg. derive two different values from coin priv) -> See CS protocols for details
+ */
+ const priv_eddsa = "1KG54M8T3X8BSFSZXCR3SQBSR7Y9P53NX61M864S7TEVMJ2XVPF0";
+ // const pub_eddsa = eddsaGetPublic(decodeCrock(priv_eddsa));
+ const bseed1 = deriveBSeed(decodeCrock(priv_eddsa), rPub);
+ const bseed2 = deriveBSeed(decodeCrock(priv_eddsa), rPub);
+ t.deepEqual(bseed1, bseed2);
+
+ /**
+ * In this scenario the nonce from the test vectors is used as b-seed and refresh.
+ * This is only used in testing to test functionality.
+ * DO NOT USE the same values for blinding-seed and nonce anywhere else.
+ *
+ * Tests whether the blinding secrets are derived as in the exchange implementation
+ */
+ const bseed = decodeCrock(nonce);
+ const secrets = deriveSecrets(bseed);
+ t.deepEqual(
+ secrets.alpha[0],
+ decodeCrock("R06FWJ4XEK4JKKKA03JARGD0PD5JAX8DK2N6J0K8CAZZMVQEJ1T0"),
+ );
+ t.deepEqual(
+ secrets.alpha[1],
+ decodeCrock("13NXE2FEHJS0Q5XCWNRF4V1NC3BSAHN6BW02WZ07PG6967156HYG"),
+ );
+ t.deepEqual(
+ secrets.beta[0],
+ decodeCrock("T3EZP42RJQXRTJ4FTDWF18Z422VX7KFGN8GJ3QCCM1QV3N456HD0"),
+ );
+ t.deepEqual(
+ secrets.beta[1],
+ decodeCrock("P3MECYGCCR58QVEDSW443699CDXVT8C8W5ZT22PPNRJ363M72H6G"),
+ );
+
+ const rBlind = await calcRBlind(pub, secrets, rPub);
+ t.deepEqual(
+ rBlind[0],
+ decodeCrock("CHK7JC4SXZ4Y9RDA3881S82F7BP99H35Q361WR6RBXN5YN2ZM1M0"),
+ );
+ t.deepEqual(
+ rBlind[1],
+ decodeCrock("4C65R74GA9PPDX4DC2B948W96T3Z6QEENK2NDJQPNB9QBTKCT590"),
+ );
+
+ const c = await csBlind(bseed, rPub, pub, decodeCrock(msg_hash));
+ t.deepEqual(
+ c[0],
+ decodeCrock("F288QXT67TR36E6DHE399G8J24RM6C3DP16HGMH74B6WZ1DETR10"),
+ );
+ t.deepEqual(
+ c[1],
+ decodeCrock("EFK5WTN01NCVS3DZCG20MQDHRHBATRG8589BA0XSZDZ6D0HFR470"),
+ );
+
+ const lMod = Array.from(
+ new Uint8Array([
+ 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x14, 0xde, 0xf9, 0xde, 0xa2, 0xf7, 0x9c, 0xd6,
+ 0x58, 0x12, 0x63, 0x1a, 0x5c, 0xf5, 0xd3, 0xed,
+ ]),
+ );
+ const L = bigint.fromArray(lMod, 256, false).toString();
+ //Lmod needs to be 2^252+27742317777372353535851937790883648493
+ if (!L.startsWith("723700")) {
+ throw new AssertionError({ message: L });
+ }
+
+ const b = 0;
+ const blindsig: CsBlindSignature = {
+ sBlind: decodeCrock("6KZF904YZA8KK4C8X5JV57E7B84SR8TDDN9GDC8QTRRSNTHJTM4G"),
+ rPubBlind: rPub[b],
+ };
+
+ const sig = await csUnblind(bseed, rPub, pub, b, blindsig);
+ t.deepEqual(
+ sig.s,
+ decodeCrock("F4ZKMFW3Q7DFN0N94KAMG2JFFHAC362T0QZ6ZCVZ73RS8P91CR70"),
+ );
+ t.deepEqual(
+ sig.rPub,
+ decodeCrock("CHK7JC4SXZ4Y9RDA3881S82F7BP99H35Q361WR6RBXN5YN2ZM1M0"),
+ );
+
+ const res = await csVerify(decodeCrock(msg_hash), sig, pub);
+ t.deepEqual(res, true);
+});
+
+test("bigint/nacl conversion", async (t) => {
+ const b1 = BigInteger(42);
+ const n1 = bigintToNaclArr(b1, 32);
+ t.is(n1[0], 42);
+ t.is(n1.length, 32);
+ const b2 = bigintFromNaclArr(n1);
+ t.true(b1.eq(b2));
+});
+
+test("taler age restriction crypto", async (t) => {
+ const priv1 = await Edx25519.keyCreate();
+ const pub1 = await Edx25519.getPublic(priv1);
+
+ const seed = getRandomBytes(32);
+
+ const priv2 = await Edx25519.privateKeyDerive(priv1, seed);
+ const pub2 = await Edx25519.publicKeyDerive(pub1, seed);
+
+ const pub2Ref = await Edx25519.getPublic(priv2);
+
+ t.deepEqual(pub2, pub2Ref);
+});
+
+test("edx signing", async (t) => {
+ const priv1 = await Edx25519.keyCreate();
+ const pub1 = await Edx25519.getPublic(priv1);
+
+ const msg = stringToBytes("hello world");
+
+ const sig = nacl.crypto_edx25519_sign_detached(msg, priv1, pub1);
+
+ t.true(nacl.crypto_edx25519_sign_detached_verify(msg, sig, pub1));
+
+ sig[0]++;
+
+ t.false(nacl.crypto_edx25519_sign_detached_verify(msg, sig, pub1));
+});
+
+test("edx test vector", async (t) => {
+ // Generated by gnunet-crypto-tvg
+ const tv = {
+ operation: "edx25519_derive",
+ priv1_edx:
+ "P0JAQ53G66M7TSGQTCFVFMPCBC7WHBRYDZGQXM8VD88C72NJANR07V1DQRAE7KSH92HZ3B62PJVRYFTVFTQM43K5AQD8R4A7HWJ3P7G",
+ pub1_edx: "4YZ6D5MGWTWCTKY4W931V4S5SW0XG7AD4A60J2Z9CSEB9WE05WB0",
+ seed: "SQ3YAVGNZ2GYER9VQAJB2M1Z903Y458HYXWBSF9S2A9YKF85R4DHYJX35YXXX82CBGFW2TRBCR1ZCWSQ7A87QW5SHC8WP9JH48P8KK8",
+ priv2_edx:
+ "GQ7NCSVNKY0QS7GQVFP2TSG6P4YN1NCK303K5TYXXBKSZ61M3R4XFZ0KA42JND6GBZRXRSJY9EX3HMMY160VQ6Y6H2NZ8H0WVQRCG1R",
+ pub2_edx: "F5X6379F0FSY87MN9210FAN84PR8KYDJQ5G5784H1N3FY12ZKAPG",
+ };
+
+ {
+ const pub1Prime = await Edx25519.getPublic(decodeCrock(tv.priv1_edx));
+ t.deepEqual(pub1Prime, decodeCrock(tv.pub1_edx));
+ }
+
+ const pub2Prime = await Edx25519.publicKeyDerive(
+ decodeCrock(tv.pub1_edx),
+ decodeCrock(tv.seed),
+ );
+ t.deepEqual(pub2Prime, decodeCrock(tv.pub2_edx));
+
+ const priv2Prime = await Edx25519.privateKeyDerive(
+ decodeCrock(tv.priv1_edx),
+ decodeCrock(tv.seed),
+ );
+ t.deepEqual(priv2Prime, decodeCrock(tv.priv2_edx));
+});
diff --git a/packages/taler-util/src/taler-crypto.ts b/packages/taler-util/src/taler-crypto.ts
@@ -0,0 +1,1378 @@
+/*
+ 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/>
+ */
+
+/**
+ * Native implementation of GNU Taler crypto.
+ */
+
+/**
+ * Imports.
+ */
+import * as nacl from "./nacl-fast.js";
+import { kdf, kdfKw } from "./kdf.js";
+import bigint from "big-integer";
+import {
+ CoinEnvelope,
+ CoinPublicKeyString,
+ DenominationPubKey,
+ DenomKeyType,
+ HashCodeString,
+} from "./taler-types.js";
+import { Logger } from "./logging.js";
+import { secretbox } from "./nacl-fast.js";
+import * as fflate from "fflate";
+import { canonicalJson } from "./helpers.js";
+
+export type Flavor<T, FlavorT extends string> = T & {
+ _flavor?: `taler.${FlavorT}`;
+};
+
+export type FlavorP<T, FlavorT extends string, S extends number> = T & {
+ _flavor?: `taler.${FlavorT}`;
+ _size?: S;
+};
+
+export function getRandomBytes(n: number): Uint8Array {
+ return nacl.randomBytes(n);
+}
+
+export function getRandomBytesF<T extends number, N extends string>(
+ n: T,
+): FlavorP<Uint8Array, N, T> {
+ return nacl.randomBytes(n);
+}
+
+const encTable = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
+
+class EncodingError extends Error {
+ constructor() {
+ super("Encoding error");
+ Object.setPrototypeOf(this, EncodingError.prototype);
+ }
+}
+
+function getValue(chr: string): number {
+ let a = chr;
+ switch (chr) {
+ case "O":
+ case "o":
+ a = "0;";
+ break;
+ case "i":
+ case "I":
+ case "l":
+ case "L":
+ a = "1";
+ break;
+ case "u":
+ case "U":
+ a = "V";
+ }
+
+ if (a >= "0" && a <= "9") {
+ return a.charCodeAt(0) - "0".charCodeAt(0);
+ }
+
+ if (a >= "a" && a <= "z") a = a.toUpperCase();
+ let dec = 0;
+ if (a >= "A" && a <= "Z") {
+ if ("I" < a) dec++;
+ if ("L" < a) dec++;
+ if ("O" < a) dec++;
+ if ("U" < a) dec++;
+ return a.charCodeAt(0) - "A".charCodeAt(0) + 10 - dec;
+ }
+ throw new EncodingError();
+}
+
+export function encodeCrock(data: ArrayBuffer): string {
+ const dataBytes = new Uint8Array(data);
+ let sb = "";
+ const size = data.byteLength;
+ let bitBuf = 0;
+ let numBits = 0;
+ let pos = 0;
+ while (pos < size || numBits > 0) {
+ if (pos < size && numBits < 5) {
+ const d = dataBytes[pos++];
+ bitBuf = (bitBuf << 8) | d;
+ numBits += 8;
+ }
+ if (numBits < 5) {
+ // zero-padding
+ bitBuf = bitBuf << (5 - numBits);
+ numBits = 5;
+ }
+ const v = (bitBuf >>> (numBits - 5)) & 31;
+ sb += encTable[v];
+ numBits -= 5;
+ }
+ return sb;
+}
+
+export function decodeCrock(encoded: string): Uint8Array {
+ const size = encoded.length;
+ let bitpos = 0;
+ let bitbuf = 0;
+ let readPosition = 0;
+ const outLen = Math.floor((size * 5) / 8);
+ const out = new Uint8Array(outLen);
+ let outPos = 0;
+
+ while (readPosition < size || bitpos > 0) {
+ if (readPosition < size) {
+ const v = getValue(encoded[readPosition++]);
+ bitbuf = (bitbuf << 5) | v;
+ bitpos += 5;
+ }
+ while (bitpos >= 8) {
+ const d = (bitbuf >>> (bitpos - 8)) & 0xff;
+ out[outPos++] = d;
+ bitpos -= 8;
+ }
+ if (readPosition == size && bitpos > 0) {
+ bitbuf = (bitbuf << (8 - bitpos)) & 0xff;
+ bitpos = bitbuf == 0 ? 0 : 8;
+ }
+ }
+ return out;
+}
+
+export function eddsaGetPublic(eddsaPriv: Uint8Array): Uint8Array {
+ const pair = nacl.crypto_sign_keyPair_fromSeed(eddsaPriv);
+ return pair.publicKey;
+}
+
+export function ecdheGetPublic(ecdhePriv: Uint8Array): Uint8Array {
+ return nacl.scalarMult_base(ecdhePriv);
+}
+
+export function keyExchangeEddsaEcdhe(
+ eddsaPriv: Uint8Array,
+ ecdhePub: Uint8Array,
+): Uint8Array {
+ const ph = nacl.hash(eddsaPriv);
+ const a = new Uint8Array(32);
+ for (let i = 0; i < 32; i++) {
+ a[i] = ph[i];
+ }
+ const x = nacl.scalarMult(a, ecdhePub);
+ return nacl.hash(x);
+}
+
+export function keyExchangeEcdheEddsa(
+ ecdhePriv: Uint8Array & MaterialEcdhePriv,
+ eddsaPub: Uint8Array & MaterialEddsaPub,
+): Uint8Array {
+ const curve25519Pub = nacl.sign_ed25519_pk_to_curve25519(eddsaPub);
+ const x = nacl.scalarMult(ecdhePriv, curve25519Pub);
+ return nacl.hash(x);
+}
+
+interface RsaPub {
+ N: bigint.BigInteger;
+ e: bigint.BigInteger;
+}
+
+/**
+ * KDF modulo a big integer.
+ */
+function kdfMod(
+ n: bigint.BigInteger,
+ ikm: Uint8Array,
+ salt: Uint8Array,
+ info: Uint8Array,
+): bigint.BigInteger {
+ const nbits = n.bitLength().toJSNumber();
+ const buflen = Math.floor((nbits - 1) / 8 + 1);
+ const mask = (1 << (8 - (buflen * 8 - nbits))) - 1;
+ let counter = 0;
+ while (true) {
+ const ctx = new Uint8Array(info.byteLength + 2);
+ ctx.set(info, 0);
+ ctx[ctx.length - 2] = (counter >>> 8) & 0xff;
+ ctx[ctx.length - 1] = counter & 0xff;
+ const buf = kdf(buflen, ikm, salt, ctx);
+ const arr = Array.from(buf);
+ arr[0] = arr[0] & mask;
+ const r = bigint.fromArray(arr, 256, false);
+ if (r.lt(n)) {
+ return r;
+ }
+ counter++;
+ }
+}
+
+function csKdfMod(
+ n: bigint.BigInteger,
+ ikm: Uint8Array,
+ salt: Uint8Array,
+ info: Uint8Array,
+): Uint8Array {
+ const nbits = n.bitLength().toJSNumber();
+ const buflen = Math.floor((nbits - 1) / 8 + 1);
+ const mask = (1 << (8 - (buflen * 8 - nbits))) - 1;
+ let counter = 0;
+ while (true) {
+ const ctx = new Uint8Array(info.byteLength + 2);
+ ctx.set(info, 0);
+ ctx[ctx.length - 2] = (counter >>> 8) & 0xff;
+ ctx[ctx.length - 1] = counter & 0xff;
+ const buf = kdf(buflen, ikm, salt, ctx);
+ const arr = Array.from(buf);
+ arr[0] = arr[0] & mask;
+ const r = bigint.fromArray(arr, 256, false);
+ if (r.lt(n)) {
+ return new Uint8Array(arr);
+ }
+ counter++;
+ }
+}
+
+// Newer versions of node have TextEncoder and TextDecoder as a global,
+// just like modern browsers.
+// In older versions of node or environments that do not have these
+// globals, they must be polyfilled (by adding them to globa/globalThis)
+// before stringToBytes or bytesToString is called the first time.
+
+let encoder: any;
+let decoder: any;
+
+export function stringToBytes(s: string): Uint8Array {
+ if (!encoder) {
+ // @ts-ignore
+ encoder = new TextEncoder();
+ }
+ return encoder.encode(s);
+}
+
+export function bytesToString(b: Uint8Array): string {
+ if (!decoder) {
+ // @ts-ignore
+ decoder = new TextDecoder();
+ }
+ return decoder.decode(b);
+}
+
+function loadBigInt(arr: Uint8Array): bigint.BigInteger {
+ return bigint.fromArray(Array.from(arr), 256, false);
+}
+
+function rsaBlindingKeyDerive(
+ rsaPub: RsaPub,
+ bks: Uint8Array,
+): bigint.BigInteger {
+ const salt = stringToBytes("Blinding KDF extractor HMAC key");
+ const info = stringToBytes("Blinding KDF");
+ return kdfMod(rsaPub.N, bks, salt, info);
+}
+
+/*
+ * Test for malicious RSA key.
+ *
+ * Assuming n is an RSA modulous and r is generated using a call to
+ * GNUNET_CRYPTO_kdf_mod_mpi, if gcd(r,n) != 1 then n must be a
+ * malicious RSA key designed to deanomize the user.
+ *
+ * @param r KDF result
+ * @param n RSA modulus of the public key
+ */
+function rsaGcdValidate(r: bigint.BigInteger, n: bigint.BigInteger): void {
+ const t = bigint.gcd(r, n);
+ if (!t.equals(bigint.one)) {
+ throw Error("malicious RSA public key");
+ }
+}
+
+function rsaFullDomainHash(hm: Uint8Array, rsaPub: RsaPub): bigint.BigInteger {
+ const info = stringToBytes("RSA-FDA FTpsW!");
+ const salt = rsaPubEncode(rsaPub);
+ const r = kdfMod(rsaPub.N, hm, salt, info);
+ rsaGcdValidate(r, rsaPub.N);
+ return r;
+}
+
+function rsaPubDecode(rsaPub: Uint8Array): RsaPub {
+ const modulusLength = (rsaPub[0] << 8) | rsaPub[1];
+ const exponentLength = (rsaPub[2] << 8) | rsaPub[3];
+ if (4 + exponentLength + modulusLength != rsaPub.length) {
+ throw Error("invalid RSA public key (format wrong)");
+ }
+ const modulus = rsaPub.slice(4, 4 + modulusLength);
+ const exponent = rsaPub.slice(
+ 4 + modulusLength,
+ 4 + modulusLength + exponentLength,
+ );
+ const res = {
+ N: loadBigInt(modulus),
+ e: loadBigInt(exponent),
+ };
+ return res;
+}
+
+function rsaPubEncode(rsaPub: RsaPub): Uint8Array {
+ const mb = rsaPub.N.toArray(256).value;
+ const eb = rsaPub.e.toArray(256).value;
+ const out = new Uint8Array(4 + mb.length + eb.length);
+ out[0] = (mb.length >>> 8) & 0xff;
+ out[1] = mb.length & 0xff;
+ out[2] = (eb.length >>> 8) & 0xff;
+ out[3] = eb.length & 0xff;
+ out.set(mb, 4);
+ out.set(eb, 4 + mb.length);
+ return out;
+}
+
+export function rsaBlind(
+ hm: Uint8Array,
+ bks: Uint8Array,
+ rsaPubEnc: Uint8Array,
+): Uint8Array {
+ const rsaPub = rsaPubDecode(rsaPubEnc);
+ const data = rsaFullDomainHash(hm, rsaPub);
+ const r = rsaBlindingKeyDerive(rsaPub, bks);
+ const r_e = r.modPow(rsaPub.e, rsaPub.N);
+ const bm = r_e.multiply(data).mod(rsaPub.N);
+ return new Uint8Array(bm.toArray(256).value);
+}
+
+export function rsaUnblind(
+ sig: Uint8Array,
+ rsaPubEnc: Uint8Array,
+ bks: Uint8Array,
+): Uint8Array {
+ const rsaPub = rsaPubDecode(rsaPubEnc);
+ const blinded_s = loadBigInt(sig);
+ const r = rsaBlindingKeyDerive(rsaPub, bks);
+ const r_inv = r.modInv(rsaPub.N);
+ const s = blinded_s.multiply(r_inv).mod(rsaPub.N);
+ return new Uint8Array(s.toArray(256).value);
+}
+
+export function rsaVerify(
+ hm: Uint8Array,
+ rsaSig: Uint8Array,
+ rsaPubEnc: Uint8Array,
+): boolean {
+ const rsaPub = rsaPubDecode(rsaPubEnc);
+ const d = rsaFullDomainHash(hm, rsaPub);
+ const sig = loadBigInt(rsaSig);
+ const sig_e = sig.modPow(rsaPub.e, rsaPub.N);
+ return sig_e.equals(d);
+}
+
+export type CsSignature = {
+ s: Uint8Array;
+ rPub: Uint8Array;
+};
+
+export type CsBlindSignature = {
+ sBlind: Uint8Array;
+ rPubBlind: Uint8Array;
+};
+
+export type CsBlindingSecrets = {
+ alpha: [Uint8Array, Uint8Array];
+ beta: [Uint8Array, Uint8Array];
+};
+
+export function typedArrayConcat(chunks: Uint8Array[]): Uint8Array {
+ let payloadLen = 0;
+ for (const c of chunks) {
+ payloadLen += c.byteLength;
+ }
+ const buf = new ArrayBuffer(payloadLen);
+ const u8buf = new Uint8Array(buf);
+ let p = 0;
+ for (const c of chunks) {
+ u8buf.set(c, p);
+ p += c.byteLength;
+ }
+ return u8buf;
+}
+
+/**
+ * Map to scalar subgroup function
+ * perform clamping as described in RFC7748
+ * @param scalar
+ */
+function mtoSS(scalar: Uint8Array): Uint8Array {
+ scalar[0] &= 248;
+ scalar[31] &= 127;
+ scalar[31] |= 64;
+ return scalar;
+}
+
+/**
+ * The function returns the CS blinding secrets from a seed
+ * @param bseed seed to derive blinding secrets
+ * @returns blinding secrets
+ */
+export function deriveSecrets(bseed: Uint8Array): CsBlindingSecrets {
+ const outLen = 130;
+ const salt = stringToBytes("alphabeta");
+ const rndout = kdf(outLen, bseed, salt);
+ const secrets: CsBlindingSecrets = {
+ alpha: [mtoSS(rndout.slice(0, 32)), mtoSS(rndout.slice(64, 96))],
+ beta: [mtoSS(rndout.slice(32, 64)), mtoSS(rndout.slice(96, 128))],
+ };
+ return secrets;
+}
+
+/**
+ * Used for testing, simple scalar multiplication with base point of Ed25519
+ * @param s scalar
+ * @returns new point sG
+ */
+export async function scalarMultBase25519(s: Uint8Array): Promise<Uint8Array> {
+ return nacl.crypto_scalarmult_ed25519_base_noclamp(s);
+}
+
+/**
+ * calculation of the blinded public point R in CS
+ * @param csPub denomination publik key
+ * @param secrets client blinding secrets
+ * @param rPub public R received from /csr API
+ */
+export async function calcRBlind(
+ csPub: Uint8Array,
+ secrets: CsBlindingSecrets,
+ rPub: [Uint8Array, Uint8Array],
+): Promise<[Uint8Array, Uint8Array]> {
+ const aG0 = nacl.crypto_scalarmult_ed25519_base_noclamp(secrets.alpha[0]);
+ const aG1 = nacl.crypto_scalarmult_ed25519_base_noclamp(secrets.alpha[1]);
+
+ const bDp0 = nacl.crypto_scalarmult_ed25519_noclamp(secrets.beta[0], csPub);
+ const bDp1 = nacl.crypto_scalarmult_ed25519_noclamp(secrets.beta[1], csPub);
+
+ const res0 = nacl.crypto_core_ed25519_add(aG0, bDp0);
+ const res1 = nacl.crypto_core_ed25519_add(aG1, bDp1);
+ return [
+ nacl.crypto_core_ed25519_add(rPub[0], res0),
+ nacl.crypto_core_ed25519_add(rPub[1], res1),
+ ];
+}
+
+/**
+ * FDH function used in CS
+ * @param hm message hash
+ * @param rPub public R included in FDH
+ * @param csPub denomination public key as context
+ * @returns mapped Curve25519 scalar
+ */
+function csFDH(
+ hm: Uint8Array,
+ rPub: Uint8Array,
+ csPub: Uint8Array,
+): Uint8Array {
+ const lMod = Array.from(
+ new Uint8Array([
+ 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x14, 0xde, 0xf9, 0xde, 0xa2, 0xf7, 0x9c, 0xd6,
+ 0x58, 0x12, 0x63, 0x1a, 0x5c, 0xf5, 0xd3, 0xed,
+ ]),
+ );
+ const L = bigint.fromArray(lMod, 256, false);
+
+ const info = stringToBytes("Curve25519FDH");
+ const preshash = nacl.hash(typedArrayConcat([rPub, hm]));
+ return csKdfMod(L, preshash, csPub, info).reverse();
+}
+
+/**
+ * blinding seed derived from coin private key
+ * @param coinPriv private key of the corresponding coin
+ * @param rPub public R received from /csr API
+ * @returns blinding seed
+ */
+export function deriveBSeed(
+ coinPriv: Uint8Array,
+ rPub: [Uint8Array, Uint8Array],
+): Uint8Array {
+ const outLen = 32;
+ const salt = stringToBytes("b-seed");
+ const ikm = typedArrayConcat([coinPriv, rPub[0], rPub[1]]);
+ return kdf(outLen, ikm, salt);
+}
+
+/**
+ * Derive withdraw nonce, used in /csr request
+ * Note: In withdraw protocol, the nonce is chosen randomly
+ * @param coinPriv coin private key
+ * @returns nonce
+ */
+export function deriveWithdrawNonce(coinPriv: Uint8Array): Uint8Array {
+ const outLen = 32;
+ const salt = stringToBytes("n");
+ return kdf(outLen, coinPriv, salt);
+}
+
+/**
+ * Blind operation for CS signatures, used after /csr call
+ * @param bseed blinding seed to derive blinding secrets
+ * @param rPub public R received from /csr
+ * @param csPub denomination public key
+ * @param hm message to blind
+ * @returns two blinded c
+ */
+export async function csBlind(
+ bseed: Uint8Array,
+ rPub: [Uint8Array, Uint8Array],
+ csPub: Uint8Array,
+ hm: Uint8Array,
+): Promise<[Uint8Array, Uint8Array]> {
+ const secrets = deriveSecrets(bseed);
+ const rPubBlind = await calcRBlind(csPub, secrets, rPub);
+ const c_0 = csFDH(hm, rPubBlind[0], csPub);
+ const c_1 = csFDH(hm, rPubBlind[1], csPub);
+ return [
+ nacl.crypto_core_ed25519_scalar_add(c_0, secrets.beta[0]),
+ nacl.crypto_core_ed25519_scalar_add(c_1, secrets.beta[1]),
+ ];
+}
+
+/**
+ * Unblind operation to unblind the signature
+ * @param bseed seed to derive secrets
+ * @param rPub public R received from /csr
+ * @param csPub denomination publick key
+ * @param b returned from exchange to select c
+ * @param csSig blinded signature
+ * @returns unblinded signature
+ */
+export async function csUnblind(
+ bseed: Uint8Array,
+ rPub: [Uint8Array, Uint8Array],
+ csPub: Uint8Array,
+ b: number,
+ csSig: CsBlindSignature,
+): Promise<CsSignature> {
+ if (b != 0 && b != 1) {
+ throw new Error();
+ }
+ const secrets = deriveSecrets(bseed);
+ const rPubDash = (await calcRBlind(csPub, secrets, rPub))[b];
+ const sig: CsSignature = {
+ s: nacl.crypto_core_ed25519_scalar_add(csSig.sBlind, secrets.alpha[b]),
+ rPub: rPubDash,
+ };
+ return sig;
+}
+
+/**
+ * Verification algorithm for CS signatures
+ * @param hm message signed
+ * @param csSig unblinded signature
+ * @param csPub denomination publick key
+ * @returns true if valid, false if invalid
+ */
+export async function csVerify(
+ hm: Uint8Array,
+ csSig: CsSignature,
+ csPub: Uint8Array,
+): Promise<boolean> {
+ const cDash = csFDH(hm, csSig.rPub, csPub);
+ const sG = nacl.crypto_scalarmult_ed25519_base_noclamp(csSig.s);
+ const cbDp = nacl.crypto_scalarmult_ed25519_noclamp(cDash, csPub);
+ const sGeq = nacl.crypto_core_ed25519_add(csSig.rPub, cbDp);
+ return nacl.verify(sG, sGeq);
+}
+
+export interface EddsaKeyPair {
+ eddsaPub: Uint8Array;
+ eddsaPriv: Uint8Array;
+}
+
+export interface EcdheKeyPair {
+ ecdhePub: Uint8Array;
+ ecdhePriv: Uint8Array;
+}
+
+export interface Edx25519Keypair {
+ edxPub: string;
+ edxPriv: string;
+}
+
+export function createEddsaKeyPair(): EddsaKeyPair {
+ const eddsaPriv = nacl.randomBytes(32);
+ const eddsaPub = eddsaGetPublic(eddsaPriv);
+ return { eddsaPriv, eddsaPub };
+}
+
+export function createEcdheKeyPair(): EcdheKeyPair {
+ const ecdhePriv = nacl.randomBytes(32);
+ const ecdhePub = ecdheGetPublic(ecdhePriv);
+ return { ecdhePriv, ecdhePub };
+}
+
+export function hash(d: Uint8Array): Uint8Array {
+ return nacl.hash(d);
+}
+
+/**
+ * Hash the input with SHA-512 and truncate the result
+ * to 32 bytes.
+ */
+export function hashTruncate32(d: Uint8Array): Uint8Array {
+ const sha512HashCode = nacl.hash(d);
+ return sha512HashCode.subarray(0, 32);
+}
+
+export function hashCoinEv(
+ coinEv: CoinEnvelope,
+ denomPubHash: HashCodeString,
+): Uint8Array {
+ const hashContext = createHashContext();
+ hashContext.update(decodeCrock(denomPubHash));
+ hashCoinEvInner(coinEv, hashContext);
+ return hashContext.finish();
+}
+
+const logger = new Logger("talerCrypto.ts");
+
+export function hashCoinEvInner(
+ coinEv: CoinEnvelope,
+ hashState: nacl.HashState,
+): void {
+ const hashInputBuf = new ArrayBuffer(4);
+ const uint8ArrayBuf = new Uint8Array(hashInputBuf);
+ const dv = new DataView(hashInputBuf);
+ dv.setUint32(0, DenomKeyType.toIntTag(coinEv.cipher));
+ hashState.update(uint8ArrayBuf);
+ switch (coinEv.cipher) {
+ case DenomKeyType.Rsa:
+ hashState.update(decodeCrock(coinEv.rsa_blinded_planchet));
+ return;
+ default:
+ throw new Error();
+ }
+}
+
+export function hashCoinPub(
+ coinPub: CoinPublicKeyString,
+ ach?: HashCodeString,
+): Uint8Array {
+ if (!ach) {
+ return hash(decodeCrock(coinPub));
+ }
+
+ return hash(typedArrayConcat([decodeCrock(coinPub), decodeCrock(ach)]));
+}
+
+/**
+ * Hash a denomination public key.
+ */
+export function hashDenomPub(pub: DenominationPubKey): Uint8Array {
+ if (pub.cipher === DenomKeyType.Rsa) {
+ const pubBuf = decodeCrock(pub.rsa_public_key);
+ const hashInputBuf = new ArrayBuffer(pubBuf.length + 4 + 4);
+ const uint8ArrayBuf = new Uint8Array(hashInputBuf);
+ const dv = new DataView(hashInputBuf);
+ dv.setUint32(0, pub.age_mask ?? 0);
+ dv.setUint32(4, DenomKeyType.toIntTag(pub.cipher));
+ uint8ArrayBuf.set(pubBuf, 8);
+ return nacl.hash(uint8ArrayBuf);
+ } else if (pub.cipher === DenomKeyType.ClauseSchnorr) {
+ const pubBuf = decodeCrock(pub.cs_public_key);
+ const hashInputBuf = new ArrayBuffer(pubBuf.length + 4 + 4);
+ const uint8ArrayBuf = new Uint8Array(hashInputBuf);
+ const dv = new DataView(hashInputBuf);
+ dv.setUint32(0, pub.age_mask ?? 0);
+ dv.setUint32(4, DenomKeyType.toIntTag(pub.cipher));
+ uint8ArrayBuf.set(pubBuf, 8);
+ return nacl.hash(uint8ArrayBuf);
+ } else {
+ throw Error(
+ `unsupported cipher (${
+ (pub as DenominationPubKey).cipher
+ }), unable to hash`,
+ );
+ }
+}
+
+export function eddsaSign(msg: Uint8Array, eddsaPriv: Uint8Array): Uint8Array {
+ const pair = nacl.crypto_sign_keyPair_fromSeed(eddsaPriv);
+ return nacl.sign_detached(msg, pair.secretKey);
+}
+
+export function eddsaVerify(
+ msg: Uint8Array,
+ sig: Uint8Array,
+ eddsaPub: Uint8Array,
+): boolean {
+ return nacl.sign_detached_verify(msg, sig, eddsaPub);
+}
+
+export function createHashContext(): nacl.HashState {
+ return new nacl.HashState();
+}
+
+export interface FreshCoin {
+ coinPub: Uint8Array;
+ coinPriv: Uint8Array;
+ bks: Uint8Array;
+ maxAge: number;
+ ageCommitmentProof: AgeCommitmentProof | undefined;
+}
+
+export function bufferForUint32(n: number): Uint8Array {
+ const arrBuf = new ArrayBuffer(4);
+ const buf = new Uint8Array(arrBuf);
+ const dv = new DataView(arrBuf);
+ dv.setUint32(0, n);
+ return buf;
+}
+
+export function bufferForUint8(n: number): Uint8Array {
+ const arrBuf = new ArrayBuffer(1);
+ const buf = new Uint8Array(arrBuf);
+ const dv = new DataView(arrBuf);
+ dv.setUint8(0, n);
+ return buf;
+}
+
+export async function setupTipPlanchet(
+ secretSeed: Uint8Array,
+ denomPub: DenominationPubKey,
+ coinNumber: number,
+): Promise<FreshCoin> {
+ const info = stringToBytes("taler-tip-coin-derivation");
+ const saltArrBuf = new ArrayBuffer(4);
+ const salt = new Uint8Array(saltArrBuf);
+ const saltDataView = new DataView(saltArrBuf);
+ saltDataView.setUint32(0, coinNumber);
+ const out = kdf(64, secretSeed, salt, info);
+ const coinPriv = out.slice(0, 32);
+ const bks = out.slice(32, 64);
+ let maybeAcp: AgeCommitmentProof | undefined;
+ if (denomPub.age_mask != 0) {
+ maybeAcp = await AgeRestriction.restrictionCommitSeeded(
+ denomPub.age_mask,
+ AgeRestriction.AGE_UNRESTRICTED,
+ secretSeed,
+ );
+ }
+ return {
+ bks,
+ coinPriv,
+ coinPub: eddsaGetPublic(coinPriv),
+ maxAge: AgeRestriction.AGE_UNRESTRICTED,
+ ageCommitmentProof: maybeAcp,
+ };
+}
+/**
+ *
+ * @param paytoUri
+ * @param salt 16-byte salt
+ * @returns
+ */
+export function hashWire(paytoUri: string, salt: string): string {
+ const r = kdf(
+ 64,
+ stringToBytes(paytoUri + "\0"),
+ decodeCrock(salt),
+ stringToBytes("merchant-wire-signature"),
+ );
+ return encodeCrock(r);
+}
+
+export enum TalerSignaturePurpose {
+ MERCHANT_TRACK_TRANSACTION = 1103,
+ WALLET_RESERVE_WITHDRAW = 1200,
+ WALLET_COIN_DEPOSIT = 1201,
+ GLOBAL_FEES = 1022,
+ MASTER_DENOMINATION_KEY_VALIDITY = 1025,
+ MASTER_WIRE_FEES = 1028,
+ MASTER_WIRE_DETAILS = 1030,
+ WALLET_COIN_MELT = 1202,
+ TEST = 4242,
+ MERCHANT_PAYMENT_OK = 1104,
+ MERCHANT_CONTRACT = 1101,
+ WALLET_COIN_RECOUP = 1203,
+ WALLET_COIN_LINK = 1204,
+ WALLET_COIN_RECOUP_REFRESH = 1206,
+ WALLET_AGE_ATTESTATION = 1207,
+ WALLET_PURSE_CREATE = 1210,
+ WALLET_PURSE_DEPOSIT = 1211,
+ WALLET_PURSE_MERGE = 1213,
+ WALLET_ACCOUNT_MERGE = 1214,
+ WALLET_PURSE_ECONTRACT = 1216,
+ EXCHANGE_CONFIRM_RECOUP = 1039,
+ EXCHANGE_CONFIRM_RECOUP_REFRESH = 1041,
+ ANASTASIS_POLICY_UPLOAD = 1400,
+ ANASTASIS_POLICY_DOWNLOAD = 1401,
+ SYNC_BACKUP_UPLOAD = 1450,
+}
+
+export const enum WalletAccountMergeFlags {
+ /**
+ * Not a legal mode!
+ */
+ None = 0,
+
+ /**
+ * We are merging a fully paid-up purse into a reserve.
+ */
+ MergeFullyPaidPurse = 1,
+
+ CreateFromPurseQuota = 2,
+
+ CreateWithPurseFee = 3,
+}
+
+export class SignaturePurposeBuilder {
+ private chunks: Uint8Array[] = [];
+
+ constructor(private purposeNum: number) {}
+
+ put(bytes: Uint8Array): SignaturePurposeBuilder {
+ this.chunks.push(Uint8Array.from(bytes));
+ return this;
+ }
+
+ build(): Uint8Array {
+ let payloadLen = 0;
+ for (const c of this.chunks) {
+ payloadLen += c.byteLength;
+ }
+ const buf = new ArrayBuffer(4 + 4 + payloadLen);
+ const u8buf = new Uint8Array(buf);
+ let p = 8;
+ for (const c of this.chunks) {
+ u8buf.set(c, p);
+ p += c.byteLength;
+ }
+ const dvbuf = new DataView(buf);
+ dvbuf.setUint32(0, payloadLen + 4 + 4);
+ dvbuf.setUint32(4, this.purposeNum);
+ return u8buf;
+ }
+}
+
+export function buildSigPS(purposeNum: number): SignaturePurposeBuilder {
+ return new SignaturePurposeBuilder(purposeNum);
+}
+
+export type OpaqueData = Flavor<Uint8Array, any>;
+export type Edx25519PublicKey = FlavorP<Uint8Array, "Edx25519PublicKey", 32>;
+export type Edx25519PrivateKey = FlavorP<Uint8Array, "Edx25519PrivateKey", 64>;
+export type Edx25519Signature = FlavorP<Uint8Array, "Edx25519Signature", 64>;
+
+export type Edx25519PublicKeyEnc = FlavorP<string, "Edx25519PublicKeyEnc", 32>;
+export type Edx25519PrivateKeyEnc = FlavorP<
+ string,
+ "Edx25519PrivateKeyEnc",
+ 64
+>;
+
+/**
+ * Convert a big integer to a fixed-size, little-endian array.
+ */
+export function bigintToNaclArr(
+ x: bigint.BigInteger,
+ size: number,
+): Uint8Array {
+ const byteArr = new Uint8Array(size);
+ const arr = x.toArray(256).value.reverse();
+ byteArr.set(arr, 0);
+ return byteArr;
+}
+
+export function bigintFromNaclArr(arr: Uint8Array): bigint.BigInteger {
+ let rev = new Uint8Array(arr);
+ rev = rev.reverse();
+ return bigint.fromArray(Array.from(rev), 256, false);
+}
+
+export namespace Edx25519 {
+ const revL = [
+ 0xed, 0xd3, 0xf5, 0x5c, 0x1a, 0x63, 0x12, 0x58, 0xd6, 0x9c, 0xf7, 0xa2,
+ 0xde, 0xf9, 0xde, 0x14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x10,
+ ];
+
+ const L = bigint.fromArray(revL.reverse(), 256, false);
+
+ export async function keyCreateFromSeed(
+ seed: OpaqueData,
+ ): Promise<Edx25519PrivateKey> {
+ return nacl.crypto_edx25519_private_key_create_from_seed(seed);
+ }
+
+ export async function keyCreate(): Promise<Edx25519PrivateKey> {
+ return nacl.crypto_edx25519_private_key_create();
+ }
+
+ export async function getPublic(
+ priv: Edx25519PrivateKey,
+ ): Promise<Edx25519PublicKey> {
+ return nacl.crypto_edx25519_get_public(priv);
+ }
+
+ export function sign(
+ msg: OpaqueData,
+ key: Edx25519PrivateKey,
+ ): Promise<Edx25519Signature> {
+ throw Error("not implemented");
+ }
+
+ async function deriveFactor(
+ pub: Edx25519PublicKey,
+ seed: OpaqueData,
+ ): Promise<OpaqueData> {
+ const res = kdfKw({
+ outputLength: 64,
+ salt: seed,
+ ikm: pub,
+ info: stringToBytes("edx25519-derivation"),
+ });
+
+ return res;
+ }
+
+ export async function privateKeyDerive(
+ priv: Edx25519PrivateKey,
+ seed: OpaqueData,
+ ): Promise<Edx25519PrivateKey> {
+ const pub = await getPublic(priv);
+ const privDec = priv;
+ const a = bigintFromNaclArr(privDec.subarray(0, 32));
+ const factorEnc = await deriveFactor(pub, seed);
+ const factorModL = bigintFromNaclArr(factorEnc).mod(L);
+
+ const aPrime = a.divide(8).multiply(factorModL).mod(L).multiply(8).mod(L);
+ const bPrime = nacl
+ .hash(typedArrayConcat([privDec.subarray(32, 64), factorEnc]))
+ .subarray(0, 32);
+
+ const newPriv = typedArrayConcat([bigintToNaclArr(aPrime, 32), bPrime]);
+
+ return newPriv;
+ }
+
+ export async function publicKeyDerive(
+ pub: Edx25519PublicKey,
+ seed: OpaqueData,
+ ): Promise<Edx25519PublicKey> {
+ const factorEnc = await deriveFactor(pub, seed);
+ const factorReduced = nacl.crypto_core_ed25519_scalar_reduce(factorEnc);
+ const res = nacl.crypto_scalarmult_ed25519_noclamp(factorReduced, pub);
+ return res;
+ }
+}
+
+export interface AgeCommitment {
+ mask: number;
+
+ /**
+ * Public keys, one for each age group specified in the age mask.
+ */
+ publicKeys: Edx25519PublicKeyEnc[];
+}
+
+export interface AgeProof {
+ /**
+ * Private keys. Typically smaller than the number of public keys,
+ * because we drop private keys from age groups that are restricted.
+ */
+ privateKeys: Edx25519PrivateKeyEnc[];
+}
+
+export interface AgeCommitmentProof {
+ commitment: AgeCommitment;
+ proof: AgeProof;
+}
+
+function invariant(cond: boolean): asserts cond {
+ if (!cond) {
+ throw Error("invariant failed");
+ }
+}
+
+export namespace AgeRestriction {
+ /**
+ * Smallest age value that the protocol considers "unrestricted".
+ */
+ export const AGE_UNRESTRICTED = 32;
+
+ export function hashCommitment(ac: AgeCommitment): HashCodeString {
+ const hc = new nacl.HashState();
+ for (const pub of ac.publicKeys) {
+ hc.update(decodeCrock(pub));
+ }
+ return encodeCrock(hc.finish().subarray(0, 32));
+ }
+
+ export function countAgeGroups(mask: number): number {
+ let count = 0;
+ let m = mask;
+ while (m > 0) {
+ count += m & 1;
+ m = m >> 1;
+ }
+ return count;
+ }
+
+ export function getAgeGroupIndex(mask: number, age: number): number {
+ invariant((mask & 1) === 1);
+ let i = 0;
+ let m = mask;
+ let a = age;
+ while (m > 0) {
+ if (a <= 0) {
+ break;
+ }
+ m = m >> 1;
+ i += m & 1;
+ a--;
+ }
+ return i;
+ }
+
+ export function ageGroupSpecToMask(ageGroupSpec: string): number {
+ throw Error("not implemented");
+ }
+
+ export async function restrictionCommit(
+ ageMask: number,
+ age: number,
+ ): Promise<AgeCommitmentProof> {
+ invariant((ageMask & 1) === 1);
+ const numPubs = countAgeGroups(ageMask) - 1;
+ const numPrivs = getAgeGroupIndex(ageMask, age);
+
+ const pubs: Edx25519PublicKey[] = [];
+ const privs: Edx25519PrivateKey[] = [];
+
+ for (let i = 0; i < numPubs; i++) {
+ const priv = await Edx25519.keyCreate();
+ const pub = await Edx25519.getPublic(priv);
+ pubs.push(pub);
+ if (i < numPrivs) {
+ privs.push(priv);
+ }
+ }
+
+ return {
+ commitment: {
+ mask: ageMask,
+ publicKeys: pubs.map((x) => encodeCrock(x)),
+ },
+ proof: {
+ privateKeys: privs.map((x) => encodeCrock(x)),
+ },
+ };
+ }
+
+ export async function restrictionCommitSeeded(
+ ageMask: number,
+ age: number,
+ seed: Uint8Array,
+ ): Promise<AgeCommitmentProof> {
+ invariant((ageMask & 1) === 1);
+ const numPubs = countAgeGroups(ageMask) - 1;
+ const numPrivs = getAgeGroupIndex(ageMask, age);
+
+ const pubs: Edx25519PublicKey[] = [];
+ const privs: Edx25519PrivateKey[] = [];
+
+ for (let i = 0; i < numPubs; i++) {
+ const privSeed = await kdfKw({
+ outputLength: 32,
+ ikm: seed,
+ info: stringToBytes("age-restriction-commit"),
+ salt: bufferForUint32(i),
+ });
+ const priv = await Edx25519.keyCreateFromSeed(privSeed);
+ const pub = await Edx25519.getPublic(priv);
+ pubs.push(pub);
+ if (i < numPrivs) {
+ privs.push(priv);
+ }
+ }
+
+ return {
+ commitment: {
+ mask: ageMask,
+ publicKeys: pubs.map((x) => encodeCrock(x)),
+ },
+ proof: {
+ privateKeys: privs.map((x) => encodeCrock(x)),
+ },
+ };
+ }
+
+ /**
+ * Check that c1 = c2*salt
+ */
+ export async function commitCompare(
+ c1: AgeCommitment,
+ c2: AgeCommitment,
+ salt: OpaqueData,
+ ): Promise<boolean> {
+ if (c1.publicKeys.length != c2.publicKeys.length) {
+ return false;
+ }
+ for (let i = 0; i < c1.publicKeys.length; i++) {
+ const k1 = decodeCrock(c1.publicKeys[i]);
+ const k2 = await Edx25519.publicKeyDerive(
+ decodeCrock(c2.publicKeys[i]),
+ salt,
+ );
+ if (k1 != k2) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ export async function commitmentDerive(
+ commitmentProof: AgeCommitmentProof,
+ salt: OpaqueData,
+ ): Promise<AgeCommitmentProof> {
+ const newPrivs: Edx25519PrivateKey[] = [];
+ const newPubs: Edx25519PublicKey[] = [];
+
+ for (const oldPub of commitmentProof.commitment.publicKeys) {
+ newPubs.push(await Edx25519.publicKeyDerive(decodeCrock(oldPub), salt));
+ }
+
+ for (const oldPriv of commitmentProof.proof.privateKeys) {
+ newPrivs.push(
+ await Edx25519.privateKeyDerive(decodeCrock(oldPriv), salt),
+ );
+ }
+
+ return {
+ commitment: {
+ mask: commitmentProof.commitment.mask,
+ publicKeys: newPubs.map((x) => encodeCrock(x)),
+ },
+ proof: {
+ privateKeys: newPrivs.map((x) => encodeCrock(x)),
+ },
+ };
+ }
+
+ export function commitmentAttest(
+ commitmentProof: AgeCommitmentProof,
+ age: number,
+ ): Edx25519Signature {
+ const d = buildSigPS(TalerSignaturePurpose.WALLET_AGE_ATTESTATION)
+ .put(bufferForUint32(commitmentProof.commitment.mask))
+ .put(bufferForUint32(age))
+ .build();
+ const group = getAgeGroupIndex(commitmentProof.commitment.mask, age);
+ if (group === 0) {
+ // No attestation required.
+ return new Uint8Array(64);
+ }
+ const priv = commitmentProof.proof.privateKeys[group - 1];
+ const pub = commitmentProof.commitment.publicKeys[group - 1];
+ const sig = nacl.crypto_edx25519_sign_detached(
+ d,
+ decodeCrock(priv),
+ decodeCrock(pub),
+ );
+ return sig;
+ }
+
+ export function commitmentVerify(
+ commitment: AgeCommitment,
+ sig: string,
+ age: number,
+ ): boolean {
+ const d = buildSigPS(TalerSignaturePurpose.WALLET_AGE_ATTESTATION)
+ .put(bufferForUint32(commitment.mask))
+ .put(bufferForUint32(age))
+ .build();
+ const group = getAgeGroupIndex(commitment.mask, age);
+ if (group === 0) {
+ // No attestation required.
+ return true;
+ }
+ const pub = commitment.publicKeys[group - 1];
+ return nacl.crypto_edx25519_sign_detached_verify(
+ d,
+ decodeCrock(sig),
+ decodeCrock(pub),
+ );
+ }
+}
+
+// FIXME: make it a branded type!
+type EncryptionNonce = FlavorP<Uint8Array, "EncryptionNonce", 24>;
+
+async function deriveKey(
+ keySeed: OpaqueData,
+ nonce: EncryptionNonce,
+ salt: string,
+): Promise<Uint8Array> {
+ return kdfKw({
+ outputLength: 32,
+ salt: nonce,
+ ikm: keySeed,
+ info: stringToBytes(salt),
+ });
+}
+
+async function encryptWithDerivedKey(
+ nonce: EncryptionNonce,
+ keySeed: OpaqueData,
+ plaintext: OpaqueData,
+ salt: string,
+): Promise<OpaqueData> {
+ const key = await deriveKey(keySeed, nonce, salt);
+ const cipherText = secretbox(plaintext, nonce, key);
+ return typedArrayConcat([nonce, cipherText]);
+}
+
+const nonceSize = 24;
+
+async function decryptWithDerivedKey(
+ ciphertext: OpaqueData,
+ keySeed: OpaqueData,
+ salt: string,
+): Promise<OpaqueData> {
+ const ctBuf = ciphertext;
+ const nonceBuf = ctBuf.slice(0, nonceSize);
+ const enc = ctBuf.slice(nonceSize);
+ const key = await deriveKey(keySeed, nonceBuf, salt);
+ const clearText = nacl.secretbox_open(enc, nonceBuf, key);
+ if (!clearText) {
+ throw Error("could not decrypt");
+ }
+ return clearText;
+}
+
+enum ContractFormatTag {
+ PaymentOffer = 0,
+ PaymentRequest = 1,
+}
+
+type MaterialEddsaPub = {
+ _materialType?: "eddsa-pub";
+ _size?: 32;
+};
+
+type MaterialEddsaPriv = {
+ _materialType?: "ecdhe-priv";
+ _size?: 32;
+};
+
+type MaterialEcdhePub = {
+ _materialType?: "ecdhe-pub";
+ _size?: 32;
+};
+
+type MaterialEcdhePriv = {
+ _materialType?: "ecdhe-priv";
+ _size?: 32;
+};
+
+type PursePublicKey = FlavorP<Uint8Array, "PursePublicKey", 32> &
+ MaterialEddsaPub;
+
+type ContractPrivateKey = FlavorP<Uint8Array, "ContractPrivateKey", 32> &
+ MaterialEcdhePriv;
+
+type MergePrivateKey = FlavorP<Uint8Array, "MergePrivateKey", 32> &
+ MaterialEddsaPriv;
+
+const mergeSalt = "p2p-merge-contract";
+const depositSalt = "p2p-deposit-contract";
+
+export function encryptContractForMerge(
+ pursePub: PursePublicKey,
+ contractPriv: ContractPrivateKey,
+ mergePriv: MergePrivateKey,
+ contractTerms: any,
+): Promise<OpaqueData> {
+ const contractTermsCanon = canonicalJson(contractTerms) + "\0";
+ const contractTermsBytes = stringToBytes(contractTermsCanon);
+ const contractTermsCompressed = fflate.zlibSync(contractTermsBytes);
+ const data = typedArrayConcat([
+ bufferForUint32(ContractFormatTag.PaymentOffer),
+ bufferForUint32(contractTermsBytes.length),
+ mergePriv,
+ contractTermsCompressed,
+ ]);
+ const key = keyExchangeEcdheEddsa(contractPriv, pursePub);
+ return encryptWithDerivedKey(getRandomBytesF(24), key, data, mergeSalt);
+}
+
+export function encryptContractForDeposit(
+ pursePub: PursePublicKey,
+ contractPriv: ContractPrivateKey,
+ contractTerms: any,
+): Promise<OpaqueData> {
+ const contractTermsCanon = canonicalJson(contractTerms) + "\0";
+ const contractTermsBytes = stringToBytes(contractTermsCanon);
+ const contractTermsCompressed = fflate.zlibSync(contractTermsBytes);
+ const data = typedArrayConcat([
+ bufferForUint32(ContractFormatTag.PaymentRequest),
+ bufferForUint32(contractTermsBytes.length),
+ contractTermsCompressed,
+ ]);
+ const key = keyExchangeEcdheEddsa(contractPriv, pursePub);
+ return encryptWithDerivedKey(getRandomBytesF(24), key, data, depositSalt);
+}
+
+export interface DecryptForMergeResult {
+ contractTerms: any;
+ mergePriv: Uint8Array;
+}
+
+export interface DecryptForDepositResult {
+ contractTerms: any;
+}
+
+export async function decryptContractForMerge(
+ enc: OpaqueData,
+ pursePub: PursePublicKey,
+ contractPriv: ContractPrivateKey,
+): Promise<DecryptForMergeResult> {
+ const key = keyExchangeEcdheEddsa(contractPriv, pursePub);
+ const dec = await decryptWithDerivedKey(enc, key, mergeSalt);
+ const mergePriv = dec.slice(8, 8 + 32);
+ const contractTermsCompressed = dec.slice(8 + 32);
+ const contractTermsBuf = fflate.unzlibSync(contractTermsCompressed);
+ // Slice of the '\0' at the end and decode to a string
+ const contractTermsString = bytesToString(
+ contractTermsBuf.slice(0, contractTermsBuf.length - 1),
+ );
+ return {
+ mergePriv: mergePriv,
+ contractTerms: JSON.parse(contractTermsString),
+ };
+}
+
+export async function decryptContractForDeposit(
+ enc: OpaqueData,
+ pursePub: PursePublicKey,
+ contractPriv: ContractPrivateKey,
+): Promise<DecryptForDepositResult> {
+ const key = keyExchangeEcdheEddsa(contractPriv, pursePub);
+ const dec = await decryptWithDerivedKey(enc, key, depositSalt);
+ const contractTermsCompressed = dec.slice(8);
+ const contractTermsBuf = fflate.unzlibSync(contractTermsCompressed);
+ // Slice of the '\0' at the end and decode to a string
+ const contractTermsString = bytesToString(
+ contractTermsBuf.slice(0, contractTermsBuf.length - 1),
+ );
+ return {
+ contractTerms: JSON.parse(contractTermsString),
+ };
+}
diff --git a/packages/taler-util/src/taler-types.ts b/packages/taler-util/src/taler-types.ts
@@ -0,0 +1,2028 @@
+/*
+ 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 and helpers for the core GNU Taler protocol.
+ *
+ * Even though the rest of the wallet uses camelCase for fields, use snake_case
+ * here, since that's the convention for the Taler JSON+HTTP API.
+ */
+
+/**
+ * Imports.
+ */
+
+import { codecForAmountString } from "./amounts.js";
+import {
+ buildCodecForObject,
+ buildCodecForUnion,
+ Codec,
+ codecForAny,
+ codecForBoolean,
+ codecForConstNumber,
+ codecForConstString,
+ codecForList,
+ codecForMap,
+ codecForNumber,
+ codecForString,
+ codecOptional,
+} from "./codec.js";
+import { strcmp } from "./helpers.js";
+import { AgeCommitmentProof, Edx25519PublicKeyEnc } from "./taler-crypto.js";
+import {
+ codecForAbsoluteTime,
+ codecForDuration,
+ codecForTimestamp,
+ TalerProtocolDuration,
+ TalerProtocolTimestamp,
+} from "./time.js";
+
+/**
+ * Denomination as found in the /keys response from the exchange.
+ */
+export class ExchangeDenomination {
+ /**
+ * Value of one coin of the denomination.
+ */
+ value: string;
+
+ /**
+ * Public signing key of the denomination.
+ */
+ denom_pub: DenominationPubKey;
+
+ /**
+ * Fee for withdrawing.
+ */
+ fee_withdraw: string;
+
+ /**
+ * Fee for depositing.
+ */
+ fee_deposit: string;
+
+ /**
+ * Fee for refreshing.
+ */
+ fee_refresh: string;
+
+ /**
+ * Fee for refunding.
+ */
+ fee_refund: string;
+
+ /**
+ * Start date from which withdraw is allowed.
+ */
+ stamp_start: TalerProtocolTimestamp;
+
+ /**
+ * End date for withdrawing.
+ */
+ stamp_expire_withdraw: TalerProtocolTimestamp;
+
+ /**
+ * Expiration date after which the exchange can forget about
+ * the currency.
+ */
+ stamp_expire_legal: TalerProtocolTimestamp;
+
+ /**
+ * Date after which the coins of this denomination can't be
+ * deposited anymore.
+ */
+ stamp_expire_deposit: TalerProtocolTimestamp;
+
+ /**
+ * Signature over the denomination information by the exchange's master
+ * signing key.
+ */
+ master_sig: string;
+}
+
+/**
+ * Signature by the auditor that a particular denomination key is audited.
+ */
+export class AuditorDenomSig {
+ /**
+ * Denomination public key's hash.
+ */
+ denom_pub_h: string;
+
+ /**
+ * The signature.
+ */
+ auditor_sig: string;
+}
+
+/**
+ * Auditor information as given by the exchange in /keys.
+ */
+export class ExchangeAuditor {
+ /**
+ * Auditor's public key.
+ */
+ auditor_pub: string;
+
+ /**
+ * Base URL of the auditor.
+ */
+ auditor_url: string;
+
+ /**
+ * List of signatures for denominations by the auditor.
+ */
+ denomination_keys: AuditorDenomSig[];
+}
+
+export type ExchangeWithdrawValue =
+ | ExchangeRsaWithdrawValue
+ | ExchangeCsWithdrawValue;
+
+export interface ExchangeRsaWithdrawValue {
+ cipher: "RSA";
+}
+
+export interface ExchangeCsWithdrawValue {
+ cipher: "CS";
+
+ /**
+ * CSR R0 value
+ */
+ r_pub_0: string;
+
+ /**
+ * CSR R1 value
+ */
+ r_pub_1: string;
+}
+
+export interface RecoupRequest {
+ /**
+ * Hashed denomination public key of the coin we want to get
+ * paid back.
+ */
+ denom_pub_hash: string;
+
+ /**
+ * Signature over the coin public key by the denomination.
+ *
+ * The string variant is for the legacy exchange protocol.
+ */
+ denom_sig: UnblindedSignature;
+
+ /**
+ * Blinding key that was used during withdraw,
+ * used to prove that we were actually withdrawing the coin.
+ */
+ coin_blind_key_secret: string;
+
+ /**
+ * Signature of TALER_RecoupRequestPS created with the coin's private key.
+ */
+ coin_sig: string;
+
+ ewv: ExchangeWithdrawValue;
+}
+
+export interface RecoupRefreshRequest {
+ /**
+ * Hashed enomination public key of the coin we want to get
+ * paid back.
+ */
+ denom_pub_hash: string;
+
+ /**
+ * Signature over the coin public key by the denomination.
+ *
+ * The string variant is for the legacy exchange protocol.
+ */
+ denom_sig: UnblindedSignature;
+
+ /**
+ * Coin's blinding factor.
+ */
+ coin_blind_key_secret: string;
+
+ /**
+ * Signature of TALER_RecoupRefreshRequestPS created with
+ * the coin's private key.
+ */
+ coin_sig: string;
+
+ ewv: ExchangeWithdrawValue;
+}
+
+/**
+ * Response that we get from the exchange for a payback request.
+ */
+export interface RecoupConfirmation {
+ /**
+ * Public key of the reserve that will receive the payback.
+ */
+ reserve_pub?: string;
+
+ /**
+ * Public key of the old coin that will receive the recoup,
+ * provided if refreshed was true.
+ */
+ old_coin_pub?: string;
+}
+
+export type UnblindedSignature = RsaUnblindedSignature;
+
+export interface RsaUnblindedSignature {
+ cipher: DenomKeyType.Rsa;
+ rsa_signature: string;
+}
+
+/**
+ * Deposit permission for a single coin.
+ */
+export interface CoinDepositPermission {
+ /**
+ * Signature by the coin.
+ */
+ coin_sig: string;
+
+ /**
+ * Public key of the coin being spend.
+ */
+ coin_pub: string;
+
+ /**
+ * Signature made by the denomination public key.
+ *
+ * The string variant is for legacy protocol support.
+ */
+
+ ub_sig: UnblindedSignature;
+
+ /**
+ * The denomination public key associated with this coin.
+ */
+ h_denom: string;
+
+ /**
+ * The amount that is subtracted from this coin with this payment.
+ */
+ contribution: string;
+
+ /**
+ * URL of the exchange this coin was withdrawn from.
+ */
+ exchange_url: string;
+
+ minimum_age_sig?: EddsaSignatureString;
+
+ age_commitment?: Edx25519PublicKeyEnc[];
+}
+
+/**
+ * Information about an exchange as stored inside a
+ * merchant's contract terms.
+ */
+export interface ExchangeHandle {
+ /**
+ * Master public signing key of the exchange.
+ */
+ master_pub: string;
+
+ /**
+ * Base URL of the exchange.
+ */
+ url: string;
+}
+
+export interface AuditorHandle {
+ /**
+ * Official name of the auditor.
+ */
+ name: string;
+
+ /**
+ * Master public signing key of the auditor.
+ */
+ auditor_pub: string;
+
+ /**
+ * Base URL of the auditor.
+ */
+ url: string;
+}
+
+// Delivery location, loosely modeled as a subset of
+// ISO20022's PostalAddress25.
+export interface Location {
+ // Nation with its own government.
+ country?: string;
+
+ // Identifies a subdivision of a country such as state, region, county.
+ country_subdivision?: string;
+
+ // Identifies a subdivision within a country sub-division.
+ district?: string;
+
+ // Name of a built-up area, with defined boundaries, and a local government.
+ town?: string;
+
+ // Specific location name within the town.
+ town_location?: string;
+
+ // Identifier consisting of a group of letters and/or numbers that
+ // is added to a postal address to assist the sorting of mail.
+ post_code?: string;
+
+ // Name of a street or thoroughfare.
+ street?: string;
+
+ // Name of the building or house.
+ building_name?: string;
+
+ // Number that identifies the position of a building on a street.
+ building_number?: string;
+
+ // Free-form address lines, should not exceed 7 elements.
+ address_lines?: string[];
+}
+
+export interface MerchantInfo {
+ name: string;
+ jurisdiction?: Location;
+ address?: Location;
+ logo?: string;
+ website?: string;
+ email?: string;
+}
+
+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?: TalerProtocolTimestamp;
+}
+
+export interface InternationalizedString {
+ [lang_tag: string]: string;
+}
+
+/**
+ * Contract terms from a merchant.
+ */
+export interface ContractTerms {
+ /**
+ * Hash of the merchant's wire details.
+ */
+ h_wire: string;
+
+ /**
+ * Hash of the merchant's wire details.
+ */
+ auto_refund?: TalerProtocolDuration;
+
+ /**
+ * Wire method the merchant wants to use.
+ */
+ wire_method: string;
+
+ /**
+ * Human-readable short summary of the contract.
+ */
+ summary: string;
+
+ summary_i18n?: InternationalizedString;
+
+ /**
+ * Nonce used to ensure freshness.
+ */
+ nonce: string;
+
+ /**
+ * Total amount payable.
+ */
+ amount: string;
+
+ /**
+ * Auditors accepted by the merchant.
+ */
+ auditors: AuditorHandle[];
+
+ /**
+ * Deadline to pay for the contract.
+ */
+ pay_deadline: TalerProtocolTimestamp;
+
+ /**
+ * Maximum deposit fee covered by the merchant.
+ */
+ max_fee: string;
+
+ /**
+ * Information about the merchant.
+ */
+ merchant: MerchantInfo;
+
+ /**
+ * Public key of the merchant.
+ */
+ merchant_pub: string;
+
+ /**
+ * Time indicating when the order should be delivered.
+ * May be overwritten by individual products.
+ */
+ delivery_date?: TalerProtocolTimestamp;
+
+ /**
+ * Delivery location for (all!) products.
+ */
+ delivery_location?: Location;
+
+ /**
+ * List of accepted exchanges.
+ */
+ exchanges: ExchangeHandle[];
+
+ /**
+ * Products that are sold in this contract.
+ */
+ products?: Product[];
+
+ /**
+ * Deadline for refunds.
+ */
+ refund_deadline: TalerProtocolTimestamp;
+
+ /**
+ * Deadline for the wire transfer.
+ */
+ wire_transfer_deadline: TalerProtocolTimestamp;
+
+ /**
+ * Time when the contract was generated by the merchant.
+ */
+ timestamp: TalerProtocolTimestamp;
+
+ /**
+ * Order id to uniquely identify the purchase within
+ * one merchant instance.
+ */
+ order_id: string;
+
+ /**
+ * Base URL of the merchant's backend.
+ */
+ merchant_base_url: string;
+
+ /**
+ * Fulfillment URL to view the product or
+ * delivery status.
+ */
+ fulfillment_url?: string;
+
+ /**
+ * URL meant to share the shopping cart.
+ */
+ public_reorder_url?: string;
+
+ /**
+ * Plain text fulfillment message in the merchant's default language.
+ */
+ fulfillment_message?: string;
+
+ /**
+ * Internationalized fulfillment messages.
+ */
+ fulfillment_message_i18n?: InternationalizedString;
+
+ /**
+ * Share of the wire fee that must be settled with one payment.
+ */
+ wire_fee_amortization?: number;
+
+ /**
+ * Maximum wire fee that the merchant agrees to pay for.
+ */
+ max_wire_fee?: string;
+
+ minimum_age?: number;
+
+ /**
+ * Extra data, interpreted by the mechant only.
+ */
+ extra?: any;
+}
+
+/**
+ * Refund permission in the format that the merchant gives it to us.
+ */
+export interface MerchantAbortPayRefundDetails {
+ /**
+ * Amount to be refunded.
+ */
+ refund_amount: string;
+
+ /**
+ * Fee for the refund.
+ */
+ refund_fee: string;
+
+ /**
+ * Public key of the coin being refunded.
+ */
+ coin_pub: string;
+
+ /**
+ * Refund transaction ID between merchant and exchange.
+ */
+ rtransaction_id: number;
+
+ /**
+ * Exchange's key used for the signature.
+ */
+ exchange_pub?: string;
+
+ /**
+ * Exchange's signature to confirm the refund.
+ */
+ exchange_sig?: string;
+
+ /**
+ * Error replay from the exchange (if any).
+ */
+ exchange_reply?: any;
+
+ /**
+ * Error code from the exchange (if any).
+ */
+ exchange_code?: number;
+
+ /**
+ * HTTP status code of the exchange's response
+ * to the merchant's refund request.
+ */
+ exchange_http_status: number;
+}
+
+/**
+ * Response for a refund pickup or a /pay in abort mode.
+ */
+export interface MerchantRefundResponse {
+ /**
+ * Public key of the merchant
+ */
+ merchant_pub: string;
+
+ /**
+ * Contract terms hash of the contract that
+ * is being refunded.
+ */
+ h_contract_terms: string;
+
+ /**
+ * The signed refund permissions, to be sent to the exchange.
+ */
+ refunds: MerchantAbortPayRefundDetails[];
+}
+
+/**
+ * Planchet detail sent to the merchant.
+ */
+export interface TipPlanchetDetail {
+ /**
+ * Hashed denomination public key.
+ */
+ denom_pub_hash: string;
+
+ /**
+ * Coin's blinded public key.
+ */
+ coin_ev: CoinEnvelope;
+}
+
+/**
+ * Request sent to the merchant to pick up a tip.
+ */
+export interface TipPickupRequest {
+ /**
+ * Identifier of the tip.
+ */
+ tip_id: string;
+
+ /**
+ * List of planchets the wallet wants to use for the tip.
+ */
+ planchets: TipPlanchetDetail[];
+}
+
+/**
+ * Reserve signature, defined as separate class to facilitate
+ * schema validation.
+ */
+export interface MerchantBlindSigWrapperV1 {
+ /**
+ * Reserve signature.
+ */
+ blind_sig: string;
+}
+
+/**
+ * Response of the merchant
+ * to the TipPickupRequest.
+ */
+export interface MerchantTipResponseV1 {
+ /**
+ * The order of the signatures matches the planchets list.
+ */
+ blind_sigs: MerchantBlindSigWrapperV1[];
+}
+
+export interface MerchantBlindSigWrapperV2 {
+ blind_sig: BlindedDenominationSignature;
+}
+
+/**
+ * Response of the merchant
+ * to the TipPickupRequest.
+ */
+export interface MerchantTipResponseV2 {
+ /**
+ * The order of the signatures matches the planchets list.
+ */
+ blind_sigs: MerchantBlindSigWrapperV2[];
+}
+
+/**
+ * Element of the payback list that the
+ * exchange gives us in /keys.
+ */
+export class Recoup {
+ /**
+ * The hash of the denomination public key for which the payback is offered.
+ */
+ h_denom_pub: string;
+}
+
+/**
+ * Structure of one exchange signing key in the /keys response.
+ */
+export class ExchangeSignKeyJson {
+ stamp_start: TalerProtocolTimestamp;
+ stamp_expire: TalerProtocolTimestamp;
+ stamp_end: TalerProtocolTimestamp;
+ key: EddsaPublicKeyString;
+ master_sig: EddsaSignatureString;
+}
+
+/**
+ * Structure that the exchange gives us in /keys.
+ */
+export class ExchangeKeysJson {
+ /**
+ * List of offered denominations.
+ */
+ denoms: ExchangeDenomination[];
+
+ /**
+ * The exchange's master public key.
+ */
+ master_public_key: string;
+
+ /**
+ * The list of auditors (partially) auditing the exchange.
+ */
+ auditors: ExchangeAuditor[];
+
+ /**
+ * Timestamp when this response was issued.
+ */
+ list_issue_date: TalerProtocolTimestamp;
+
+ /**
+ * List of revoked denominations.
+ */
+ recoup?: Recoup[];
+
+ /**
+ * Short-lived signing keys used to sign online
+ * responses.
+ */
+ signkeys: ExchangeSignKeyJson[];
+
+ /**
+ * Protocol version.
+ */
+ version: string;
+
+ reserve_closing_delay: TalerProtocolDuration;
+
+ global_fees: GlobalFees[];
+}
+
+export interface GlobalFees {
+ // What date (inclusive) does these fees go into effect?
+ start_date: TalerProtocolTimestamp;
+
+ // What date (exclusive) does this fees stop going into effect?
+ end_date: TalerProtocolTimestamp;
+
+ // KYC fee, charged when a user wants to create an account.
+ // The first year of the account_annual_fee after the KYC is
+ // always included.
+ kyc_fee: AmountString;
+
+ // Account history fee, charged when a user wants to
+ // obtain a reserve/account history.
+ history_fee: AmountString;
+
+ // Annual fee charged for having an open account at the
+ // exchange. Charged to the account. If the account
+ // balance is insufficient to cover this fee, the account
+ // is automatically deleted/closed. (Note that the exchange
+ // will keep the account history around for longer for
+ // regulatory reasons.)
+ account_fee: AmountString;
+
+ // Purse fee, charged only if a purse is abandoned
+ // and was not covered by the account limit.
+ purse_fee: AmountString;
+
+ // How long will the exchange preserve the account history?
+ // After an account was deleted/closed, the exchange will
+ // retain the account history for legal reasons until this time.
+ history_expiration: TalerProtocolDuration;
+
+ // How long does the exchange promise to keep funds
+ // an account for which the KYC has never happened
+ // after a purse was merged into an account? Basically,
+ // after this time funds in an account without KYC are
+ // forfeit.
+ account_kyc_timeout: TalerProtocolDuration;
+
+ // Non-negative number of concurrent purses that any
+ // account holder is allowed to create without having
+ // to pay the purse_fee.
+ purse_account_limit: number;
+
+ // How long does an exchange keep a purse around after a purse
+ // has expired (or been successfully merged)? A 'GET' request
+ // for a purse will succeed until the purse expiration time
+ // plus this value.
+ purse_timeout: TalerProtocolDuration;
+
+ // Signature of TALER_GlobalFeesPS.
+ master_sig: string;
+}
+/**
+ * Wire fees as announced by the exchange.
+ */
+export class WireFeesJson {
+ /**
+ * Cost of a wire transfer.
+ */
+ wire_fee: string;
+
+ wad_fee: string;
+
+ /**
+ * Cost of clising a reserve.
+ */
+ closing_fee: string;
+
+ /**
+ * Signature made with the exchange's master key.
+ */
+ sig: string;
+
+ /**
+ * Date from which the fee applies.
+ */
+ start_date: TalerProtocolTimestamp;
+
+ /**
+ * Data after which the fee doesn't apply anymore.
+ */
+ end_date: TalerProtocolTimestamp;
+}
+
+export interface AccountInfo {
+ payto_uri: string;
+ master_sig: string;
+}
+
+export interface ExchangeWireJson {
+ accounts: AccountInfo[];
+ fees: { [methodName: string]: WireFeesJson[] };
+}
+
+/**
+ * Proposal returned from the contract URL.
+ */
+export class Proposal {
+ /**
+ * Contract terms for the propoal.
+ * Raw, un-decoded JSON object.
+ */
+ contract_terms: any;
+
+ /**
+ * Signature over contract, made by the merchant. The public key used for signing
+ * must be contract_terms.merchant_pub.
+ */
+ sig: string;
+}
+
+/**
+ * Response from the internal merchant API.
+ */
+export class CheckPaymentResponse {
+ order_status: string;
+ refunded: boolean | undefined;
+ refunded_amount: string | undefined;
+ contract_terms: any | undefined;
+ taler_pay_uri: string | undefined;
+ contract_url: string | undefined;
+}
+
+/**
+ * Response from the bank.
+ */
+export class WithdrawOperationStatusResponse {
+ selection_done: boolean;
+
+ transfer_done: boolean;
+
+ aborted: boolean;
+
+ amount: string;
+
+ sender_wire?: string;
+
+ suggested_exchange?: string;
+
+ confirm_transfer_url?: string;
+
+ wire_types: string[];
+}
+
+/**
+ * Response from the merchant.
+ */
+export class TipPickupGetResponse {
+ tip_amount: string;
+
+ exchange_url: string;
+
+ expiration: TalerProtocolTimestamp;
+}
+
+export enum DenomKeyType {
+ Rsa = "RSA",
+ ClauseSchnorr = "CS",
+}
+
+export namespace DenomKeyType {
+ export function toIntTag(t: DenomKeyType): number {
+ switch (t) {
+ case DenomKeyType.Rsa:
+ return 1;
+ case DenomKeyType.ClauseSchnorr:
+ return 2;
+ }
+ }
+}
+
+export interface RsaBlindedDenominationSignature {
+ cipher: DenomKeyType.Rsa;
+ blinded_rsa_signature: string;
+}
+
+export interface CSBlindedDenominationSignature {
+ cipher: DenomKeyType.ClauseSchnorr;
+}
+
+export type BlindedDenominationSignature =
+ | RsaBlindedDenominationSignature
+ | CSBlindedDenominationSignature;
+
+export const codecForRsaBlindedDenominationSignature = () =>
+ buildCodecForObject<RsaBlindedDenominationSignature>()
+ .property("cipher", codecForConstString(DenomKeyType.Rsa))
+ .property("blinded_rsa_signature", codecForString())
+ .build("RsaBlindedDenominationSignature");
+
+export const codecForBlindedDenominationSignature = () =>
+ buildCodecForUnion<BlindedDenominationSignature>()
+ .discriminateOn("cipher")
+ .alternative(DenomKeyType.Rsa, codecForRsaBlindedDenominationSignature())
+ .build("BlindedDenominationSignature");
+
+export class WithdrawResponse {
+ ev_sig: BlindedDenominationSignature;
+}
+
+export class WithdrawBatchResponse {
+ ev_sigs: WithdrawResponse[];
+}
+
+/**
+ * Easy to process format for the public data of coins
+ * managed by the wallet.
+ */
+export interface CoinDumpJson {
+ coins: Array<{
+ /**
+ * The coin's denomination's public key.
+ */
+ denom_pub: DenominationPubKey;
+ /**
+ * Hash of denom_pub.
+ */
+ denom_pub_hash: string;
+ /**
+ * Value of the denomination (without any fees).
+ */
+ denom_value: string;
+ /**
+ * Public key of the coin.
+ */
+ coin_pub: string;
+ /**
+ * Base URL of the exchange for the coin.
+ */
+ exchange_base_url: string;
+ /**
+ * Remaining value on the coin, to the knowledge of
+ * the wallet.
+ */
+ remaining_value: string;
+ /**
+ * Public key of the parent coin.
+ * Only present if this coin was obtained via refreshing.
+ */
+ refresh_parent_coin_pub: string | undefined;
+ /**
+ * Public key of the reserve for this coin.
+ * Only present if this coin was obtained via refreshing.
+ */
+ withdrawal_reserve_pub: string | undefined;
+ /**
+ * Is the coin suspended?
+ * Suspended coins are not considered for payments.
+ */
+ coin_suspended: boolean;
+
+ /**
+ * Information about the age restriction
+ */
+ ageCommitmentProof: AgeCommitmentProof | undefined;
+ }>;
+}
+
+export interface MerchantPayResponse {
+ sig: string;
+}
+
+export interface ExchangeMeltRequest {
+ coin_pub: CoinPublicKeyString;
+ confirm_sig: EddsaSignatureString;
+ denom_pub_hash: HashCodeString;
+ denom_sig: UnblindedSignature;
+ rc: string;
+ value_with_fee: AmountString;
+ age_commitment_hash?: HashCodeString;
+}
+
+export interface ExchangeMeltResponse {
+ /**
+ * Which of the kappa indices does the client not have to reveal.
+ */
+ noreveal_index: number;
+
+ /**
+ * Signature of TALER_RefreshMeltConfirmationPS whereby the exchange
+ * affirms the successful melt and confirming the noreveal_index
+ */
+ exchange_sig: EddsaSignatureString;
+
+ /*
+ * public EdDSA key of the exchange that was used to generate the signature.
+ * Should match one of the exchange's signing keys from /keys. Again given
+ * explicitly as the client might otherwise be confused by clock skew as to
+ * which signing key was used.
+ */
+ exchange_pub: EddsaPublicKeyString;
+
+ /*
+ * Base URL to use for operations on the refresh context
+ * (so the reveal operation). If not given,
+ * the base URL is the same as the one used for this request.
+ * Can be used if the base URL for /refreshes/ differs from that
+ * for /coins/, i.e. for load balancing. Clients SHOULD
+ * respect the refresh_base_url if provided. Any HTTP server
+ * belonging to an exchange MUST generate a 307 or 308 redirection
+ * to the correct base URL should a client uses the wrong base
+ * URL, or if the base URL has changed since the melt.
+ *
+ * When melting the same coin twice (technically allowed
+ * as the response might have been lost on the network),
+ * the exchange may return different values for the refresh_base_url.
+ */
+ refresh_base_url?: string;
+}
+
+export interface ExchangeRevealItem {
+ ev_sig: BlindedDenominationSignature;
+}
+
+export interface ExchangeRevealResponse {
+ // List of the exchange's blinded RSA signatures on the new coins.
+ ev_sigs: ExchangeRevealItem[];
+}
+
+interface MerchantOrderStatusPaid {
+ // Was the payment refunded (even partially, via refund or abort)?
+ refunded: boolean;
+
+ // Is any amount of the refund still waiting to be picked up (even partially)?
+ refund_pending: boolean;
+
+ // Amount that was refunded in total.
+ refund_amount: AmountString;
+
+ // Amount that already taken by the wallet.
+ refund_taken: AmountString;
+}
+
+interface MerchantOrderRefundResponse {
+ /**
+ * Amount that was refunded in total.
+ */
+ refund_amount: AmountString;
+
+ /**
+ * Successful refunds for this payment, empty array for none.
+ */
+ refunds: MerchantCoinRefundStatus[];
+
+ /**
+ * Public key of the merchant.
+ */
+ merchant_pub: EddsaPublicKeyString;
+}
+
+export type MerchantCoinRefundStatus =
+ | MerchantCoinRefundSuccessStatus
+ | MerchantCoinRefundFailureStatus;
+
+export interface MerchantCoinRefundSuccessStatus {
+ type: "success";
+
+ // HTTP status of the exchange request, 200 (integer) required for refund confirmations.
+ exchange_status: 200;
+
+ // the EdDSA :ref:signature (binary-only) with purpose
+ // TALER_SIGNATURE_EXCHANGE_CONFIRM_REFUND using a current signing key of the
+ // exchange affirming the successful refund
+ exchange_sig: EddsaSignatureString;
+
+ // public EdDSA key of the exchange that was used to generate the signature.
+ // Should match one of the exchange's signing keys from /keys. It is given
+ // explicitly as the client might otherwise be confused by clock skew as to
+ // which signing key was used.
+ exchange_pub: EddsaPublicKeyString;
+
+ // Refund transaction ID.
+ rtransaction_id: number;
+
+ // public key of a coin that was refunded
+ coin_pub: EddsaPublicKeyString;
+
+ // Amount that was refunded, including refund fee charged by the exchange
+ // to the customer.
+ refund_amount: AmountString;
+
+ execution_time: TalerProtocolTimestamp;
+}
+
+export interface MerchantCoinRefundFailureStatus {
+ type: "failure";
+
+ // HTTP status of the exchange request, must NOT be 200.
+ exchange_status: number;
+
+ // Taler error code from the exchange reply, if available.
+ exchange_code?: number;
+
+ // If available, HTTP reply from the exchange.
+ exchange_reply?: any;
+
+ // Refund transaction ID.
+ rtransaction_id: number;
+
+ // public key of a coin that was refunded
+ coin_pub: EddsaPublicKeyString;
+
+ // Amount that was refunded, including refund fee charged by the exchange
+ // to the customer.
+ refund_amount: AmountString;
+
+ execution_time: TalerProtocolTimestamp;
+}
+
+export interface MerchantOrderStatusUnpaid {
+ /**
+ * URI that the wallet must process to complete the payment.
+ */
+ taler_pay_uri: string;
+
+ /**
+ * Alternative order ID which was paid for already in the same session.
+ *
+ * Only given if the same product was purchased before in the same session.
+ */
+ already_paid_order_id?: string;
+}
+
+/**
+ * Response body for the following endpoint:
+ *
+ * POST {talerBankIntegrationApi}/withdrawal-operation/{wopid}
+ */
+export interface BankWithdrawalOperationPostResponse {
+ transfer_done: boolean;
+}
+
+export type DenominationPubKey = RsaDenominationPubKey | CsDenominationPubKey;
+
+export interface RsaDenominationPubKey {
+ readonly cipher: DenomKeyType.Rsa;
+ readonly rsa_public_key: string;
+ readonly age_mask: number;
+}
+
+export interface CsDenominationPubKey {
+ readonly cipher: DenomKeyType.ClauseSchnorr;
+ readonly age_mask: number;
+ readonly cs_public_key: string;
+}
+
+export namespace DenominationPubKey {
+ export function cmp(
+ p1: DenominationPubKey,
+ p2: DenominationPubKey,
+ ): -1 | 0 | 1 {
+ if (p1.cipher < p2.cipher) {
+ return -1;
+ } else if (p1.cipher > p2.cipher) {
+ return +1;
+ } else if (
+ p1.cipher === DenomKeyType.Rsa &&
+ p2.cipher === DenomKeyType.Rsa
+ ) {
+ if ((p1.age_mask ?? 0) < (p2.age_mask ?? 0)) {
+ return -1;
+ } else if ((p1.age_mask ?? 0) > (p2.age_mask ?? 0)) {
+ return 1;
+ }
+ return strcmp(p1.rsa_public_key, p2.rsa_public_key);
+ } else if (
+ p1.cipher === DenomKeyType.ClauseSchnorr &&
+ p2.cipher === DenomKeyType.ClauseSchnorr
+ ) {
+ if ((p1.age_mask ?? 0) < (p2.age_mask ?? 0)) {
+ return -1;
+ } else if ((p1.age_mask ?? 0) > (p2.age_mask ?? 0)) {
+ return 1;
+ }
+ return strcmp(p1.cs_public_key, p2.cs_public_key);
+ } else {
+ throw Error("unsupported cipher");
+ }
+ }
+}
+
+export const codecForRsaDenominationPubKey = () =>
+ buildCodecForObject<RsaDenominationPubKey>()
+ .property("cipher", codecForConstString(DenomKeyType.Rsa))
+ .property("rsa_public_key", codecForString())
+ .property("age_mask", codecForNumber())
+ .build("DenominationPubKey");
+
+export const codecForCsDenominationPubKey = () =>
+ buildCodecForObject<CsDenominationPubKey>()
+ .property("cipher", codecForConstString(DenomKeyType.ClauseSchnorr))
+ .property("cs_public_key", codecForString())
+ .property("age_mask", codecForNumber())
+ .build("CsDenominationPubKey");
+
+export const codecForDenominationPubKey = () =>
+ buildCodecForUnion<DenominationPubKey>()
+ .discriminateOn("cipher")
+ .alternative(DenomKeyType.Rsa, codecForRsaDenominationPubKey())
+ .alternative(DenomKeyType.ClauseSchnorr, codecForCsDenominationPubKey())
+ .build("DenominationPubKey");
+
+export const codecForBankWithdrawalOperationPostResponse =
+ (): Codec<BankWithdrawalOperationPostResponse> =>
+ buildCodecForObject<BankWithdrawalOperationPostResponse>()
+ .property("transfer_done", codecForBoolean())
+ .build("BankWithdrawalOperationPostResponse");
+
+export type AmountString = string;
+export type Base32String = string;
+export type EddsaSignatureString = string;
+export type EddsaPublicKeyString = string;
+export type CoinPublicKeyString = string;
+
+export const codecForDenomination = (): Codec<ExchangeDenomination> =>
+ buildCodecForObject<ExchangeDenomination>()
+ .property("value", codecForString())
+ .property("denom_pub", codecForDenominationPubKey())
+ .property("fee_withdraw", codecForString())
+ .property("fee_deposit", codecForString())
+ .property("fee_refresh", codecForString())
+ .property("fee_refund", codecForString())
+ .property("stamp_start", codecForTimestamp)
+ .property("stamp_expire_withdraw", codecForTimestamp)
+ .property("stamp_expire_legal", codecForTimestamp)
+ .property("stamp_expire_deposit", codecForTimestamp)
+ .property("master_sig", codecForString())
+ .build("Denomination");
+
+export const codecForAuditorDenomSig = (): Codec<AuditorDenomSig> =>
+ buildCodecForObject<AuditorDenomSig>()
+ .property("denom_pub_h", codecForString())
+ .property("auditor_sig", codecForString())
+ .build("AuditorDenomSig");
+
+export const codecForAuditor = (): Codec<ExchangeAuditor> =>
+ buildCodecForObject<ExchangeAuditor>()
+ .property("auditor_pub", codecForString())
+ .property("auditor_url", codecForString())
+ .property("denomination_keys", codecForList(codecForAuditorDenomSig()))
+ .build("Auditor");
+
+export const codecForExchangeHandle = (): Codec<ExchangeHandle> =>
+ buildCodecForObject<ExchangeHandle>()
+ .property("master_pub", codecForString())
+ .property("url", codecForString())
+ .build("ExchangeHandle");
+
+export const codecForAuditorHandle = (): Codec<AuditorHandle> =>
+ buildCodecForObject<AuditorHandle>()
+ .property("name", codecForString())
+ .property("auditor_pub", codecForString())
+ .property("url", codecForString())
+ .build("AuditorHandle");
+
+export const codecForLocation = (): Codec<Location> =>
+ buildCodecForObject<Location>()
+ .property("country", codecOptional(codecForString()))
+ .property("country_subdivision", codecOptional(codecForString()))
+ .property("building_name", codecOptional(codecForString()))
+ .property("building_number", codecOptional(codecForString()))
+ .property("district", codecOptional(codecForString()))
+ .property("street", codecOptional(codecForString()))
+ .property("post_code", codecOptional(codecForString()))
+ .property("town", codecOptional(codecForString()))
+ .property("town_location", codecOptional(codecForString()))
+ .property("address_lines", codecOptional(codecForList(codecForString())))
+ .build("Location");
+
+export const codecForMerchantInfo = (): Codec<MerchantInfo> =>
+ buildCodecForObject<MerchantInfo>()
+ .property("name", codecForString())
+ .property("address", codecOptional(codecForLocation()))
+ .property("jurisdiction", codecOptional(codecForLocation()))
+ .build("MerchantInfo");
+
+export const codecForTax = (): Codec<Tax> =>
+ buildCodecForObject<Tax>()
+ .property("name", codecForString())
+ .property("tax", codecForString())
+ .build("Tax");
+
+export const codecForInternationalizedString =
+ (): Codec<InternationalizedString> => codecForMap(codecForString());
+
+export const codecForProduct = (): Codec<Product> =>
+ buildCodecForObject<Product>()
+ .property("product_id", codecOptional(codecForString()))
+ .property("description", codecForString())
+ .property(
+ "description_i18n",
+ codecOptional(codecForInternationalizedString()),
+ )
+ .property("quantity", codecOptional(codecForNumber()))
+ .property("unit", codecOptional(codecForString()))
+ .property("price", codecOptional(codecForString()))
+ .build("Tax");
+
+export const codecForContractTerms = (): Codec<ContractTerms> =>
+ buildCodecForObject<ContractTerms>()
+ .property("order_id", codecForString())
+ .property("fulfillment_url", codecOptional(codecForString()))
+ .property("fulfillment_message", codecOptional(codecForString()))
+ .property(
+ "fulfillment_message_i18n",
+ codecOptional(codecForInternationalizedString()),
+ )
+ .property("merchant_base_url", codecForString())
+ .property("h_wire", codecForString())
+ .property("auto_refund", codecOptional(codecForDuration))
+ .property("wire_method", codecForString())
+ .property("summary", codecForString())
+ .property("summary_i18n", codecOptional(codecForInternationalizedString()))
+ .property("nonce", codecForString())
+ .property("amount", codecForString())
+ .property("auditors", codecForList(codecForAuditorHandle()))
+ .property("pay_deadline", codecForTimestamp)
+ .property("refund_deadline", codecForTimestamp)
+ .property("wire_transfer_deadline", codecForTimestamp)
+ .property("timestamp", codecForTimestamp)
+ .property("delivery_location", codecOptional(codecForLocation()))
+ .property("delivery_date", codecOptional(codecForTimestamp))
+ .property("max_fee", codecForString())
+ .property("max_wire_fee", codecOptional(codecForString()))
+ .property("merchant", codecForMerchantInfo())
+ .property("merchant_pub", codecForString())
+ .property("exchanges", codecForList(codecForExchangeHandle()))
+ .property("products", codecOptional(codecForList(codecForProduct())))
+ .property("extra", codecForAny())
+ .property("minimum_age", codecOptional(codecForNumber()))
+ .build("ContractTerms");
+
+export const codecForMerchantRefundPermission =
+ (): Codec<MerchantAbortPayRefundDetails> =>
+ buildCodecForObject<MerchantAbortPayRefundDetails>()
+ .property("refund_amount", codecForAmountString())
+ .property("refund_fee", codecForAmountString())
+ .property("coin_pub", codecForString())
+ .property("rtransaction_id", codecForNumber())
+ .property("exchange_http_status", codecForNumber())
+ .property("exchange_code", codecOptional(codecForNumber()))
+ .property("exchange_reply", codecOptional(codecForAny()))
+ .property("exchange_sig", codecOptional(codecForString()))
+ .property("exchange_pub", codecOptional(codecForString()))
+ .build("MerchantRefundPermission");
+
+export const codecForMerchantRefundResponse =
+ (): Codec<MerchantRefundResponse> =>
+ buildCodecForObject<MerchantRefundResponse>()
+ .property("merchant_pub", codecForString())
+ .property("h_contract_terms", codecForString())
+ .property("refunds", codecForList(codecForMerchantRefundPermission()))
+ .build("MerchantRefundResponse");
+
+export const codecForBlindSigWrapperV2 = (): Codec<MerchantBlindSigWrapperV2> =>
+ buildCodecForObject<MerchantBlindSigWrapperV2>()
+ .property("blind_sig", codecForBlindedDenominationSignature())
+ .build("MerchantBlindSigWrapperV2");
+
+export const codecForMerchantTipResponseV2 = (): Codec<MerchantTipResponseV2> =>
+ buildCodecForObject<MerchantTipResponseV2>()
+ .property("blind_sigs", codecForList(codecForBlindSigWrapperV2()))
+ .build("MerchantTipResponseV2");
+
+export const codecForRecoup = (): Codec<Recoup> =>
+ buildCodecForObject<Recoup>()
+ .property("h_denom_pub", codecForString())
+ .build("Recoup");
+
+export const codecForExchangeSigningKey = (): Codec<ExchangeSignKeyJson> =>
+ buildCodecForObject<ExchangeSignKeyJson>()
+ .property("key", codecForString())
+ .property("master_sig", codecForString())
+ .property("stamp_end", codecForTimestamp)
+ .property("stamp_start", codecForTimestamp)
+ .property("stamp_expire", codecForTimestamp)
+ .build("ExchangeSignKeyJson");
+
+export const codecForGlobalFees = (): Codec<GlobalFees> =>
+ buildCodecForObject<GlobalFees>()
+ .property("start_date", codecForTimestamp)
+ .property("end_date", codecForTimestamp)
+ .property("kyc_fee", codecForAmountString())
+ .property("history_fee", codecForAmountString())
+ .property("account_fee", codecForAmountString())
+ .property("purse_fee", codecForAmountString())
+ .property("history_expiration", codecForDuration)
+ .property("account_kyc_timeout", codecForDuration)
+ .property("purse_account_limit", codecForNumber())
+ .property("purse_timeout", codecForDuration)
+ .property("master_sig", codecForString())
+ .build("GlobalFees");
+
+export const codecForExchangeKeysJson = (): Codec<ExchangeKeysJson> =>
+ buildCodecForObject<ExchangeKeysJson>()
+ .property("denoms", codecForList(codecForDenomination()))
+ .property("master_public_key", codecForString())
+ .property("auditors", codecForList(codecForAuditor()))
+ .property("list_issue_date", codecForTimestamp)
+ .property("recoup", codecOptional(codecForList(codecForRecoup())))
+ .property("signkeys", codecForList(codecForExchangeSigningKey()))
+ .property("version", codecForString())
+ .property("reserve_closing_delay", codecForDuration)
+ .property("global_fees", codecForList(codecForGlobalFees()))
+ .build("ExchangeKeysJson");
+
+export const codecForWireFeesJson = (): Codec<WireFeesJson> =>
+ buildCodecForObject<WireFeesJson>()
+ .property("wire_fee", codecForString())
+ .property("closing_fee", codecForString())
+ .property("wad_fee", codecForString())
+ .property("sig", codecForString())
+ .property("start_date", codecForTimestamp)
+ .property("end_date", codecForTimestamp)
+ .build("WireFeesJson");
+
+export const codecForAccountInfo = (): Codec<AccountInfo> =>
+ buildCodecForObject<AccountInfo>()
+ .property("payto_uri", codecForString())
+ .property("master_sig", codecForString())
+ .build("AccountInfo");
+
+export const codecForExchangeWireJson = (): Codec<ExchangeWireJson> =>
+ buildCodecForObject<ExchangeWireJson>()
+ .property("accounts", codecForList(codecForAccountInfo()))
+ .property("fees", codecForMap(codecForList(codecForWireFeesJson())))
+ .build("ExchangeWireJson");
+
+export const codecForProposal = (): Codec<Proposal> =>
+ buildCodecForObject<Proposal>()
+ .property("contract_terms", codecForAny())
+ .property("sig", codecForString())
+ .build("Proposal");
+
+export const codecForCheckPaymentResponse = (): Codec<CheckPaymentResponse> =>
+ buildCodecForObject<CheckPaymentResponse>()
+ .property("order_status", codecForString())
+ .property("refunded", codecOptional(codecForBoolean()))
+ .property("refunded_amount", codecOptional(codecForString()))
+ .property("contract_terms", codecOptional(codecForAny()))
+ .property("taler_pay_uri", codecOptional(codecForString()))
+ .property("contract_url", codecOptional(codecForString()))
+ .build("CheckPaymentResponse");
+
+export const codecForWithdrawOperationStatusResponse =
+ (): Codec<WithdrawOperationStatusResponse> =>
+ buildCodecForObject<WithdrawOperationStatusResponse>()
+ .property("selection_done", codecForBoolean())
+ .property("transfer_done", codecForBoolean())
+ .property("aborted", codecForBoolean())
+ .property("amount", codecForString())
+ .property("sender_wire", codecOptional(codecForString()))
+ .property("suggested_exchange", codecOptional(codecForString()))
+ .property("confirm_transfer_url", codecOptional(codecForString()))
+ .property("wire_types", codecForList(codecForString()))
+ .build("WithdrawOperationStatusResponse");
+
+export const codecForTipPickupGetResponse = (): Codec<TipPickupGetResponse> =>
+ buildCodecForObject<TipPickupGetResponse>()
+ .property("tip_amount", codecForString())
+ .property("exchange_url", codecForString())
+ .property("expiration", codecForTimestamp)
+ .build("TipPickupGetResponse");
+
+export const codecForRecoupConfirmation = (): Codec<RecoupConfirmation> =>
+ buildCodecForObject<RecoupConfirmation>()
+ .property("reserve_pub", codecOptional(codecForString()))
+ .property("old_coin_pub", codecOptional(codecForString()))
+ .build("RecoupConfirmation");
+
+export const codecForWithdrawResponse = (): Codec<WithdrawResponse> =>
+ buildCodecForObject<WithdrawResponse>()
+ .property("ev_sig", codecForBlindedDenominationSignature())
+ .build("WithdrawResponse");
+
+export const codecForWithdrawBatchResponse = (): Codec<WithdrawBatchResponse> =>
+ buildCodecForObject<WithdrawBatchResponse>()
+ .property("ev_sigs", codecForList(codecForWithdrawResponse()))
+ .build("WithdrawBatchResponse");
+
+export const codecForMerchantPayResponse = (): Codec<MerchantPayResponse> =>
+ buildCodecForObject<MerchantPayResponse>()
+ .property("sig", codecForString())
+ .build("MerchantPayResponse");
+
+export const codecForExchangeMeltResponse = (): Codec<ExchangeMeltResponse> =>
+ buildCodecForObject<ExchangeMeltResponse>()
+ .property("exchange_pub", codecForString())
+ .property("exchange_sig", codecForString())
+ .property("noreveal_index", codecForNumber())
+ .property("refresh_base_url", codecOptional(codecForString()))
+ .build("ExchangeMeltResponse");
+
+export const codecForExchangeRevealItem = (): Codec<ExchangeRevealItem> =>
+ buildCodecForObject<ExchangeRevealItem>()
+ .property("ev_sig", codecForBlindedDenominationSignature())
+ .build("ExchangeRevealItem");
+
+export const codecForExchangeRevealResponse =
+ (): Codec<ExchangeRevealResponse> =>
+ buildCodecForObject<ExchangeRevealResponse>()
+ .property("ev_sigs", codecForList(codecForExchangeRevealItem()))
+ .build("ExchangeRevealResponse");
+
+export const codecForMerchantCoinRefundSuccessStatus =
+ (): Codec<MerchantCoinRefundSuccessStatus> =>
+ buildCodecForObject<MerchantCoinRefundSuccessStatus>()
+ .property("type", codecForConstString("success"))
+ .property("coin_pub", codecForString())
+ .property("exchange_status", codecForConstNumber(200))
+ .property("exchange_sig", codecForString())
+ .property("rtransaction_id", codecForNumber())
+ .property("refund_amount", codecForString())
+ .property("exchange_pub", codecForString())
+ .property("execution_time", codecForTimestamp)
+ .build("MerchantCoinRefundSuccessStatus");
+
+export const codecForMerchantCoinRefundFailureStatus =
+ (): Codec<MerchantCoinRefundFailureStatus> =>
+ buildCodecForObject<MerchantCoinRefundFailureStatus>()
+ .property("type", codecForConstString("failure"))
+ .property("coin_pub", codecForString())
+ .property("exchange_status", codecForNumber())
+ .property("rtransaction_id", codecForNumber())
+ .property("refund_amount", codecForString())
+ .property("exchange_code", codecOptional(codecForNumber()))
+ .property("exchange_reply", codecOptional(codecForAny()))
+ .property("execution_time", codecForTimestamp)
+ .build("MerchantCoinRefundFailureStatus");
+
+export const codecForMerchantCoinRefundStatus =
+ (): Codec<MerchantCoinRefundStatus> =>
+ buildCodecForUnion<MerchantCoinRefundStatus>()
+ .discriminateOn("type")
+ .alternative("success", codecForMerchantCoinRefundSuccessStatus())
+ .alternative("failure", codecForMerchantCoinRefundFailureStatus())
+ .build("MerchantCoinRefundStatus");
+
+export const codecForMerchantOrderStatusPaid =
+ (): Codec<MerchantOrderStatusPaid> =>
+ buildCodecForObject<MerchantOrderStatusPaid>()
+ .property("refund_amount", codecForString())
+ .property("refund_taken", codecForString())
+ .property("refund_pending", codecForBoolean())
+ .property("refunded", codecForBoolean())
+ .build("MerchantOrderStatusPaid");
+
+export const codecForMerchantOrderRefundPickupResponse =
+ (): Codec<MerchantOrderRefundResponse> =>
+ buildCodecForObject<MerchantOrderRefundResponse>()
+ .property("merchant_pub", codecForString())
+ .property("refund_amount", codecForString())
+ .property("refunds", codecForList(codecForMerchantCoinRefundStatus()))
+ .build("MerchantOrderRefundPickupResponse");
+
+export const codecForMerchantOrderStatusUnpaid =
+ (): Codec<MerchantOrderStatusUnpaid> =>
+ buildCodecForObject<MerchantOrderStatusUnpaid>()
+ .property("taler_pay_uri", codecForString())
+ .property("already_paid_order_id", codecOptional(codecForString()))
+ .build("MerchantOrderStatusUnpaid");
+
+export interface AbortRequest {
+ // hash of the order's contract terms (this is used to authenticate the
+ // wallet/customer in case $ORDER_ID is guessable).
+ h_contract: string;
+
+ // List of coins the wallet would like to see refunds for.
+ // (Should be limited to the coins for which the original
+ // payment succeeded, as far as the wallet knows.)
+ coins: AbortingCoin[];
+}
+
+export interface AbortingCoin {
+ // Public key of a coin for which the wallet is requesting an abort-related refund.
+ coin_pub: EddsaPublicKeyString;
+
+ // The amount to be refunded (matches the original contribution)
+ contribution: AmountString;
+
+ // URL of the exchange this coin was withdrawn from.
+ exchange_url: string;
+}
+
+export interface AbortResponse {
+ // List of refund responses about the coins that the wallet
+ // requested an abort for. In the same order as the 'coins'
+ // from the original request.
+ // The rtransaction_id is implied to be 0.
+ refunds: MerchantAbortPayRefundStatus[];
+}
+
+export const codecForMerchantAbortPayRefundSuccessStatus =
+ (): Codec<MerchantAbortPayRefundSuccessStatus> =>
+ buildCodecForObject<MerchantAbortPayRefundSuccessStatus>()
+ .property("exchange_pub", codecForString())
+ .property("exchange_sig", codecForString())
+ .property("exchange_status", codecForConstNumber(200))
+ .property("type", codecForConstString("success"))
+ .build("MerchantAbortPayRefundSuccessStatus");
+
+export const codecForMerchantAbortPayRefundFailureStatus =
+ (): Codec<MerchantAbortPayRefundFailureStatus> =>
+ buildCodecForObject<MerchantAbortPayRefundFailureStatus>()
+ .property("exchange_code", codecForNumber())
+ .property("exchange_reply", codecForAny())
+ .property("exchange_status", codecForNumber())
+ .property("type", codecForConstString("failure"))
+ .build("MerchantAbortPayRefundFailureStatus");
+
+export const codecForMerchantAbortPayRefundStatus =
+ (): Codec<MerchantAbortPayRefundStatus> =>
+ buildCodecForUnion<MerchantAbortPayRefundStatus>()
+ .discriminateOn("type")
+ .alternative("success", codecForMerchantAbortPayRefundSuccessStatus())
+ .alternative("failure", codecForMerchantAbortPayRefundFailureStatus())
+ .build("MerchantAbortPayRefundStatus");
+
+export const codecForAbortResponse = (): Codec<AbortResponse> =>
+ buildCodecForObject<AbortResponse>()
+ .property("refunds", codecForList(codecForMerchantAbortPayRefundStatus()))
+ .build("AbortResponse");
+
+export type MerchantAbortPayRefundStatus =
+ | MerchantAbortPayRefundSuccessStatus
+ | MerchantAbortPayRefundFailureStatus;
+
+// Details about why a refund failed.
+export interface MerchantAbortPayRefundFailureStatus {
+ // Used as tag for the sum type RefundStatus sum type.
+ type: "failure";
+
+ // HTTP status of the exchange request, must NOT be 200.
+ exchange_status: number;
+
+ // Taler error code from the exchange reply, if available.
+ exchange_code?: number;
+
+ // If available, HTTP reply from the exchange.
+ exchange_reply?: unknown;
+}
+
+// Additional details needed to verify the refund confirmation signature
+// (h_contract_terms and merchant_pub) are already known
+// to the wallet and thus not included.
+export interface MerchantAbortPayRefundSuccessStatus {
+ // Used as tag for the sum type MerchantCoinRefundStatus sum type.
+ type: "success";
+
+ // HTTP status of the exchange request, 200 (integer) required for refund confirmations.
+ exchange_status: 200;
+
+ // the EdDSA :ref:signature (binary-only) with purpose
+ // TALER_SIGNATURE_EXCHANGE_CONFIRM_REFUND using a current signing key of the
+ // exchange affirming the successful refund
+ exchange_sig: string;
+
+ // public EdDSA key of the exchange that was used to generate the signature.
+ // Should match one of the exchange's signing keys from /keys. It is given
+ // explicitly as the client might otherwise be confused by clock skew as to
+ // which signing key was used.
+ exchange_pub: string;
+}
+
+export interface TalerConfigResponse {
+ name: string;
+ version: string;
+ currency?: string;
+}
+
+export const codecForTalerConfigResponse = (): Codec<TalerConfigResponse> =>
+ buildCodecForObject<TalerConfigResponse>()
+ .property("name", codecForString())
+ .property("version", codecForString())
+ .property("currency", codecOptional(codecForString()))
+ .build("TalerConfigResponse");
+
+export interface FutureKeysResponse {
+ future_denoms: any[];
+
+ future_signkeys: any[];
+
+ master_pub: string;
+
+ denom_secmod_public_key: string;
+
+ // Public key of the signkey security module.
+ signkey_secmod_public_key: string;
+}
+
+export const codecForKeysManagementResponse = (): Codec<FutureKeysResponse> =>
+ buildCodecForObject<FutureKeysResponse>()
+ .property("master_pub", codecForString())
+ .property("future_signkeys", codecForList(codecForAny()))
+ .property("future_denoms", codecForList(codecForAny()))
+ .property("denom_secmod_public_key", codecForAny())
+ .property("signkey_secmod_public_key", codecForAny())
+ .build("FutureKeysResponse");
+
+export interface MerchantConfigResponse {
+ currency: string;
+ name: string;
+ version: string;
+}
+
+export const codecForMerchantConfigResponse =
+ (): Codec<MerchantConfigResponse> =>
+ buildCodecForObject<MerchantConfigResponse>()
+ .property("currency", codecForString())
+ .property("name", codecForString())
+ .property("version", codecForString())
+ .build("MerchantConfigResponse");
+
+export enum ExchangeProtocolVersion {
+ /**
+ * Current version supported by the wallet.
+ */
+ V12 = 12,
+}
+
+export enum MerchantProtocolVersion {
+ /**
+ * Current version supported by the wallet.
+ */
+ V3 = 3,
+}
+
+export type CoinEnvelope = CoinEnvelopeRsa | CoinEnvelopeCs;
+
+export interface CoinEnvelopeRsa {
+ cipher: DenomKeyType.Rsa;
+ rsa_blinded_planchet: string;
+}
+
+export interface CoinEnvelopeCs {
+ cipher: DenomKeyType.ClauseSchnorr;
+ // FIXME: add remaining fields
+}
+
+export type HashCodeString = string;
+
+export interface ExchangeWithdrawRequest {
+ denom_pub_hash: HashCodeString;
+ reserve_sig: EddsaSignatureString;
+ coin_ev: CoinEnvelope;
+}
+
+export interface ExchangeRefreshRevealRequest {
+ new_denoms_h: HashCodeString[];
+ coin_evs: CoinEnvelope[];
+ /**
+ * kappa - 1 transfer private keys (ephemeral ECDHE keys).
+ */
+ transfer_privs: string[];
+
+ transfer_pub: EddsaPublicKeyString;
+
+ link_sigs: EddsaSignatureString[];
+
+ /**
+ * Iff the corresponding denomination has support for age restriction,
+ * the client MUST provide the original age commitment, i.e. the vector
+ * of public keys.
+ */
+ old_age_commitment?: Edx25519PublicKeyEnc[];
+}
+
+export interface DepositSuccess {
+ // Optional base URL of the exchange for looking up wire transfers
+ // associated with this transaction. If not given,
+ // the base URL is the same as the one used for this request.
+ // Can be used if the base URL for /transactions/ differs from that
+ // for /coins/, i.e. for load balancing. Clients SHOULD
+ // respect the transaction_base_url if provided. Any HTTP server
+ // belonging to an exchange MUST generate a 307 or 308 redirection
+ // to the correct base URL should a client uses the wrong base
+ // URL, or if the base URL has changed since the deposit.
+ transaction_base_url?: string;
+
+ // timestamp when the deposit was received by the exchange.
+ exchange_timestamp: TalerProtocolTimestamp;
+
+ // the EdDSA signature of TALER_DepositConfirmationPS using a current
+ // signing key of the exchange affirming the successful
+ // deposit and that the exchange will transfer the funds after the refund
+ // deadline, or as soon as possible if the refund deadline is zero.
+ exchange_sig: string;
+
+ // public EdDSA key of the exchange that was used to
+ // generate the signature.
+ // Should match one of the exchange's signing keys from /keys. It is given
+ // explicitly as the client might otherwise be confused by clock skew as to
+ // which signing key was used.
+ exchange_pub: string;
+}
+
+export const codecForDepositSuccess = (): Codec<DepositSuccess> =>
+ buildCodecForObject<DepositSuccess>()
+ .property("exchange_pub", codecForString())
+ .property("exchange_sig", codecForString())
+ .property("exchange_timestamp", codecForTimestamp)
+ .property("transaction_base_url", codecOptional(codecForString()))
+ .build("DepositSuccess");
+
+export interface PurseDeposit {
+ /**
+ * Amount to be deposited, can be a fraction of the
+ * coin's total value.
+ */
+ amount: AmountString;
+
+ /**
+ * Hash of denomination RSA key with which the coin is signed.
+ */
+ denom_pub_hash: HashCodeString;
+
+ /**
+ * Exchange's unblinded RSA signature of the coin.
+ */
+ ub_sig: UnblindedSignature;
+
+ /**
+ * Age commitment for the coin, if the denomination is age-restricted.
+ */
+ age_commitment?: string[];
+
+ /**
+ * Attestation for the minimum age, if the denomination is age-restricted.
+ */
+ attest?: string;
+
+ /**
+ * Signature over TALER_PurseDepositSignaturePS
+ * of purpose TALER_SIGNATURE_WALLET_PURSE_DEPOSIT
+ * made by the customer with the
+ * coin's private key.
+ */
+ coin_sig: EddsaSignatureString;
+
+ /**
+ * Public key of the coin being deposited into the purse.
+ */
+ coin_pub: EddsaPublicKeyString;
+}
+
+export interface ExchangePurseMergeRequest {
+ // payto://-URI of the account the purse is to be merged into.
+ // Must be of the form: 'payto://taler/$EXCHANGE_URL/$RESERVE_PUB'.
+ payto_uri: string;
+
+ // EdDSA signature of the account/reserve affirming the merge
+ // over a TALER_AccountMergeSignaturePS.
+ // Must be of purpose TALER_SIGNATURE_ACCOUNT_MERGE
+ reserve_sig: EddsaSignatureString;
+
+ // EdDSA signature of the purse private key affirming the merge
+ // over a TALER_PurseMergeSignaturePS.
+ // Must be of purpose TALER_SIGNATURE_PURSE_MERGE.
+ merge_sig: EddsaSignatureString;
+
+ // Client-side timestamp of when the merge request was made.
+ merge_timestamp: TalerProtocolTimestamp;
+}
+
+export interface ExchangeGetContractResponse {
+ purse_pub: string;
+ econtract_sig: string;
+ econtract: string;
+}
+
+export const codecForExchangeGetContractResponse =
+ (): Codec<ExchangeGetContractResponse> =>
+ buildCodecForObject<ExchangeGetContractResponse>()
+ .property("purse_pub", codecForString())
+ .property("econtract_sig", codecForString())
+ .property("econtract", codecForString())
+ .build("ExchangeGetContractResponse");
+
+/**
+ * Contract terms between two wallets (as opposed to a merchant and wallet).
+ */
+export interface PeerContractTerms {
+ amount: AmountString;
+ summary: string;
+ purse_expiration: TalerProtocolTimestamp;
+}
+
+export interface EncryptedContract {
+ // Encrypted contract.
+ econtract: string;
+
+ // Signature over the (encrypted) contract.
+ econtract_sig: string;
+
+ // Ephemeral public key for the DH operation to decrypt the encrypted contract.
+ contract_pub: string;
+}
+
+/**
+ * Payload for /reserves/{reserve_pub}/purse
+ * endpoint of the exchange.
+ */
+export interface ExchangeReservePurseRequest {
+ /**
+ * Minimum amount that must be credited to the reserve, that is
+ * the total value of the purse minus the deposit fees.
+ * If the deposit fees are lower, the contribution to the
+ * reserve can be higher!
+ */
+ purse_value: AmountString;
+
+ // Minimum age required for all coins deposited into the purse.
+ min_age: number;
+
+ // Purse fee the reserve owner is willing to pay
+ // for the purse creation. Optional, if not present
+ // the purse is to be created from the purse quota
+ // of the reserve.
+ purse_fee: AmountString;
+
+ // Optional encrypted contract, in case the buyer is
+ // proposing the contract and thus establishing the
+ // purse with the payment.
+ econtract?: EncryptedContract;
+
+ // EdDSA public key used to approve merges of this purse.
+ merge_pub: EddsaPublicKeyString;
+
+ // EdDSA signature of the purse private key affirming the merge
+ // over a TALER_PurseMergeSignaturePS.
+ // Must be of purpose TALER_SIGNATURE_PURSE_MERGE.
+ merge_sig: EddsaSignatureString;
+
+ // EdDSA signature of the account/reserve affirming the merge.
+ // Must be of purpose TALER_SIGNATURE_WALLET_ACCOUNT_MERGE
+ reserve_sig: EddsaSignatureString;
+
+ // Purse public key.
+ purse_pub: EddsaPublicKeyString;
+
+ // EdDSA signature of the purse over
+ // TALER_PurseRequestSignaturePS of
+ // purpose TALER_SIGNATURE_PURSE_REQUEST
+ // confirming that the
+ // above details hold for this purse.
+ purse_sig: EddsaSignatureString;
+
+ // SHA-512 hash of the contact of the purse.
+ h_contract_terms: HashCodeString;
+
+ // Client-side timestamp of when the merge request was made.
+ merge_timestamp: TalerProtocolTimestamp;
+
+ // Indicative time by which the purse should expire
+ // if it has not been paid.
+ purse_expiration: TalerProtocolTimestamp;
+}
+
+export interface ExchangePurseDeposits {
+ // Array of coins to deposit into the purse.
+ deposits: PurseDeposit[];
+}
diff --git a/packages/taler-util/src/talerCrypto.test.ts b/packages/taler-util/src/talerCrypto.test.ts
@@ -1,431 +0,0 @@
-/*
- 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/>
- */
-
-/**
- * Imports
- */
-import test from "ava";
-import {
- encodeCrock,
- decodeCrock,
- ecdheGetPublic,
- eddsaGetPublic,
- keyExchangeEddsaEcdhe,
- keyExchangeEcdheEddsa,
- stringToBytes,
- bytesToString,
- deriveBSeed,
- csBlind,
- csUnblind,
- csVerify,
- scalarMultBase25519,
- deriveSecrets,
- calcRBlind,
- Edx25519,
- getRandomBytes,
- bigintToNaclArr,
- bigintFromNaclArr,
-} from "./talerCrypto.js";
-import { sha512, kdf } from "./kdf.js";
-import * as nacl from "./nacl-fast.js";
-import { initNodePrng } from "./prng-node.js";
-
-// Since we import nacl-fast directly (and not via index.node.ts), we need to
-// init the PRNG manually.
-initNodePrng();
-import bigint from "big-integer";
-import { AssertionError } from "assert";
-import BigInteger from "big-integer";
-
-test("encoding", (t) => {
- const s = "Hello, World";
- const encStr = encodeCrock(stringToBytes(s));
- const outBuf = decodeCrock(encStr);
- const sOut = bytesToString(outBuf);
- t.deepEqual(s, sOut);
-});
-
-test("taler-exchange-tvg hash code", (t) => {
- const input = "91JPRV3F5GG4EKJN41A62V35E8";
- const output =
- "CW96WR74JS8T53EC8GKSGD49QKH4ZNFTZXDAWMMV5GJ1E4BM6B8GPN5NVHDJ8ZVXNCW7Q4WBYCV61HCA3PZC2YJD850DT29RHHN7ESR";
-
- const myOutput = encodeCrock(sha512(decodeCrock(input)));
-
- t.deepEqual(myOutput, output);
-});
-
-test("taler-exchange-tvg ecdhe key", (t) => {
- const priv1 = "X4T4N0M8PVQXQEBW2BA7049KFSM7J437NSDFC6GDNM3N5J9367A0";
- const pub1 = "M997P494MS6A95G1P0QYWW2VNPSHSX5Q6JBY5B9YMNYWP0B50X3G";
- const priv2 = "14A0MMQ64DCV8HE0CS3WBC9DHFJAHXRGV7NEARFJPC5R5E1697E0";
- const skm =
- "NXRY2YCY7H9B6KM928ZD55WG964G59YR0CPX041DYXKBZZ85SAWNPQ8B30QRM5FMHYCXJAN0EAADJYWEF1X3PAC2AJN28626TR5A6AR";
-
- const myPub1 = nacl.scalarMult_base(decodeCrock(priv1));
- t.deepEqual(encodeCrock(myPub1), pub1);
-
- const mySkm = nacl.hash(
- nacl.scalarMult(decodeCrock(priv2), decodeCrock(pub1)),
- );
- t.deepEqual(encodeCrock(mySkm), skm);
-});
-
-test("taler-exchange-tvg eddsa key", (t) => {
- const priv = "9TM70AKDTS57AWY9JK2J4TMBTMW6K62WHHGZWYDG0VM5ABPZKD40";
- const pub = "8GSJZ649T2PXMKZC01Y4ANNBE7MF14QVK9SQEC4E46ZHKCVG8AS0";
-
- const pair = nacl.crypto_sign_keyPair_fromSeed(decodeCrock(priv));
- t.deepEqual(encodeCrock(pair.publicKey), pub);
-});
-
-test("taler-exchange-tvg kdf", (t) => {
- const salt = "94KPT83PCNS7J83KC5P78Y8";
- const ikm = "94KPT83MD1JJ0WV5CDS6AX10D5Q70XBM41NPAY90DNGQ8SBJD5GPR";
- const ctx =
- "94KPT83141HPYVKMCNW78833D1TPWTSC41GPRWVF41NPWVVQDRG62WS04XMPWSKF4WG6JVH0EHM6A82J8S1G";
- const outLen = 64;
- const out =
- "GTMR4QT05Z9WF5HKVG0WK9RPXGHSMHJNW377G9GJXCA8B0FEKPF4D27RJMSJZYWSQNTBJ5EYVV7ZW18B48Z0JVJJ80RHB706Y96Q358";
-
- const myOut = kdf(
- outLen,
- decodeCrock(ikm),
- decodeCrock(salt),
- decodeCrock(ctx),
- );
-
- t.deepEqual(encodeCrock(myOut), out);
-});
-
-test("taler-exchange-tvg eddsa_ecdh", (t) => {
- const priv_ecdhe = "4AFZWMSGTVCHZPQ0R81NWXDCK4N58G7SDBBE5KXE080Y50370JJG";
- const pub_ecdhe = "FXFN5GPAFTKVPWJDPVXQ87167S8T82T5ZV8CDYC0NH2AE14X0M30";
- const priv_eddsa = "1KG54M8T3X8BSFSZXCR3SQBSR7Y9P53NX61M864S7TEVMJ2XVPF0";
- const pub_eddsa = "7BXWKG6N224C57RTDV8XEAHR108HG78NMA995BE8QAT5GC1S7E80";
- const key_material =
- "PKZ42Z56SVK2796HG1QYBRJ6ZQM2T9QGA3JA4AAZ8G7CWK9FPX175Q9JE5P0ZAX3HWWPHAQV4DPCK10R9X3SAXHRV0WF06BHEC2ZTKR";
-
- const myEcdhePub = ecdheGetPublic(decodeCrock(priv_ecdhe));
- t.deepEqual(encodeCrock(myEcdhePub), pub_ecdhe);
-
- const myEddsaPub = eddsaGetPublic(decodeCrock(priv_eddsa));
- t.deepEqual(encodeCrock(myEddsaPub), pub_eddsa);
-
- const myKm1 = keyExchangeEddsaEcdhe(
- decodeCrock(priv_eddsa),
- decodeCrock(pub_ecdhe),
- );
- t.deepEqual(encodeCrock(myKm1), key_material);
-
- const myKm2 = keyExchangeEcdheEddsa(
- decodeCrock(priv_ecdhe),
- decodeCrock(pub_eddsa),
- );
- t.deepEqual(encodeCrock(myKm2), key_material);
-});
-
-test("incremental hashing #1", (t) => {
- const n = 1024;
- const d = nacl.randomBytes(n);
-
- const h1 = nacl.hash(d);
- const h2 = new nacl.HashState().update(d).finish();
-
- const s = new nacl.HashState();
- for (let i = 0; i < n; i++) {
- const b = new Uint8Array(1);
- b[0] = d[i];
- s.update(b);
- }
-
- const h3 = s.finish();
-
- t.deepEqual(encodeCrock(h1), encodeCrock(h2));
- t.deepEqual(encodeCrock(h1), encodeCrock(h3));
-});
-
-test("incremental hashing #2", (t) => {
- const n = 10;
- const d = nacl.randomBytes(n);
-
- const h1 = nacl.hash(d);
- const h2 = new nacl.HashState().update(d).finish();
- const s = new nacl.HashState();
- for (let i = 0; i < n; i++) {
- const b = new Uint8Array(1);
- b[0] = d[i];
- s.update(b);
- }
-
- const h3 = s.finish();
-
- t.deepEqual(encodeCrock(h1), encodeCrock(h3));
- t.deepEqual(encodeCrock(h1), encodeCrock(h2));
-});
-
-test("taler-exchange-tvg eddsa_ecdh #2", (t) => {
- const priv_ecdhe = "W5FH9CFS3YPGSCV200GE8TH6MAACPKKGEG2A5JTFSD1HZ5RYT7Q0";
- const pub_ecdhe = "FER9CRS2T8783TAANPZ134R704773XT0ZT1XPFXZJ9D4QX67ZN00";
- const priv_eddsa = "MSZ1TBKC6YQ19ZFP3NTJVKWNVGFP35BBRW8FTAQJ9Z2B96VC9P4G";
- const pub_eddsa = "Y7MKG85PBT8ZEGHF08JBVZXEV70TS0PY5Y2CMEN1WXEDN63KP1A0";
- const key_material =
- "G6RA58N61K7MT3WA13Q7VRTE1FQS6H43RX9HK8Z5TGAB61601GEGX51JRHHQMNKNM2R9AVC1STSGQDRHGKWVYP584YGBCTVMMJYQF30";
-
- const myEcdhePub = ecdheGetPublic(decodeCrock(priv_ecdhe));
- t.deepEqual(encodeCrock(myEcdhePub), pub_ecdhe);
-
- const myEddsaPub = eddsaGetPublic(decodeCrock(priv_eddsa));
- t.deepEqual(encodeCrock(myEddsaPub), pub_eddsa);
-
- const myKm1 = keyExchangeEddsaEcdhe(
- decodeCrock(priv_eddsa),
- decodeCrock(pub_ecdhe),
- );
- t.deepEqual(encodeCrock(myKm1), key_material);
-
- const myKm2 = keyExchangeEcdheEddsa(
- decodeCrock(priv_ecdhe),
- decodeCrock(pub_eddsa),
- );
- t.deepEqual(encodeCrock(myKm2), key_material);
-});
-
-test("taler CS blind c", async (t) => {
- /**$
- * Test Vectors:
- {
- "operation": "cs_blind_signing",
- "message_hash": "KZ7540050MWFPPPJ6C0910TC15AWD6KN6GMK4YH8PY5Z2RKP7NQMHZ1NDD7JHD9CA2CZXDKYN7XRX521YERAF6N50VJZMHWPH18TCFG",
- "cs_public_key": "1903SZ7QE1K8T4BHTJ32KDJ153SBXT22DGNQDY5NKJE535J72H2G",
- "cs_private_key": "K43QAMEPE9KJJTX6AJZD6N4SN1N3ARVAXZ2MRNPT85FHD4QD2C60",
- "cs_nonce": "GWPVFP9160XNADYQZ4T6S7RACB2482KG1JCY0X2Z5R52W74YXY3G",
- "cs_r_priv_0": "B01FJCRCST8JM10K17SJXY7S7HH7T65JMFQ03H6PNYY9Z167Q1T0",
- "cs_r_priv_1": "N3GW5X6VYSB8PY83CYNHJ3PN6TCA5N5BCS4WT2WEEQH7MTK915P0",
- "cs_r_pub_0": "J5XFBKFP9T6BM02H6ZV6Y568PQ2K398MD339036F25XTSP1A7T3G",
- "cs_r_pub_1": "GA2CZKJ6CWFS81ZN1T5R4GQFHF7XJV6HWHDR1JA9VATKKXQN89J0",
- "cs_bs_alpha_0": "R06FWJ4XEK4JKKKA03JARGD0PD5JAX8DK2N6J0K8CAZZMVQEJ1T0",
- "cs_bs_alpha_1": "13NXE2FEHJS0Q5XCWNRF4V1NC3BSAHN6BW02WZ07PG6967156HYG",
- "cs_bs_beta_0": "T3EZP42RJQXRTJ4FTDWF18Z422VX7KFGN8GJ3QCCM1QV3N456HD0",
- "cs_bs_beta_1": "P3MECYGCCR58QVEDSW443699CDXVT8C8W5ZT22PPNRJ363M72H6G",
- "cs_r_pub_blind_0": "CHK7JC4SXZ4Y9RDA3881S82F7BP99H35Q361WR6RBXN5YN2ZM1M0",
- "cs_r_pub_blind_1": "4C65R74GA9PPDX4DC2B948W96T3Z6QEENK2NDJQPNB9QBTKCT590",
- "cs_c_0": "F288QXT67TR36E6DHE399G8J24RM6C3DP16HGMH74B6WZ1DETR10",
- "cs_c_1": "EFK5WTN01NCVS3DZCG20MQDHRHBATRG8589BA0XSZDZ6D0HFR470",
- "cs_blind_s": "6KZF904YZA8KK4C8X5JV57E7B84SR8TDDN9GDC8QTRRSNTHJTM4G",
- "cs_b": "0000000",
- "cs_sig_s": "F4ZKMFW3Q7DFN0N94KAMG2JFFHAC362T0QZ6ZCVZ73RS8P91CR70",
- "cs_sig_R": "CHK7JC4SXZ4Y9RDA3881S82F7BP99H35Q361WR6RBXN5YN2ZM1M0",
- "cs_c_blind_0": "6TN5454DZCHBDXFAGQFXQY37FNX6YRKW0MPFEX4TG5EHXC98M840",
- "cs_c_blind_1": "EX6MYRZX6EC93YB4EE3M7AR3PQDYYG4092917YF29HD36X58NG0G",
- "cs_prehash_0": "D29BBP762HEN6ZHZ5T2T6S4VMV400K9Y659M1QQZYZ0WJS3V3EJSF0FVXSCD1E99JJJMW295EY8TEE97YEGSGEQ0Q0A9DDMS2NCAG9R",
- "cs_prehash_1": "9BYD02BC29ZF26BG88DWFCCENCS8CD8VZN76XP8JPWKTN9JS73MBCD0F36N0JSM223MRNJZACNYPMW23SGRHYVSP6BTT79GSSK5R228"
- }
- */
-
- type CsBlindSignature = {
- sBlind: Uint8Array;
- rPubBlind: Uint8Array;
- };
- /**
- * CS denomination keypair
- */
- const priv = "K43QAMEPE9KJJTX6AJZD6N4SN1N3ARVAXZ2MRNPT85FHD4QD2C60";
- const pub_cmp = "1903SZ7QE1K8T4BHTJ32KDJ153SBXT22DGNQDY5NKJE535J72H2G";
- const pub = await scalarMultBase25519(decodeCrock(priv));
- t.deepEqual(decodeCrock(pub_cmp), pub);
-
- const nonce = "GWPVFP9160XNADYQZ4T6S7RACB2482KG1JCY0X2Z5R52W74YXY3G";
- const msg_hash =
- "KZ7540050MWFPPPJ6C0910TC15AWD6KN6GMK4YH8PY5Z2RKP7NQMHZ1NDD7JHD9CA2CZXDKYN7XRX521YERAF6N50VJZMHWPH18TCFG";
-
- /**
- * rPub is returned from the exchange's new /csr API
- */
- const rPriv0 = "B01FJCRCST8JM10K17SJXY7S7HH7T65JMFQ03H6PNYY9Z167Q1T0";
- const rPriv1 = "N3GW5X6VYSB8PY83CYNHJ3PN6TCA5N5BCS4WT2WEEQH7MTK915P0";
- const rPub0 = await scalarMultBase25519(decodeCrock(rPriv0));
- const rPub1 = await scalarMultBase25519(decodeCrock(rPriv1));
-
- const rPub: [Uint8Array, Uint8Array] = [rPub0, rPub1];
-
- t.deepEqual(
- rPub[0],
- decodeCrock("J5XFBKFP9T6BM02H6ZV6Y568PQ2K398MD339036F25XTSP1A7T3G"),
- );
- t.deepEqual(
- rPub[1],
- decodeCrock("GA2CZKJ6CWFS81ZN1T5R4GQFHF7XJV6HWHDR1JA9VATKKXQN89J0"),
- );
-
- /**
- * Test if blinding seed derivation is deterministic
- * In the wallet the b-seed MUST be different from the Withdraw-Nonce or Refresh Nonce!
- * (Eg. derive two different values from coin priv) -> See CS protocols for details
- */
- const priv_eddsa = "1KG54M8T3X8BSFSZXCR3SQBSR7Y9P53NX61M864S7TEVMJ2XVPF0";
- // const pub_eddsa = eddsaGetPublic(decodeCrock(priv_eddsa));
- const bseed1 = deriveBSeed(decodeCrock(priv_eddsa), rPub);
- const bseed2 = deriveBSeed(decodeCrock(priv_eddsa), rPub);
- t.deepEqual(bseed1, bseed2);
-
- /**
- * In this scenario the nonce from the test vectors is used as b-seed and refresh.
- * This is only used in testing to test functionality.
- * DO NOT USE the same values for blinding-seed and nonce anywhere else.
- *
- * Tests whether the blinding secrets are derived as in the exchange implementation
- */
- const bseed = decodeCrock(nonce);
- const secrets = deriveSecrets(bseed);
- t.deepEqual(
- secrets.alpha[0],
- decodeCrock("R06FWJ4XEK4JKKKA03JARGD0PD5JAX8DK2N6J0K8CAZZMVQEJ1T0"),
- );
- t.deepEqual(
- secrets.alpha[1],
- decodeCrock("13NXE2FEHJS0Q5XCWNRF4V1NC3BSAHN6BW02WZ07PG6967156HYG"),
- );
- t.deepEqual(
- secrets.beta[0],
- decodeCrock("T3EZP42RJQXRTJ4FTDWF18Z422VX7KFGN8GJ3QCCM1QV3N456HD0"),
- );
- t.deepEqual(
- secrets.beta[1],
- decodeCrock("P3MECYGCCR58QVEDSW443699CDXVT8C8W5ZT22PPNRJ363M72H6G"),
- );
-
- const rBlind = await calcRBlind(pub, secrets, rPub);
- t.deepEqual(
- rBlind[0],
- decodeCrock("CHK7JC4SXZ4Y9RDA3881S82F7BP99H35Q361WR6RBXN5YN2ZM1M0"),
- );
- t.deepEqual(
- rBlind[1],
- decodeCrock("4C65R74GA9PPDX4DC2B948W96T3Z6QEENK2NDJQPNB9QBTKCT590"),
- );
-
- const c = await csBlind(bseed, rPub, pub, decodeCrock(msg_hash));
- t.deepEqual(
- c[0],
- decodeCrock("F288QXT67TR36E6DHE399G8J24RM6C3DP16HGMH74B6WZ1DETR10"),
- );
- t.deepEqual(
- c[1],
- decodeCrock("EFK5WTN01NCVS3DZCG20MQDHRHBATRG8589BA0XSZDZ6D0HFR470"),
- );
-
- const lMod = Array.from(
- new Uint8Array([
- 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
- 0x00, 0x00, 0x00, 0x00, 0x14, 0xde, 0xf9, 0xde, 0xa2, 0xf7, 0x9c, 0xd6,
- 0x58, 0x12, 0x63, 0x1a, 0x5c, 0xf5, 0xd3, 0xed,
- ]),
- );
- const L = bigint.fromArray(lMod, 256, false).toString();
- //Lmod needs to be 2^252+27742317777372353535851937790883648493
- if (!L.startsWith("723700")) {
- throw new AssertionError({ message: L });
- }
-
- const b = 0;
- const blindsig: CsBlindSignature = {
- sBlind: decodeCrock("6KZF904YZA8KK4C8X5JV57E7B84SR8TDDN9GDC8QTRRSNTHJTM4G"),
- rPubBlind: rPub[b],
- };
-
- const sig = await csUnblind(bseed, rPub, pub, b, blindsig);
- t.deepEqual(
- sig.s,
- decodeCrock("F4ZKMFW3Q7DFN0N94KAMG2JFFHAC362T0QZ6ZCVZ73RS8P91CR70"),
- );
- t.deepEqual(
- sig.rPub,
- decodeCrock("CHK7JC4SXZ4Y9RDA3881S82F7BP99H35Q361WR6RBXN5YN2ZM1M0"),
- );
-
- const res = await csVerify(decodeCrock(msg_hash), sig, pub);
- t.deepEqual(res, true);
-});
-
-test("bigint/nacl conversion", async (t) => {
- const b1 = BigInteger(42);
- const n1 = bigintToNaclArr(b1, 32);
- t.is(n1[0], 42);
- t.is(n1.length, 32);
- const b2 = bigintFromNaclArr(n1);
- t.true(b1.eq(b2));
-});
-
-test("taler age restriction crypto", async (t) => {
- const priv1 = await Edx25519.keyCreate();
- const pub1 = await Edx25519.getPublic(priv1);
-
- const seed = getRandomBytes(32);
-
- const priv2 = await Edx25519.privateKeyDerive(priv1, seed);
- const pub2 = await Edx25519.publicKeyDerive(pub1, seed);
-
- const pub2Ref = await Edx25519.getPublic(priv2);
-
- t.deepEqual(pub2, pub2Ref);
-});
-
-test("edx signing", async (t) => {
- const priv1 = await Edx25519.keyCreate();
- const pub1 = await Edx25519.getPublic(priv1);
-
- const msg = stringToBytes("hello world");
-
- const sig = nacl.crypto_edx25519_sign_detached(msg, priv1, pub1);
-
- t.true(nacl.crypto_edx25519_sign_detached_verify(msg, sig, pub1));
-
- sig[0]++;
-
- t.false(nacl.crypto_edx25519_sign_detached_verify(msg, sig, pub1));
-});
-
-test("edx test vector", async (t) => {
- // Generated by gnunet-crypto-tvg
- const tv = {
- operation: "edx25519_derive",
- priv1_edx:
- "P0JAQ53G66M7TSGQTCFVFMPCBC7WHBRYDZGQXM8VD88C72NJANR07V1DQRAE7KSH92HZ3B62PJVRYFTVFTQM43K5AQD8R4A7HWJ3P7G",
- pub1_edx: "4YZ6D5MGWTWCTKY4W931V4S5SW0XG7AD4A60J2Z9CSEB9WE05WB0",
- seed: "SQ3YAVGNZ2GYER9VQAJB2M1Z903Y458HYXWBSF9S2A9YKF85R4DHYJX35YXXX82CBGFW2TRBCR1ZCWSQ7A87QW5SHC8WP9JH48P8KK8",
- priv2_edx:
- "GQ7NCSVNKY0QS7GQVFP2TSG6P4YN1NCK303K5TYXXBKSZ61M3R4XFZ0KA42JND6GBZRXRSJY9EX3HMMY160VQ6Y6H2NZ8H0WVQRCG1R",
- pub2_edx: "F5X6379F0FSY87MN9210FAN84PR8KYDJQ5G5784H1N3FY12ZKAPG",
- };
-
- {
- const pub1Prime = await Edx25519.getPublic(decodeCrock(tv.priv1_edx));
- t.deepEqual(pub1Prime, decodeCrock(tv.pub1_edx));
- }
-
- const pub2Prime = await Edx25519.publicKeyDerive(
- decodeCrock(tv.pub1_edx),
- decodeCrock(tv.seed),
- );
- t.deepEqual(pub2Prime, decodeCrock(tv.pub2_edx));
-
- const priv2Prime = await Edx25519.privateKeyDerive(
- decodeCrock(tv.priv1_edx),
- decodeCrock(tv.seed),
- );
- t.deepEqual(priv2Prime, decodeCrock(tv.priv2_edx));
-});
diff --git a/packages/taler-util/src/talerCrypto.ts b/packages/taler-util/src/talerCrypto.ts
@@ -1,1378 +0,0 @@
-/*
- 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/>
- */
-
-/**
- * Native implementation of GNU Taler crypto.
- */
-
-/**
- * Imports.
- */
-import * as nacl from "./nacl-fast.js";
-import { kdf, kdfKw } from "./kdf.js";
-import bigint from "big-integer";
-import {
- CoinEnvelope,
- CoinPublicKeyString,
- DenominationPubKey,
- DenomKeyType,
- HashCodeString,
-} from "./talerTypes.js";
-import { Logger } from "./logging.js";
-import { secretbox } from "./nacl-fast.js";
-import * as fflate from "fflate";
-import { canonicalJson } from "./helpers.js";
-
-export type Flavor<T, FlavorT extends string> = T & {
- _flavor?: `taler.${FlavorT}`;
-};
-
-export type FlavorP<T, FlavorT extends string, S extends number> = T & {
- _flavor?: `taler.${FlavorT}`;
- _size?: S;
-};
-
-export function getRandomBytes(n: number): Uint8Array {
- return nacl.randomBytes(n);
-}
-
-export function getRandomBytesF<T extends number, N extends string>(
- n: T,
-): FlavorP<Uint8Array, N, T> {
- return nacl.randomBytes(n);
-}
-
-const encTable = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
-
-class EncodingError extends Error {
- constructor() {
- super("Encoding error");
- Object.setPrototypeOf(this, EncodingError.prototype);
- }
-}
-
-function getValue(chr: string): number {
- let a = chr;
- switch (chr) {
- case "O":
- case "o":
- a = "0;";
- break;
- case "i":
- case "I":
- case "l":
- case "L":
- a = "1";
- break;
- case "u":
- case "U":
- a = "V";
- }
-
- if (a >= "0" && a <= "9") {
- return a.charCodeAt(0) - "0".charCodeAt(0);
- }
-
- if (a >= "a" && a <= "z") a = a.toUpperCase();
- let dec = 0;
- if (a >= "A" && a <= "Z") {
- if ("I" < a) dec++;
- if ("L" < a) dec++;
- if ("O" < a) dec++;
- if ("U" < a) dec++;
- return a.charCodeAt(0) - "A".charCodeAt(0) + 10 - dec;
- }
- throw new EncodingError();
-}
-
-export function encodeCrock(data: ArrayBuffer): string {
- const dataBytes = new Uint8Array(data);
- let sb = "";
- const size = data.byteLength;
- let bitBuf = 0;
- let numBits = 0;
- let pos = 0;
- while (pos < size || numBits > 0) {
- if (pos < size && numBits < 5) {
- const d = dataBytes[pos++];
- bitBuf = (bitBuf << 8) | d;
- numBits += 8;
- }
- if (numBits < 5) {
- // zero-padding
- bitBuf = bitBuf << (5 - numBits);
- numBits = 5;
- }
- const v = (bitBuf >>> (numBits - 5)) & 31;
- sb += encTable[v];
- numBits -= 5;
- }
- return sb;
-}
-
-export function decodeCrock(encoded: string): Uint8Array {
- const size = encoded.length;
- let bitpos = 0;
- let bitbuf = 0;
- let readPosition = 0;
- const outLen = Math.floor((size * 5) / 8);
- const out = new Uint8Array(outLen);
- let outPos = 0;
-
- while (readPosition < size || bitpos > 0) {
- if (readPosition < size) {
- const v = getValue(encoded[readPosition++]);
- bitbuf = (bitbuf << 5) | v;
- bitpos += 5;
- }
- while (bitpos >= 8) {
- const d = (bitbuf >>> (bitpos - 8)) & 0xff;
- out[outPos++] = d;
- bitpos -= 8;
- }
- if (readPosition == size && bitpos > 0) {
- bitbuf = (bitbuf << (8 - bitpos)) & 0xff;
- bitpos = bitbuf == 0 ? 0 : 8;
- }
- }
- return out;
-}
-
-export function eddsaGetPublic(eddsaPriv: Uint8Array): Uint8Array {
- const pair = nacl.crypto_sign_keyPair_fromSeed(eddsaPriv);
- return pair.publicKey;
-}
-
-export function ecdheGetPublic(ecdhePriv: Uint8Array): Uint8Array {
- return nacl.scalarMult_base(ecdhePriv);
-}
-
-export function keyExchangeEddsaEcdhe(
- eddsaPriv: Uint8Array,
- ecdhePub: Uint8Array,
-): Uint8Array {
- const ph = nacl.hash(eddsaPriv);
- const a = new Uint8Array(32);
- for (let i = 0; i < 32; i++) {
- a[i] = ph[i];
- }
- const x = nacl.scalarMult(a, ecdhePub);
- return nacl.hash(x);
-}
-
-export function keyExchangeEcdheEddsa(
- ecdhePriv: Uint8Array & MaterialEcdhePriv,
- eddsaPub: Uint8Array & MaterialEddsaPub,
-): Uint8Array {
- const curve25519Pub = nacl.sign_ed25519_pk_to_curve25519(eddsaPub);
- const x = nacl.scalarMult(ecdhePriv, curve25519Pub);
- return nacl.hash(x);
-}
-
-interface RsaPub {
- N: bigint.BigInteger;
- e: bigint.BigInteger;
-}
-
-/**
- * KDF modulo a big integer.
- */
-function kdfMod(
- n: bigint.BigInteger,
- ikm: Uint8Array,
- salt: Uint8Array,
- info: Uint8Array,
-): bigint.BigInteger {
- const nbits = n.bitLength().toJSNumber();
- const buflen = Math.floor((nbits - 1) / 8 + 1);
- const mask = (1 << (8 - (buflen * 8 - nbits))) - 1;
- let counter = 0;
- while (true) {
- const ctx = new Uint8Array(info.byteLength + 2);
- ctx.set(info, 0);
- ctx[ctx.length - 2] = (counter >>> 8) & 0xff;
- ctx[ctx.length - 1] = counter & 0xff;
- const buf = kdf(buflen, ikm, salt, ctx);
- const arr = Array.from(buf);
- arr[0] = arr[0] & mask;
- const r = bigint.fromArray(arr, 256, false);
- if (r.lt(n)) {
- return r;
- }
- counter++;
- }
-}
-
-function csKdfMod(
- n: bigint.BigInteger,
- ikm: Uint8Array,
- salt: Uint8Array,
- info: Uint8Array,
-): Uint8Array {
- const nbits = n.bitLength().toJSNumber();
- const buflen = Math.floor((nbits - 1) / 8 + 1);
- const mask = (1 << (8 - (buflen * 8 - nbits))) - 1;
- let counter = 0;
- while (true) {
- const ctx = new Uint8Array(info.byteLength + 2);
- ctx.set(info, 0);
- ctx[ctx.length - 2] = (counter >>> 8) & 0xff;
- ctx[ctx.length - 1] = counter & 0xff;
- const buf = kdf(buflen, ikm, salt, ctx);
- const arr = Array.from(buf);
- arr[0] = arr[0] & mask;
- const r = bigint.fromArray(arr, 256, false);
- if (r.lt(n)) {
- return new Uint8Array(arr);
- }
- counter++;
- }
-}
-
-// Newer versions of node have TextEncoder and TextDecoder as a global,
-// just like modern browsers.
-// In older versions of node or environments that do not have these
-// globals, they must be polyfilled (by adding them to globa/globalThis)
-// before stringToBytes or bytesToString is called the first time.
-
-let encoder: any;
-let decoder: any;
-
-export function stringToBytes(s: string): Uint8Array {
- if (!encoder) {
- // @ts-ignore
- encoder = new TextEncoder();
- }
- return encoder.encode(s);
-}
-
-export function bytesToString(b: Uint8Array): string {
- if (!decoder) {
- // @ts-ignore
- decoder = new TextDecoder();
- }
- return decoder.decode(b);
-}
-
-function loadBigInt(arr: Uint8Array): bigint.BigInteger {
- return bigint.fromArray(Array.from(arr), 256, false);
-}
-
-function rsaBlindingKeyDerive(
- rsaPub: RsaPub,
- bks: Uint8Array,
-): bigint.BigInteger {
- const salt = stringToBytes("Blinding KDF extractor HMAC key");
- const info = stringToBytes("Blinding KDF");
- return kdfMod(rsaPub.N, bks, salt, info);
-}
-
-/*
- * Test for malicious RSA key.
- *
- * Assuming n is an RSA modulous and r is generated using a call to
- * GNUNET_CRYPTO_kdf_mod_mpi, if gcd(r,n) != 1 then n must be a
- * malicious RSA key designed to deanomize the user.
- *
- * @param r KDF result
- * @param n RSA modulus of the public key
- */
-function rsaGcdValidate(r: bigint.BigInteger, n: bigint.BigInteger): void {
- const t = bigint.gcd(r, n);
- if (!t.equals(bigint.one)) {
- throw Error("malicious RSA public key");
- }
-}
-
-function rsaFullDomainHash(hm: Uint8Array, rsaPub: RsaPub): bigint.BigInteger {
- const info = stringToBytes("RSA-FDA FTpsW!");
- const salt = rsaPubEncode(rsaPub);
- const r = kdfMod(rsaPub.N, hm, salt, info);
- rsaGcdValidate(r, rsaPub.N);
- return r;
-}
-
-function rsaPubDecode(rsaPub: Uint8Array): RsaPub {
- const modulusLength = (rsaPub[0] << 8) | rsaPub[1];
- const exponentLength = (rsaPub[2] << 8) | rsaPub[3];
- if (4 + exponentLength + modulusLength != rsaPub.length) {
- throw Error("invalid RSA public key (format wrong)");
- }
- const modulus = rsaPub.slice(4, 4 + modulusLength);
- const exponent = rsaPub.slice(
- 4 + modulusLength,
- 4 + modulusLength + exponentLength,
- );
- const res = {
- N: loadBigInt(modulus),
- e: loadBigInt(exponent),
- };
- return res;
-}
-
-function rsaPubEncode(rsaPub: RsaPub): Uint8Array {
- const mb = rsaPub.N.toArray(256).value;
- const eb = rsaPub.e.toArray(256).value;
- const out = new Uint8Array(4 + mb.length + eb.length);
- out[0] = (mb.length >>> 8) & 0xff;
- out[1] = mb.length & 0xff;
- out[2] = (eb.length >>> 8) & 0xff;
- out[3] = eb.length & 0xff;
- out.set(mb, 4);
- out.set(eb, 4 + mb.length);
- return out;
-}
-
-export function rsaBlind(
- hm: Uint8Array,
- bks: Uint8Array,
- rsaPubEnc: Uint8Array,
-): Uint8Array {
- const rsaPub = rsaPubDecode(rsaPubEnc);
- const data = rsaFullDomainHash(hm, rsaPub);
- const r = rsaBlindingKeyDerive(rsaPub, bks);
- const r_e = r.modPow(rsaPub.e, rsaPub.N);
- const bm = r_e.multiply(data).mod(rsaPub.N);
- return new Uint8Array(bm.toArray(256).value);
-}
-
-export function rsaUnblind(
- sig: Uint8Array,
- rsaPubEnc: Uint8Array,
- bks: Uint8Array,
-): Uint8Array {
- const rsaPub = rsaPubDecode(rsaPubEnc);
- const blinded_s = loadBigInt(sig);
- const r = rsaBlindingKeyDerive(rsaPub, bks);
- const r_inv = r.modInv(rsaPub.N);
- const s = blinded_s.multiply(r_inv).mod(rsaPub.N);
- return new Uint8Array(s.toArray(256).value);
-}
-
-export function rsaVerify(
- hm: Uint8Array,
- rsaSig: Uint8Array,
- rsaPubEnc: Uint8Array,
-): boolean {
- const rsaPub = rsaPubDecode(rsaPubEnc);
- const d = rsaFullDomainHash(hm, rsaPub);
- const sig = loadBigInt(rsaSig);
- const sig_e = sig.modPow(rsaPub.e, rsaPub.N);
- return sig_e.equals(d);
-}
-
-export type CsSignature = {
- s: Uint8Array;
- rPub: Uint8Array;
-};
-
-export type CsBlindSignature = {
- sBlind: Uint8Array;
- rPubBlind: Uint8Array;
-};
-
-export type CsBlindingSecrets = {
- alpha: [Uint8Array, Uint8Array];
- beta: [Uint8Array, Uint8Array];
-};
-
-export function typedArrayConcat(chunks: Uint8Array[]): Uint8Array {
- let payloadLen = 0;
- for (const c of chunks) {
- payloadLen += c.byteLength;
- }
- const buf = new ArrayBuffer(payloadLen);
- const u8buf = new Uint8Array(buf);
- let p = 0;
- for (const c of chunks) {
- u8buf.set(c, p);
- p += c.byteLength;
- }
- return u8buf;
-}
-
-/**
- * Map to scalar subgroup function
- * perform clamping as described in RFC7748
- * @param scalar
- */
-function mtoSS(scalar: Uint8Array): Uint8Array {
- scalar[0] &= 248;
- scalar[31] &= 127;
- scalar[31] |= 64;
- return scalar;
-}
-
-/**
- * The function returns the CS blinding secrets from a seed
- * @param bseed seed to derive blinding secrets
- * @returns blinding secrets
- */
-export function deriveSecrets(bseed: Uint8Array): CsBlindingSecrets {
- const outLen = 130;
- const salt = stringToBytes("alphabeta");
- const rndout = kdf(outLen, bseed, salt);
- const secrets: CsBlindingSecrets = {
- alpha: [mtoSS(rndout.slice(0, 32)), mtoSS(rndout.slice(64, 96))],
- beta: [mtoSS(rndout.slice(32, 64)), mtoSS(rndout.slice(96, 128))],
- };
- return secrets;
-}
-
-/**
- * Used for testing, simple scalar multiplication with base point of Ed25519
- * @param s scalar
- * @returns new point sG
- */
-export async function scalarMultBase25519(s: Uint8Array): Promise<Uint8Array> {
- return nacl.crypto_scalarmult_ed25519_base_noclamp(s);
-}
-
-/**
- * calculation of the blinded public point R in CS
- * @param csPub denomination publik key
- * @param secrets client blinding secrets
- * @param rPub public R received from /csr API
- */
-export async function calcRBlind(
- csPub: Uint8Array,
- secrets: CsBlindingSecrets,
- rPub: [Uint8Array, Uint8Array],
-): Promise<[Uint8Array, Uint8Array]> {
- const aG0 = nacl.crypto_scalarmult_ed25519_base_noclamp(secrets.alpha[0]);
- const aG1 = nacl.crypto_scalarmult_ed25519_base_noclamp(secrets.alpha[1]);
-
- const bDp0 = nacl.crypto_scalarmult_ed25519_noclamp(secrets.beta[0], csPub);
- const bDp1 = nacl.crypto_scalarmult_ed25519_noclamp(secrets.beta[1], csPub);
-
- const res0 = nacl.crypto_core_ed25519_add(aG0, bDp0);
- const res1 = nacl.crypto_core_ed25519_add(aG1, bDp1);
- return [
- nacl.crypto_core_ed25519_add(rPub[0], res0),
- nacl.crypto_core_ed25519_add(rPub[1], res1),
- ];
-}
-
-/**
- * FDH function used in CS
- * @param hm message hash
- * @param rPub public R included in FDH
- * @param csPub denomination public key as context
- * @returns mapped Curve25519 scalar
- */
-function csFDH(
- hm: Uint8Array,
- rPub: Uint8Array,
- csPub: Uint8Array,
-): Uint8Array {
- const lMod = Array.from(
- new Uint8Array([
- 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
- 0x00, 0x00, 0x00, 0x00, 0x14, 0xde, 0xf9, 0xde, 0xa2, 0xf7, 0x9c, 0xd6,
- 0x58, 0x12, 0x63, 0x1a, 0x5c, 0xf5, 0xd3, 0xed,
- ]),
- );
- const L = bigint.fromArray(lMod, 256, false);
-
- const info = stringToBytes("Curve25519FDH");
- const preshash = nacl.hash(typedArrayConcat([rPub, hm]));
- return csKdfMod(L, preshash, csPub, info).reverse();
-}
-
-/**
- * blinding seed derived from coin private key
- * @param coinPriv private key of the corresponding coin
- * @param rPub public R received from /csr API
- * @returns blinding seed
- */
-export function deriveBSeed(
- coinPriv: Uint8Array,
- rPub: [Uint8Array, Uint8Array],
-): Uint8Array {
- const outLen = 32;
- const salt = stringToBytes("b-seed");
- const ikm = typedArrayConcat([coinPriv, rPub[0], rPub[1]]);
- return kdf(outLen, ikm, salt);
-}
-
-/**
- * Derive withdraw nonce, used in /csr request
- * Note: In withdraw protocol, the nonce is chosen randomly
- * @param coinPriv coin private key
- * @returns nonce
- */
-export function deriveWithdrawNonce(coinPriv: Uint8Array): Uint8Array {
- const outLen = 32;
- const salt = stringToBytes("n");
- return kdf(outLen, coinPriv, salt);
-}
-
-/**
- * Blind operation for CS signatures, used after /csr call
- * @param bseed blinding seed to derive blinding secrets
- * @param rPub public R received from /csr
- * @param csPub denomination public key
- * @param hm message to blind
- * @returns two blinded c
- */
-export async function csBlind(
- bseed: Uint8Array,
- rPub: [Uint8Array, Uint8Array],
- csPub: Uint8Array,
- hm: Uint8Array,
-): Promise<[Uint8Array, Uint8Array]> {
- const secrets = deriveSecrets(bseed);
- const rPubBlind = await calcRBlind(csPub, secrets, rPub);
- const c_0 = csFDH(hm, rPubBlind[0], csPub);
- const c_1 = csFDH(hm, rPubBlind[1], csPub);
- return [
- nacl.crypto_core_ed25519_scalar_add(c_0, secrets.beta[0]),
- nacl.crypto_core_ed25519_scalar_add(c_1, secrets.beta[1]),
- ];
-}
-
-/**
- * Unblind operation to unblind the signature
- * @param bseed seed to derive secrets
- * @param rPub public R received from /csr
- * @param csPub denomination publick key
- * @param b returned from exchange to select c
- * @param csSig blinded signature
- * @returns unblinded signature
- */
-export async function csUnblind(
- bseed: Uint8Array,
- rPub: [Uint8Array, Uint8Array],
- csPub: Uint8Array,
- b: number,
- csSig: CsBlindSignature,
-): Promise<CsSignature> {
- if (b != 0 && b != 1) {
- throw new Error();
- }
- const secrets = deriveSecrets(bseed);
- const rPubDash = (await calcRBlind(csPub, secrets, rPub))[b];
- const sig: CsSignature = {
- s: nacl.crypto_core_ed25519_scalar_add(csSig.sBlind, secrets.alpha[b]),
- rPub: rPubDash,
- };
- return sig;
-}
-
-/**
- * Verification algorithm for CS signatures
- * @param hm message signed
- * @param csSig unblinded signature
- * @param csPub denomination publick key
- * @returns true if valid, false if invalid
- */
-export async function csVerify(
- hm: Uint8Array,
- csSig: CsSignature,
- csPub: Uint8Array,
-): Promise<boolean> {
- const cDash = csFDH(hm, csSig.rPub, csPub);
- const sG = nacl.crypto_scalarmult_ed25519_base_noclamp(csSig.s);
- const cbDp = nacl.crypto_scalarmult_ed25519_noclamp(cDash, csPub);
- const sGeq = nacl.crypto_core_ed25519_add(csSig.rPub, cbDp);
- return nacl.verify(sG, sGeq);
-}
-
-export interface EddsaKeyPair {
- eddsaPub: Uint8Array;
- eddsaPriv: Uint8Array;
-}
-
-export interface EcdheKeyPair {
- ecdhePub: Uint8Array;
- ecdhePriv: Uint8Array;
-}
-
-export interface Edx25519Keypair {
- edxPub: string;
- edxPriv: string;
-}
-
-export function createEddsaKeyPair(): EddsaKeyPair {
- const eddsaPriv = nacl.randomBytes(32);
- const eddsaPub = eddsaGetPublic(eddsaPriv);
- return { eddsaPriv, eddsaPub };
-}
-
-export function createEcdheKeyPair(): EcdheKeyPair {
- const ecdhePriv = nacl.randomBytes(32);
- const ecdhePub = ecdheGetPublic(ecdhePriv);
- return { ecdhePriv, ecdhePub };
-}
-
-export function hash(d: Uint8Array): Uint8Array {
- return nacl.hash(d);
-}
-
-/**
- * Hash the input with SHA-512 and truncate the result
- * to 32 bytes.
- */
-export function hashTruncate32(d: Uint8Array): Uint8Array {
- const sha512HashCode = nacl.hash(d);
- return sha512HashCode.subarray(0, 32);
-}
-
-export function hashCoinEv(
- coinEv: CoinEnvelope,
- denomPubHash: HashCodeString,
-): Uint8Array {
- const hashContext = createHashContext();
- hashContext.update(decodeCrock(denomPubHash));
- hashCoinEvInner(coinEv, hashContext);
- return hashContext.finish();
-}
-
-const logger = new Logger("talerCrypto.ts");
-
-export function hashCoinEvInner(
- coinEv: CoinEnvelope,
- hashState: nacl.HashState,
-): void {
- const hashInputBuf = new ArrayBuffer(4);
- const uint8ArrayBuf = new Uint8Array(hashInputBuf);
- const dv = new DataView(hashInputBuf);
- dv.setUint32(0, DenomKeyType.toIntTag(coinEv.cipher));
- hashState.update(uint8ArrayBuf);
- switch (coinEv.cipher) {
- case DenomKeyType.Rsa:
- hashState.update(decodeCrock(coinEv.rsa_blinded_planchet));
- return;
- default:
- throw new Error();
- }
-}
-
-export function hashCoinPub(
- coinPub: CoinPublicKeyString,
- ach?: HashCodeString,
-): Uint8Array {
- if (!ach) {
- return hash(decodeCrock(coinPub));
- }
-
- return hash(typedArrayConcat([decodeCrock(coinPub), decodeCrock(ach)]));
-}
-
-/**
- * Hash a denomination public key.
- */
-export function hashDenomPub(pub: DenominationPubKey): Uint8Array {
- if (pub.cipher === DenomKeyType.Rsa) {
- const pubBuf = decodeCrock(pub.rsa_public_key);
- const hashInputBuf = new ArrayBuffer(pubBuf.length + 4 + 4);
- const uint8ArrayBuf = new Uint8Array(hashInputBuf);
- const dv = new DataView(hashInputBuf);
- dv.setUint32(0, pub.age_mask ?? 0);
- dv.setUint32(4, DenomKeyType.toIntTag(pub.cipher));
- uint8ArrayBuf.set(pubBuf, 8);
- return nacl.hash(uint8ArrayBuf);
- } else if (pub.cipher === DenomKeyType.ClauseSchnorr) {
- const pubBuf = decodeCrock(pub.cs_public_key);
- const hashInputBuf = new ArrayBuffer(pubBuf.length + 4 + 4);
- const uint8ArrayBuf = new Uint8Array(hashInputBuf);
- const dv = new DataView(hashInputBuf);
- dv.setUint32(0, pub.age_mask ?? 0);
- dv.setUint32(4, DenomKeyType.toIntTag(pub.cipher));
- uint8ArrayBuf.set(pubBuf, 8);
- return nacl.hash(uint8ArrayBuf);
- } else {
- throw Error(
- `unsupported cipher (${
- (pub as DenominationPubKey).cipher
- }), unable to hash`,
- );
- }
-}
-
-export function eddsaSign(msg: Uint8Array, eddsaPriv: Uint8Array): Uint8Array {
- const pair = nacl.crypto_sign_keyPair_fromSeed(eddsaPriv);
- return nacl.sign_detached(msg, pair.secretKey);
-}
-
-export function eddsaVerify(
- msg: Uint8Array,
- sig: Uint8Array,
- eddsaPub: Uint8Array,
-): boolean {
- return nacl.sign_detached_verify(msg, sig, eddsaPub);
-}
-
-export function createHashContext(): nacl.HashState {
- return new nacl.HashState();
-}
-
-export interface FreshCoin {
- coinPub: Uint8Array;
- coinPriv: Uint8Array;
- bks: Uint8Array;
- maxAge: number;
- ageCommitmentProof: AgeCommitmentProof | undefined;
-}
-
-export function bufferForUint32(n: number): Uint8Array {
- const arrBuf = new ArrayBuffer(4);
- const buf = new Uint8Array(arrBuf);
- const dv = new DataView(arrBuf);
- dv.setUint32(0, n);
- return buf;
-}
-
-export function bufferForUint8(n: number): Uint8Array {
- const arrBuf = new ArrayBuffer(1);
- const buf = new Uint8Array(arrBuf);
- const dv = new DataView(arrBuf);
- dv.setUint8(0, n);
- return buf;
-}
-
-export async function setupTipPlanchet(
- secretSeed: Uint8Array,
- denomPub: DenominationPubKey,
- coinNumber: number,
-): Promise<FreshCoin> {
- const info = stringToBytes("taler-tip-coin-derivation");
- const saltArrBuf = new ArrayBuffer(4);
- const salt = new Uint8Array(saltArrBuf);
- const saltDataView = new DataView(saltArrBuf);
- saltDataView.setUint32(0, coinNumber);
- const out = kdf(64, secretSeed, salt, info);
- const coinPriv = out.slice(0, 32);
- const bks = out.slice(32, 64);
- let maybeAcp: AgeCommitmentProof | undefined;
- if (denomPub.age_mask != 0) {
- maybeAcp = await AgeRestriction.restrictionCommitSeeded(
- denomPub.age_mask,
- AgeRestriction.AGE_UNRESTRICTED,
- secretSeed,
- );
- }
- return {
- bks,
- coinPriv,
- coinPub: eddsaGetPublic(coinPriv),
- maxAge: AgeRestriction.AGE_UNRESTRICTED,
- ageCommitmentProof: maybeAcp,
- };
-}
-/**
- *
- * @param paytoUri
- * @param salt 16-byte salt
- * @returns
- */
-export function hashWire(paytoUri: string, salt: string): string {
- const r = kdf(
- 64,
- stringToBytes(paytoUri + "\0"),
- decodeCrock(salt),
- stringToBytes("merchant-wire-signature"),
- );
- return encodeCrock(r);
-}
-
-export enum TalerSignaturePurpose {
- MERCHANT_TRACK_TRANSACTION = 1103,
- WALLET_RESERVE_WITHDRAW = 1200,
- WALLET_COIN_DEPOSIT = 1201,
- GLOBAL_FEES = 1022,
- MASTER_DENOMINATION_KEY_VALIDITY = 1025,
- MASTER_WIRE_FEES = 1028,
- MASTER_WIRE_DETAILS = 1030,
- WALLET_COIN_MELT = 1202,
- TEST = 4242,
- MERCHANT_PAYMENT_OK = 1104,
- MERCHANT_CONTRACT = 1101,
- WALLET_COIN_RECOUP = 1203,
- WALLET_COIN_LINK = 1204,
- WALLET_COIN_RECOUP_REFRESH = 1206,
- WALLET_AGE_ATTESTATION = 1207,
- WALLET_PURSE_CREATE = 1210,
- WALLET_PURSE_DEPOSIT = 1211,
- WALLET_PURSE_MERGE = 1213,
- WALLET_ACCOUNT_MERGE = 1214,
- WALLET_PURSE_ECONTRACT = 1216,
- EXCHANGE_CONFIRM_RECOUP = 1039,
- EXCHANGE_CONFIRM_RECOUP_REFRESH = 1041,
- ANASTASIS_POLICY_UPLOAD = 1400,
- ANASTASIS_POLICY_DOWNLOAD = 1401,
- SYNC_BACKUP_UPLOAD = 1450,
-}
-
-export const enum WalletAccountMergeFlags {
- /**
- * Not a legal mode!
- */
- None = 0,
-
- /**
- * We are merging a fully paid-up purse into a reserve.
- */
- MergeFullyPaidPurse = 1,
-
- CreateFromPurseQuota = 2,
-
- CreateWithPurseFee = 3,
-}
-
-export class SignaturePurposeBuilder {
- private chunks: Uint8Array[] = [];
-
- constructor(private purposeNum: number) {}
-
- put(bytes: Uint8Array): SignaturePurposeBuilder {
- this.chunks.push(Uint8Array.from(bytes));
- return this;
- }
-
- build(): Uint8Array {
- let payloadLen = 0;
- for (const c of this.chunks) {
- payloadLen += c.byteLength;
- }
- const buf = new ArrayBuffer(4 + 4 + payloadLen);
- const u8buf = new Uint8Array(buf);
- let p = 8;
- for (const c of this.chunks) {
- u8buf.set(c, p);
- p += c.byteLength;
- }
- const dvbuf = new DataView(buf);
- dvbuf.setUint32(0, payloadLen + 4 + 4);
- dvbuf.setUint32(4, this.purposeNum);
- return u8buf;
- }
-}
-
-export function buildSigPS(purposeNum: number): SignaturePurposeBuilder {
- return new SignaturePurposeBuilder(purposeNum);
-}
-
-export type OpaqueData = Flavor<Uint8Array, any>;
-export type Edx25519PublicKey = FlavorP<Uint8Array, "Edx25519PublicKey", 32>;
-export type Edx25519PrivateKey = FlavorP<Uint8Array, "Edx25519PrivateKey", 64>;
-export type Edx25519Signature = FlavorP<Uint8Array, "Edx25519Signature", 64>;
-
-export type Edx25519PublicKeyEnc = FlavorP<string, "Edx25519PublicKeyEnc", 32>;
-export type Edx25519PrivateKeyEnc = FlavorP<
- string,
- "Edx25519PrivateKeyEnc",
- 64
->;
-
-/**
- * Convert a big integer to a fixed-size, little-endian array.
- */
-export function bigintToNaclArr(
- x: bigint.BigInteger,
- size: number,
-): Uint8Array {
- const byteArr = new Uint8Array(size);
- const arr = x.toArray(256).value.reverse();
- byteArr.set(arr, 0);
- return byteArr;
-}
-
-export function bigintFromNaclArr(arr: Uint8Array): bigint.BigInteger {
- let rev = new Uint8Array(arr);
- rev = rev.reverse();
- return bigint.fromArray(Array.from(rev), 256, false);
-}
-
-export namespace Edx25519 {
- const revL = [
- 0xed, 0xd3, 0xf5, 0x5c, 0x1a, 0x63, 0x12, 0x58, 0xd6, 0x9c, 0xf7, 0xa2,
- 0xde, 0xf9, 0xde, 0x14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x10,
- ];
-
- const L = bigint.fromArray(revL.reverse(), 256, false);
-
- export async function keyCreateFromSeed(
- seed: OpaqueData,
- ): Promise<Edx25519PrivateKey> {
- return nacl.crypto_edx25519_private_key_create_from_seed(seed);
- }
-
- export async function keyCreate(): Promise<Edx25519PrivateKey> {
- return nacl.crypto_edx25519_private_key_create();
- }
-
- export async function getPublic(
- priv: Edx25519PrivateKey,
- ): Promise<Edx25519PublicKey> {
- return nacl.crypto_edx25519_get_public(priv);
- }
-
- export function sign(
- msg: OpaqueData,
- key: Edx25519PrivateKey,
- ): Promise<Edx25519Signature> {
- throw Error("not implemented");
- }
-
- async function deriveFactor(
- pub: Edx25519PublicKey,
- seed: OpaqueData,
- ): Promise<OpaqueData> {
- const res = kdfKw({
- outputLength: 64,
- salt: seed,
- ikm: pub,
- info: stringToBytes("edx25519-derivation"),
- });
-
- return res;
- }
-
- export async function privateKeyDerive(
- priv: Edx25519PrivateKey,
- seed: OpaqueData,
- ): Promise<Edx25519PrivateKey> {
- const pub = await getPublic(priv);
- const privDec = priv;
- const a = bigintFromNaclArr(privDec.subarray(0, 32));
- const factorEnc = await deriveFactor(pub, seed);
- const factorModL = bigintFromNaclArr(factorEnc).mod(L);
-
- const aPrime = a.divide(8).multiply(factorModL).mod(L).multiply(8).mod(L);
- const bPrime = nacl
- .hash(typedArrayConcat([privDec.subarray(32, 64), factorEnc]))
- .subarray(0, 32);
-
- const newPriv = typedArrayConcat([bigintToNaclArr(aPrime, 32), bPrime]);
-
- return newPriv;
- }
-
- export async function publicKeyDerive(
- pub: Edx25519PublicKey,
- seed: OpaqueData,
- ): Promise<Edx25519PublicKey> {
- const factorEnc = await deriveFactor(pub, seed);
- const factorReduced = nacl.crypto_core_ed25519_scalar_reduce(factorEnc);
- const res = nacl.crypto_scalarmult_ed25519_noclamp(factorReduced, pub);
- return res;
- }
-}
-
-export interface AgeCommitment {
- mask: number;
-
- /**
- * Public keys, one for each age group specified in the age mask.
- */
- publicKeys: Edx25519PublicKeyEnc[];
-}
-
-export interface AgeProof {
- /**
- * Private keys. Typically smaller than the number of public keys,
- * because we drop private keys from age groups that are restricted.
- */
- privateKeys: Edx25519PrivateKeyEnc[];
-}
-
-export interface AgeCommitmentProof {
- commitment: AgeCommitment;
- proof: AgeProof;
-}
-
-function invariant(cond: boolean): asserts cond {
- if (!cond) {
- throw Error("invariant failed");
- }
-}
-
-export namespace AgeRestriction {
- /**
- * Smallest age value that the protocol considers "unrestricted".
- */
- export const AGE_UNRESTRICTED = 32;
-
- export function hashCommitment(ac: AgeCommitment): HashCodeString {
- const hc = new nacl.HashState();
- for (const pub of ac.publicKeys) {
- hc.update(decodeCrock(pub));
- }
- return encodeCrock(hc.finish().subarray(0, 32));
- }
-
- export function countAgeGroups(mask: number): number {
- let count = 0;
- let m = mask;
- while (m > 0) {
- count += m & 1;
- m = m >> 1;
- }
- return count;
- }
-
- export function getAgeGroupIndex(mask: number, age: number): number {
- invariant((mask & 1) === 1);
- let i = 0;
- let m = mask;
- let a = age;
- while (m > 0) {
- if (a <= 0) {
- break;
- }
- m = m >> 1;
- i += m & 1;
- a--;
- }
- return i;
- }
-
- export function ageGroupSpecToMask(ageGroupSpec: string): number {
- throw Error("not implemented");
- }
-
- export async function restrictionCommit(
- ageMask: number,
- age: number,
- ): Promise<AgeCommitmentProof> {
- invariant((ageMask & 1) === 1);
- const numPubs = countAgeGroups(ageMask) - 1;
- const numPrivs = getAgeGroupIndex(ageMask, age);
-
- const pubs: Edx25519PublicKey[] = [];
- const privs: Edx25519PrivateKey[] = [];
-
- for (let i = 0; i < numPubs; i++) {
- const priv = await Edx25519.keyCreate();
- const pub = await Edx25519.getPublic(priv);
- pubs.push(pub);
- if (i < numPrivs) {
- privs.push(priv);
- }
- }
-
- return {
- commitment: {
- mask: ageMask,
- publicKeys: pubs.map((x) => encodeCrock(x)),
- },
- proof: {
- privateKeys: privs.map((x) => encodeCrock(x)),
- },
- };
- }
-
- export async function restrictionCommitSeeded(
- ageMask: number,
- age: number,
- seed: Uint8Array,
- ): Promise<AgeCommitmentProof> {
- invariant((ageMask & 1) === 1);
- const numPubs = countAgeGroups(ageMask) - 1;
- const numPrivs = getAgeGroupIndex(ageMask, age);
-
- const pubs: Edx25519PublicKey[] = [];
- const privs: Edx25519PrivateKey[] = [];
-
- for (let i = 0; i < numPubs; i++) {
- const privSeed = await kdfKw({
- outputLength: 32,
- ikm: seed,
- info: stringToBytes("age-restriction-commit"),
- salt: bufferForUint32(i),
- });
- const priv = await Edx25519.keyCreateFromSeed(privSeed);
- const pub = await Edx25519.getPublic(priv);
- pubs.push(pub);
- if (i < numPrivs) {
- privs.push(priv);
- }
- }
-
- return {
- commitment: {
- mask: ageMask,
- publicKeys: pubs.map((x) => encodeCrock(x)),
- },
- proof: {
- privateKeys: privs.map((x) => encodeCrock(x)),
- },
- };
- }
-
- /**
- * Check that c1 = c2*salt
- */
- export async function commitCompare(
- c1: AgeCommitment,
- c2: AgeCommitment,
- salt: OpaqueData,
- ): Promise<boolean> {
- if (c1.publicKeys.length != c2.publicKeys.length) {
- return false;
- }
- for (let i = 0; i < c1.publicKeys.length; i++) {
- const k1 = decodeCrock(c1.publicKeys[i]);
- const k2 = await Edx25519.publicKeyDerive(
- decodeCrock(c2.publicKeys[i]),
- salt,
- );
- if (k1 != k2) {
- return false;
- }
- }
- return true;
- }
-
- export async function commitmentDerive(
- commitmentProof: AgeCommitmentProof,
- salt: OpaqueData,
- ): Promise<AgeCommitmentProof> {
- const newPrivs: Edx25519PrivateKey[] = [];
- const newPubs: Edx25519PublicKey[] = [];
-
- for (const oldPub of commitmentProof.commitment.publicKeys) {
- newPubs.push(await Edx25519.publicKeyDerive(decodeCrock(oldPub), salt));
- }
-
- for (const oldPriv of commitmentProof.proof.privateKeys) {
- newPrivs.push(
- await Edx25519.privateKeyDerive(decodeCrock(oldPriv), salt),
- );
- }
-
- return {
- commitment: {
- mask: commitmentProof.commitment.mask,
- publicKeys: newPubs.map((x) => encodeCrock(x)),
- },
- proof: {
- privateKeys: newPrivs.map((x) => encodeCrock(x)),
- },
- };
- }
-
- export function commitmentAttest(
- commitmentProof: AgeCommitmentProof,
- age: number,
- ): Edx25519Signature {
- const d = buildSigPS(TalerSignaturePurpose.WALLET_AGE_ATTESTATION)
- .put(bufferForUint32(commitmentProof.commitment.mask))
- .put(bufferForUint32(age))
- .build();
- const group = getAgeGroupIndex(commitmentProof.commitment.mask, age);
- if (group === 0) {
- // No attestation required.
- return new Uint8Array(64);
- }
- const priv = commitmentProof.proof.privateKeys[group - 1];
- const pub = commitmentProof.commitment.publicKeys[group - 1];
- const sig = nacl.crypto_edx25519_sign_detached(
- d,
- decodeCrock(priv),
- decodeCrock(pub),
- );
- return sig;
- }
-
- export function commitmentVerify(
- commitment: AgeCommitment,
- sig: string,
- age: number,
- ): boolean {
- const d = buildSigPS(TalerSignaturePurpose.WALLET_AGE_ATTESTATION)
- .put(bufferForUint32(commitment.mask))
- .put(bufferForUint32(age))
- .build();
- const group = getAgeGroupIndex(commitment.mask, age);
- if (group === 0) {
- // No attestation required.
- return true;
- }
- const pub = commitment.publicKeys[group - 1];
- return nacl.crypto_edx25519_sign_detached_verify(
- d,
- decodeCrock(sig),
- decodeCrock(pub),
- );
- }
-}
-
-// FIXME: make it a branded type!
-type EncryptionNonce = FlavorP<Uint8Array, "EncryptionNonce", 24>;
-
-async function deriveKey(
- keySeed: OpaqueData,
- nonce: EncryptionNonce,
- salt: string,
-): Promise<Uint8Array> {
- return kdfKw({
- outputLength: 32,
- salt: nonce,
- ikm: keySeed,
- info: stringToBytes(salt),
- });
-}
-
-async function encryptWithDerivedKey(
- nonce: EncryptionNonce,
- keySeed: OpaqueData,
- plaintext: OpaqueData,
- salt: string,
-): Promise<OpaqueData> {
- const key = await deriveKey(keySeed, nonce, salt);
- const cipherText = secretbox(plaintext, nonce, key);
- return typedArrayConcat([nonce, cipherText]);
-}
-
-const nonceSize = 24;
-
-async function decryptWithDerivedKey(
- ciphertext: OpaqueData,
- keySeed: OpaqueData,
- salt: string,
-): Promise<OpaqueData> {
- const ctBuf = ciphertext;
- const nonceBuf = ctBuf.slice(0, nonceSize);
- const enc = ctBuf.slice(nonceSize);
- const key = await deriveKey(keySeed, nonceBuf, salt);
- const clearText = nacl.secretbox_open(enc, nonceBuf, key);
- if (!clearText) {
- throw Error("could not decrypt");
- }
- return clearText;
-}
-
-enum ContractFormatTag {
- PaymentOffer = 0,
- PaymentRequest = 1,
-}
-
-type MaterialEddsaPub = {
- _materialType?: "eddsa-pub";
- _size?: 32;
-};
-
-type MaterialEddsaPriv = {
- _materialType?: "ecdhe-priv";
- _size?: 32;
-};
-
-type MaterialEcdhePub = {
- _materialType?: "ecdhe-pub";
- _size?: 32;
-};
-
-type MaterialEcdhePriv = {
- _materialType?: "ecdhe-priv";
- _size?: 32;
-};
-
-type PursePublicKey = FlavorP<Uint8Array, "PursePublicKey", 32> &
- MaterialEddsaPub;
-
-type ContractPrivateKey = FlavorP<Uint8Array, "ContractPrivateKey", 32> &
- MaterialEcdhePriv;
-
-type MergePrivateKey = FlavorP<Uint8Array, "MergePrivateKey", 32> &
- MaterialEddsaPriv;
-
-const mergeSalt = "p2p-merge-contract";
-const depositSalt = "p2p-deposit-contract";
-
-export function encryptContractForMerge(
- pursePub: PursePublicKey,
- contractPriv: ContractPrivateKey,
- mergePriv: MergePrivateKey,
- contractTerms: any,
-): Promise<OpaqueData> {
- const contractTermsCanon = canonicalJson(contractTerms) + "\0";
- const contractTermsBytes = stringToBytes(contractTermsCanon);
- const contractTermsCompressed = fflate.zlibSync(contractTermsBytes);
- const data = typedArrayConcat([
- bufferForUint32(ContractFormatTag.PaymentOffer),
- bufferForUint32(contractTermsBytes.length),
- mergePriv,
- contractTermsCompressed,
- ]);
- const key = keyExchangeEcdheEddsa(contractPriv, pursePub);
- return encryptWithDerivedKey(getRandomBytesF(24), key, data, mergeSalt);
-}
-
-export function encryptContractForDeposit(
- pursePub: PursePublicKey,
- contractPriv: ContractPrivateKey,
- contractTerms: any,
-): Promise<OpaqueData> {
- const contractTermsCanon = canonicalJson(contractTerms) + "\0";
- const contractTermsBytes = stringToBytes(contractTermsCanon);
- const contractTermsCompressed = fflate.zlibSync(contractTermsBytes);
- const data = typedArrayConcat([
- bufferForUint32(ContractFormatTag.PaymentRequest),
- bufferForUint32(contractTermsBytes.length),
- contractTermsCompressed,
- ]);
- const key = keyExchangeEcdheEddsa(contractPriv, pursePub);
- return encryptWithDerivedKey(getRandomBytesF(24), key, data, depositSalt);
-}
-
-export interface DecryptForMergeResult {
- contractTerms: any;
- mergePriv: Uint8Array;
-}
-
-export interface DecryptForDepositResult {
- contractTerms: any;
-}
-
-export async function decryptContractForMerge(
- enc: OpaqueData,
- pursePub: PursePublicKey,
- contractPriv: ContractPrivateKey,
-): Promise<DecryptForMergeResult> {
- const key = keyExchangeEcdheEddsa(contractPriv, pursePub);
- const dec = await decryptWithDerivedKey(enc, key, mergeSalt);
- const mergePriv = dec.slice(8, 8 + 32);
- const contractTermsCompressed = dec.slice(8 + 32);
- const contractTermsBuf = fflate.unzlibSync(contractTermsCompressed);
- // Slice of the '\0' at the end and decode to a string
- const contractTermsString = bytesToString(
- contractTermsBuf.slice(0, contractTermsBuf.length - 1),
- );
- return {
- mergePriv: mergePriv,
- contractTerms: JSON.parse(contractTermsString),
- };
-}
-
-export async function decryptContractForDeposit(
- enc: OpaqueData,
- pursePub: PursePublicKey,
- contractPriv: ContractPrivateKey,
-): Promise<DecryptForDepositResult> {
- const key = keyExchangeEcdheEddsa(contractPriv, pursePub);
- const dec = await decryptWithDerivedKey(enc, key, depositSalt);
- const contractTermsCompressed = dec.slice(8);
- const contractTermsBuf = fflate.unzlibSync(contractTermsCompressed);
- // Slice of the '\0' at the end and decode to a string
- const contractTermsString = bytesToString(
- contractTermsBuf.slice(0, contractTermsBuf.length - 1),
- );
- return {
- contractTerms: JSON.parse(contractTermsString),
- };
-}
diff --git a/packages/taler-util/src/talerTypes.ts b/packages/taler-util/src/talerTypes.ts
@@ -1,2028 +0,0 @@
-/*
- 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 and helpers for the core GNU Taler protocol.
- *
- * Even though the rest of the wallet uses camelCase for fields, use snake_case
- * here, since that's the convention for the Taler JSON+HTTP API.
- */
-
-/**
- * Imports.
- */
-
-import { codecForAmountString } from "./amounts.js";
-import {
- buildCodecForObject,
- buildCodecForUnion,
- Codec,
- codecForAny,
- codecForBoolean,
- codecForConstNumber,
- codecForConstString,
- codecForList,
- codecForMap,
- codecForNumber,
- codecForString,
- codecOptional,
-} from "./codec.js";
-import { strcmp } from "./helpers.js";
-import { AgeCommitmentProof, Edx25519PublicKeyEnc } from "./talerCrypto.js";
-import {
- codecForAbsoluteTime,
- codecForDuration,
- codecForTimestamp,
- TalerProtocolDuration,
- TalerProtocolTimestamp,
-} from "./time.js";
-
-/**
- * Denomination as found in the /keys response from the exchange.
- */
-export class ExchangeDenomination {
- /**
- * Value of one coin of the denomination.
- */
- value: string;
-
- /**
- * Public signing key of the denomination.
- */
- denom_pub: DenominationPubKey;
-
- /**
- * Fee for withdrawing.
- */
- fee_withdraw: string;
-
- /**
- * Fee for depositing.
- */
- fee_deposit: string;
-
- /**
- * Fee for refreshing.
- */
- fee_refresh: string;
-
- /**
- * Fee for refunding.
- */
- fee_refund: string;
-
- /**
- * Start date from which withdraw is allowed.
- */
- stamp_start: TalerProtocolTimestamp;
-
- /**
- * End date for withdrawing.
- */
- stamp_expire_withdraw: TalerProtocolTimestamp;
-
- /**
- * Expiration date after which the exchange can forget about
- * the currency.
- */
- stamp_expire_legal: TalerProtocolTimestamp;
-
- /**
- * Date after which the coins of this denomination can't be
- * deposited anymore.
- */
- stamp_expire_deposit: TalerProtocolTimestamp;
-
- /**
- * Signature over the denomination information by the exchange's master
- * signing key.
- */
- master_sig: string;
-}
-
-/**
- * Signature by the auditor that a particular denomination key is audited.
- */
-export class AuditorDenomSig {
- /**
- * Denomination public key's hash.
- */
- denom_pub_h: string;
-
- /**
- * The signature.
- */
- auditor_sig: string;
-}
-
-/**
- * Auditor information as given by the exchange in /keys.
- */
-export class ExchangeAuditor {
- /**
- * Auditor's public key.
- */
- auditor_pub: string;
-
- /**
- * Base URL of the auditor.
- */
- auditor_url: string;
-
- /**
- * List of signatures for denominations by the auditor.
- */
- denomination_keys: AuditorDenomSig[];
-}
-
-export type ExchangeWithdrawValue =
- | ExchangeRsaWithdrawValue
- | ExchangeCsWithdrawValue;
-
-export interface ExchangeRsaWithdrawValue {
- cipher: "RSA";
-}
-
-export interface ExchangeCsWithdrawValue {
- cipher: "CS";
-
- /**
- * CSR R0 value
- */
- r_pub_0: string;
-
- /**
- * CSR R1 value
- */
- r_pub_1: string;
-}
-
-export interface RecoupRequest {
- /**
- * Hashed denomination public key of the coin we want to get
- * paid back.
- */
- denom_pub_hash: string;
-
- /**
- * Signature over the coin public key by the denomination.
- *
- * The string variant is for the legacy exchange protocol.
- */
- denom_sig: UnblindedSignature;
-
- /**
- * Blinding key that was used during withdraw,
- * used to prove that we were actually withdrawing the coin.
- */
- coin_blind_key_secret: string;
-
- /**
- * Signature of TALER_RecoupRequestPS created with the coin's private key.
- */
- coin_sig: string;
-
- ewv: ExchangeWithdrawValue;
-}
-
-export interface RecoupRefreshRequest {
- /**
- * Hashed enomination public key of the coin we want to get
- * paid back.
- */
- denom_pub_hash: string;
-
- /**
- * Signature over the coin public key by the denomination.
- *
- * The string variant is for the legacy exchange protocol.
- */
- denom_sig: UnblindedSignature;
-
- /**
- * Coin's blinding factor.
- */
- coin_blind_key_secret: string;
-
- /**
- * Signature of TALER_RecoupRefreshRequestPS created with
- * the coin's private key.
- */
- coin_sig: string;
-
- ewv: ExchangeWithdrawValue;
-}
-
-/**
- * Response that we get from the exchange for a payback request.
- */
-export interface RecoupConfirmation {
- /**
- * Public key of the reserve that will receive the payback.
- */
- reserve_pub?: string;
-
- /**
- * Public key of the old coin that will receive the recoup,
- * provided if refreshed was true.
- */
- old_coin_pub?: string;
-}
-
-export type UnblindedSignature = RsaUnblindedSignature;
-
-export interface RsaUnblindedSignature {
- cipher: DenomKeyType.Rsa;
- rsa_signature: string;
-}
-
-/**
- * Deposit permission for a single coin.
- */
-export interface CoinDepositPermission {
- /**
- * Signature by the coin.
- */
- coin_sig: string;
-
- /**
- * Public key of the coin being spend.
- */
- coin_pub: string;
-
- /**
- * Signature made by the denomination public key.
- *
- * The string variant is for legacy protocol support.
- */
-
- ub_sig: UnblindedSignature;
-
- /**
- * The denomination public key associated with this coin.
- */
- h_denom: string;
-
- /**
- * The amount that is subtracted from this coin with this payment.
- */
- contribution: string;
-
- /**
- * URL of the exchange this coin was withdrawn from.
- */
- exchange_url: string;
-
- minimum_age_sig?: EddsaSignatureString;
-
- age_commitment?: Edx25519PublicKeyEnc[];
-}
-
-/**
- * Information about an exchange as stored inside a
- * merchant's contract terms.
- */
-export interface ExchangeHandle {
- /**
- * Master public signing key of the exchange.
- */
- master_pub: string;
-
- /**
- * Base URL of the exchange.
- */
- url: string;
-}
-
-export interface AuditorHandle {
- /**
- * Official name of the auditor.
- */
- name: string;
-
- /**
- * Master public signing key of the auditor.
- */
- auditor_pub: string;
-
- /**
- * Base URL of the auditor.
- */
- url: string;
-}
-
-// Delivery location, loosely modeled as a subset of
-// ISO20022's PostalAddress25.
-export interface Location {
- // Nation with its own government.
- country?: string;
-
- // Identifies a subdivision of a country such as state, region, county.
- country_subdivision?: string;
-
- // Identifies a subdivision within a country sub-division.
- district?: string;
-
- // Name of a built-up area, with defined boundaries, and a local government.
- town?: string;
-
- // Specific location name within the town.
- town_location?: string;
-
- // Identifier consisting of a group of letters and/or numbers that
- // is added to a postal address to assist the sorting of mail.
- post_code?: string;
-
- // Name of a street or thoroughfare.
- street?: string;
-
- // Name of the building or house.
- building_name?: string;
-
- // Number that identifies the position of a building on a street.
- building_number?: string;
-
- // Free-form address lines, should not exceed 7 elements.
- address_lines?: string[];
-}
-
-export interface MerchantInfo {
- name: string;
- jurisdiction?: Location;
- address?: Location;
- logo?: string;
- website?: string;
- email?: string;
-}
-
-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?: TalerProtocolTimestamp;
-}
-
-export interface InternationalizedString {
- [lang_tag: string]: string;
-}
-
-/**
- * Contract terms from a merchant.
- */
-export interface ContractTerms {
- /**
- * Hash of the merchant's wire details.
- */
- h_wire: string;
-
- /**
- * Hash of the merchant's wire details.
- */
- auto_refund?: TalerProtocolDuration;
-
- /**
- * Wire method the merchant wants to use.
- */
- wire_method: string;
-
- /**
- * Human-readable short summary of the contract.
- */
- summary: string;
-
- summary_i18n?: InternationalizedString;
-
- /**
- * Nonce used to ensure freshness.
- */
- nonce: string;
-
- /**
- * Total amount payable.
- */
- amount: string;
-
- /**
- * Auditors accepted by the merchant.
- */
- auditors: AuditorHandle[];
-
- /**
- * Deadline to pay for the contract.
- */
- pay_deadline: TalerProtocolTimestamp;
-
- /**
- * Maximum deposit fee covered by the merchant.
- */
- max_fee: string;
-
- /**
- * Information about the merchant.
- */
- merchant: MerchantInfo;
-
- /**
- * Public key of the merchant.
- */
- merchant_pub: string;
-
- /**
- * Time indicating when the order should be delivered.
- * May be overwritten by individual products.
- */
- delivery_date?: TalerProtocolTimestamp;
-
- /**
- * Delivery location for (all!) products.
- */
- delivery_location?: Location;
-
- /**
- * List of accepted exchanges.
- */
- exchanges: ExchangeHandle[];
-
- /**
- * Products that are sold in this contract.
- */
- products?: Product[];
-
- /**
- * Deadline for refunds.
- */
- refund_deadline: TalerProtocolTimestamp;
-
- /**
- * Deadline for the wire transfer.
- */
- wire_transfer_deadline: TalerProtocolTimestamp;
-
- /**
- * Time when the contract was generated by the merchant.
- */
- timestamp: TalerProtocolTimestamp;
-
- /**
- * Order id to uniquely identify the purchase within
- * one merchant instance.
- */
- order_id: string;
-
- /**
- * Base URL of the merchant's backend.
- */
- merchant_base_url: string;
-
- /**
- * Fulfillment URL to view the product or
- * delivery status.
- */
- fulfillment_url?: string;
-
- /**
- * URL meant to share the shopping cart.
- */
- public_reorder_url?: string;
-
- /**
- * Plain text fulfillment message in the merchant's default language.
- */
- fulfillment_message?: string;
-
- /**
- * Internationalized fulfillment messages.
- */
- fulfillment_message_i18n?: InternationalizedString;
-
- /**
- * Share of the wire fee that must be settled with one payment.
- */
- wire_fee_amortization?: number;
-
- /**
- * Maximum wire fee that the merchant agrees to pay for.
- */
- max_wire_fee?: string;
-
- minimum_age?: number;
-
- /**
- * Extra data, interpreted by the mechant only.
- */
- extra?: any;
-}
-
-/**
- * Refund permission in the format that the merchant gives it to us.
- */
-export interface MerchantAbortPayRefundDetails {
- /**
- * Amount to be refunded.
- */
- refund_amount: string;
-
- /**
- * Fee for the refund.
- */
- refund_fee: string;
-
- /**
- * Public key of the coin being refunded.
- */
- coin_pub: string;
-
- /**
- * Refund transaction ID between merchant and exchange.
- */
- rtransaction_id: number;
-
- /**
- * Exchange's key used for the signature.
- */
- exchange_pub?: string;
-
- /**
- * Exchange's signature to confirm the refund.
- */
- exchange_sig?: string;
-
- /**
- * Error replay from the exchange (if any).
- */
- exchange_reply?: any;
-
- /**
- * Error code from the exchange (if any).
- */
- exchange_code?: number;
-
- /**
- * HTTP status code of the exchange's response
- * to the merchant's refund request.
- */
- exchange_http_status: number;
-}
-
-/**
- * Response for a refund pickup or a /pay in abort mode.
- */
-export interface MerchantRefundResponse {
- /**
- * Public key of the merchant
- */
- merchant_pub: string;
-
- /**
- * Contract terms hash of the contract that
- * is being refunded.
- */
- h_contract_terms: string;
-
- /**
- * The signed refund permissions, to be sent to the exchange.
- */
- refunds: MerchantAbortPayRefundDetails[];
-}
-
-/**
- * Planchet detail sent to the merchant.
- */
-export interface TipPlanchetDetail {
- /**
- * Hashed denomination public key.
- */
- denom_pub_hash: string;
-
- /**
- * Coin's blinded public key.
- */
- coin_ev: CoinEnvelope;
-}
-
-/**
- * Request sent to the merchant to pick up a tip.
- */
-export interface TipPickupRequest {
- /**
- * Identifier of the tip.
- */
- tip_id: string;
-
- /**
- * List of planchets the wallet wants to use for the tip.
- */
- planchets: TipPlanchetDetail[];
-}
-
-/**
- * Reserve signature, defined as separate class to facilitate
- * schema validation.
- */
-export interface MerchantBlindSigWrapperV1 {
- /**
- * Reserve signature.
- */
- blind_sig: string;
-}
-
-/**
- * Response of the merchant
- * to the TipPickupRequest.
- */
-export interface MerchantTipResponseV1 {
- /**
- * The order of the signatures matches the planchets list.
- */
- blind_sigs: MerchantBlindSigWrapperV1[];
-}
-
-export interface MerchantBlindSigWrapperV2 {
- blind_sig: BlindedDenominationSignature;
-}
-
-/**
- * Response of the merchant
- * to the TipPickupRequest.
- */
-export interface MerchantTipResponseV2 {
- /**
- * The order of the signatures matches the planchets list.
- */
- blind_sigs: MerchantBlindSigWrapperV2[];
-}
-
-/**
- * Element of the payback list that the
- * exchange gives us in /keys.
- */
-export class Recoup {
- /**
- * The hash of the denomination public key for which the payback is offered.
- */
- h_denom_pub: string;
-}
-
-/**
- * Structure of one exchange signing key in the /keys response.
- */
-export class ExchangeSignKeyJson {
- stamp_start: TalerProtocolTimestamp;
- stamp_expire: TalerProtocolTimestamp;
- stamp_end: TalerProtocolTimestamp;
- key: EddsaPublicKeyString;
- master_sig: EddsaSignatureString;
-}
-
-/**
- * Structure that the exchange gives us in /keys.
- */
-export class ExchangeKeysJson {
- /**
- * List of offered denominations.
- */
- denoms: ExchangeDenomination[];
-
- /**
- * The exchange's master public key.
- */
- master_public_key: string;
-
- /**
- * The list of auditors (partially) auditing the exchange.
- */
- auditors: ExchangeAuditor[];
-
- /**
- * Timestamp when this response was issued.
- */
- list_issue_date: TalerProtocolTimestamp;
-
- /**
- * List of revoked denominations.
- */
- recoup?: Recoup[];
-
- /**
- * Short-lived signing keys used to sign online
- * responses.
- */
- signkeys: ExchangeSignKeyJson[];
-
- /**
- * Protocol version.
- */
- version: string;
-
- reserve_closing_delay: TalerProtocolDuration;
-
- global_fees: GlobalFees[];
-}
-
-export interface GlobalFees {
- // What date (inclusive) does these fees go into effect?
- start_date: TalerProtocolTimestamp;
-
- // What date (exclusive) does this fees stop going into effect?
- end_date: TalerProtocolTimestamp;
-
- // KYC fee, charged when a user wants to create an account.
- // The first year of the account_annual_fee after the KYC is
- // always included.
- kyc_fee: AmountString;
-
- // Account history fee, charged when a user wants to
- // obtain a reserve/account history.
- history_fee: AmountString;
-
- // Annual fee charged for having an open account at the
- // exchange. Charged to the account. If the account
- // balance is insufficient to cover this fee, the account
- // is automatically deleted/closed. (Note that the exchange
- // will keep the account history around for longer for
- // regulatory reasons.)
- account_fee: AmountString;
-
- // Purse fee, charged only if a purse is abandoned
- // and was not covered by the account limit.
- purse_fee: AmountString;
-
- // How long will the exchange preserve the account history?
- // After an account was deleted/closed, the exchange will
- // retain the account history for legal reasons until this time.
- history_expiration: TalerProtocolDuration;
-
- // How long does the exchange promise to keep funds
- // an account for which the KYC has never happened
- // after a purse was merged into an account? Basically,
- // after this time funds in an account without KYC are
- // forfeit.
- account_kyc_timeout: TalerProtocolDuration;
-
- // Non-negative number of concurrent purses that any
- // account holder is allowed to create without having
- // to pay the purse_fee.
- purse_account_limit: number;
-
- // How long does an exchange keep a purse around after a purse
- // has expired (or been successfully merged)? A 'GET' request
- // for a purse will succeed until the purse expiration time
- // plus this value.
- purse_timeout: TalerProtocolDuration;
-
- // Signature of TALER_GlobalFeesPS.
- master_sig: string;
-}
-/**
- * Wire fees as announced by the exchange.
- */
-export class WireFeesJson {
- /**
- * Cost of a wire transfer.
- */
- wire_fee: string;
-
- wad_fee: string;
-
- /**
- * Cost of clising a reserve.
- */
- closing_fee: string;
-
- /**
- * Signature made with the exchange's master key.
- */
- sig: string;
-
- /**
- * Date from which the fee applies.
- */
- start_date: TalerProtocolTimestamp;
-
- /**
- * Data after which the fee doesn't apply anymore.
- */
- end_date: TalerProtocolTimestamp;
-}
-
-export interface AccountInfo {
- payto_uri: string;
- master_sig: string;
-}
-
-export interface ExchangeWireJson {
- accounts: AccountInfo[];
- fees: { [methodName: string]: WireFeesJson[] };
-}
-
-/**
- * Proposal returned from the contract URL.
- */
-export class Proposal {
- /**
- * Contract terms for the propoal.
- * Raw, un-decoded JSON object.
- */
- contract_terms: any;
-
- /**
- * Signature over contract, made by the merchant. The public key used for signing
- * must be contract_terms.merchant_pub.
- */
- sig: string;
-}
-
-/**
- * Response from the internal merchant API.
- */
-export class CheckPaymentResponse {
- order_status: string;
- refunded: boolean | undefined;
- refunded_amount: string | undefined;
- contract_terms: any | undefined;
- taler_pay_uri: string | undefined;
- contract_url: string | undefined;
-}
-
-/**
- * Response from the bank.
- */
-export class WithdrawOperationStatusResponse {
- selection_done: boolean;
-
- transfer_done: boolean;
-
- aborted: boolean;
-
- amount: string;
-
- sender_wire?: string;
-
- suggested_exchange?: string;
-
- confirm_transfer_url?: string;
-
- wire_types: string[];
-}
-
-/**
- * Response from the merchant.
- */
-export class TipPickupGetResponse {
- tip_amount: string;
-
- exchange_url: string;
-
- expiration: TalerProtocolTimestamp;
-}
-
-export enum DenomKeyType {
- Rsa = "RSA",
- ClauseSchnorr = "CS",
-}
-
-export namespace DenomKeyType {
- export function toIntTag(t: DenomKeyType): number {
- switch (t) {
- case DenomKeyType.Rsa:
- return 1;
- case DenomKeyType.ClauseSchnorr:
- return 2;
- }
- }
-}
-
-export interface RsaBlindedDenominationSignature {
- cipher: DenomKeyType.Rsa;
- blinded_rsa_signature: string;
-}
-
-export interface CSBlindedDenominationSignature {
- cipher: DenomKeyType.ClauseSchnorr;
-}
-
-export type BlindedDenominationSignature =
- | RsaBlindedDenominationSignature
- | CSBlindedDenominationSignature;
-
-export const codecForRsaBlindedDenominationSignature = () =>
- buildCodecForObject<RsaBlindedDenominationSignature>()
- .property("cipher", codecForConstString(DenomKeyType.Rsa))
- .property("blinded_rsa_signature", codecForString())
- .build("RsaBlindedDenominationSignature");
-
-export const codecForBlindedDenominationSignature = () =>
- buildCodecForUnion<BlindedDenominationSignature>()
- .discriminateOn("cipher")
- .alternative(DenomKeyType.Rsa, codecForRsaBlindedDenominationSignature())
- .build("BlindedDenominationSignature");
-
-export class WithdrawResponse {
- ev_sig: BlindedDenominationSignature;
-}
-
-export class WithdrawBatchResponse {
- ev_sigs: WithdrawResponse[];
-}
-
-/**
- * Easy to process format for the public data of coins
- * managed by the wallet.
- */
-export interface CoinDumpJson {
- coins: Array<{
- /**
- * The coin's denomination's public key.
- */
- denom_pub: DenominationPubKey;
- /**
- * Hash of denom_pub.
- */
- denom_pub_hash: string;
- /**
- * Value of the denomination (without any fees).
- */
- denom_value: string;
- /**
- * Public key of the coin.
- */
- coin_pub: string;
- /**
- * Base URL of the exchange for the coin.
- */
- exchange_base_url: string;
- /**
- * Remaining value on the coin, to the knowledge of
- * the wallet.
- */
- remaining_value: string;
- /**
- * Public key of the parent coin.
- * Only present if this coin was obtained via refreshing.
- */
- refresh_parent_coin_pub: string | undefined;
- /**
- * Public key of the reserve for this coin.
- * Only present if this coin was obtained via refreshing.
- */
- withdrawal_reserve_pub: string | undefined;
- /**
- * Is the coin suspended?
- * Suspended coins are not considered for payments.
- */
- coin_suspended: boolean;
-
- /**
- * Information about the age restriction
- */
- ageCommitmentProof: AgeCommitmentProof | undefined;
- }>;
-}
-
-export interface MerchantPayResponse {
- sig: string;
-}
-
-export interface ExchangeMeltRequest {
- coin_pub: CoinPublicKeyString;
- confirm_sig: EddsaSignatureString;
- denom_pub_hash: HashCodeString;
- denom_sig: UnblindedSignature;
- rc: string;
- value_with_fee: AmountString;
- age_commitment_hash?: HashCodeString;
-}
-
-export interface ExchangeMeltResponse {
- /**
- * Which of the kappa indices does the client not have to reveal.
- */
- noreveal_index: number;
-
- /**
- * Signature of TALER_RefreshMeltConfirmationPS whereby the exchange
- * affirms the successful melt and confirming the noreveal_index
- */
- exchange_sig: EddsaSignatureString;
-
- /*
- * public EdDSA key of the exchange that was used to generate the signature.
- * Should match one of the exchange's signing keys from /keys. Again given
- * explicitly as the client might otherwise be confused by clock skew as to
- * which signing key was used.
- */
- exchange_pub: EddsaPublicKeyString;
-
- /*
- * Base URL to use for operations on the refresh context
- * (so the reveal operation). If not given,
- * the base URL is the same as the one used for this request.
- * Can be used if the base URL for /refreshes/ differs from that
- * for /coins/, i.e. for load balancing. Clients SHOULD
- * respect the refresh_base_url if provided. Any HTTP server
- * belonging to an exchange MUST generate a 307 or 308 redirection
- * to the correct base URL should a client uses the wrong base
- * URL, or if the base URL has changed since the melt.
- *
- * When melting the same coin twice (technically allowed
- * as the response might have been lost on the network),
- * the exchange may return different values for the refresh_base_url.
- */
- refresh_base_url?: string;
-}
-
-export interface ExchangeRevealItem {
- ev_sig: BlindedDenominationSignature;
-}
-
-export interface ExchangeRevealResponse {
- // List of the exchange's blinded RSA signatures on the new coins.
- ev_sigs: ExchangeRevealItem[];
-}
-
-interface MerchantOrderStatusPaid {
- // Was the payment refunded (even partially, via refund or abort)?
- refunded: boolean;
-
- // Is any amount of the refund still waiting to be picked up (even partially)?
- refund_pending: boolean;
-
- // Amount that was refunded in total.
- refund_amount: AmountString;
-
- // Amount that already taken by the wallet.
- refund_taken: AmountString;
-}
-
-interface MerchantOrderRefundResponse {
- /**
- * Amount that was refunded in total.
- */
- refund_amount: AmountString;
-
- /**
- * Successful refunds for this payment, empty array for none.
- */
- refunds: MerchantCoinRefundStatus[];
-
- /**
- * Public key of the merchant.
- */
- merchant_pub: EddsaPublicKeyString;
-}
-
-export type MerchantCoinRefundStatus =
- | MerchantCoinRefundSuccessStatus
- | MerchantCoinRefundFailureStatus;
-
-export interface MerchantCoinRefundSuccessStatus {
- type: "success";
-
- // HTTP status of the exchange request, 200 (integer) required for refund confirmations.
- exchange_status: 200;
-
- // the EdDSA :ref:signature (binary-only) with purpose
- // TALER_SIGNATURE_EXCHANGE_CONFIRM_REFUND using a current signing key of the
- // exchange affirming the successful refund
- exchange_sig: EddsaSignatureString;
-
- // public EdDSA key of the exchange that was used to generate the signature.
- // Should match one of the exchange's signing keys from /keys. It is given
- // explicitly as the client might otherwise be confused by clock skew as to
- // which signing key was used.
- exchange_pub: EddsaPublicKeyString;
-
- // Refund transaction ID.
- rtransaction_id: number;
-
- // public key of a coin that was refunded
- coin_pub: EddsaPublicKeyString;
-
- // Amount that was refunded, including refund fee charged by the exchange
- // to the customer.
- refund_amount: AmountString;
-
- execution_time: TalerProtocolTimestamp;
-}
-
-export interface MerchantCoinRefundFailureStatus {
- type: "failure";
-
- // HTTP status of the exchange request, must NOT be 200.
- exchange_status: number;
-
- // Taler error code from the exchange reply, if available.
- exchange_code?: number;
-
- // If available, HTTP reply from the exchange.
- exchange_reply?: any;
-
- // Refund transaction ID.
- rtransaction_id: number;
-
- // public key of a coin that was refunded
- coin_pub: EddsaPublicKeyString;
-
- // Amount that was refunded, including refund fee charged by the exchange
- // to the customer.
- refund_amount: AmountString;
-
- execution_time: TalerProtocolTimestamp;
-}
-
-export interface MerchantOrderStatusUnpaid {
- /**
- * URI that the wallet must process to complete the payment.
- */
- taler_pay_uri: string;
-
- /**
- * Alternative order ID which was paid for already in the same session.
- *
- * Only given if the same product was purchased before in the same session.
- */
- already_paid_order_id?: string;
-}
-
-/**
- * Response body for the following endpoint:
- *
- * POST {talerBankIntegrationApi}/withdrawal-operation/{wopid}
- */
-export interface BankWithdrawalOperationPostResponse {
- transfer_done: boolean;
-}
-
-export type DenominationPubKey = RsaDenominationPubKey | CsDenominationPubKey;
-
-export interface RsaDenominationPubKey {
- readonly cipher: DenomKeyType.Rsa;
- readonly rsa_public_key: string;
- readonly age_mask: number;
-}
-
-export interface CsDenominationPubKey {
- readonly cipher: DenomKeyType.ClauseSchnorr;
- readonly age_mask: number;
- readonly cs_public_key: string;
-}
-
-export namespace DenominationPubKey {
- export function cmp(
- p1: DenominationPubKey,
- p2: DenominationPubKey,
- ): -1 | 0 | 1 {
- if (p1.cipher < p2.cipher) {
- return -1;
- } else if (p1.cipher > p2.cipher) {
- return +1;
- } else if (
- p1.cipher === DenomKeyType.Rsa &&
- p2.cipher === DenomKeyType.Rsa
- ) {
- if ((p1.age_mask ?? 0) < (p2.age_mask ?? 0)) {
- return -1;
- } else if ((p1.age_mask ?? 0) > (p2.age_mask ?? 0)) {
- return 1;
- }
- return strcmp(p1.rsa_public_key, p2.rsa_public_key);
- } else if (
- p1.cipher === DenomKeyType.ClauseSchnorr &&
- p2.cipher === DenomKeyType.ClauseSchnorr
- ) {
- if ((p1.age_mask ?? 0) < (p2.age_mask ?? 0)) {
- return -1;
- } else if ((p1.age_mask ?? 0) > (p2.age_mask ?? 0)) {
- return 1;
- }
- return strcmp(p1.cs_public_key, p2.cs_public_key);
- } else {
- throw Error("unsupported cipher");
- }
- }
-}
-
-export const codecForRsaDenominationPubKey = () =>
- buildCodecForObject<RsaDenominationPubKey>()
- .property("cipher", codecForConstString(DenomKeyType.Rsa))
- .property("rsa_public_key", codecForString())
- .property("age_mask", codecForNumber())
- .build("DenominationPubKey");
-
-export const codecForCsDenominationPubKey = () =>
- buildCodecForObject<CsDenominationPubKey>()
- .property("cipher", codecForConstString(DenomKeyType.ClauseSchnorr))
- .property("cs_public_key", codecForString())
- .property("age_mask", codecForNumber())
- .build("CsDenominationPubKey");
-
-export const codecForDenominationPubKey = () =>
- buildCodecForUnion<DenominationPubKey>()
- .discriminateOn("cipher")
- .alternative(DenomKeyType.Rsa, codecForRsaDenominationPubKey())
- .alternative(DenomKeyType.ClauseSchnorr, codecForCsDenominationPubKey())
- .build("DenominationPubKey");
-
-export const codecForBankWithdrawalOperationPostResponse =
- (): Codec<BankWithdrawalOperationPostResponse> =>
- buildCodecForObject<BankWithdrawalOperationPostResponse>()
- .property("transfer_done", codecForBoolean())
- .build("BankWithdrawalOperationPostResponse");
-
-export type AmountString = string;
-export type Base32String = string;
-export type EddsaSignatureString = string;
-export type EddsaPublicKeyString = string;
-export type CoinPublicKeyString = string;
-
-export const codecForDenomination = (): Codec<ExchangeDenomination> =>
- buildCodecForObject<ExchangeDenomination>()
- .property("value", codecForString())
- .property("denom_pub", codecForDenominationPubKey())
- .property("fee_withdraw", codecForString())
- .property("fee_deposit", codecForString())
- .property("fee_refresh", codecForString())
- .property("fee_refund", codecForString())
- .property("stamp_start", codecForTimestamp)
- .property("stamp_expire_withdraw", codecForTimestamp)
- .property("stamp_expire_legal", codecForTimestamp)
- .property("stamp_expire_deposit", codecForTimestamp)
- .property("master_sig", codecForString())
- .build("Denomination");
-
-export const codecForAuditorDenomSig = (): Codec<AuditorDenomSig> =>
- buildCodecForObject<AuditorDenomSig>()
- .property("denom_pub_h", codecForString())
- .property("auditor_sig", codecForString())
- .build("AuditorDenomSig");
-
-export const codecForAuditor = (): Codec<ExchangeAuditor> =>
- buildCodecForObject<ExchangeAuditor>()
- .property("auditor_pub", codecForString())
- .property("auditor_url", codecForString())
- .property("denomination_keys", codecForList(codecForAuditorDenomSig()))
- .build("Auditor");
-
-export const codecForExchangeHandle = (): Codec<ExchangeHandle> =>
- buildCodecForObject<ExchangeHandle>()
- .property("master_pub", codecForString())
- .property("url", codecForString())
- .build("ExchangeHandle");
-
-export const codecForAuditorHandle = (): Codec<AuditorHandle> =>
- buildCodecForObject<AuditorHandle>()
- .property("name", codecForString())
- .property("auditor_pub", codecForString())
- .property("url", codecForString())
- .build("AuditorHandle");
-
-export const codecForLocation = (): Codec<Location> =>
- buildCodecForObject<Location>()
- .property("country", codecOptional(codecForString()))
- .property("country_subdivision", codecOptional(codecForString()))
- .property("building_name", codecOptional(codecForString()))
- .property("building_number", codecOptional(codecForString()))
- .property("district", codecOptional(codecForString()))
- .property("street", codecOptional(codecForString()))
- .property("post_code", codecOptional(codecForString()))
- .property("town", codecOptional(codecForString()))
- .property("town_location", codecOptional(codecForString()))
- .property("address_lines", codecOptional(codecForList(codecForString())))
- .build("Location");
-
-export const codecForMerchantInfo = (): Codec<MerchantInfo> =>
- buildCodecForObject<MerchantInfo>()
- .property("name", codecForString())
- .property("address", codecOptional(codecForLocation()))
- .property("jurisdiction", codecOptional(codecForLocation()))
- .build("MerchantInfo");
-
-export const codecForTax = (): Codec<Tax> =>
- buildCodecForObject<Tax>()
- .property("name", codecForString())
- .property("tax", codecForString())
- .build("Tax");
-
-export const codecForInternationalizedString =
- (): Codec<InternationalizedString> => codecForMap(codecForString());
-
-export const codecForProduct = (): Codec<Product> =>
- buildCodecForObject<Product>()
- .property("product_id", codecOptional(codecForString()))
- .property("description", codecForString())
- .property(
- "description_i18n",
- codecOptional(codecForInternationalizedString()),
- )
- .property("quantity", codecOptional(codecForNumber()))
- .property("unit", codecOptional(codecForString()))
- .property("price", codecOptional(codecForString()))
- .build("Tax");
-
-export const codecForContractTerms = (): Codec<ContractTerms> =>
- buildCodecForObject<ContractTerms>()
- .property("order_id", codecForString())
- .property("fulfillment_url", codecOptional(codecForString()))
- .property("fulfillment_message", codecOptional(codecForString()))
- .property(
- "fulfillment_message_i18n",
- codecOptional(codecForInternationalizedString()),
- )
- .property("merchant_base_url", codecForString())
- .property("h_wire", codecForString())
- .property("auto_refund", codecOptional(codecForDuration))
- .property("wire_method", codecForString())
- .property("summary", codecForString())
- .property("summary_i18n", codecOptional(codecForInternationalizedString()))
- .property("nonce", codecForString())
- .property("amount", codecForString())
- .property("auditors", codecForList(codecForAuditorHandle()))
- .property("pay_deadline", codecForTimestamp)
- .property("refund_deadline", codecForTimestamp)
- .property("wire_transfer_deadline", codecForTimestamp)
- .property("timestamp", codecForTimestamp)
- .property("delivery_location", codecOptional(codecForLocation()))
- .property("delivery_date", codecOptional(codecForTimestamp))
- .property("max_fee", codecForString())
- .property("max_wire_fee", codecOptional(codecForString()))
- .property("merchant", codecForMerchantInfo())
- .property("merchant_pub", codecForString())
- .property("exchanges", codecForList(codecForExchangeHandle()))
- .property("products", codecOptional(codecForList(codecForProduct())))
- .property("extra", codecForAny())
- .property("minimum_age", codecOptional(codecForNumber()))
- .build("ContractTerms");
-
-export const codecForMerchantRefundPermission =
- (): Codec<MerchantAbortPayRefundDetails> =>
- buildCodecForObject<MerchantAbortPayRefundDetails>()
- .property("refund_amount", codecForAmountString())
- .property("refund_fee", codecForAmountString())
- .property("coin_pub", codecForString())
- .property("rtransaction_id", codecForNumber())
- .property("exchange_http_status", codecForNumber())
- .property("exchange_code", codecOptional(codecForNumber()))
- .property("exchange_reply", codecOptional(codecForAny()))
- .property("exchange_sig", codecOptional(codecForString()))
- .property("exchange_pub", codecOptional(codecForString()))
- .build("MerchantRefundPermission");
-
-export const codecForMerchantRefundResponse =
- (): Codec<MerchantRefundResponse> =>
- buildCodecForObject<MerchantRefundResponse>()
- .property("merchant_pub", codecForString())
- .property("h_contract_terms", codecForString())
- .property("refunds", codecForList(codecForMerchantRefundPermission()))
- .build("MerchantRefundResponse");
-
-export const codecForBlindSigWrapperV2 = (): Codec<MerchantBlindSigWrapperV2> =>
- buildCodecForObject<MerchantBlindSigWrapperV2>()
- .property("blind_sig", codecForBlindedDenominationSignature())
- .build("MerchantBlindSigWrapperV2");
-
-export const codecForMerchantTipResponseV2 = (): Codec<MerchantTipResponseV2> =>
- buildCodecForObject<MerchantTipResponseV2>()
- .property("blind_sigs", codecForList(codecForBlindSigWrapperV2()))
- .build("MerchantTipResponseV2");
-
-export const codecForRecoup = (): Codec<Recoup> =>
- buildCodecForObject<Recoup>()
- .property("h_denom_pub", codecForString())
- .build("Recoup");
-
-export const codecForExchangeSigningKey = (): Codec<ExchangeSignKeyJson> =>
- buildCodecForObject<ExchangeSignKeyJson>()
- .property("key", codecForString())
- .property("master_sig", codecForString())
- .property("stamp_end", codecForTimestamp)
- .property("stamp_start", codecForTimestamp)
- .property("stamp_expire", codecForTimestamp)
- .build("ExchangeSignKeyJson");
-
-export const codecForGlobalFees = (): Codec<GlobalFees> =>
- buildCodecForObject<GlobalFees>()
- .property("start_date", codecForTimestamp)
- .property("end_date", codecForTimestamp)
- .property("kyc_fee", codecForAmountString())
- .property("history_fee", codecForAmountString())
- .property("account_fee", codecForAmountString())
- .property("purse_fee", codecForAmountString())
- .property("history_expiration", codecForDuration)
- .property("account_kyc_timeout", codecForDuration)
- .property("purse_account_limit", codecForNumber())
- .property("purse_timeout", codecForDuration)
- .property("master_sig", codecForString())
- .build("GlobalFees");
-
-export const codecForExchangeKeysJson = (): Codec<ExchangeKeysJson> =>
- buildCodecForObject<ExchangeKeysJson>()
- .property("denoms", codecForList(codecForDenomination()))
- .property("master_public_key", codecForString())
- .property("auditors", codecForList(codecForAuditor()))
- .property("list_issue_date", codecForTimestamp)
- .property("recoup", codecOptional(codecForList(codecForRecoup())))
- .property("signkeys", codecForList(codecForExchangeSigningKey()))
- .property("version", codecForString())
- .property("reserve_closing_delay", codecForDuration)
- .property("global_fees", codecForList(codecForGlobalFees()))
- .build("ExchangeKeysJson");
-
-export const codecForWireFeesJson = (): Codec<WireFeesJson> =>
- buildCodecForObject<WireFeesJson>()
- .property("wire_fee", codecForString())
- .property("closing_fee", codecForString())
- .property("wad_fee", codecForString())
- .property("sig", codecForString())
- .property("start_date", codecForTimestamp)
- .property("end_date", codecForTimestamp)
- .build("WireFeesJson");
-
-export const codecForAccountInfo = (): Codec<AccountInfo> =>
- buildCodecForObject<AccountInfo>()
- .property("payto_uri", codecForString())
- .property("master_sig", codecForString())
- .build("AccountInfo");
-
-export const codecForExchangeWireJson = (): Codec<ExchangeWireJson> =>
- buildCodecForObject<ExchangeWireJson>()
- .property("accounts", codecForList(codecForAccountInfo()))
- .property("fees", codecForMap(codecForList(codecForWireFeesJson())))
- .build("ExchangeWireJson");
-
-export const codecForProposal = (): Codec<Proposal> =>
- buildCodecForObject<Proposal>()
- .property("contract_terms", codecForAny())
- .property("sig", codecForString())
- .build("Proposal");
-
-export const codecForCheckPaymentResponse = (): Codec<CheckPaymentResponse> =>
- buildCodecForObject<CheckPaymentResponse>()
- .property("order_status", codecForString())
- .property("refunded", codecOptional(codecForBoolean()))
- .property("refunded_amount", codecOptional(codecForString()))
- .property("contract_terms", codecOptional(codecForAny()))
- .property("taler_pay_uri", codecOptional(codecForString()))
- .property("contract_url", codecOptional(codecForString()))
- .build("CheckPaymentResponse");
-
-export const codecForWithdrawOperationStatusResponse =
- (): Codec<WithdrawOperationStatusResponse> =>
- buildCodecForObject<WithdrawOperationStatusResponse>()
- .property("selection_done", codecForBoolean())
- .property("transfer_done", codecForBoolean())
- .property("aborted", codecForBoolean())
- .property("amount", codecForString())
- .property("sender_wire", codecOptional(codecForString()))
- .property("suggested_exchange", codecOptional(codecForString()))
- .property("confirm_transfer_url", codecOptional(codecForString()))
- .property("wire_types", codecForList(codecForString()))
- .build("WithdrawOperationStatusResponse");
-
-export const codecForTipPickupGetResponse = (): Codec<TipPickupGetResponse> =>
- buildCodecForObject<TipPickupGetResponse>()
- .property("tip_amount", codecForString())
- .property("exchange_url", codecForString())
- .property("expiration", codecForTimestamp)
- .build("TipPickupGetResponse");
-
-export const codecForRecoupConfirmation = (): Codec<RecoupConfirmation> =>
- buildCodecForObject<RecoupConfirmation>()
- .property("reserve_pub", codecOptional(codecForString()))
- .property("old_coin_pub", codecOptional(codecForString()))
- .build("RecoupConfirmation");
-
-export const codecForWithdrawResponse = (): Codec<WithdrawResponse> =>
- buildCodecForObject<WithdrawResponse>()
- .property("ev_sig", codecForBlindedDenominationSignature())
- .build("WithdrawResponse");
-
-export const codecForWithdrawBatchResponse = (): Codec<WithdrawBatchResponse> =>
- buildCodecForObject<WithdrawBatchResponse>()
- .property("ev_sigs", codecForList(codecForWithdrawResponse()))
- .build("WithdrawBatchResponse");
-
-export const codecForMerchantPayResponse = (): Codec<MerchantPayResponse> =>
- buildCodecForObject<MerchantPayResponse>()
- .property("sig", codecForString())
- .build("MerchantPayResponse");
-
-export const codecForExchangeMeltResponse = (): Codec<ExchangeMeltResponse> =>
- buildCodecForObject<ExchangeMeltResponse>()
- .property("exchange_pub", codecForString())
- .property("exchange_sig", codecForString())
- .property("noreveal_index", codecForNumber())
- .property("refresh_base_url", codecOptional(codecForString()))
- .build("ExchangeMeltResponse");
-
-export const codecForExchangeRevealItem = (): Codec<ExchangeRevealItem> =>
- buildCodecForObject<ExchangeRevealItem>()
- .property("ev_sig", codecForBlindedDenominationSignature())
- .build("ExchangeRevealItem");
-
-export const codecForExchangeRevealResponse =
- (): Codec<ExchangeRevealResponse> =>
- buildCodecForObject<ExchangeRevealResponse>()
- .property("ev_sigs", codecForList(codecForExchangeRevealItem()))
- .build("ExchangeRevealResponse");
-
-export const codecForMerchantCoinRefundSuccessStatus =
- (): Codec<MerchantCoinRefundSuccessStatus> =>
- buildCodecForObject<MerchantCoinRefundSuccessStatus>()
- .property("type", codecForConstString("success"))
- .property("coin_pub", codecForString())
- .property("exchange_status", codecForConstNumber(200))
- .property("exchange_sig", codecForString())
- .property("rtransaction_id", codecForNumber())
- .property("refund_amount", codecForString())
- .property("exchange_pub", codecForString())
- .property("execution_time", codecForTimestamp)
- .build("MerchantCoinRefundSuccessStatus");
-
-export const codecForMerchantCoinRefundFailureStatus =
- (): Codec<MerchantCoinRefundFailureStatus> =>
- buildCodecForObject<MerchantCoinRefundFailureStatus>()
- .property("type", codecForConstString("failure"))
- .property("coin_pub", codecForString())
- .property("exchange_status", codecForNumber())
- .property("rtransaction_id", codecForNumber())
- .property("refund_amount", codecForString())
- .property("exchange_code", codecOptional(codecForNumber()))
- .property("exchange_reply", codecOptional(codecForAny()))
- .property("execution_time", codecForTimestamp)
- .build("MerchantCoinRefundFailureStatus");
-
-export const codecForMerchantCoinRefundStatus =
- (): Codec<MerchantCoinRefundStatus> =>
- buildCodecForUnion<MerchantCoinRefundStatus>()
- .discriminateOn("type")
- .alternative("success", codecForMerchantCoinRefundSuccessStatus())
- .alternative("failure", codecForMerchantCoinRefundFailureStatus())
- .build("MerchantCoinRefundStatus");
-
-export const codecForMerchantOrderStatusPaid =
- (): Codec<MerchantOrderStatusPaid> =>
- buildCodecForObject<MerchantOrderStatusPaid>()
- .property("refund_amount", codecForString())
- .property("refund_taken", codecForString())
- .property("refund_pending", codecForBoolean())
- .property("refunded", codecForBoolean())
- .build("MerchantOrderStatusPaid");
-
-export const codecForMerchantOrderRefundPickupResponse =
- (): Codec<MerchantOrderRefundResponse> =>
- buildCodecForObject<MerchantOrderRefundResponse>()
- .property("merchant_pub", codecForString())
- .property("refund_amount", codecForString())
- .property("refunds", codecForList(codecForMerchantCoinRefundStatus()))
- .build("MerchantOrderRefundPickupResponse");
-
-export const codecForMerchantOrderStatusUnpaid =
- (): Codec<MerchantOrderStatusUnpaid> =>
- buildCodecForObject<MerchantOrderStatusUnpaid>()
- .property("taler_pay_uri", codecForString())
- .property("already_paid_order_id", codecOptional(codecForString()))
- .build("MerchantOrderStatusUnpaid");
-
-export interface AbortRequest {
- // hash of the order's contract terms (this is used to authenticate the
- // wallet/customer in case $ORDER_ID is guessable).
- h_contract: string;
-
- // List of coins the wallet would like to see refunds for.
- // (Should be limited to the coins for which the original
- // payment succeeded, as far as the wallet knows.)
- coins: AbortingCoin[];
-}
-
-export interface AbortingCoin {
- // Public key of a coin for which the wallet is requesting an abort-related refund.
- coin_pub: EddsaPublicKeyString;
-
- // The amount to be refunded (matches the original contribution)
- contribution: AmountString;
-
- // URL of the exchange this coin was withdrawn from.
- exchange_url: string;
-}
-
-export interface AbortResponse {
- // List of refund responses about the coins that the wallet
- // requested an abort for. In the same order as the 'coins'
- // from the original request.
- // The rtransaction_id is implied to be 0.
- refunds: MerchantAbortPayRefundStatus[];
-}
-
-export const codecForMerchantAbortPayRefundSuccessStatus =
- (): Codec<MerchantAbortPayRefundSuccessStatus> =>
- buildCodecForObject<MerchantAbortPayRefundSuccessStatus>()
- .property("exchange_pub", codecForString())
- .property("exchange_sig", codecForString())
- .property("exchange_status", codecForConstNumber(200))
- .property("type", codecForConstString("success"))
- .build("MerchantAbortPayRefundSuccessStatus");
-
-export const codecForMerchantAbortPayRefundFailureStatus =
- (): Codec<MerchantAbortPayRefundFailureStatus> =>
- buildCodecForObject<MerchantAbortPayRefundFailureStatus>()
- .property("exchange_code", codecForNumber())
- .property("exchange_reply", codecForAny())
- .property("exchange_status", codecForNumber())
- .property("type", codecForConstString("failure"))
- .build("MerchantAbortPayRefundFailureStatus");
-
-export const codecForMerchantAbortPayRefundStatus =
- (): Codec<MerchantAbortPayRefundStatus> =>
- buildCodecForUnion<MerchantAbortPayRefundStatus>()
- .discriminateOn("type")
- .alternative("success", codecForMerchantAbortPayRefundSuccessStatus())
- .alternative("failure", codecForMerchantAbortPayRefundFailureStatus())
- .build("MerchantAbortPayRefundStatus");
-
-export const codecForAbortResponse = (): Codec<AbortResponse> =>
- buildCodecForObject<AbortResponse>()
- .property("refunds", codecForList(codecForMerchantAbortPayRefundStatus()))
- .build("AbortResponse");
-
-export type MerchantAbortPayRefundStatus =
- | MerchantAbortPayRefundSuccessStatus
- | MerchantAbortPayRefundFailureStatus;
-
-// Details about why a refund failed.
-export interface MerchantAbortPayRefundFailureStatus {
- // Used as tag for the sum type RefundStatus sum type.
- type: "failure";
-
- // HTTP status of the exchange request, must NOT be 200.
- exchange_status: number;
-
- // Taler error code from the exchange reply, if available.
- exchange_code?: number;
-
- // If available, HTTP reply from the exchange.
- exchange_reply?: unknown;
-}
-
-// Additional details needed to verify the refund confirmation signature
-// (h_contract_terms and merchant_pub) are already known
-// to the wallet and thus not included.
-export interface MerchantAbortPayRefundSuccessStatus {
- // Used as tag for the sum type MerchantCoinRefundStatus sum type.
- type: "success";
-
- // HTTP status of the exchange request, 200 (integer) required for refund confirmations.
- exchange_status: 200;
-
- // the EdDSA :ref:signature (binary-only) with purpose
- // TALER_SIGNATURE_EXCHANGE_CONFIRM_REFUND using a current signing key of the
- // exchange affirming the successful refund
- exchange_sig: string;
-
- // public EdDSA key of the exchange that was used to generate the signature.
- // Should match one of the exchange's signing keys from /keys. It is given
- // explicitly as the client might otherwise be confused by clock skew as to
- // which signing key was used.
- exchange_pub: string;
-}
-
-export interface TalerConfigResponse {
- name: string;
- version: string;
- currency?: string;
-}
-
-export const codecForTalerConfigResponse = (): Codec<TalerConfigResponse> =>
- buildCodecForObject<TalerConfigResponse>()
- .property("name", codecForString())
- .property("version", codecForString())
- .property("currency", codecOptional(codecForString()))
- .build("TalerConfigResponse");
-
-export interface FutureKeysResponse {
- future_denoms: any[];
-
- future_signkeys: any[];
-
- master_pub: string;
-
- denom_secmod_public_key: string;
-
- // Public key of the signkey security module.
- signkey_secmod_public_key: string;
-}
-
-export const codecForKeysManagementResponse = (): Codec<FutureKeysResponse> =>
- buildCodecForObject<FutureKeysResponse>()
- .property("master_pub", codecForString())
- .property("future_signkeys", codecForList(codecForAny()))
- .property("future_denoms", codecForList(codecForAny()))
- .property("denom_secmod_public_key", codecForAny())
- .property("signkey_secmod_public_key", codecForAny())
- .build("FutureKeysResponse");
-
-export interface MerchantConfigResponse {
- currency: string;
- name: string;
- version: string;
-}
-
-export const codecForMerchantConfigResponse =
- (): Codec<MerchantConfigResponse> =>
- buildCodecForObject<MerchantConfigResponse>()
- .property("currency", codecForString())
- .property("name", codecForString())
- .property("version", codecForString())
- .build("MerchantConfigResponse");
-
-export enum ExchangeProtocolVersion {
- /**
- * Current version supported by the wallet.
- */
- V12 = 12,
-}
-
-export enum MerchantProtocolVersion {
- /**
- * Current version supported by the wallet.
- */
- V3 = 3,
-}
-
-export type CoinEnvelope = CoinEnvelopeRsa | CoinEnvelopeCs;
-
-export interface CoinEnvelopeRsa {
- cipher: DenomKeyType.Rsa;
- rsa_blinded_planchet: string;
-}
-
-export interface CoinEnvelopeCs {
- cipher: DenomKeyType.ClauseSchnorr;
- // FIXME: add remaining fields
-}
-
-export type HashCodeString = string;
-
-export interface ExchangeWithdrawRequest {
- denom_pub_hash: HashCodeString;
- reserve_sig: EddsaSignatureString;
- coin_ev: CoinEnvelope;
-}
-
-export interface ExchangeRefreshRevealRequest {
- new_denoms_h: HashCodeString[];
- coin_evs: CoinEnvelope[];
- /**
- * kappa - 1 transfer private keys (ephemeral ECDHE keys).
- */
- transfer_privs: string[];
-
- transfer_pub: EddsaPublicKeyString;
-
- link_sigs: EddsaSignatureString[];
-
- /**
- * Iff the corresponding denomination has support for age restriction,
- * the client MUST provide the original age commitment, i.e. the vector
- * of public keys.
- */
- old_age_commitment?: Edx25519PublicKeyEnc[];
-}
-
-export interface DepositSuccess {
- // Optional base URL of the exchange for looking up wire transfers
- // associated with this transaction. If not given,
- // the base URL is the same as the one used for this request.
- // Can be used if the base URL for /transactions/ differs from that
- // for /coins/, i.e. for load balancing. Clients SHOULD
- // respect the transaction_base_url if provided. Any HTTP server
- // belonging to an exchange MUST generate a 307 or 308 redirection
- // to the correct base URL should a client uses the wrong base
- // URL, or if the base URL has changed since the deposit.
- transaction_base_url?: string;
-
- // timestamp when the deposit was received by the exchange.
- exchange_timestamp: TalerProtocolTimestamp;
-
- // the EdDSA signature of TALER_DepositConfirmationPS using a current
- // signing key of the exchange affirming the successful
- // deposit and that the exchange will transfer the funds after the refund
- // deadline, or as soon as possible if the refund deadline is zero.
- exchange_sig: string;
-
- // public EdDSA key of the exchange that was used to
- // generate the signature.
- // Should match one of the exchange's signing keys from /keys. It is given
- // explicitly as the client might otherwise be confused by clock skew as to
- // which signing key was used.
- exchange_pub: string;
-}
-
-export const codecForDepositSuccess = (): Codec<DepositSuccess> =>
- buildCodecForObject<DepositSuccess>()
- .property("exchange_pub", codecForString())
- .property("exchange_sig", codecForString())
- .property("exchange_timestamp", codecForTimestamp)
- .property("transaction_base_url", codecOptional(codecForString()))
- .build("DepositSuccess");
-
-export interface PurseDeposit {
- /**
- * Amount to be deposited, can be a fraction of the
- * coin's total value.
- */
- amount: AmountString;
-
- /**
- * Hash of denomination RSA key with which the coin is signed.
- */
- denom_pub_hash: HashCodeString;
-
- /**
- * Exchange's unblinded RSA signature of the coin.
- */
- ub_sig: UnblindedSignature;
-
- /**
- * Age commitment for the coin, if the denomination is age-restricted.
- */
- age_commitment?: string[];
-
- /**
- * Attestation for the minimum age, if the denomination is age-restricted.
- */
- attest?: string;
-
- /**
- * Signature over TALER_PurseDepositSignaturePS
- * of purpose TALER_SIGNATURE_WALLET_PURSE_DEPOSIT
- * made by the customer with the
- * coin's private key.
- */
- coin_sig: EddsaSignatureString;
-
- /**
- * Public key of the coin being deposited into the purse.
- */
- coin_pub: EddsaPublicKeyString;
-}
-
-export interface ExchangePurseMergeRequest {
- // payto://-URI of the account the purse is to be merged into.
- // Must be of the form: 'payto://taler/$EXCHANGE_URL/$RESERVE_PUB'.
- payto_uri: string;
-
- // EdDSA signature of the account/reserve affirming the merge
- // over a TALER_AccountMergeSignaturePS.
- // Must be of purpose TALER_SIGNATURE_ACCOUNT_MERGE
- reserve_sig: EddsaSignatureString;
-
- // EdDSA signature of the purse private key affirming the merge
- // over a TALER_PurseMergeSignaturePS.
- // Must be of purpose TALER_SIGNATURE_PURSE_MERGE.
- merge_sig: EddsaSignatureString;
-
- // Client-side timestamp of when the merge request was made.
- merge_timestamp: TalerProtocolTimestamp;
-}
-
-export interface ExchangeGetContractResponse {
- purse_pub: string;
- econtract_sig: string;
- econtract: string;
-}
-
-export const codecForExchangeGetContractResponse =
- (): Codec<ExchangeGetContractResponse> =>
- buildCodecForObject<ExchangeGetContractResponse>()
- .property("purse_pub", codecForString())
- .property("econtract_sig", codecForString())
- .property("econtract", codecForString())
- .build("ExchangeGetContractResponse");
-
-/**
- * Contract terms between two wallets (as opposed to a merchant and wallet).
- */
-export interface PeerContractTerms {
- amount: AmountString;
- summary: string;
- purse_expiration: TalerProtocolTimestamp;
-}
-
-export interface EncryptedContract {
- // Encrypted contract.
- econtract: string;
-
- // Signature over the (encrypted) contract.
- econtract_sig: string;
-
- // Ephemeral public key for the DH operation to decrypt the encrypted contract.
- contract_pub: string;
-}
-
-/**
- * Payload for /reserves/{reserve_pub}/purse
- * endpoint of the exchange.
- */
-export interface ExchangeReservePurseRequest {
- /**
- * Minimum amount that must be credited to the reserve, that is
- * the total value of the purse minus the deposit fees.
- * If the deposit fees are lower, the contribution to the
- * reserve can be higher!
- */
- purse_value: AmountString;
-
- // Minimum age required for all coins deposited into the purse.
- min_age: number;
-
- // Purse fee the reserve owner is willing to pay
- // for the purse creation. Optional, if not present
- // the purse is to be created from the purse quota
- // of the reserve.
- purse_fee: AmountString;
-
- // Optional encrypted contract, in case the buyer is
- // proposing the contract and thus establishing the
- // purse with the payment.
- econtract?: EncryptedContract;
-
- // EdDSA public key used to approve merges of this purse.
- merge_pub: EddsaPublicKeyString;
-
- // EdDSA signature of the purse private key affirming the merge
- // over a TALER_PurseMergeSignaturePS.
- // Must be of purpose TALER_SIGNATURE_PURSE_MERGE.
- merge_sig: EddsaSignatureString;
-
- // EdDSA signature of the account/reserve affirming the merge.
- // Must be of purpose TALER_SIGNATURE_WALLET_ACCOUNT_MERGE
- reserve_sig: EddsaSignatureString;
-
- // Purse public key.
- purse_pub: EddsaPublicKeyString;
-
- // EdDSA signature of the purse over
- // TALER_PurseRequestSignaturePS of
- // purpose TALER_SIGNATURE_PURSE_REQUEST
- // confirming that the
- // above details hold for this purse.
- purse_sig: EddsaSignatureString;
-
- // SHA-512 hash of the contact of the purse.
- h_contract_terms: HashCodeString;
-
- // Client-side timestamp of when the merge request was made.
- merge_timestamp: TalerProtocolTimestamp;
-
- // Indicative time by which the purse should expire
- // if it has not been paid.
- purse_expiration: TalerProtocolTimestamp;
-}
-
-export interface ExchangePurseDeposits {
- // Array of coins to deposit into the purse.
- deposits: PurseDeposit[];
-}
diff --git a/packages/taler-util/src/transactions-types.ts b/packages/taler-util/src/transactions-types.ts
@@ -0,0 +1,568 @@
+/*
+ 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 { TalerProtocolTimestamp } from "./time.js";
+import {
+ AmountString,
+ Product,
+ InternationalizedString,
+ MerchantInfo,
+ codecForInternationalizedString,
+ codecForMerchantInfo,
+ codecForProduct,
+ Location,
+} from "./taler-types.js";
+import {
+ Codec,
+ buildCodecForObject,
+ codecOptional,
+ codecForString,
+ codecForList,
+ codecForAny,
+} from "./codec.js";
+import {
+ RefreshReason,
+ TalerErrorDetail,
+ TransactionIdStr,
+} from "./wallet-types.js";
+
+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: TransactionIdStr;
+
+ // the type of the transaction; different types might provide additional information
+ type: TransactionType;
+
+ // main timestamp of the transaction
+ timestamp: TalerProtocolTimestamp;
+
+ // 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;
+
+ /**
+ * True if the transaction encountered a problem that might be
+ * permanent. A frozen transaction won't be automatically retried.
+ */
+ frozen: 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?: TalerErrorDetail;
+}
+
+export type Transaction =
+ | TransactionWithdrawal
+ | TransactionPayment
+ | TransactionRefund
+ | TransactionTip
+ | TransactionRefresh
+ | TransactionDeposit
+ | TransactionPeerPullCredit
+ | TransactionPeerPullDebit
+ | TransactionPeerPushCredit
+ | TransactionPeerPushDebit;
+
+export enum TransactionType {
+ Withdrawal = "withdrawal",
+ Payment = "payment",
+ Refund = "refund",
+ Refresh = "refresh",
+ Tip = "tip",
+ Deposit = "deposit",
+ PeerPushDebit = "peer-push-debit",
+ PeerPushCredit = "peer-push-credit",
+ PeerPullDebit = "peer-pull-debit",
+ PeerPullCredit = "peer-pull-credit",
+}
+
+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[];
+
+ // Public key of the reserve
+ reservePub: 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;
+
+ // Public key of the reserve
+ reservePub: string;
+}
+
+// This should only be used for actual withdrawals
+// and not for tips that have their own transactions type.
+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;
+}
+
+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.
+ */
+ talerUri: string;
+}
+
+/**
+ * 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.
+ */
+ 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;
+}
+
+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;
+
+ /**
+ * 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[];
+}
+
+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;
+
+ /**
+ * Time indicating when the order should be delivered.
+ * May be overwritten by individual products.
+ */
+ delivery_date?: TalerProtocolTimestamp;
+
+ /**
+ * Delivery location for (all!) products.
+ */
+ delivery_location?: Location;
+
+ /**
+ * 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;
+}
+
+export 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 pending to be picked up
+ */
+ refundPending: AmountString | undefined;
+
+ // 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;
+}
+
+export interface TransactionTip extends TransactionCommon {
+ type: TransactionType.Tip;
+
+ // Raw amount of the tip, without extra fees that apply
+ amountRaw: AmountString;
+
+ /**
+ * More information about the merchant
+ */
+ // merchant: MerchantInfo;
+
+ // Amount will be (or was) added to the wallet's balance after fees and refreshing
+ amountEffective: AmountString;
+
+ merchantBaseUrl: string;
+}
+
+/**
+ * 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;
+
+ /**
+ * Exchange that the coins are refreshed with
+ */
+ exchangeBaseUrl: string;
+
+ 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.
+ */
+ amountEffective: 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;
+}
+
+export interface TransactionByIdRequest {
+ transactionId: string;
+}
+
+export const codecForTransactionByIdRequest =
+ (): Codec<TransactionByIdRequest> =>
+ buildCodecForObject<TransactionByIdRequest>()
+ .property("transactionId", codecForString())
+ .build("TransactionByIdRequest");
+
+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");
diff --git a/packages/taler-util/src/transactionsTypes.ts b/packages/taler-util/src/transactionsTypes.ts
@@ -1,564 +0,0 @@
-/*
- 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 { TalerProtocolTimestamp } from "./time.js";
-import {
- AmountString,
- Product,
- InternationalizedString,
- MerchantInfo,
- codecForInternationalizedString,
- codecForMerchantInfo,
- codecForProduct,
- Location,
-} from "./talerTypes.js";
-import {
- Codec,
- buildCodecForObject,
- codecOptional,
- codecForString,
- codecForList,
- codecForAny,
-} from "./codec.js";
-import { RefreshReason, TalerErrorDetail } from "./walletTypes.js";
-
-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: TalerProtocolTimestamp;
-
- // 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;
-
- /**
- * True if the transaction encountered a problem that might be
- * permanent. A frozen transaction won't be automatically retried.
- */
- frozen: 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?: TalerErrorDetail;
-}
-
-export type Transaction =
- | TransactionWithdrawal
- | TransactionPayment
- | TransactionRefund
- | TransactionTip
- | TransactionRefresh
- | TransactionDeposit
- | TransactionPeerPullCredit
- | TransactionPeerPullDebit
- | TransactionPeerPushCredit
- | TransactionPeerPushDebit;
-
-export enum TransactionType {
- Withdrawal = "withdrawal",
- Payment = "payment",
- Refund = "refund",
- Refresh = "refresh",
- Tip = "tip",
- Deposit = "deposit",
- PeerPushDebit = "peer-push-debit",
- PeerPushCredit = "peer-push-credit",
- PeerPullDebit = "peer-pull-debit",
- PeerPullCredit = "peer-pull-credit",
-}
-
-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[];
-
- // Public key of the reserve
- reservePub: 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;
-
- // Public key of the reserve
- reservePub: string;
-}
-
-// This should only be used for actual withdrawals
-// and not for tips that have their own transactions type.
-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;
-}
-
-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.
- */
- talerUri: string;
-}
-
-/**
- * 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.
- */
- 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;
-}
-
-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;
-
- /**
- * 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[];
-}
-
-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;
-
- /**
- * Time indicating when the order should be delivered.
- * May be overwritten by individual products.
- */
- delivery_date?: TalerProtocolTimestamp;
-
- /**
- * Delivery location for (all!) products.
- */
- delivery_location?: Location;
-
- /**
- * 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;
-}
-
-export 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 pending to be picked up
- */
- refundPending: AmountString | undefined;
-
- // 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;
-}
-
-export interface TransactionTip extends TransactionCommon {
- type: TransactionType.Tip;
-
- // Raw amount of the tip, without extra fees that apply
- amountRaw: AmountString;
-
- /**
- * More information about the merchant
- */
- // merchant: MerchantInfo;
-
- // Amount will be (or was) added to the wallet's balance after fees and refreshing
- amountEffective: AmountString;
-
- merchantBaseUrl: string;
-}
-
-/**
- * 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;
-
- /**
- * Exchange that the coins are refreshed with
- */
- exchangeBaseUrl: string;
-
- 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.
- */
- amountEffective: 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;
-}
-
-export interface TransactionByIdRequest {
- transactionId: string;
-}
-
-export const codecForTransactionByIdRequest =
- (): Codec<TransactionByIdRequest> =>
- buildCodecForObject<TransactionByIdRequest>()
- .property("transactionId", codecForString())
- .build("TransactionByIdRequest");
-
-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");
diff --git a/packages/taler-util/src/types-test.ts b/packages/taler-util/src/types-test.ts
@@ -15,7 +15,7 @@
*/
import test from "ava";
-import { codecForContractTerms } from "./talerTypes.js";
+import { codecForContractTerms } from "./taler-types.js";
test("contract terms validation", (t) => {
const c = {
diff --git a/packages/taler-util/src/wallet-types.ts b/packages/taler-util/src/wallet-types.ts
@@ -0,0 +1,1836 @@
+/*
+ This file is part of GNU Taler
+ (C) 2015-2020 Taler Systems SA
+
+ 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.
+
+ 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
+ TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ * Types used by clients of the wallet.
+ *
+ * These types are defined in a separate file make tree shaking easier, since
+ * some components use these types (via RPC) but do not depend on the wallet
+ * code directly.
+ *
+ * @author Florian Dold <dold@taler.net>
+ */
+
+/**
+ * Imports.
+ */
+import {
+ AmountJson,
+ codecForAmountJson,
+ codecForAmountString,
+} from "./amounts.js";
+import {
+ AbsoluteTime,
+ codecForAbsoluteTime,
+ codecForTimestamp,
+ TalerProtocolDuration,
+ TalerProtocolTimestamp,
+} from "./time.js";
+import {
+ buildCodecForObject,
+ codecForString,
+ codecOptional,
+ Codec,
+ codecForList,
+ codecForBoolean,
+ codecForConstString,
+ codecForAny,
+ buildCodecForUnion,
+ codecForNumber,
+ codecForMap,
+} from "./codec.js";
+import {
+ AmountString,
+ AuditorDenomSig,
+ codecForContractTerms,
+ CoinEnvelope,
+ ContractTerms,
+ DenominationPubKey,
+ DenomKeyType,
+ ExchangeAuditor,
+ UnblindedSignature,
+} from "./taler-types.js";
+import { OrderShortInfo, codecForOrderShortInfo } from "./transactions-types.js";
+import { BackupRecovery } from "./backup-types.js";
+import { PaytoUri } from "./payto.js";
+import { TalerErrorCode } from "./taler-error-codes.js";
+import { AgeCommitmentProof } from "./taler-crypto.js";
+import { VersionMatchResult } from "./libtool-version.js";
+
+/**
+ * Identifier for a transaction in the wallet.
+ */
+export type TransactionIdStr = `tx:${string}:${string}`;
+
+/**
+ * Identifier for a pending task in the wallet.
+ */
+export type PendingIdStr = `pd:${string}:string`;
+
+/**
+ * Response for the create reserve request to the wallet.
+ */
+export class CreateReserveResponse {
+ /**
+ * Exchange URL where the bank should create the reserve.
+ * The URL is canonicalized in the response.
+ */
+ exchange: string;
+
+ /**
+ * Reserve public key of the newly created reserve.
+ */
+ reservePub: string;
+}
+
+export interface Balance {
+ available: AmountString;
+ pendingIncoming: AmountString;
+ pendingOutgoing: AmountString;
+
+ // Does the balance for this currency have a pending
+ // transaction?
+ hasPendingTransactions: boolean;
+
+ // Is there a pending transaction that would affect the balance
+ // and requires user input?
+ requiresUserInput: boolean;
+}
+
+export interface BalancesResponse {
+ balances: Balance[];
+}
+
+export const codecForBalance = (): Codec<Balance> =>
+ buildCodecForObject<Balance>()
+ .property("available", codecForString())
+ .property("hasPendingTransactions", codecForBoolean())
+ .property("pendingIncoming", codecForString())
+ .property("pendingOutgoing", codecForString())
+ .property("requiresUserInput", codecForBoolean())
+ .build("Balance");
+
+export const codecForBalancesResponse = (): Codec<BalancesResponse> =>
+ buildCodecForObject<BalancesResponse>()
+ .property("balances", codecForList(codecForBalance()))
+ .build("BalancesResponse");
+
+/**
+ * For terseness.
+ */
+export function mkAmount(
+ value: number,
+ fraction: number,
+ currency: string,
+): AmountJson {
+ return { value, fraction, currency };
+}
+
+export enum ConfirmPayResultType {
+ Done = "done",
+ Pending = "pending",
+}
+
+/**
+ * Result for confirmPay
+ */
+export interface ConfirmPayResultDone {
+ type: ConfirmPayResultType.Done;
+ contractTerms: ContractTerms;
+ transactionId: string;
+}
+
+export interface ConfirmPayResultPending {
+ type: ConfirmPayResultType.Pending;
+ transactionId: string;
+ lastError: TalerErrorDetail | undefined;
+}
+
+export type ConfirmPayResult = ConfirmPayResultDone | ConfirmPayResultPending;
+
+export const codecForConfirmPayResultPending =
+ (): Codec<ConfirmPayResultPending> =>
+ buildCodecForObject<ConfirmPayResultPending>()
+ .property("lastError", codecForAny())
+ .property("transactionId", codecForString())
+ .property("type", codecForConstString(ConfirmPayResultType.Pending))
+ .build("ConfirmPayResultPending");
+
+export const codecForConfirmPayResultDone = (): Codec<ConfirmPayResultDone> =>
+ buildCodecForObject<ConfirmPayResultDone>()
+ .property("type", codecForConstString(ConfirmPayResultType.Done))
+ .property("transactionId", codecForString())
+ .property("contractTerms", codecForContractTerms())
+ .build("ConfirmPayResultDone");
+
+export const codecForConfirmPayResult = (): Codec<ConfirmPayResult> =>
+ buildCodecForUnion<ConfirmPayResult>()
+ .discriminateOn("type")
+ .alternative(
+ ConfirmPayResultType.Pending,
+ codecForConfirmPayResultPending(),
+ )
+ .alternative(ConfirmPayResultType.Done, codecForConfirmPayResultDone())
+ .build("ConfirmPayResult");
+
+/**
+ * Information about all sender wire details known to the wallet,
+ * as well as exchanges that accept these wire types.
+ */
+export interface SenderWireInfos {
+ /**
+ * Mapping from exchange base url to list of accepted
+ * wire types.
+ */
+ exchangeWireTypes: { [exchangeBaseUrl: string]: string[] };
+
+ /**
+ * Sender wire information stored in the wallet.
+ */
+ senderWires: string[];
+}
+
+/**
+ * Request to create a reserve.
+ */
+export interface CreateReserveRequest {
+ /**
+ * The initial amount for the reserve.
+ */
+ amount: AmountJson;
+
+ /**
+ * Exchange URL where the bank should create the reserve.
+ */
+ exchange: string;
+
+ /**
+ * Payto URI that identifies the exchange's account that the funds
+ * for this reserve go into.
+ */
+ exchangePaytoUri?: string;
+
+ /**
+ * Wire details (as a payto URI) for the bank account that sent the funds to
+ * the exchange.
+ */
+ senderWire?: string;
+
+ /**
+ * URL to fetch the withdraw status from the bank.
+ */
+ bankWithdrawStatusUrl?: string;
+
+ /**
+ * Forced denomination selection for the first withdrawal
+ * from this reserve, only used for testing.
+ */
+ forcedDenomSel?: ForcedDenomSel;
+
+ restrictAge?: number;
+}
+
+export const codecForCreateReserveRequest = (): Codec<CreateReserveRequest> =>
+ buildCodecForObject<CreateReserveRequest>()
+ .property("amount", codecForAmountJson())
+ .property("exchange", codecForString())
+ .property("exchangePaytoUri", codecForString())
+ .property("senderWire", codecOptional(codecForString()))
+ .property("bankWithdrawStatusUrl", codecOptional(codecForString()))
+ .property("forcedDenomSel", codecForAny())
+ .build("CreateReserveRequest");
+
+/**
+ * Request to mark a reserve as confirmed.
+ */
+export interface ConfirmReserveRequest {
+ /**
+ * Public key of then reserve that should be marked
+ * as confirmed.
+ */
+ reservePub: string;
+}
+
+export const codecForConfirmReserveRequest = (): Codec<ConfirmReserveRequest> =>
+ buildCodecForObject<ConfirmReserveRequest>()
+ .property("reservePub", codecForString())
+ .build("ConfirmReserveRequest");
+
+/**
+ * Wire coins to the user's own bank account.
+ */
+export class ReturnCoinsRequest {
+ /**
+ * The amount to wire.
+ */
+ amount: AmountJson;
+
+ /**
+ * The exchange to take the coins from.
+ */
+ exchange: string;
+
+ /**
+ * Wire details for the bank account of the customer that will
+ * receive the funds.
+ */
+ senderWire?: string;
+
+ /**
+ * Verify that a value matches the schema of this class and convert it into a
+ * member.
+ */
+ static checked: (obj: any) => ReturnCoinsRequest;
+}
+
+export interface PrepareRefundResult {
+ proposalId: string;
+
+ effectivePaid: AmountString;
+ gone: AmountString;
+ granted: AmountString;
+ pending: boolean;
+ awaiting: AmountString;
+
+ info: OrderShortInfo;
+}
+
+export interface PrepareTipResult {
+ /**
+ * Unique ID for the tip assigned by the wallet.
+ * Typically different from the merchant-generated tip ID.
+ */
+ walletTipId: string;
+
+ /**
+ * Has the tip already been accepted?
+ */
+ accepted: boolean;
+
+ /**
+ * Amount that the merchant gave.
+ */
+ tipAmountRaw: AmountString;
+
+ /**
+ * Amount that arrived at the wallet.
+ * Might be lower than the raw amount due to fees.
+ */
+ tipAmountEffective: AmountString;
+
+ /**
+ * Base URL of the merchant backend giving then tip.
+ */
+ merchantBaseUrl: string;
+
+ /**
+ * Base URL of the exchange that is used to withdraw the tip.
+ * Determined by the merchant, the wallet/user has no choice here.
+ */
+ exchangeBaseUrl: string;
+
+ /**
+ * Time when the tip will expire. After it expired, it can't be picked
+ * up anymore.
+ */
+ expirationTimestamp: TalerProtocolTimestamp;
+}
+
+export interface AcceptTipResponse {
+ transactionId: string;
+}
+
+export const codecForPrepareTipResult = (): Codec<PrepareTipResult> =>
+ buildCodecForObject<PrepareTipResult>()
+ .property("accepted", codecForBoolean())
+ .property("tipAmountRaw", codecForAmountString())
+ .property("tipAmountEffective", codecForAmountString())
+ .property("exchangeBaseUrl", codecForString())
+ .property("merchantBaseUrl", codecForString())
+ .property("expirationTimestamp", codecForTimestamp)
+ .property("walletTipId", codecForString())
+ .build("PrepareTipResult");
+
+export interface BenchmarkResult {
+ time: { [s: string]: number };
+ repetitions: number;
+}
+
+export enum PreparePayResultType {
+ PaymentPossible = "payment-possible",
+ InsufficientBalance = "insufficient-balance",
+ AlreadyConfirmed = "already-confirmed",
+}
+
+export const codecForPreparePayResultPaymentPossible =
+ (): Codec<PreparePayResultPaymentPossible> =>
+ buildCodecForObject<PreparePayResultPaymentPossible>()
+ .property("amountEffective", codecForAmountString())
+ .property("amountRaw", codecForAmountString())
+ .property("contractTerms", codecForContractTerms())
+ .property("proposalId", codecForString())
+ .property("contractTermsHash", codecForString())
+ .property("noncePriv", codecForString())
+ .property(
+ "status",
+ codecForConstString(PreparePayResultType.PaymentPossible),
+ )
+ .build("PreparePayResultPaymentPossible");
+
+export const codecForPreparePayResultInsufficientBalance =
+ (): Codec<PreparePayResultInsufficientBalance> =>
+ buildCodecForObject<PreparePayResultInsufficientBalance>()
+ .property("amountRaw", codecForAmountString())
+ .property("contractTerms", codecForAny())
+ .property("proposalId", codecForString())
+ .property("noncePriv", codecForString())
+ .property(
+ "status",
+ codecForConstString(PreparePayResultType.InsufficientBalance),
+ )
+ .build("PreparePayResultInsufficientBalance");
+
+export const codecForPreparePayResultAlreadyConfirmed =
+ (): Codec<PreparePayResultAlreadyConfirmed> =>
+ buildCodecForObject<PreparePayResultAlreadyConfirmed>()
+ .property(
+ "status",
+ codecForConstString(PreparePayResultType.AlreadyConfirmed),
+ )
+ .property("amountEffective", codecForAmountString())
+ .property("amountRaw", codecForAmountString())
+ .property("paid", codecForBoolean())
+ .property("contractTerms", codecForAny())
+ .property("contractTermsHash", codecForString())
+ .property("proposalId", codecForString())
+ .build("PreparePayResultAlreadyConfirmed");
+
+export const codecForPreparePayResult = (): Codec<PreparePayResult> =>
+ buildCodecForUnion<PreparePayResult>()
+ .discriminateOn("status")
+ .alternative(
+ PreparePayResultType.AlreadyConfirmed,
+ codecForPreparePayResultAlreadyConfirmed(),
+ )
+ .alternative(
+ PreparePayResultType.InsufficientBalance,
+ codecForPreparePayResultInsufficientBalance(),
+ )
+ .alternative(
+ PreparePayResultType.PaymentPossible,
+ codecForPreparePayResultPaymentPossible(),
+ )
+ .build("PreparePayResult");
+
+/**
+ * Result of a prepare pay operation.
+ */
+export type PreparePayResult =
+ | PreparePayResultInsufficientBalance
+ | PreparePayResultAlreadyConfirmed
+ | PreparePayResultPaymentPossible;
+
+/**
+ * Payment is possible.
+ */
+export interface PreparePayResultPaymentPossible {
+ status: PreparePayResultType.PaymentPossible;
+ proposalId: string;
+ contractTerms: ContractTerms;
+ contractTermsHash: string;
+ amountRaw: string;
+ amountEffective: string;
+ noncePriv: string;
+}
+
+export interface PreparePayResultInsufficientBalance {
+ status: PreparePayResultType.InsufficientBalance;
+ proposalId: string;
+ contractTerms: ContractTerms;
+ amountRaw: string;
+ noncePriv: string;
+}
+
+export interface PreparePayResultAlreadyConfirmed {
+ status: PreparePayResultType.AlreadyConfirmed;
+ contractTerms: ContractTerms;
+ paid: boolean;
+ amountRaw: string;
+ amountEffective: string;
+ contractTermsHash: string;
+ proposalId: string;
+}
+
+export interface BankWithdrawDetails {
+ selectionDone: boolean;
+ transferDone: boolean;
+ amount: AmountJson;
+ senderWire?: string;
+ suggestedExchange?: string;
+ confirmTransferUrl?: string;
+ wireTypes: string[];
+}
+
+export interface AcceptWithdrawalResponse {
+ reservePub: string;
+ confirmTransferUrl?: string;
+ transactionId: string;
+}
+
+/**
+ * Details about a purchase, including refund status.
+ */
+export interface PurchaseDetails {
+ contractTerms: Record<string, undefined>;
+ hasRefund: boolean;
+ totalRefundAmount: AmountJson;
+ totalRefundAndRefreshFees: AmountJson;
+}
+
+export interface WalletDiagnostics {
+ walletManifestVersion: string;
+ walletManifestDisplayVersion: string;
+ errors: string[];
+ firefoxIdbProblem: boolean;
+ dbOutdated: boolean;
+}
+
+export interface TalerErrorDetail {
+ code: TalerErrorCode;
+ hint?: string;
+ [x: string]: unknown;
+}
+
+/**
+ * Minimal information needed about a planchet for unblinding a signature.
+ *
+ * Can be a withdrawal/tipping/refresh planchet.
+ */
+export interface PlanchetUnblindInfo {
+ denomPub: DenominationPubKey;
+ blindingKey: string;
+}
+
+export interface WithdrawalPlanchet {
+ coinPub: string;
+ coinPriv: string;
+ reservePub: string;
+ denomPubHash: string;
+ denomPub: DenominationPubKey;
+ blindingKey: string;
+ withdrawSig: string;
+ coinEv: CoinEnvelope;
+ coinValue: AmountJson;
+ coinEvHash: string;
+ ageCommitmentProof?: AgeCommitmentProof;
+}
+
+export interface PlanchetCreationRequest {
+ secretSeed: string;
+ coinIndex: number;
+ value: AmountJson;
+ feeWithdraw: AmountJson;
+ denomPub: DenominationPubKey;
+ reservePub: string;
+ reservePriv: string;
+ restrictAge?: number;
+}
+
+/**
+ * Reasons for why a coin is being refreshed.
+ */
+export enum RefreshReason {
+ Manual = "manual",
+ PayMerchant = "pay-merchant",
+ PayDeposit = "pay-deposit",
+ PayPeerPush = "pay-peer-push",
+ PayPeerPull = "pay-peer-pull",
+ Refund = "refund",
+ AbortPay = "abort-pay",
+ Recoup = "recoup",
+ BackupRestored = "backup-restored",
+ Scheduled = "scheduled",
+}
+
+/**
+ * Wrapper for coin public keys.
+ */
+export interface CoinPublicKey {
+ readonly coinPub: string;
+}
+
+/**
+ * Wrapper for refresh group IDs.
+ */
+export interface RefreshGroupId {
+ readonly refreshGroupId: string;
+}
+
+/**
+ * Private data required to make a deposit permission.
+ */
+export interface DepositInfo {
+ exchangeBaseUrl: string;
+ contractTermsHash: string;
+ coinPub: string;
+ coinPriv: string;
+ spendAmount: AmountJson;
+ timestamp: TalerProtocolTimestamp;
+ refundDeadline: TalerProtocolTimestamp;
+ merchantPub: string;
+ feeDeposit: AmountJson;
+ wireInfoHash: string;
+ denomKeyType: DenomKeyType;
+ denomPubHash: string;
+ denomSig: UnblindedSignature;
+
+ requiredMinimumAge?: number;
+
+ ageCommitmentProof?: AgeCommitmentProof;
+}
+
+export interface ExchangesListResponse {
+ exchanges: ExchangeListItem[];
+}
+
+export interface ExchangeDetailedResponse {
+ exchange: ExchangeFullDetails;
+}
+
+export interface WalletCoreVersion {
+ hash: string | undefined;
+ version: string;
+ exchange: string;
+ merchant: string;
+ bank: string;
+ devMode?: boolean;
+}
+
+export interface KnownBankAccountsInfo {
+ uri: PaytoUri;
+ kyc_completed: boolean;
+ currency: string;
+ alias: string;
+}
+
+export interface KnownBankAccounts {
+ accounts: KnownBankAccountsInfo[];
+}
+
+export interface ExchangeTosStatusDetails {
+ acceptedVersion?: string;
+ currentVersion?: string;
+ contentType?: string;
+ content?: string;
+}
+
+/**
+ * Wire fee for one wire method
+ */
+export interface WireFee {
+ /**
+ * Fee for wire transfers.
+ */
+ wireFee: AmountJson;
+
+ /**
+ * Fees to close and refund a reserve.
+ */
+ closingFee: AmountJson;
+
+ /**
+ * Fees for inter-exchange transfers from P2P payments.
+ */
+ wadFee: AmountJson;
+
+ /**
+ * Start date of the fee.
+ */
+ startStamp: TalerProtocolTimestamp;
+
+ /**
+ * End date of the fee.
+ */
+ endStamp: TalerProtocolTimestamp;
+
+ /**
+ * Signature made by the exchange master key.
+ */
+ sig: string;
+}
+
+/**
+ * Information about one of the exchange's bank accounts.
+ */
+export interface ExchangeAccount {
+ payto_uri: string;
+ master_sig: string;
+}
+
+export type WireFeeMap = { [wireMethod: string]: WireFee[] };
+
+export interface WireInfo {
+ feesForType: WireFeeMap;
+ accounts: ExchangeAccount[];
+}
+
+export interface ExchangeGlobalFees {
+ startDate: TalerProtocolTimestamp;
+ endDate: TalerProtocolTimestamp;
+
+ kycFee: AmountJson;
+ historyFee: AmountJson;
+ accountFee: AmountJson;
+ purseFee: AmountJson;
+
+ historyTimeout: TalerProtocolDuration;
+ kycTimeout: TalerProtocolDuration;
+ purseTimeout: TalerProtocolDuration;
+
+ purseLimit: number;
+
+ signature: string;
+}
+
+const codecForExchangeAccount = (): Codec<ExchangeAccount> =>
+ buildCodecForObject<ExchangeAccount>()
+ .property("payto_uri", codecForString())
+ .property("master_sig", codecForString())
+ .build("codecForExchangeAccount");
+
+const codecForWireFee = (): Codec<WireFee> =>
+ buildCodecForObject<WireFee>()
+ .property("sig", codecForString())
+ .property("wireFee", codecForAmountJson())
+ .property("wadFee", codecForAmountJson())
+ .property("closingFee", codecForAmountJson())
+ .property("startStamp", codecForTimestamp)
+ .property("endStamp", codecForTimestamp)
+ .build("codecForWireFee");
+
+const codecForWireInfo = (): Codec<WireInfo> =>
+ buildCodecForObject<WireInfo>()
+ .property("feesForType", codecForMap(codecForList(codecForWireFee())))
+ .property("accounts", codecForList(codecForExchangeAccount()))
+ .build("codecForWireInfo");
+
+export interface DenominationInfo {
+ /**
+ * Value of one coin of the denomination.
+ */
+ value: AmountJson;
+
+ /**
+ * Hash of the denomination public key.
+ * Stored in the database for faster lookups.
+ */
+ denomPubHash: string;
+
+ denomPub: DenominationPubKey;
+
+ /**
+ * Fee for withdrawing.
+ */
+ feeWithdraw: AmountJson;
+
+ /**
+ * Fee for depositing.
+ */
+ feeDeposit: AmountJson;
+
+ /**
+ * Fee for refreshing.
+ */
+ feeRefresh: AmountJson;
+
+ /**
+ * Fee for refunding.
+ */
+ feeRefund: AmountJson;
+
+ /**
+ * Validity start date of the denomination.
+ */
+ stampStart: TalerProtocolTimestamp;
+
+ /**
+ * Date after which the currency can't be withdrawn anymore.
+ */
+ stampExpireWithdraw: TalerProtocolTimestamp;
+
+ /**
+ * Date after the denomination officially doesn't exist anymore.
+ */
+ stampExpireLegal: TalerProtocolTimestamp;
+
+ /**
+ * Data after which coins of this denomination can't be deposited anymore.
+ */
+ stampExpireDeposit: TalerProtocolTimestamp;
+
+ exchangeBaseUrl: string;
+}
+
+export type DenomOperation = "deposit" | "withdraw" | "refresh" | "refund";
+export type DenomOperationMap<T> = { [op in DenomOperation]: T };
+
+export interface FeeDescription {
+ group: string;
+ from: AbsoluteTime;
+ until: AbsoluteTime;
+ fee?: AmountJson;
+}
+
+export interface FeeDescriptionPair {
+ group: string;
+ from: AbsoluteTime;
+ until: AbsoluteTime;
+ left?: AmountJson;
+ right?: AmountJson;
+}
+
+export interface TimePoint<T> {
+ id: string;
+ group: string;
+ fee: AmountJson;
+ type: "start" | "end";
+ moment: AbsoluteTime;
+ denom: T;
+}
+
+export interface ExchangeFullDetails {
+ exchangeBaseUrl: string;
+ currency: string;
+ paytoUris: string[];
+ tos: ExchangeTosStatusDetails;
+ auditors: ExchangeAuditor[];
+ wireInfo: WireInfo;
+ denomFees: DenomOperationMap<FeeDescription[]>;
+ transferFees: Record<string, FeeDescription[]>;
+ globalFees: FeeDescription[];
+}
+
+export interface ExchangeListItem {
+ exchangeBaseUrl: string;
+ currency: string;
+ paytoUris: string[];
+ tos: ExchangeTosStatusDetails;
+}
+
+const codecForAuditorDenomSig = (): Codec<AuditorDenomSig> =>
+ buildCodecForObject<AuditorDenomSig>()
+ .property("denom_pub_h", codecForString())
+ .property("auditor_sig", codecForString())
+ .build("AuditorDenomSig");
+
+const codecForExchangeAuditor = (): Codec<ExchangeAuditor> =>
+ buildCodecForObject<ExchangeAuditor>()
+ .property("auditor_pub", codecForString())
+ .property("auditor_url", codecForString())
+ .property("denomination_keys", codecForList(codecForAuditorDenomSig()))
+ .build("codecForExchangeAuditor");
+
+const codecForExchangeTos = (): Codec<ExchangeTosStatusDetails> =>
+ buildCodecForObject<ExchangeTosStatusDetails>()
+ .property("acceptedVersion", codecOptional(codecForString()))
+ .property("currentVersion", codecOptional(codecForString()))
+ .property("contentType", codecOptional(codecForString()))
+ .property("content", codecOptional(codecForString()))
+ .build("ExchangeTos");
+
+export const codecForFeeDescriptionPair = (): Codec<FeeDescriptionPair> =>
+ buildCodecForObject<FeeDescriptionPair>()
+ .property("group", codecForString())
+ .property("from", codecForAbsoluteTime)
+ .property("until", codecForAbsoluteTime)
+ .property("left", codecOptional(codecForAmountJson()))
+ .property("right", codecOptional(codecForAmountJson()))
+ .build("FeeDescriptionPair");
+
+export const codecForFeeDescription = (): Codec<FeeDescription> =>
+ buildCodecForObject<FeeDescription>()
+ .property("group", codecForString())
+ .property("from", codecForAbsoluteTime)
+ .property("until", codecForAbsoluteTime)
+ .property("fee", codecOptional(codecForAmountJson()))
+ .build("FeeDescription");
+
+export const codecForFeesByOperations = (): Codec<
+ DenomOperationMap<FeeDescription[]>
+> =>
+ buildCodecForObject<DenomOperationMap<FeeDescription[]>>()
+ .property("deposit", codecForList(codecForFeeDescription()))
+ .property("withdraw", codecForList(codecForFeeDescription()))
+ .property("refresh", codecForList(codecForFeeDescription()))
+ .property("refund", codecForList(codecForFeeDescription()))
+ .build("DenomOperationMap");
+
+export const codecForExchangeFullDetails = (): Codec<ExchangeFullDetails> =>
+ buildCodecForObject<ExchangeFullDetails>()
+ .property("currency", codecForString())
+ .property("exchangeBaseUrl", codecForString())
+ .property("paytoUris", codecForList(codecForString()))
+ .property("tos", codecForExchangeTos())
+ .property("auditors", codecForList(codecForExchangeAuditor()))
+ .property("wireInfo", codecForWireInfo())
+ .property("denomFees", codecForFeesByOperations())
+ .property(
+ "transferFees",
+ codecForMap(codecForList(codecForFeeDescription())),
+ )
+ .property("globalFees", codecForList(codecForFeeDescription()))
+ .build("ExchangeFullDetails");
+
+export const codecForExchangeListItem = (): Codec<ExchangeListItem> =>
+ buildCodecForObject<ExchangeListItem>()
+ .property("currency", codecForString())
+ .property("exchangeBaseUrl", codecForString())
+ .property("paytoUris", codecForList(codecForString()))
+ .property("tos", codecForExchangeTos())
+ .build("ExchangeListItem");
+
+export const codecForExchangesListResponse = (): Codec<ExchangesListResponse> =>
+ buildCodecForObject<ExchangesListResponse>()
+ .property("exchanges", codecForList(codecForExchangeListItem()))
+ .build("ExchangesListResponse");
+
+export interface AcceptManualWithdrawalResult {
+ /**
+ * Payto URIs that can be used to fund the withdrawal.
+ */
+ exchangePaytoUris: string[];
+
+ /**
+ * Public key of the newly created reserve.
+ */
+ reservePub: string;
+
+ transactionId: string;
+}
+
+export interface ManualWithdrawalDetails {
+ /**
+ * Did the user accept the current version of the exchange's
+ * terms of service?
+ */
+ tosAccepted: boolean;
+
+ /**
+ * Amount that the user will transfer to the exchange.
+ */
+ amountRaw: AmountString;
+
+ /**
+ * Amount that will be added to the user's wallet balance.
+ */
+ amountEffective: AmountString;
+
+ /**
+ * Ways to pay the exchange.
+ */
+ paytoUris: string[];
+
+ /**
+ * If the exchange supports age-restricted coins it will return
+ * the array of ages.
+ */
+ ageRestrictionOptions?: number[];
+}
+
+/**
+ * Selected denominations withn some extra info.
+ */
+export interface DenomSelectionState {
+ totalCoinValue: AmountJson;
+ totalWithdrawCost: AmountJson;
+ selectedDenoms: {
+ denomPubHash: string;
+ count: number;
+ }[];
+}
+
+/**
+ * Information about what will happen doing a withdrawal.
+ *
+ * Sent to the wallet frontend to be rendered and shown to the user.
+ */
+export interface ExchangeWithdrawalDetails {
+ exchangePaytoUris: string[];
+
+ /**
+ * Filtered wire info to send to the bank.
+ */
+ exchangeWireAccounts: string[];
+
+ /**
+ * Selected denominations for withdraw.
+ */
+ selectedDenoms: DenomSelectionState;
+
+ /**
+ * Does the wallet know about an auditor for
+ * the exchange that the reserve.
+ */
+ isAudited: boolean;
+
+ /**
+ * Did the user already accept the current terms of service for the exchange?
+ */
+ termsOfServiceAccepted: boolean;
+
+ /**
+ * The exchange is trusted directly.
+ */
+ isTrusted: boolean;
+
+ /**
+ * The earliest deposit expiration of the selected coins.
+ */
+ earliestDepositExpiration: TalerProtocolTimestamp;
+
+ /**
+ * Number of currently offered denominations.
+ */
+ numOfferedDenoms: number;
+
+ /**
+ * Public keys of trusted auditors for the currency we're withdrawing.
+ */
+ trustedAuditorPubs: string[];
+
+ /**
+ * Result of checking the wallet's version
+ * against the exchange's version.
+ *
+ * Older exchanges don't return version information.
+ */
+ versionMatch: VersionMatchResult | undefined;
+
+ /**
+ * Libtool-style version string for the exchange or "unknown"
+ * for older exchanges.
+ */
+ exchangeVersion: string;
+
+ /**
+ * Libtool-style version string for the wallet.
+ */
+ walletVersion: string;
+
+ /**
+ * Amount that will be subtracted from the reserve's balance.
+ */
+ withdrawalAmountRaw: AmountString;
+
+ /**
+ * Amount that will actually be added to the wallet's balance.
+ */
+ withdrawalAmountEffective: AmountString;
+
+ /**
+ * If the exchange supports age-restricted coins it will return
+ * the array of ages.
+ *
+ */
+ ageRestrictionOptions?: number[];
+}
+
+export interface GetExchangeTosResult {
+ /**
+ * Markdown version of the current ToS.
+ */
+ content: string;
+
+ /**
+ * Version tag of the current ToS.
+ */
+ currentEtag: string;
+
+ /**
+ * Version tag of the last ToS that the user has accepted,
+ * if any.
+ */
+ acceptedEtag: string | undefined;
+
+ /**
+ * Accepted content type
+ */
+ contentType: string;
+}
+
+export interface TestPayArgs {
+ merchantBaseUrl: string;
+ merchantAuthToken?: string;
+ amount: string;
+ summary: string;
+ forcedCoinSel?: ForcedCoinSel;
+}
+
+export const codecForTestPayArgs = (): Codec<TestPayArgs> =>
+ buildCodecForObject<TestPayArgs>()
+ .property("merchantBaseUrl", codecForString())
+ .property("merchantAuthToken", codecOptional(codecForString()))
+ .property("amount", codecForString())
+ .property("summary", codecForString())
+ .property("forcedCoinSel", codecForAny())
+ .build("TestPayArgs");
+
+export interface IntegrationTestArgs {
+ exchangeBaseUrl: string;
+ bankBaseUrl: string;
+ bankAccessApiBaseUrl?: string;
+ merchantBaseUrl: string;
+ merchantAuthToken?: string;
+ amountToWithdraw: string;
+ amountToSpend: string;
+}
+
+export const codecForIntegrationTestArgs = (): Codec<IntegrationTestArgs> =>
+ buildCodecForObject<IntegrationTestArgs>()
+ .property("exchangeBaseUrl", codecForString())
+ .property("bankBaseUrl", codecForString())
+ .property("merchantBaseUrl", codecForString())
+ .property("merchantAuthToken", codecOptional(codecForString()))
+ .property("amountToSpend", codecForAmountString())
+ .property("amountToWithdraw", codecForAmountString())
+ .property("bankAccessApiBaseUrl", codecOptional(codecForAmountString()))
+ .build("IntegrationTestArgs");
+
+export interface AddExchangeRequest {
+ exchangeBaseUrl: string;
+ forceUpdate?: boolean;
+}
+
+export const codecForAddExchangeRequest = (): Codec<AddExchangeRequest> =>
+ buildCodecForObject<AddExchangeRequest>()
+ .property("exchangeBaseUrl", codecForString())
+ .property("forceUpdate", codecOptional(codecForBoolean()))
+ .build("AddExchangeRequest");
+
+export interface ForceExchangeUpdateRequest {
+ exchangeBaseUrl: string;
+}
+
+export const codecForForceExchangeUpdateRequest =
+ (): Codec<AddExchangeRequest> =>
+ buildCodecForObject<AddExchangeRequest>()
+ .property("exchangeBaseUrl", codecForString())
+ .build("AddExchangeRequest");
+
+export interface GetExchangeTosRequest {
+ exchangeBaseUrl: string;
+ acceptedFormat?: string[];
+}
+
+export const codecForGetExchangeTosRequest = (): Codec<GetExchangeTosRequest> =>
+ buildCodecForObject<GetExchangeTosRequest>()
+ .property("exchangeBaseUrl", codecForString())
+ .property("acceptedFormat", codecOptional(codecForList(codecForString())))
+ .build("GetExchangeTosRequest");
+
+export interface AcceptManualWithdrawalRequest {
+ exchangeBaseUrl: string;
+ amount: string;
+ restrictAge?: number;
+}
+
+export const codecForAcceptManualWithdrawalRequet =
+ (): Codec<AcceptManualWithdrawalRequest> =>
+ buildCodecForObject<AcceptManualWithdrawalRequest>()
+ .property("exchangeBaseUrl", codecForString())
+ .property("amount", codecForString())
+ .property("restrictAge", codecOptional(codecForNumber()))
+ .build("AcceptManualWithdrawalRequest");
+
+export interface GetWithdrawalDetailsForAmountRequest {
+ exchangeBaseUrl: string;
+ amount: string;
+ restrictAge?: number;
+}
+
+export interface AcceptBankIntegratedWithdrawalRequest {
+ talerWithdrawUri: string;
+ exchangeBaseUrl: string;
+ forcedDenomSel?: ForcedDenomSel;
+ restrictAge?: number;
+}
+
+export const codecForAcceptBankIntegratedWithdrawalRequest =
+ (): Codec<AcceptBankIntegratedWithdrawalRequest> =>
+ buildCodecForObject<AcceptBankIntegratedWithdrawalRequest>()
+ .property("exchangeBaseUrl", codecForString())
+ .property("talerWithdrawUri", codecForString())
+ .property("forcedDenomSel", codecForAny())
+ .property("restrictAge", codecOptional(codecForNumber()))
+ .build("AcceptBankIntegratedWithdrawalRequest");
+
+export const codecForGetWithdrawalDetailsForAmountRequest =
+ (): Codec<GetWithdrawalDetailsForAmountRequest> =>
+ buildCodecForObject<GetWithdrawalDetailsForAmountRequest>()
+ .property("exchangeBaseUrl", codecForString())
+ .property("amount", codecForString())
+ .property("restrictAge", codecOptional(codecForNumber()))
+ .build("GetWithdrawalDetailsForAmountRequest");
+
+export interface AcceptExchangeTosRequest {
+ exchangeBaseUrl: string;
+ etag: string | undefined;
+}
+
+export const codecForAcceptExchangeTosRequest =
+ (): Codec<AcceptExchangeTosRequest> =>
+ buildCodecForObject<AcceptExchangeTosRequest>()
+ .property("exchangeBaseUrl", codecForString())
+ .property("etag", codecOptional(codecForString()))
+ .build("AcceptExchangeTosRequest");
+
+export interface ApplyRefundRequest {
+ talerRefundUri: string;
+}
+
+export const codecForApplyRefundRequest = (): Codec<ApplyRefundRequest> =>
+ buildCodecForObject<ApplyRefundRequest>()
+ .property("talerRefundUri", codecForString())
+ .build("ApplyRefundRequest");
+
+export interface ApplyRefundFromPurchaseIdRequest {
+ purchaseId: string;
+}
+
+export const codecForApplyRefundFromPurchaseIdRequest =
+ (): Codec<ApplyRefundFromPurchaseIdRequest> =>
+ buildCodecForObject<ApplyRefundFromPurchaseIdRequest>()
+ .property("purchaseId", codecForString())
+ .build("ApplyRefundFromPurchaseIdRequest");
+
+export interface GetWithdrawalDetailsForUriRequest {
+ talerWithdrawUri: string;
+ restrictAge?: number;
+}
+export const codecForGetWithdrawalDetailsForUri =
+ (): Codec<GetWithdrawalDetailsForUriRequest> =>
+ buildCodecForObject<GetWithdrawalDetailsForUriRequest>()
+ .property("talerWithdrawUri", codecForString())
+ .property("restrictAge", codecOptional(codecForNumber()))
+ .build("GetWithdrawalDetailsForUriRequest");
+
+export interface ListKnownBankAccountsRequest {
+ currency?: string;
+}
+export const codecForListKnownBankAccounts =
+ (): Codec<ListKnownBankAccountsRequest> =>
+ buildCodecForObject<ListKnownBankAccountsRequest>()
+ .property("currency", codecOptional(codecForString()))
+ .build("ListKnownBankAccountsRequest");
+
+export interface AddKnownBankAccountsRequest {
+ payto: string;
+ alias: string;
+ currency: string;
+}
+export const codecForAddKnownBankAccounts =
+ (): Codec<AddKnownBankAccountsRequest> =>
+ buildCodecForObject<AddKnownBankAccountsRequest>()
+ .property("payto", codecForString())
+ .property("alias", codecForString())
+ .property("currency", codecForString())
+ .build("AddKnownBankAccountsRequest");
+
+export interface ForgetKnownBankAccountsRequest {
+ payto: string;
+}
+
+export const codecForForgetKnownBankAccounts =
+ (): Codec<ForgetKnownBankAccountsRequest> =>
+ buildCodecForObject<ForgetKnownBankAccountsRequest>()
+ .property("payto", codecForString())
+ .build("ForgetKnownBankAccountsRequest");
+
+export interface AbortProposalRequest {
+ proposalId: string;
+}
+
+export const codecForAbortProposalRequest = (): Codec<AbortProposalRequest> =>
+ buildCodecForObject<AbortProposalRequest>()
+ .property("proposalId", codecForString())
+ .build("AbortProposalRequest");
+
+interface GetContractTermsDetailsRequest {
+ proposalId: string;
+}
+
+export const codecForGetContractTermsDetails =
+ (): Codec<GetContractTermsDetailsRequest> =>
+ buildCodecForObject<GetContractTermsDetailsRequest>()
+ .property("proposalId", codecForString())
+ .build("GetContractTermsDetails");
+
+export interface PreparePayRequest {
+ talerPayUri: string;
+}
+
+export const codecForPreparePayRequest = (): Codec<PreparePayRequest> =>
+ buildCodecForObject<PreparePayRequest>()
+ .property("talerPayUri", codecForString())
+ .build("PreparePay");
+
+export interface ConfirmPayRequest {
+ proposalId: string;
+ sessionId?: string;
+ forcedCoinSel?: ForcedCoinSel;
+}
+
+export const codecForConfirmPayRequest = (): Codec<ConfirmPayRequest> =>
+ buildCodecForObject<ConfirmPayRequest>()
+ .property("proposalId", codecForString())
+ .property("sessionId", codecOptional(codecForString()))
+ .property("forcedCoinSel", codecForAny())
+ .build("ConfirmPay");
+
+export type CoreApiResponse = CoreApiResponseSuccess | CoreApiResponseError;
+
+export type CoreApiEnvelope = CoreApiResponse | CoreApiNotification;
+
+export interface CoreApiNotification {
+ type: "notification";
+ payload: unknown;
+}
+
+export interface CoreApiResponseSuccess {
+ // To distinguish the message from notifications
+ type: "response";
+ operation: string;
+ id: string;
+ result: unknown;
+}
+
+export interface CoreApiResponseError {
+ // To distinguish the message from notifications
+ type: "error";
+ operation: string;
+ id: string;
+ error: TalerErrorDetail;
+}
+
+export interface WithdrawTestBalanceRequest {
+ amount: string;
+ bankBaseUrl: string;
+ /**
+ * Bank access API base URL. Defaults to the bankBaseUrl.
+ */
+ bankAccessApiBaseUrl?: string;
+ exchangeBaseUrl: string;
+ forcedDenomSel?: ForcedDenomSel;
+}
+
+export const withdrawTestBalanceDefaults = {
+ amount: "TESTKUDOS:10",
+ bankBaseUrl: "https://bank.test.taler.net/",
+ exchangeBaseUrl: "https://exchange.test.taler.net/",
+};
+
+/**
+ * Request to the crypto worker to make a sync signature.
+ */
+export interface MakeSyncSignatureRequest {
+ accountPriv: string;
+ oldHash: string | undefined;
+ newHash: string;
+}
+
+/**
+ * Planchet for a coin during refresh.
+ */
+export interface RefreshPlanchetInfo {
+ /**
+ * Public key for the coin.
+ */
+ coinPub: string;
+
+ /**
+ * Private key for the coin.
+ */
+ coinPriv: string;
+
+ /**
+ * Blinded public key.
+ */
+ coinEv: CoinEnvelope;
+
+ coinEvHash: string;
+
+ /**
+ * Blinding key used.
+ */
+ blindingKey: string;
+
+ maxAge: number;
+ ageCommitmentProof?: AgeCommitmentProof;
+}
+
+/**
+ * Strategy for loading recovery information.
+ */
+export enum RecoveryMergeStrategy {
+ /**
+ * Keep the local wallet root key, import and take over providers.
+ */
+ Ours = "ours",
+
+ /**
+ * Migrate to the wallet root key from the recovery information.
+ */
+ Theirs = "theirs",
+}
+
+/**
+ * Load recovery information into the wallet.
+ */
+export interface RecoveryLoadRequest {
+ recovery: BackupRecovery;
+ strategy?: RecoveryMergeStrategy;
+}
+
+export const codecForWithdrawTestBalance =
+ (): Codec<WithdrawTestBalanceRequest> =>
+ buildCodecForObject<WithdrawTestBalanceRequest>()
+ .property("amount", codecForString())
+ .property("bankBaseUrl", codecForString())
+ .property("exchangeBaseUrl", codecForString())
+ .property("forcedDenomSel", codecForAny())
+ .property("bankAccessApiBaseUrl", codecOptional(codecForString()))
+ .build("WithdrawTestBalanceRequest");
+
+export interface ApplyRefundResponse {
+ contractTermsHash: string;
+
+ transactionId: string;
+
+ proposalId: string;
+
+ amountEffectivePaid: AmountString;
+
+ amountRefundGranted: AmountString;
+
+ amountRefundGone: AmountString;
+
+ pendingAtExchange: boolean;
+
+ info: OrderShortInfo;
+}
+
+export const codecForApplyRefundResponse = (): Codec<ApplyRefundResponse> =>
+ buildCodecForObject<ApplyRefundResponse>()
+ .property("amountEffectivePaid", codecForAmountString())
+ .property("amountRefundGone", codecForAmountString())
+ .property("amountRefundGranted", codecForAmountString())
+ .property("contractTermsHash", codecForString())
+ .property("pendingAtExchange", codecForBoolean())
+ .property("proposalId", codecForString())
+ .property("transactionId", codecForString())
+ .property("info", codecForOrderShortInfo())
+ .build("ApplyRefundResponse");
+
+export interface SetCoinSuspendedRequest {
+ coinPub: string;
+ suspended: boolean;
+}
+
+export const codecForSetCoinSuspendedRequest =
+ (): Codec<SetCoinSuspendedRequest> =>
+ buildCodecForObject<SetCoinSuspendedRequest>()
+ .property("coinPub", codecForString())
+ .property("suspended", codecForBoolean())
+ .build("SetCoinSuspendedRequest");
+
+export interface ForceRefreshRequest {
+ coinPubList: string[];
+}
+
+export const codecForForceRefreshRequest = (): Codec<ForceRefreshRequest> =>
+ buildCodecForObject<ForceRefreshRequest>()
+ .property("coinPubList", codecForList(codecForString()))
+ .build("ForceRefreshRequest");
+
+export interface PrepareRefundRequest {
+ talerRefundUri: string;
+}
+
+export const codecForPrepareRefundRequest = (): Codec<PrepareRefundRequest> =>
+ buildCodecForObject<PrepareRefundRequest>()
+ .property("talerRefundUri", codecForString())
+ .build("PrepareRefundRequest");
+
+export interface PrepareTipRequest {
+ talerTipUri: string;
+}
+
+export const codecForPrepareTipRequest = (): Codec<PrepareTipRequest> =>
+ buildCodecForObject<PrepareTipRequest>()
+ .property("talerTipUri", codecForString())
+ .build("PrepareTipRequest");
+
+export interface AcceptTipRequest {
+ walletTipId: string;
+}
+
+export const codecForAcceptTipRequest = (): Codec<AcceptTipRequest> =>
+ buildCodecForObject<AcceptTipRequest>()
+ .property("walletTipId", codecForString())
+ .build("AcceptTipRequest");
+
+export interface AbortPayWithRefundRequest {
+ proposalId: string;
+}
+
+export const codecForAbortPayWithRefundRequest =
+ (): Codec<AbortPayWithRefundRequest> =>
+ buildCodecForObject<AbortPayWithRefundRequest>()
+ .property("proposalId", codecForString())
+ .build("AbortPayWithRefundRequest");
+
+export interface GetFeeForDepositRequest {
+ depositPaytoUri: string;
+ amount: AmountString;
+}
+
+export interface DepositGroupFees {
+ coin: AmountJson;
+ wire: AmountJson;
+ refresh: AmountJson;
+}
+
+export interface CreateDepositGroupRequest {
+ depositPaytoUri: string;
+ amount: AmountString;
+}
+
+export const codecForGetFeeForDeposit = (): Codec<GetFeeForDepositRequest> =>
+ buildCodecForObject<GetFeeForDepositRequest>()
+ .property("amount", codecForAmountString())
+ .property("depositPaytoUri", codecForString())
+ .build("GetFeeForDepositRequest");
+
+export interface PrepareDepositRequest {
+ depositPaytoUri: string;
+ amount: AmountString;
+}
+export const codecForPrepareDepositRequest = (): Codec<PrepareDepositRequest> =>
+ buildCodecForObject<PrepareDepositRequest>()
+ .property("amount", codecForAmountString())
+ .property("depositPaytoUri", codecForString())
+ .build("PrepareDepositRequest");
+
+export interface PrepareDepositResponse {
+ totalDepositCost: AmountJson;
+ effectiveDepositAmount: AmountJson;
+}
+
+export const codecForCreateDepositGroupRequest =
+ (): Codec<CreateDepositGroupRequest> =>
+ buildCodecForObject<CreateDepositGroupRequest>()
+ .property("amount", codecForAmountString())
+ .property("depositPaytoUri", codecForString())
+ .build("CreateDepositGroupRequest");
+
+export interface CreateDepositGroupResponse {
+ depositGroupId: string;
+ transactionId: string;
+}
+
+export interface TrackDepositGroupRequest {
+ depositGroupId: string;
+}
+
+export interface TrackDepositGroupResponse {
+ responses: {
+ status: number;
+ body: any;
+ }[];
+}
+
+export const codecForTrackDepositGroupRequest =
+ (): Codec<TrackDepositGroupRequest> =>
+ buildCodecForObject<TrackDepositGroupRequest>()
+ .property("depositGroupId", codecForAmountString())
+ .build("TrackDepositGroupRequest");
+
+export interface WithdrawUriInfoResponse {
+ amount: AmountString;
+ defaultExchangeBaseUrl?: string;
+ possibleExchanges: ExchangeListItem[];
+}
+
+export const codecForWithdrawUriInfoResponse =
+ (): Codec<WithdrawUriInfoResponse> =>
+ buildCodecForObject<WithdrawUriInfoResponse>()
+ .property("amount", codecForAmountString())
+ .property("defaultExchangeBaseUrl", codecOptional(codecForString()))
+ .property("possibleExchanges", codecForList(codecForExchangeListItem()))
+ .build("WithdrawUriInfoResponse");
+
+export interface WalletCurrencyInfo {
+ trustedAuditors: {
+ currency: string;
+ auditorPub: string;
+ auditorBaseUrl: string;
+ }[];
+ trustedExchanges: {
+ currency: string;
+ exchangeMasterPub: string;
+ exchangeBaseUrl: string;
+ }[];
+}
+
+export interface DeleteTransactionRequest {
+ transactionId: string;
+}
+
+export interface RetryTransactionRequest {
+ transactionId: string;
+}
+
+export const codecForDeleteTransactionRequest =
+ (): Codec<DeleteTransactionRequest> =>
+ buildCodecForObject<DeleteTransactionRequest>()
+ .property("transactionId", codecForString())
+ .build("DeleteTransactionRequest");
+
+export const codecForRetryTransactionRequest =
+ (): Codec<RetryTransactionRequest> =>
+ buildCodecForObject<RetryTransactionRequest>()
+ .property("transactionId", codecForString())
+ .build("RetryTransactionRequest");
+
+export interface SetWalletDeviceIdRequest {
+ /**
+ * New wallet device ID to set.
+ */
+ walletDeviceId: string;
+}
+
+export const codecForSetWalletDeviceIdRequest =
+ (): Codec<SetWalletDeviceIdRequest> =>
+ buildCodecForObject<SetWalletDeviceIdRequest>()
+ .property("walletDeviceId", codecForString())
+ .build("SetWalletDeviceIdRequest");
+
+export interface WithdrawFakebankRequest {
+ amount: AmountString;
+ exchange: string;
+ bank: string;
+}
+
+export const codecForWithdrawFakebankRequest =
+ (): Codec<WithdrawFakebankRequest> =>
+ buildCodecForObject<WithdrawFakebankRequest>()
+ .property("amount", codecForAmountString())
+ .property("bank", codecForString())
+ .property("exchange", codecForString())
+ .build("WithdrawFakebankRequest");
+
+export interface ImportDb {
+ dump: any;
+}
+
+export const codecForImportDbRequest = (): Codec<ImportDb> =>
+ buildCodecForObject<ImportDb>()
+ .property("dump", codecForAny())
+ .build("ImportDbRequest");
+
+export interface ForcedDenomSel {
+ denoms: {
+ value: AmountString;
+ count: number;
+ }[];
+}
+
+/**
+ * Forced coin selection for deposits/payments.
+ */
+export interface ForcedCoinSel {
+ coins: {
+ value: AmountString;
+ contribution: AmountString;
+ }[];
+}
+
+export interface TestPayResult {
+ payCoinSelection: PayCoinSelection;
+}
+
+/**
+ * Result of selecting coins, contains the exchange, and selected
+ * coins with their denomination.
+ */
+export interface PayCoinSelection {
+ /**
+ * Amount requested by the merchant.
+ */
+ paymentAmount: AmountJson;
+
+ /**
+ * Public keys of the coins that were selected.
+ */
+ coinPubs: string[];
+
+ /**
+ * Amount that each coin contributes.
+ */
+ coinContributions: AmountJson[];
+
+ /**
+ * How much of the wire fees is the customer paying?
+ */
+ customerWireFees: AmountJson;
+
+ /**
+ * How much of the deposit fees is the customer paying?
+ */
+ customerDepositFees: AmountJson;
+}
+
+export interface InitiatePeerPushPaymentRequest {
+ amount: AmountString;
+ partialContractTerms: any;
+}
+
+export interface InitiatePeerPushPaymentResponse {
+ exchangeBaseUrl: string;
+ pursePub: string;
+ mergePriv: string;
+ contractPriv: string;
+ talerUri: string;
+ transactionId: string;
+}
+
+export const codecForInitiatePeerPushPaymentRequest =
+ (): Codec<InitiatePeerPushPaymentRequest> =>
+ buildCodecForObject<InitiatePeerPushPaymentRequest>()
+ .property("amount", codecForAmountString())
+ .property("partialContractTerms", codecForAny())
+ .build("InitiatePeerPushPaymentRequest");
+
+export interface CheckPeerPushPaymentRequest {
+ talerUri: string;
+}
+
+export interface CheckPeerPullPaymentRequest {
+ talerUri: string;
+}
+
+export interface CheckPeerPushPaymentResponse {
+ contractTerms: any;
+ amount: AmountString;
+ peerPushPaymentIncomingId: string;
+}
+
+export interface CheckPeerPullPaymentResponse {
+ contractTerms: any;
+ amount: AmountString;
+ peerPullPaymentIncomingId: string;
+}
+
+export const codecForCheckPeerPushPaymentRequest =
+ (): Codec<CheckPeerPushPaymentRequest> =>
+ buildCodecForObject<CheckPeerPushPaymentRequest>()
+ .property("talerUri", codecForString())
+ .build("CheckPeerPushPaymentRequest");
+
+export const codecForCheckPeerPullPaymentRequest =
+ (): Codec<CheckPeerPullPaymentRequest> =>
+ buildCodecForObject<CheckPeerPullPaymentRequest>()
+ .property("talerUri", codecForString())
+ .build("CheckPeerPullPaymentRequest");
+
+export interface AcceptPeerPushPaymentRequest {
+ /**
+ * Transparent identifier of the incoming peer push payment.
+ */
+ peerPushPaymentIncomingId: string;
+}
+export interface AcceptPeerPushPaymentResponse {
+ transactionId: string;
+}
+
+export interface AcceptPeerPullPaymentResponse {
+ transactionId: string;
+}
+
+export const codecForAcceptPeerPushPaymentRequest =
+ (): Codec<AcceptPeerPushPaymentRequest> =>
+ buildCodecForObject<AcceptPeerPushPaymentRequest>()
+ .property("peerPushPaymentIncomingId", codecForString())
+ .build("AcceptPeerPushPaymentRequest");
+
+export interface AcceptPeerPullPaymentRequest {
+ /**
+ * Transparent identifier of the incoming peer pull payment.
+ */
+ peerPullPaymentIncomingId: string;
+}
+
+export interface SetDevModeRequest {
+ devModeEnabled: boolean;
+}
+
+export const codecForSetDevModeRequest = (): Codec<SetDevModeRequest> =>
+ buildCodecForObject<SetDevModeRequest>()
+ .property("devModeEnabled", codecForBoolean())
+ .build("SetDevModeRequest");
+
+export interface ApplyDevExperimentRequest {
+ devExperimentUri: string;
+}
+
+export const codecForApplyDevExperiment =
+ (): Codec<ApplyDevExperimentRequest> =>
+ buildCodecForObject<ApplyDevExperimentRequest>()
+ .property("devExperimentUri", codecForString())
+ .build("ApplyDevExperimentRequest");
+
+export const codecForAcceptPeerPullPaymentRequest =
+ (): Codec<AcceptPeerPullPaymentRequest> =>
+ buildCodecForObject<AcceptPeerPullPaymentRequest>()
+ .property("peerPullPaymentIncomingId", codecForString())
+ .build("AcceptPeerPllPaymentRequest");
+
+export interface InitiatePeerPullPaymentRequest {
+ /**
+ * FIXME: Make this optional?
+ */
+ exchangeBaseUrl: string;
+ amount: AmountString;
+ partialContractTerms: any;
+}
+
+export const codecForInitiatePeerPullPaymentRequest =
+ (): Codec<InitiatePeerPullPaymentRequest> =>
+ buildCodecForObject<InitiatePeerPullPaymentRequest>()
+ .property("partialContractTerms", codecForAny())
+ .property("amount", codecForAmountString())
+ .property("exchangeBaseUrl", codecForAmountString())
+ .build("InitiatePeerPullPaymentRequest");
+
+export interface InitiatePeerPullPaymentResponse {
+ /**
+ * Taler URI for the other party to make the payment
+ * that was requested.
+ */
+ talerUri: string;
+
+ transactionId: string;
+}
diff --git a/packages/taler-util/src/walletTypes.ts b/packages/taler-util/src/walletTypes.ts
@@ -1,1826 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2015-2020 Taler Systems SA
-
- 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.
-
- 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
- TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
-/**
- * Types used by clients of the wallet.
- *
- * These types are defined in a separate file make tree shaking easier, since
- * some components use these types (via RPC) but do not depend on the wallet
- * code directly.
- *
- * @author Florian Dold <dold@taler.net>
- */
-
-/**
- * Imports.
- */
-import {
- AmountJson,
- codecForAmountJson,
- codecForAmountString,
-} from "./amounts.js";
-import {
- AbsoluteTime,
- codecForAbsoluteTime,
- codecForTimestamp,
- TalerProtocolDuration,
- TalerProtocolTimestamp,
-} from "./time.js";
-import {
- buildCodecForObject,
- codecForString,
- codecOptional,
- Codec,
- codecForList,
- codecForBoolean,
- codecForConstString,
- codecForAny,
- buildCodecForUnion,
- codecForNumber,
- codecForMap,
-} from "./codec.js";
-import {
- AmountString,
- AuditorDenomSig,
- codecForContractTerms,
- CoinEnvelope,
- ContractTerms,
- DenominationPubKey,
- DenomKeyType,
- ExchangeAuditor,
- UnblindedSignature,
-} from "./talerTypes.js";
-import { OrderShortInfo, codecForOrderShortInfo } from "./transactionsTypes.js";
-import { BackupRecovery } from "./backupTypes.js";
-import { PaytoUri } from "./payto.js";
-import { TalerErrorCode } from "./taler-error-codes.js";
-import { AgeCommitmentProof } from "./talerCrypto.js";
-import { VersionMatchResult } from "./libtool-version.js";
-
-/**
- * Response for the create reserve request to the wallet.
- */
-export class CreateReserveResponse {
- /**
- * Exchange URL where the bank should create the reserve.
- * The URL is canonicalized in the response.
- */
- exchange: string;
-
- /**
- * Reserve public key of the newly created reserve.
- */
- reservePub: string;
-}
-
-export interface Balance {
- available: AmountString;
- pendingIncoming: AmountString;
- pendingOutgoing: AmountString;
-
- // Does the balance for this currency have a pending
- // transaction?
- hasPendingTransactions: boolean;
-
- // Is there a pending transaction that would affect the balance
- // and requires user input?
- requiresUserInput: boolean;
-}
-
-export interface BalancesResponse {
- balances: Balance[];
-}
-
-export const codecForBalance = (): Codec<Balance> =>
- buildCodecForObject<Balance>()
- .property("available", codecForString())
- .property("hasPendingTransactions", codecForBoolean())
- .property("pendingIncoming", codecForString())
- .property("pendingOutgoing", codecForString())
- .property("requiresUserInput", codecForBoolean())
- .build("Balance");
-
-export const codecForBalancesResponse = (): Codec<BalancesResponse> =>
- buildCodecForObject<BalancesResponse>()
- .property("balances", codecForList(codecForBalance()))
- .build("BalancesResponse");
-
-/**
- * For terseness.
- */
-export function mkAmount(
- value: number,
- fraction: number,
- currency: string,
-): AmountJson {
- return { value, fraction, currency };
-}
-
-export enum ConfirmPayResultType {
- Done = "done",
- Pending = "pending",
-}
-
-/**
- * Result for confirmPay
- */
-export interface ConfirmPayResultDone {
- type: ConfirmPayResultType.Done;
- contractTerms: ContractTerms;
- transactionId: string;
-}
-
-export interface ConfirmPayResultPending {
- type: ConfirmPayResultType.Pending;
- transactionId: string;
- lastError: TalerErrorDetail | undefined;
-}
-
-export type ConfirmPayResult = ConfirmPayResultDone | ConfirmPayResultPending;
-
-export const codecForConfirmPayResultPending =
- (): Codec<ConfirmPayResultPending> =>
- buildCodecForObject<ConfirmPayResultPending>()
- .property("lastError", codecForAny())
- .property("transactionId", codecForString())
- .property("type", codecForConstString(ConfirmPayResultType.Pending))
- .build("ConfirmPayResultPending");
-
-export const codecForConfirmPayResultDone = (): Codec<ConfirmPayResultDone> =>
- buildCodecForObject<ConfirmPayResultDone>()
- .property("type", codecForConstString(ConfirmPayResultType.Done))
- .property("transactionId", codecForString())
- .property("contractTerms", codecForContractTerms())
- .build("ConfirmPayResultDone");
-
-export const codecForConfirmPayResult = (): Codec<ConfirmPayResult> =>
- buildCodecForUnion<ConfirmPayResult>()
- .discriminateOn("type")
- .alternative(
- ConfirmPayResultType.Pending,
- codecForConfirmPayResultPending(),
- )
- .alternative(ConfirmPayResultType.Done, codecForConfirmPayResultDone())
- .build("ConfirmPayResult");
-
-/**
- * Information about all sender wire details known to the wallet,
- * as well as exchanges that accept these wire types.
- */
-export interface SenderWireInfos {
- /**
- * Mapping from exchange base url to list of accepted
- * wire types.
- */
- exchangeWireTypes: { [exchangeBaseUrl: string]: string[] };
-
- /**
- * Sender wire information stored in the wallet.
- */
- senderWires: string[];
-}
-
-/**
- * Request to create a reserve.
- */
-export interface CreateReserveRequest {
- /**
- * The initial amount for the reserve.
- */
- amount: AmountJson;
-
- /**
- * Exchange URL where the bank should create the reserve.
- */
- exchange: string;
-
- /**
- * Payto URI that identifies the exchange's account that the funds
- * for this reserve go into.
- */
- exchangePaytoUri?: string;
-
- /**
- * Wire details (as a payto URI) for the bank account that sent the funds to
- * the exchange.
- */
- senderWire?: string;
-
- /**
- * URL to fetch the withdraw status from the bank.
- */
- bankWithdrawStatusUrl?: string;
-
- /**
- * Forced denomination selection for the first withdrawal
- * from this reserve, only used for testing.
- */
- forcedDenomSel?: ForcedDenomSel;
-
- restrictAge?: number;
-}
-
-export const codecForCreateReserveRequest = (): Codec<CreateReserveRequest> =>
- buildCodecForObject<CreateReserveRequest>()
- .property("amount", codecForAmountJson())
- .property("exchange", codecForString())
- .property("exchangePaytoUri", codecForString())
- .property("senderWire", codecOptional(codecForString()))
- .property("bankWithdrawStatusUrl", codecOptional(codecForString()))
- .property("forcedDenomSel", codecForAny())
- .build("CreateReserveRequest");
-
-/**
- * Request to mark a reserve as confirmed.
- */
-export interface ConfirmReserveRequest {
- /**
- * Public key of then reserve that should be marked
- * as confirmed.
- */
- reservePub: string;
-}
-
-export const codecForConfirmReserveRequest = (): Codec<ConfirmReserveRequest> =>
- buildCodecForObject<ConfirmReserveRequest>()
- .property("reservePub", codecForString())
- .build("ConfirmReserveRequest");
-
-/**
- * Wire coins to the user's own bank account.
- */
-export class ReturnCoinsRequest {
- /**
- * The amount to wire.
- */
- amount: AmountJson;
-
- /**
- * The exchange to take the coins from.
- */
- exchange: string;
-
- /**
- * Wire details for the bank account of the customer that will
- * receive the funds.
- */
- senderWire?: string;
-
- /**
- * Verify that a value matches the schema of this class and convert it into a
- * member.
- */
- static checked: (obj: any) => ReturnCoinsRequest;
-}
-
-export interface PrepareRefundResult {
- proposalId: string;
-
- effectivePaid: AmountString;
- gone: AmountString;
- granted: AmountString;
- pending: boolean;
- awaiting: AmountString;
-
- info: OrderShortInfo;
-}
-
-export interface PrepareTipResult {
- /**
- * Unique ID for the tip assigned by the wallet.
- * Typically different from the merchant-generated tip ID.
- */
- walletTipId: string;
-
- /**
- * Has the tip already been accepted?
- */
- accepted: boolean;
-
- /**
- * Amount that the merchant gave.
- */
- tipAmountRaw: AmountString;
-
- /**
- * Amount that arrived at the wallet.
- * Might be lower than the raw amount due to fees.
- */
- tipAmountEffective: AmountString;
-
- /**
- * Base URL of the merchant backend giving then tip.
- */
- merchantBaseUrl: string;
-
- /**
- * Base URL of the exchange that is used to withdraw the tip.
- * Determined by the merchant, the wallet/user has no choice here.
- */
- exchangeBaseUrl: string;
-
- /**
- * Time when the tip will expire. After it expired, it can't be picked
- * up anymore.
- */
- expirationTimestamp: TalerProtocolTimestamp;
-}
-
-export interface AcceptTipResponse {
- transactionId: string;
-}
-
-export const codecForPrepareTipResult = (): Codec<PrepareTipResult> =>
- buildCodecForObject<PrepareTipResult>()
- .property("accepted", codecForBoolean())
- .property("tipAmountRaw", codecForAmountString())
- .property("tipAmountEffective", codecForAmountString())
- .property("exchangeBaseUrl", codecForString())
- .property("merchantBaseUrl", codecForString())
- .property("expirationTimestamp", codecForTimestamp)
- .property("walletTipId", codecForString())
- .build("PrepareTipResult");
-
-export interface BenchmarkResult {
- time: { [s: string]: number };
- repetitions: number;
-}
-
-export enum PreparePayResultType {
- PaymentPossible = "payment-possible",
- InsufficientBalance = "insufficient-balance",
- AlreadyConfirmed = "already-confirmed",
-}
-
-export const codecForPreparePayResultPaymentPossible =
- (): Codec<PreparePayResultPaymentPossible> =>
- buildCodecForObject<PreparePayResultPaymentPossible>()
- .property("amountEffective", codecForAmountString())
- .property("amountRaw", codecForAmountString())
- .property("contractTerms", codecForContractTerms())
- .property("proposalId", codecForString())
- .property("contractTermsHash", codecForString())
- .property("noncePriv", codecForString())
- .property(
- "status",
- codecForConstString(PreparePayResultType.PaymentPossible),
- )
- .build("PreparePayResultPaymentPossible");
-
-export const codecForPreparePayResultInsufficientBalance =
- (): Codec<PreparePayResultInsufficientBalance> =>
- buildCodecForObject<PreparePayResultInsufficientBalance>()
- .property("amountRaw", codecForAmountString())
- .property("contractTerms", codecForAny())
- .property("proposalId", codecForString())
- .property("noncePriv", codecForString())
- .property(
- "status",
- codecForConstString(PreparePayResultType.InsufficientBalance),
- )
- .build("PreparePayResultInsufficientBalance");
-
-export const codecForPreparePayResultAlreadyConfirmed =
- (): Codec<PreparePayResultAlreadyConfirmed> =>
- buildCodecForObject<PreparePayResultAlreadyConfirmed>()
- .property(
- "status",
- codecForConstString(PreparePayResultType.AlreadyConfirmed),
- )
- .property("amountEffective", codecForAmountString())
- .property("amountRaw", codecForAmountString())
- .property("paid", codecForBoolean())
- .property("contractTerms", codecForAny())
- .property("contractTermsHash", codecForString())
- .property("proposalId", codecForString())
- .build("PreparePayResultAlreadyConfirmed");
-
-export const codecForPreparePayResult = (): Codec<PreparePayResult> =>
- buildCodecForUnion<PreparePayResult>()
- .discriminateOn("status")
- .alternative(
- PreparePayResultType.AlreadyConfirmed,
- codecForPreparePayResultAlreadyConfirmed(),
- )
- .alternative(
- PreparePayResultType.InsufficientBalance,
- codecForPreparePayResultInsufficientBalance(),
- )
- .alternative(
- PreparePayResultType.PaymentPossible,
- codecForPreparePayResultPaymentPossible(),
- )
- .build("PreparePayResult");
-
-/**
- * Result of a prepare pay operation.
- */
-export type PreparePayResult =
- | PreparePayResultInsufficientBalance
- | PreparePayResultAlreadyConfirmed
- | PreparePayResultPaymentPossible;
-
-/**
- * Payment is possible.
- */
-export interface PreparePayResultPaymentPossible {
- status: PreparePayResultType.PaymentPossible;
- proposalId: string;
- contractTerms: ContractTerms;
- contractTermsHash: string;
- amountRaw: string;
- amountEffective: string;
- noncePriv: string;
-}
-
-export interface PreparePayResultInsufficientBalance {
- status: PreparePayResultType.InsufficientBalance;
- proposalId: string;
- contractTerms: ContractTerms;
- amountRaw: string;
- noncePriv: string;
-}
-
-export interface PreparePayResultAlreadyConfirmed {
- status: PreparePayResultType.AlreadyConfirmed;
- contractTerms: ContractTerms;
- paid: boolean;
- amountRaw: string;
- amountEffective: string;
- contractTermsHash: string;
- proposalId: string;
-}
-
-export interface BankWithdrawDetails {
- selectionDone: boolean;
- transferDone: boolean;
- amount: AmountJson;
- senderWire?: string;
- suggestedExchange?: string;
- confirmTransferUrl?: string;
- wireTypes: string[];
-}
-
-export interface AcceptWithdrawalResponse {
- reservePub: string;
- confirmTransferUrl?: string;
- transactionId: string;
-}
-
-/**
- * Details about a purchase, including refund status.
- */
-export interface PurchaseDetails {
- contractTerms: Record<string, undefined>;
- hasRefund: boolean;
- totalRefundAmount: AmountJson;
- totalRefundAndRefreshFees: AmountJson;
-}
-
-export interface WalletDiagnostics {
- walletManifestVersion: string;
- walletManifestDisplayVersion: string;
- errors: string[];
- firefoxIdbProblem: boolean;
- dbOutdated: boolean;
-}
-
-export interface TalerErrorDetail {
- code: TalerErrorCode;
- hint?: string;
- [x: string]: unknown;
-}
-
-/**
- * Minimal information needed about a planchet for unblinding a signature.
- *
- * Can be a withdrawal/tipping/refresh planchet.
- */
-export interface PlanchetUnblindInfo {
- denomPub: DenominationPubKey;
- blindingKey: string;
-}
-
-export interface WithdrawalPlanchet {
- coinPub: string;
- coinPriv: string;
- reservePub: string;
- denomPubHash: string;
- denomPub: DenominationPubKey;
- blindingKey: string;
- withdrawSig: string;
- coinEv: CoinEnvelope;
- coinValue: AmountJson;
- coinEvHash: string;
- ageCommitmentProof?: AgeCommitmentProof;
-}
-
-export interface PlanchetCreationRequest {
- secretSeed: string;
- coinIndex: number;
- value: AmountJson;
- feeWithdraw: AmountJson;
- denomPub: DenominationPubKey;
- reservePub: string;
- reservePriv: string;
- restrictAge?: number;
-}
-
-/**
- * Reasons for why a coin is being refreshed.
- */
-export enum RefreshReason {
- Manual = "manual",
- PayMerchant = "pay-merchant",
- PayDeposit = "pay-deposit",
- PayPeerPush = "pay-peer-push",
- PayPeerPull = "pay-peer-pull",
- Refund = "refund",
- AbortPay = "abort-pay",
- Recoup = "recoup",
- BackupRestored = "backup-restored",
- Scheduled = "scheduled",
-}
-
-/**
- * Wrapper for coin public keys.
- */
-export interface CoinPublicKey {
- readonly coinPub: string;
-}
-
-/**
- * Wrapper for refresh group IDs.
- */
-export interface RefreshGroupId {
- readonly refreshGroupId: string;
-}
-
-/**
- * Private data required to make a deposit permission.
- */
-export interface DepositInfo {
- exchangeBaseUrl: string;
- contractTermsHash: string;
- coinPub: string;
- coinPriv: string;
- spendAmount: AmountJson;
- timestamp: TalerProtocolTimestamp;
- refundDeadline: TalerProtocolTimestamp;
- merchantPub: string;
- feeDeposit: AmountJson;
- wireInfoHash: string;
- denomKeyType: DenomKeyType;
- denomPubHash: string;
- denomSig: UnblindedSignature;
-
- requiredMinimumAge?: number;
-
- ageCommitmentProof?: AgeCommitmentProof;
-}
-
-export interface ExchangesListResponse {
- exchanges: ExchangeListItem[];
-}
-
-export interface ExchangeDetailedResponse {
- exchange: ExchangeFullDetails;
-}
-
-export interface WalletCoreVersion {
- hash: string | undefined;
- version: string;
- exchange: string;
- merchant: string;
- bank: string;
- devMode?: boolean;
-}
-
-export interface KnownBankAccountsInfo {
- uri: PaytoUri;
- kyc_completed: boolean;
- currency: string;
- alias: string;
-}
-
-export interface KnownBankAccounts {
- accounts: KnownBankAccountsInfo[];
-}
-
-export interface ExchangeTosStatusDetails {
- acceptedVersion?: string;
- currentVersion?: string;
- contentType?: string;
- content?: string;
-}
-
-/**
- * Wire fee for one wire method
- */
-export interface WireFee {
- /**
- * Fee for wire transfers.
- */
- wireFee: AmountJson;
-
- /**
- * Fees to close and refund a reserve.
- */
- closingFee: AmountJson;
-
- /**
- * Fees for inter-exchange transfers from P2P payments.
- */
- wadFee: AmountJson;
-
- /**
- * Start date of the fee.
- */
- startStamp: TalerProtocolTimestamp;
-
- /**
- * End date of the fee.
- */
- endStamp: TalerProtocolTimestamp;
-
- /**
- * Signature made by the exchange master key.
- */
- sig: string;
-}
-
-/**
- * Information about one of the exchange's bank accounts.
- */
-export interface ExchangeAccount {
- payto_uri: string;
- master_sig: string;
-}
-
-export type WireFeeMap = { [wireMethod: string]: WireFee[] };
-
-export interface WireInfo {
- feesForType: WireFeeMap;
- accounts: ExchangeAccount[];
-}
-
-export interface ExchangeGlobalFees {
- startDate: TalerProtocolTimestamp;
- endDate: TalerProtocolTimestamp;
-
- kycFee: AmountJson;
- historyFee: AmountJson;
- accountFee: AmountJson;
- purseFee: AmountJson;
-
- historyTimeout: TalerProtocolDuration;
- kycTimeout: TalerProtocolDuration;
- purseTimeout: TalerProtocolDuration;
-
- purseLimit: number;
-
- signature: string;
-}
-
-const codecForExchangeAccount = (): Codec<ExchangeAccount> =>
- buildCodecForObject<ExchangeAccount>()
- .property("payto_uri", codecForString())
- .property("master_sig", codecForString())
- .build("codecForExchangeAccount");
-
-const codecForWireFee = (): Codec<WireFee> =>
- buildCodecForObject<WireFee>()
- .property("sig", codecForString())
- .property("wireFee", codecForAmountJson())
- .property("wadFee", codecForAmountJson())
- .property("closingFee", codecForAmountJson())
- .property("startStamp", codecForTimestamp)
- .property("endStamp", codecForTimestamp)
- .build("codecForWireFee");
-
-const codecForWireInfo = (): Codec<WireInfo> =>
- buildCodecForObject<WireInfo>()
- .property("feesForType", codecForMap(codecForList(codecForWireFee())))
- .property("accounts", codecForList(codecForExchangeAccount()))
- .build("codecForWireInfo");
-
-export interface DenominationInfo {
- /**
- * Value of one coin of the denomination.
- */
- value: AmountJson;
-
- /**
- * Hash of the denomination public key.
- * Stored in the database for faster lookups.
- */
- denomPubHash: string;
-
- denomPub: DenominationPubKey;
-
- /**
- * Fee for withdrawing.
- */
- feeWithdraw: AmountJson;
-
- /**
- * Fee for depositing.
- */
- feeDeposit: AmountJson;
-
- /**
- * Fee for refreshing.
- */
- feeRefresh: AmountJson;
-
- /**
- * Fee for refunding.
- */
- feeRefund: AmountJson;
-
- /**
- * Validity start date of the denomination.
- */
- stampStart: TalerProtocolTimestamp;
-
- /**
- * Date after which the currency can't be withdrawn anymore.
- */
- stampExpireWithdraw: TalerProtocolTimestamp;
-
- /**
- * Date after the denomination officially doesn't exist anymore.
- */
- stampExpireLegal: TalerProtocolTimestamp;
-
- /**
- * Data after which coins of this denomination can't be deposited anymore.
- */
- stampExpireDeposit: TalerProtocolTimestamp;
-
- exchangeBaseUrl: string;
-}
-
-export type DenomOperation = "deposit" | "withdraw" | "refresh" | "refund";
-export type DenomOperationMap<T> = { [op in DenomOperation]: T };
-
-export interface FeeDescription {
- group: string;
- from: AbsoluteTime;
- until: AbsoluteTime;
- fee?: AmountJson;
-}
-
-export interface FeeDescriptionPair {
- group: string;
- from: AbsoluteTime;
- until: AbsoluteTime;
- left?: AmountJson;
- right?: AmountJson;
-}
-
-export interface TimePoint<T> {
- id: string;
- group: string;
- fee: AmountJson;
- type: "start" | "end";
- moment: AbsoluteTime;
- denom: T;
-}
-
-export interface ExchangeFullDetails {
- exchangeBaseUrl: string;
- currency: string;
- paytoUris: string[];
- tos: ExchangeTosStatusDetails;
- auditors: ExchangeAuditor[];
- wireInfo: WireInfo;
- denomFees: DenomOperationMap<FeeDescription[]>;
- transferFees: Record<string, FeeDescription[]>;
- globalFees: FeeDescription[];
-}
-
-export interface ExchangeListItem {
- exchangeBaseUrl: string;
- currency: string;
- paytoUris: string[];
- tos: ExchangeTosStatusDetails;
-}
-
-const codecForAuditorDenomSig = (): Codec<AuditorDenomSig> =>
- buildCodecForObject<AuditorDenomSig>()
- .property("denom_pub_h", codecForString())
- .property("auditor_sig", codecForString())
- .build("AuditorDenomSig");
-
-const codecForExchangeAuditor = (): Codec<ExchangeAuditor> =>
- buildCodecForObject<ExchangeAuditor>()
- .property("auditor_pub", codecForString())
- .property("auditor_url", codecForString())
- .property("denomination_keys", codecForList(codecForAuditorDenomSig()))
- .build("codecForExchangeAuditor");
-
-const codecForExchangeTos = (): Codec<ExchangeTosStatusDetails> =>
- buildCodecForObject<ExchangeTosStatusDetails>()
- .property("acceptedVersion", codecOptional(codecForString()))
- .property("currentVersion", codecOptional(codecForString()))
- .property("contentType", codecOptional(codecForString()))
- .property("content", codecOptional(codecForString()))
- .build("ExchangeTos");
-
-export const codecForFeeDescriptionPair = (): Codec<FeeDescriptionPair> =>
- buildCodecForObject<FeeDescriptionPair>()
- .property("group", codecForString())
- .property("from", codecForAbsoluteTime)
- .property("until", codecForAbsoluteTime)
- .property("left", codecOptional(codecForAmountJson()))
- .property("right", codecOptional(codecForAmountJson()))
- .build("FeeDescriptionPair");
-
-export const codecForFeeDescription = (): Codec<FeeDescription> =>
- buildCodecForObject<FeeDescription>()
- .property("group", codecForString())
- .property("from", codecForAbsoluteTime)
- .property("until", codecForAbsoluteTime)
- .property("fee", codecOptional(codecForAmountJson()))
- .build("FeeDescription");
-
-export const codecForFeesByOperations = (): Codec<
- DenomOperationMap<FeeDescription[]>
-> =>
- buildCodecForObject<DenomOperationMap<FeeDescription[]>>()
- .property("deposit", codecForList(codecForFeeDescription()))
- .property("withdraw", codecForList(codecForFeeDescription()))
- .property("refresh", codecForList(codecForFeeDescription()))
- .property("refund", codecForList(codecForFeeDescription()))
- .build("DenomOperationMap");
-
-export const codecForExchangeFullDetails = (): Codec<ExchangeFullDetails> =>
- buildCodecForObject<ExchangeFullDetails>()
- .property("currency", codecForString())
- .property("exchangeBaseUrl", codecForString())
- .property("paytoUris", codecForList(codecForString()))
- .property("tos", codecForExchangeTos())
- .property("auditors", codecForList(codecForExchangeAuditor()))
- .property("wireInfo", codecForWireInfo())
- .property("denomFees", codecForFeesByOperations())
- .property(
- "transferFees",
- codecForMap(codecForList(codecForFeeDescription())),
- )
- .property("globalFees", codecForList(codecForFeeDescription()))
- .build("ExchangeFullDetails");
-
-export const codecForExchangeListItem = (): Codec<ExchangeListItem> =>
- buildCodecForObject<ExchangeListItem>()
- .property("currency", codecForString())
- .property("exchangeBaseUrl", codecForString())
- .property("paytoUris", codecForList(codecForString()))
- .property("tos", codecForExchangeTos())
- .build("ExchangeListItem");
-
-export const codecForExchangesListResponse = (): Codec<ExchangesListResponse> =>
- buildCodecForObject<ExchangesListResponse>()
- .property("exchanges", codecForList(codecForExchangeListItem()))
- .build("ExchangesListResponse");
-
-export interface AcceptManualWithdrawalResult {
- /**
- * Payto URIs that can be used to fund the withdrawal.
- */
- exchangePaytoUris: string[];
-
- /**
- * Public key of the newly created reserve.
- */
- reservePub: string;
-
- transactionId: string;
-}
-
-export interface ManualWithdrawalDetails {
- /**
- * Did the user accept the current version of the exchange's
- * terms of service?
- */
- tosAccepted: boolean;
-
- /**
- * Amount that the user will transfer to the exchange.
- */
- amountRaw: AmountString;
-
- /**
- * Amount that will be added to the user's wallet balance.
- */
- amountEffective: AmountString;
-
- /**
- * Ways to pay the exchange.
- */
- paytoUris: string[];
-
- /**
- * If the exchange supports age-restricted coins it will return
- * the array of ages.
- */
- ageRestrictionOptions?: number[];
-}
-
-/**
- * Selected denominations withn some extra info.
- */
-export interface DenomSelectionState {
- totalCoinValue: AmountJson;
- totalWithdrawCost: AmountJson;
- selectedDenoms: {
- denomPubHash: string;
- count: number;
- }[];
-}
-
-/**
- * Information about what will happen doing a withdrawal.
- *
- * Sent to the wallet frontend to be rendered and shown to the user.
- */
-export interface ExchangeWithdrawalDetails {
- exchangePaytoUris: string[];
-
- /**
- * Filtered wire info to send to the bank.
- */
- exchangeWireAccounts: string[];
-
- /**
- * Selected denominations for withdraw.
- */
- selectedDenoms: DenomSelectionState;
-
- /**
- * Does the wallet know about an auditor for
- * the exchange that the reserve.
- */
- isAudited: boolean;
-
- /**
- * Did the user already accept the current terms of service for the exchange?
- */
- termsOfServiceAccepted: boolean;
-
- /**
- * The exchange is trusted directly.
- */
- isTrusted: boolean;
-
- /**
- * The earliest deposit expiration of the selected coins.
- */
- earliestDepositExpiration: TalerProtocolTimestamp;
-
- /**
- * Number of currently offered denominations.
- */
- numOfferedDenoms: number;
-
- /**
- * Public keys of trusted auditors for the currency we're withdrawing.
- */
- trustedAuditorPubs: string[];
-
- /**
- * Result of checking the wallet's version
- * against the exchange's version.
- *
- * Older exchanges don't return version information.
- */
- versionMatch: VersionMatchResult | undefined;
-
- /**
- * Libtool-style version string for the exchange or "unknown"
- * for older exchanges.
- */
- exchangeVersion: string;
-
- /**
- * Libtool-style version string for the wallet.
- */
- walletVersion: string;
-
- /**
- * Amount that will be subtracted from the reserve's balance.
- */
- withdrawalAmountRaw: AmountString;
-
- /**
- * Amount that will actually be added to the wallet's balance.
- */
- withdrawalAmountEffective: AmountString;
-
- /**
- * If the exchange supports age-restricted coins it will return
- * the array of ages.
- *
- */
- ageRestrictionOptions?: number[];
-}
-
-export interface GetExchangeTosResult {
- /**
- * Markdown version of the current ToS.
- */
- content: string;
-
- /**
- * Version tag of the current ToS.
- */
- currentEtag: string;
-
- /**
- * Version tag of the last ToS that the user has accepted,
- * if any.
- */
- acceptedEtag: string | undefined;
-
- /**
- * Accepted content type
- */
- contentType: string;
-}
-
-export interface TestPayArgs {
- merchantBaseUrl: string;
- merchantAuthToken?: string;
- amount: string;
- summary: string;
- forcedCoinSel?: ForcedCoinSel;
-}
-
-export const codecForTestPayArgs = (): Codec<TestPayArgs> =>
- buildCodecForObject<TestPayArgs>()
- .property("merchantBaseUrl", codecForString())
- .property("merchantAuthToken", codecOptional(codecForString()))
- .property("amount", codecForString())
- .property("summary", codecForString())
- .property("forcedCoinSel", codecForAny())
- .build("TestPayArgs");
-
-export interface IntegrationTestArgs {
- exchangeBaseUrl: string;
- bankBaseUrl: string;
- bankAccessApiBaseUrl?: string;
- merchantBaseUrl: string;
- merchantAuthToken?: string;
- amountToWithdraw: string;
- amountToSpend: string;
-}
-
-export const codecForIntegrationTestArgs = (): Codec<IntegrationTestArgs> =>
- buildCodecForObject<IntegrationTestArgs>()
- .property("exchangeBaseUrl", codecForString())
- .property("bankBaseUrl", codecForString())
- .property("merchantBaseUrl", codecForString())
- .property("merchantAuthToken", codecOptional(codecForString()))
- .property("amountToSpend", codecForAmountString())
- .property("amountToWithdraw", codecForAmountString())
- .property("bankAccessApiBaseUrl", codecOptional(codecForAmountString()))
- .build("IntegrationTestArgs");
-
-export interface AddExchangeRequest {
- exchangeBaseUrl: string;
- forceUpdate?: boolean;
-}
-
-export const codecForAddExchangeRequest = (): Codec<AddExchangeRequest> =>
- buildCodecForObject<AddExchangeRequest>()
- .property("exchangeBaseUrl", codecForString())
- .property("forceUpdate", codecOptional(codecForBoolean()))
- .build("AddExchangeRequest");
-
-export interface ForceExchangeUpdateRequest {
- exchangeBaseUrl: string;
-}
-
-export const codecForForceExchangeUpdateRequest =
- (): Codec<AddExchangeRequest> =>
- buildCodecForObject<AddExchangeRequest>()
- .property("exchangeBaseUrl", codecForString())
- .build("AddExchangeRequest");
-
-export interface GetExchangeTosRequest {
- exchangeBaseUrl: string;
- acceptedFormat?: string[];
-}
-
-export const codecForGetExchangeTosRequest = (): Codec<GetExchangeTosRequest> =>
- buildCodecForObject<GetExchangeTosRequest>()
- .property("exchangeBaseUrl", codecForString())
- .property("acceptedFormat", codecOptional(codecForList(codecForString())))
- .build("GetExchangeTosRequest");
-
-export interface AcceptManualWithdrawalRequest {
- exchangeBaseUrl: string;
- amount: string;
- restrictAge?: number;
-}
-
-export const codecForAcceptManualWithdrawalRequet =
- (): Codec<AcceptManualWithdrawalRequest> =>
- buildCodecForObject<AcceptManualWithdrawalRequest>()
- .property("exchangeBaseUrl", codecForString())
- .property("amount", codecForString())
- .property("restrictAge", codecOptional(codecForNumber()))
- .build("AcceptManualWithdrawalRequest");
-
-export interface GetWithdrawalDetailsForAmountRequest {
- exchangeBaseUrl: string;
- amount: string;
- restrictAge?: number;
-}
-
-export interface AcceptBankIntegratedWithdrawalRequest {
- talerWithdrawUri: string;
- exchangeBaseUrl: string;
- forcedDenomSel?: ForcedDenomSel;
- restrictAge?: number;
-}
-
-export const codecForAcceptBankIntegratedWithdrawalRequest =
- (): Codec<AcceptBankIntegratedWithdrawalRequest> =>
- buildCodecForObject<AcceptBankIntegratedWithdrawalRequest>()
- .property("exchangeBaseUrl", codecForString())
- .property("talerWithdrawUri", codecForString())
- .property("forcedDenomSel", codecForAny())
- .property("restrictAge", codecOptional(codecForNumber()))
- .build("AcceptBankIntegratedWithdrawalRequest");
-
-export const codecForGetWithdrawalDetailsForAmountRequest =
- (): Codec<GetWithdrawalDetailsForAmountRequest> =>
- buildCodecForObject<GetWithdrawalDetailsForAmountRequest>()
- .property("exchangeBaseUrl", codecForString())
- .property("amount", codecForString())
- .property("restrictAge", codecOptional(codecForNumber()))
- .build("GetWithdrawalDetailsForAmountRequest");
-
-export interface AcceptExchangeTosRequest {
- exchangeBaseUrl: string;
- etag: string | undefined;
-}
-
-export const codecForAcceptExchangeTosRequest =
- (): Codec<AcceptExchangeTosRequest> =>
- buildCodecForObject<AcceptExchangeTosRequest>()
- .property("exchangeBaseUrl", codecForString())
- .property("etag", codecOptional(codecForString()))
- .build("AcceptExchangeTosRequest");
-
-export interface ApplyRefundRequest {
- talerRefundUri: string;
-}
-
-export const codecForApplyRefundRequest = (): Codec<ApplyRefundRequest> =>
- buildCodecForObject<ApplyRefundRequest>()
- .property("talerRefundUri", codecForString())
- .build("ApplyRefundRequest");
-
-export interface ApplyRefundFromPurchaseIdRequest {
- purchaseId: string;
-}
-
-export const codecForApplyRefundFromPurchaseIdRequest =
- (): Codec<ApplyRefundFromPurchaseIdRequest> =>
- buildCodecForObject<ApplyRefundFromPurchaseIdRequest>()
- .property("purchaseId", codecForString())
- .build("ApplyRefundFromPurchaseIdRequest");
-
-export interface GetWithdrawalDetailsForUriRequest {
- talerWithdrawUri: string;
- restrictAge?: number;
-}
-export const codecForGetWithdrawalDetailsForUri =
- (): Codec<GetWithdrawalDetailsForUriRequest> =>
- buildCodecForObject<GetWithdrawalDetailsForUriRequest>()
- .property("talerWithdrawUri", codecForString())
- .property("restrictAge", codecOptional(codecForNumber()))
- .build("GetWithdrawalDetailsForUriRequest");
-
-export interface ListKnownBankAccountsRequest {
- currency?: string;
-}
-export const codecForListKnownBankAccounts =
- (): Codec<ListKnownBankAccountsRequest> =>
- buildCodecForObject<ListKnownBankAccountsRequest>()
- .property("currency", codecOptional(codecForString()))
- .build("ListKnownBankAccountsRequest");
-
-export interface AddKnownBankAccountsRequest {
- payto: string;
- alias: string;
- currency: string;
-}
-export const codecForAddKnownBankAccounts =
- (): Codec<AddKnownBankAccountsRequest> =>
- buildCodecForObject<AddKnownBankAccountsRequest>()
- .property("payto", codecForString())
- .property("alias", codecForString())
- .property("currency", codecForString())
- .build("AddKnownBankAccountsRequest");
-
-export interface ForgetKnownBankAccountsRequest {
- payto: string;
-}
-
-export const codecForForgetKnownBankAccounts =
- (): Codec<ForgetKnownBankAccountsRequest> =>
- buildCodecForObject<ForgetKnownBankAccountsRequest>()
- .property("payto", codecForString())
- .build("ForgetKnownBankAccountsRequest");
-
-export interface AbortProposalRequest {
- proposalId: string;
-}
-
-export const codecForAbortProposalRequest = (): Codec<AbortProposalRequest> =>
- buildCodecForObject<AbortProposalRequest>()
- .property("proposalId", codecForString())
- .build("AbortProposalRequest");
-
-interface GetContractTermsDetailsRequest {
- proposalId: string;
-}
-
-export const codecForGetContractTermsDetails =
- (): Codec<GetContractTermsDetailsRequest> =>
- buildCodecForObject<GetContractTermsDetailsRequest>()
- .property("proposalId", codecForString())
- .build("GetContractTermsDetails");
-
-export interface PreparePayRequest {
- talerPayUri: string;
-}
-
-export const codecForPreparePayRequest = (): Codec<PreparePayRequest> =>
- buildCodecForObject<PreparePayRequest>()
- .property("talerPayUri", codecForString())
- .build("PreparePay");
-
-export interface ConfirmPayRequest {
- proposalId: string;
- sessionId?: string;
- forcedCoinSel?: ForcedCoinSel;
-}
-
-export const codecForConfirmPayRequest = (): Codec<ConfirmPayRequest> =>
- buildCodecForObject<ConfirmPayRequest>()
- .property("proposalId", codecForString())
- .property("sessionId", codecOptional(codecForString()))
- .property("forcedCoinSel", codecForAny())
- .build("ConfirmPay");
-
-export type CoreApiResponse = CoreApiResponseSuccess | CoreApiResponseError;
-
-export type CoreApiEnvelope = CoreApiResponse | CoreApiNotification;
-
-export interface CoreApiNotification {
- type: "notification";
- payload: unknown;
-}
-
-export interface CoreApiResponseSuccess {
- // To distinguish the message from notifications
- type: "response";
- operation: string;
- id: string;
- result: unknown;
-}
-
-export interface CoreApiResponseError {
- // To distinguish the message from notifications
- type: "error";
- operation: string;
- id: string;
- error: TalerErrorDetail;
-}
-
-export interface WithdrawTestBalanceRequest {
- amount: string;
- bankBaseUrl: string;
- /**
- * Bank access API base URL. Defaults to the bankBaseUrl.
- */
- bankAccessApiBaseUrl?: string;
- exchangeBaseUrl: string;
- forcedDenomSel?: ForcedDenomSel;
-}
-
-export const withdrawTestBalanceDefaults = {
- amount: "TESTKUDOS:10",
- bankBaseUrl: "https://bank.test.taler.net/",
- exchangeBaseUrl: "https://exchange.test.taler.net/",
-};
-
-/**
- * Request to the crypto worker to make a sync signature.
- */
-export interface MakeSyncSignatureRequest {
- accountPriv: string;
- oldHash: string | undefined;
- newHash: string;
-}
-
-/**
- * Planchet for a coin during refresh.
- */
-export interface RefreshPlanchetInfo {
- /**
- * Public key for the coin.
- */
- coinPub: string;
-
- /**
- * Private key for the coin.
- */
- coinPriv: string;
-
- /**
- * Blinded public key.
- */
- coinEv: CoinEnvelope;
-
- coinEvHash: string;
-
- /**
- * Blinding key used.
- */
- blindingKey: string;
-
- maxAge: number;
- ageCommitmentProof?: AgeCommitmentProof;
-}
-
-/**
- * Strategy for loading recovery information.
- */
-export enum RecoveryMergeStrategy {
- /**
- * Keep the local wallet root key, import and take over providers.
- */
- Ours = "ours",
-
- /**
- * Migrate to the wallet root key from the recovery information.
- */
- Theirs = "theirs",
-}
-
-/**
- * Load recovery information into the wallet.
- */
-export interface RecoveryLoadRequest {
- recovery: BackupRecovery;
- strategy?: RecoveryMergeStrategy;
-}
-
-export const codecForWithdrawTestBalance =
- (): Codec<WithdrawTestBalanceRequest> =>
- buildCodecForObject<WithdrawTestBalanceRequest>()
- .property("amount", codecForString())
- .property("bankBaseUrl", codecForString())
- .property("exchangeBaseUrl", codecForString())
- .property("forcedDenomSel", codecForAny())
- .property("bankAccessApiBaseUrl", codecOptional(codecForString()))
- .build("WithdrawTestBalanceRequest");
-
-export interface ApplyRefundResponse {
- contractTermsHash: string;
-
- transactionId: string;
-
- proposalId: string;
-
- amountEffectivePaid: AmountString;
-
- amountRefundGranted: AmountString;
-
- amountRefundGone: AmountString;
-
- pendingAtExchange: boolean;
-
- info: OrderShortInfo;
-}
-
-export const codecForApplyRefundResponse = (): Codec<ApplyRefundResponse> =>
- buildCodecForObject<ApplyRefundResponse>()
- .property("amountEffectivePaid", codecForAmountString())
- .property("amountRefundGone", codecForAmountString())
- .property("amountRefundGranted", codecForAmountString())
- .property("contractTermsHash", codecForString())
- .property("pendingAtExchange", codecForBoolean())
- .property("proposalId", codecForString())
- .property("transactionId", codecForString())
- .property("info", codecForOrderShortInfo())
- .build("ApplyRefundResponse");
-
-export interface SetCoinSuspendedRequest {
- coinPub: string;
- suspended: boolean;
-}
-
-export const codecForSetCoinSuspendedRequest =
- (): Codec<SetCoinSuspendedRequest> =>
- buildCodecForObject<SetCoinSuspendedRequest>()
- .property("coinPub", codecForString())
- .property("suspended", codecForBoolean())
- .build("SetCoinSuspendedRequest");
-
-export interface ForceRefreshRequest {
- coinPubList: string[];
-}
-
-export const codecForForceRefreshRequest = (): Codec<ForceRefreshRequest> =>
- buildCodecForObject<ForceRefreshRequest>()
- .property("coinPubList", codecForList(codecForString()))
- .build("ForceRefreshRequest");
-
-export interface PrepareRefundRequest {
- talerRefundUri: string;
-}
-
-export const codecForPrepareRefundRequest = (): Codec<PrepareRefundRequest> =>
- buildCodecForObject<PrepareRefundRequest>()
- .property("talerRefundUri", codecForString())
- .build("PrepareRefundRequest");
-
-export interface PrepareTipRequest {
- talerTipUri: string;
-}
-
-export const codecForPrepareTipRequest = (): Codec<PrepareTipRequest> =>
- buildCodecForObject<PrepareTipRequest>()
- .property("talerTipUri", codecForString())
- .build("PrepareTipRequest");
-
-export interface AcceptTipRequest {
- walletTipId: string;
-}
-
-export const codecForAcceptTipRequest = (): Codec<AcceptTipRequest> =>
- buildCodecForObject<AcceptTipRequest>()
- .property("walletTipId", codecForString())
- .build("AcceptTipRequest");
-
-export interface AbortPayWithRefundRequest {
- proposalId: string;
-}
-
-export const codecForAbortPayWithRefundRequest =
- (): Codec<AbortPayWithRefundRequest> =>
- buildCodecForObject<AbortPayWithRefundRequest>()
- .property("proposalId", codecForString())
- .build("AbortPayWithRefundRequest");
-
-export interface GetFeeForDepositRequest {
- depositPaytoUri: string;
- amount: AmountString;
-}
-
-export interface DepositGroupFees {
- coin: AmountJson;
- wire: AmountJson;
- refresh: AmountJson;
-}
-
-export interface CreateDepositGroupRequest {
- depositPaytoUri: string;
- amount: AmountString;
-}
-
-export const codecForGetFeeForDeposit = (): Codec<GetFeeForDepositRequest> =>
- buildCodecForObject<GetFeeForDepositRequest>()
- .property("amount", codecForAmountString())
- .property("depositPaytoUri", codecForString())
- .build("GetFeeForDepositRequest");
-
-export interface PrepareDepositRequest {
- depositPaytoUri: string;
- amount: AmountString;
-}
-export const codecForPrepareDepositRequest = (): Codec<PrepareDepositRequest> =>
- buildCodecForObject<PrepareDepositRequest>()
- .property("amount", codecForAmountString())
- .property("depositPaytoUri", codecForString())
- .build("PrepareDepositRequest");
-
-export interface PrepareDepositResponse {
- totalDepositCost: AmountJson;
- effectiveDepositAmount: AmountJson;
-}
-
-export const codecForCreateDepositGroupRequest =
- (): Codec<CreateDepositGroupRequest> =>
- buildCodecForObject<CreateDepositGroupRequest>()
- .property("amount", codecForAmountString())
- .property("depositPaytoUri", codecForString())
- .build("CreateDepositGroupRequest");
-
-export interface CreateDepositGroupResponse {
- depositGroupId: string;
- transactionId: string;
-}
-
-export interface TrackDepositGroupRequest {
- depositGroupId: string;
-}
-
-export interface TrackDepositGroupResponse {
- responses: {
- status: number;
- body: any;
- }[];
-}
-
-export const codecForTrackDepositGroupRequest =
- (): Codec<TrackDepositGroupRequest> =>
- buildCodecForObject<TrackDepositGroupRequest>()
- .property("depositGroupId", codecForAmountString())
- .build("TrackDepositGroupRequest");
-
-export interface WithdrawUriInfoResponse {
- amount: AmountString;
- defaultExchangeBaseUrl?: string;
- possibleExchanges: ExchangeListItem[];
-}
-
-export const codecForWithdrawUriInfoResponse =
- (): Codec<WithdrawUriInfoResponse> =>
- buildCodecForObject<WithdrawUriInfoResponse>()
- .property("amount", codecForAmountString())
- .property("defaultExchangeBaseUrl", codecOptional(codecForString()))
- .property("possibleExchanges", codecForList(codecForExchangeListItem()))
- .build("WithdrawUriInfoResponse");
-
-export interface WalletCurrencyInfo {
- trustedAuditors: {
- currency: string;
- auditorPub: string;
- auditorBaseUrl: string;
- }[];
- trustedExchanges: {
- currency: string;
- exchangeMasterPub: string;
- exchangeBaseUrl: string;
- }[];
-}
-
-export interface DeleteTransactionRequest {
- transactionId: string;
-}
-
-export interface RetryTransactionRequest {
- transactionId: string;
-}
-
-export const codecForDeleteTransactionRequest =
- (): Codec<DeleteTransactionRequest> =>
- buildCodecForObject<DeleteTransactionRequest>()
- .property("transactionId", codecForString())
- .build("DeleteTransactionRequest");
-
-export const codecForRetryTransactionRequest =
- (): Codec<RetryTransactionRequest> =>
- buildCodecForObject<RetryTransactionRequest>()
- .property("transactionId", codecForString())
- .build("RetryTransactionRequest");
-
-export interface SetWalletDeviceIdRequest {
- /**
- * New wallet device ID to set.
- */
- walletDeviceId: string;
-}
-
-export const codecForSetWalletDeviceIdRequest =
- (): Codec<SetWalletDeviceIdRequest> =>
- buildCodecForObject<SetWalletDeviceIdRequest>()
- .property("walletDeviceId", codecForString())
- .build("SetWalletDeviceIdRequest");
-
-export interface WithdrawFakebankRequest {
- amount: AmountString;
- exchange: string;
- bank: string;
-}
-
-export const codecForWithdrawFakebankRequest =
- (): Codec<WithdrawFakebankRequest> =>
- buildCodecForObject<WithdrawFakebankRequest>()
- .property("amount", codecForAmountString())
- .property("bank", codecForString())
- .property("exchange", codecForString())
- .build("WithdrawFakebankRequest");
-
-export interface ImportDb {
- dump: any;
-}
-
-export const codecForImportDbRequest = (): Codec<ImportDb> =>
- buildCodecForObject<ImportDb>()
- .property("dump", codecForAny())
- .build("ImportDbRequest");
-
-export interface ForcedDenomSel {
- denoms: {
- value: AmountString;
- count: number;
- }[];
-}
-
-/**
- * Forced coin selection for deposits/payments.
- */
-export interface ForcedCoinSel {
- coins: {
- value: AmountString;
- contribution: AmountString;
- }[];
-}
-
-export interface TestPayResult {
- payCoinSelection: PayCoinSelection;
-}
-
-/**
- * Result of selecting coins, contains the exchange, and selected
- * coins with their denomination.
- */
-export interface PayCoinSelection {
- /**
- * Amount requested by the merchant.
- */
- paymentAmount: AmountJson;
-
- /**
- * Public keys of the coins that were selected.
- */
- coinPubs: string[];
-
- /**
- * Amount that each coin contributes.
- */
- coinContributions: AmountJson[];
-
- /**
- * How much of the wire fees is the customer paying?
- */
- customerWireFees: AmountJson;
-
- /**
- * How much of the deposit fees is the customer paying?
- */
- customerDepositFees: AmountJson;
-}
-
-export interface InitiatePeerPushPaymentRequest {
- amount: AmountString;
- partialContractTerms: any;
-}
-
-export interface InitiatePeerPushPaymentResponse {
- exchangeBaseUrl: string;
- pursePub: string;
- mergePriv: string;
- contractPriv: string;
- talerUri: string;
- transactionId: string;
-}
-
-export const codecForInitiatePeerPushPaymentRequest =
- (): Codec<InitiatePeerPushPaymentRequest> =>
- buildCodecForObject<InitiatePeerPushPaymentRequest>()
- .property("amount", codecForAmountString())
- .property("partialContractTerms", codecForAny())
- .build("InitiatePeerPushPaymentRequest");
-
-export interface CheckPeerPushPaymentRequest {
- talerUri: string;
-}
-
-export interface CheckPeerPullPaymentRequest {
- talerUri: string;
-}
-
-export interface CheckPeerPushPaymentResponse {
- contractTerms: any;
- amount: AmountString;
- peerPushPaymentIncomingId: string;
-}
-
-export interface CheckPeerPullPaymentResponse {
- contractTerms: any;
- amount: AmountString;
- peerPullPaymentIncomingId: string;
-}
-
-export const codecForCheckPeerPushPaymentRequest =
- (): Codec<CheckPeerPushPaymentRequest> =>
- buildCodecForObject<CheckPeerPushPaymentRequest>()
- .property("talerUri", codecForString())
- .build("CheckPeerPushPaymentRequest");
-
-export const codecForCheckPeerPullPaymentRequest =
- (): Codec<CheckPeerPullPaymentRequest> =>
- buildCodecForObject<CheckPeerPullPaymentRequest>()
- .property("talerUri", codecForString())
- .build("CheckPeerPullPaymentRequest");
-
-export interface AcceptPeerPushPaymentRequest {
- /**
- * Transparent identifier of the incoming peer push payment.
- */
- peerPushPaymentIncomingId: string;
-}
-export interface AcceptPeerPushPaymentResponse {
- transactionId: string;
-}
-
-export interface AcceptPeerPullPaymentResponse {
- transactionId: string;
-}
-
-export const codecForAcceptPeerPushPaymentRequest =
- (): Codec<AcceptPeerPushPaymentRequest> =>
- buildCodecForObject<AcceptPeerPushPaymentRequest>()
- .property("peerPushPaymentIncomingId", codecForString())
- .build("AcceptPeerPushPaymentRequest");
-
-export interface AcceptPeerPullPaymentRequest {
- /**
- * Transparent identifier of the incoming peer pull payment.
- */
- peerPullPaymentIncomingId: string;
-}
-
-export interface SetDevModeRequest {
- devModeEnabled: boolean;
-}
-
-export const codecForSetDevModeRequest = (): Codec<SetDevModeRequest> =>
- buildCodecForObject<SetDevModeRequest>()
- .property("devModeEnabled", codecForBoolean())
- .build("SetDevModeRequest");
-
-export interface ApplyDevExperimentRequest {
- devExperimentUri: string;
-}
-
-export const codecForApplyDevExperiment =
- (): Codec<ApplyDevExperimentRequest> =>
- buildCodecForObject<ApplyDevExperimentRequest>()
- .property("devExperimentUri", codecForString())
- .build("ApplyDevExperimentRequest");
-
-export const codecForAcceptPeerPullPaymentRequest =
- (): Codec<AcceptPeerPullPaymentRequest> =>
- buildCodecForObject<AcceptPeerPullPaymentRequest>()
- .property("peerPullPaymentIncomingId", codecForString())
- .build("AcceptPeerPllPaymentRequest");
-
-export interface InitiatePeerPullPaymentRequest {
- /**
- * FIXME: Make this optional?
- */
- exchangeBaseUrl: string;
- amount: AmountString;
- partialContractTerms: any;
-}
-
-export const codecForInitiatePeerPullPaymentRequest =
- (): Codec<InitiatePeerPullPaymentRequest> =>
- buildCodecForObject<InitiatePeerPullPaymentRequest>()
- .property("partialContractTerms", codecForAny())
- .property("amount", codecForAmountString())
- .property("exchangeBaseUrl", codecForAmountString())
- .build("InitiatePeerPullPaymentRequest");
-
-export interface InitiatePeerPullPaymentResponse {
- /**
- * Taler URI for the other party to make the payment
- * that was requested.
- */
- talerUri: string;
-
- transactionId: string;
-}
diff --git a/packages/taler-wallet-core/src/db.ts b/packages/taler-wallet-core/src/db.ts
@@ -48,6 +48,7 @@ import {
GlobalFees,
ExchangeGlobalFees,
DenomSelectionState,
+ TransactionIdStr,
} from "@gnu-taler/taler-util";
import { RetryInfo, RetryTags } from "./util/retries.js";
import { Event, IDBDatabase } from "@gnu-taler/idb-bridge";
@@ -765,8 +766,10 @@ export interface CoinRecord {
* Coin allocation, i.e. what a coin has been used for.
*/
export interface CoinAllocation {
- // FIXME: Specify format!
- id: string;
+ /**
+ * ID of the allocation, should be the ID of the transaction that
+ */
+ id: TransactionIdStr;
amount: AmountString;
}
@@ -1065,6 +1068,9 @@ export enum PurchaseStatus {
*/
Paying = 11,
+ /**
+ * Currently in the process of aborting with a refund.
+ */
AbortingWithRefund = 12,
/**
@@ -1118,7 +1124,7 @@ export enum PurchaseStatus {
* Only contains data that is relevant for indexing on the
* "purchases" object stores.
*/
-export interface ProposalDownload {
+export interface ProposalDownloadInfo {
contractTermsHash: string;
fulfillmentUrl?: string;
currency: string;
@@ -1129,15 +1135,6 @@ export interface PurchasePayInfo {
payCoinSelection: PayCoinSelection;
totalPayCost: AmountJson;
payCoinSelectionUid: string;
-
- /**
- * Deposit permissions, available once the user has accepted the payment.
- *
- * This value is cached and derived from payCoinSelection.
- *
- * FIXME: Should probably be cached somewhere else, maybe not even in DB!
- */
- coinDepositPermissions: CoinDepositPermission[] | undefined;
}
/**
@@ -1191,11 +1188,8 @@ export interface PurchaseRecord {
/**
* Downloaded and parsed proposal data.
- *
- * FIXME: Move this into another object store,
- * to improve read/write perf on purchases.
*/
- download: ProposalDownload | undefined;
+ download: ProposalDownloadInfo | undefined;
payInfo: PurchasePayInfo | undefined;
diff --git a/packages/taler-wallet-core/src/operations/backup/import.ts b/packages/taler-wallet-core/src/operations/backup/import.ts
@@ -45,7 +45,7 @@ import {
DenominationRecord,
DenominationVerificationStatus,
OperationStatus,
- ProposalDownload,
+ ProposalDownloadInfo,
PurchaseStatus,
PurchasePayInfo,
RefreshCoinStatus,
@@ -649,7 +649,7 @@ export async function importBackup(
} else {
maxWireFee = Amounts.getZero(amount.currency);
}
- const download: ProposalDownload = {
+ const download: ProposalDownloadInfo = {
contractTermsHash,
contractTermsMerchantSig: backupPurchase.merchant_sig!,
currency: amount.currency,
@@ -665,7 +665,6 @@ export async function importBackup(
let payInfo: PurchasePayInfo | undefined = undefined;
if (backupPurchase.pay_info) {
payInfo = {
- coinDepositPermissions: undefined,
payCoinSelection: await recoverPayCoinSelection(
tx,
contractData,
diff --git a/packages/taler-wallet-core/src/operations/common.ts b/packages/taler-wallet-core/src/operations/common.ts
@@ -25,6 +25,7 @@ import {
RefreshReason,
TalerErrorCode,
TalerErrorDetail,
+ TransactionIdStr,
TransactionType,
} from "@gnu-taler/taler-util";
import { WalletStoresV1, CoinStatus, CoinRecord } from "../db.js";
@@ -37,7 +38,6 @@ import {
OperationAttemptResultType,
RetryInfo,
} from "../util/retries.js";
-import { createRefreshGroup } from "./refresh.js";
const logger = new Logger("operations/common.ts");
@@ -48,7 +48,7 @@ export interface CoinsSpendInfo {
/**
* Identifier for what the coin has been spent for.
*/
- allocationId: string;
+ allocationId: TransactionIdStr;
}
export async function makeCoinAvailable(
diff --git a/packages/taler-wallet-core/src/operations/pay-merchant.ts b/packages/taler-wallet-core/src/operations/pay-merchant.ts
@@ -80,9 +80,8 @@ import {
CoinRecord,
CoinStatus,
DenominationRecord,
- ProposalDownload,
- PurchaseStatus,
PurchaseRecord,
+ PurchaseStatus,
RefundReason,
RefundState,
WalletContractData,
@@ -115,7 +114,6 @@ import {
throwUnexpectedRequestError,
} from "../util/http.js";
import { checkDbInvariant, checkLogicInvariant } from "../util/invariants.js";
-import { GetReadOnlyAccess } from "../util/query.js";
import {
OperationAttemptResult,
OperationAttemptResultType,
@@ -124,10 +122,10 @@ import {
scheduleRetry,
} from "../util/retries.js";
import {
+ makeEventId,
spendCoins,
- storeOperationPending,
storeOperationError,
- makeEventId,
+ storeOperationPending,
} from "./common.js";
import { getExchangeDetails } from "./exchanges.js";
import { createRefreshGroup, getTotalRefreshCost } from "./refresh.js";
@@ -858,10 +856,9 @@ async function handleInsufficientFunds(
payInfo.payCoinSelection = res;
payInfo.payCoinSelection = res;
payInfo.payCoinSelectionUid = encodeCrock(getRandomBytes(32));
- payInfo.coinDepositPermissions = undefined;
await tx.purchases.put(p);
await spendCoins(ws, tx, {
- allocationId: `proposal:${p.proposalId}`,
+ allocationId: `tx:proposal:${p.proposalId}`,
coinPubs: payInfo.payCoinSelection.coinPubs,
contributions: payInfo.payCoinSelection.coinContributions,
refreshReason: RefreshReason.PayMerchant,
@@ -1732,14 +1729,13 @@ export async function confirmPay(
payCoinSelection: coinSelection,
payCoinSelectionUid: encodeCrock(getRandomBytes(16)),
totalPayCost: payCostInfo,
- coinDepositPermissions: depositPermissions,
};
p.lastSessionId = sessionId;
p.timestampAccept = TalerProtocolTimestamp.now();
p.purchaseStatus = PurchaseStatus.Paying;
await tx.purchases.put(p);
await spendCoins(ws, tx, {
- allocationId: `proposal:${p.proposalId}`,
+ allocationId: `tx:proposal:${p.proposalId}`,
coinPubs: coinSelection.coinPubs,
contributions: coinSelection.coinContributions,
refreshReason: RefreshReason.PayMerchant,
@@ -1856,17 +1852,12 @@ export async function processPurchasePay(
).href;
let depositPermissions: CoinDepositPermission[];
-
- if (purchase.payInfo?.coinDepositPermissions) {
- depositPermissions = purchase.payInfo.coinDepositPermissions;
- } else {
- // FIXME: also cache!
- depositPermissions = await generateDepositPermissions(
- ws,
- payInfo.payCoinSelection,
- download.contractData,
- );
- }
+ // FIXME: Cache!
+ depositPermissions = await generateDepositPermissions(
+ ws,
+ payInfo.payCoinSelection,
+ download.contractData,
+ );
const reqBody = {
coins: depositPermissions,