/*
 This file is part of GNU Taler
 (C) 2024-2025 Taler Systems S.A.

 GNU Taler is free software; you can redistribute it and/or modify it under the
 terms of the GNU Affero 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 Affero General Public License along with
 GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>

 SPDX-License-Identifier: AGPL-3.0-or-later
 */

import { codecForAmountString } from "./amounts.js";
import {
  Codec,
  buildCodecForObject,
  buildCodecForUnion,
  codecForAny,
  codecForBoolean,
  codecForConstString,
  codecForEither,
  codecForList,
  codecForMap,
  codecForNumber,
  codecForString,
  codecForStringURL,
  codecOptional,
  codecOptionalDefault,
} from "./codec.js";
import { strcmp } from "./helpers.js";
import {
  PaytoHash,
  Paytos,
  codecForPaytoHash,
  codecForPaytoString,
  codecFullForPaytoString,
} from "./payto.js";

import { Edx25519PublicKeyEnc } from "./taler-crypto.js";
import { TalerErrorCode } from "./taler-error-codes.js";
import { TalerFormAttributes } from "./taler-form-attributes.js";
import {
  TalerProtocolDuration,
  TalerProtocolTimestamp,
  codecForDuration,
  codecForTimestamp,
} from "./time.js";
import {
  AccessToken,
  AmlOfficerPublicKeyP,
  AmountString,
  Base32String,
  CoinPublicKeyString,
  Cs25519Point,
  CurrencySpecification,
  EddsaPublicKeyString,
  EddsaSignatureString,
  HashCodeString,
  Integer,
  InternationalizedString,
  LibtoolVersionString,
  RelativeTime,
  RsaPublicKey,
  RsaPublicKeyString,
  Timestamp,
  WireSalt,
  codecForAccessToken,
  codecForCurrencySpecificiation,
  codecForEddsaPublicKey,
  codecForEddsaSignature,
  codecForInternationalizedString,
  codecForURLString,
  codecForURN,
} from "./types-taler-common.js";

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

/**
 * Denomination as found in the /keys response from the exchange.
 */
export interface 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 interface 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 interface 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: UnblindedDenominationSignature;

  /**
   * 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: UnblindedDenominationSignature;

  /**
   * 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 UnblindedDenominationSignature = 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: UnblindedDenominationSignature;

  /**
   * 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[];

  h_age_commitment?: string;
}

/**
 * Element of the payback list that the
 * exchange gives us in /keys.
 */
export interface Recoup {
  /**
   * The hash of the denomination public key for which the payback is offered.
   */
  h_denom_pub: string;
}

/**
 * Structure that the exchange gives us in /keys.
 */
export interface ExchangeKeysResponse {
  /**
   * Canonical, public base URL of the exchange.
   */
  base_url: string;

  /**
   * The exchange's currency or asset unit.
   */
  currency: string;

  // Open banking gateway base URL where wallets can
  // initiate wire transfers to withdraw
  // digital cash from this exchange.
  // @since protocol **v30**.
  open_banking_gateway?: string;

  // Instructs wallets to use certain bank-specific
  // language (for buttons) and/or other UI/UX customization
  // for compliance with the rules of that bank.
  // The specific customizations to apply are done on a per-wallet
  // basis as requested by the specific bank. They only
  // apply when it is clear that the wallet is using digital
  // cash from that bank.  This is an advisory option, not
  // all wallets must support all compliance languages.
  // @since protocol **v24**.
  bank_compliance_language?: string;

  /**
   * Type of the asset. "fiat", "crypto", "regional"
   * or "stock".  Wallets should adjust their UI/UX
   * based on this value.
   */
  asset_type: string;

  /**
   * How wallets should render the exchange's currency.
   */
  currency_specification?: CurrencySpecification;

  /**
   * 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[];

  accounts: ExchangeWireAccount[];

  wire_fees: { [methodName: string]: WireFeesJson[] };

  denominations: DenomGroup[];

  // Threshold amounts beyond which wallet should
  // trigger the KYC process of the issuing exchange.
  // Optional option, if not given there is no limit.
  // Currency must match currency.
  wallet_balance_limit_without_kyc?: AmountString[];

  // Array of limits that apply to all accounts.
  // All of the given limits will be hard limits.
  // Wallets and merchants are expected to obey them
  // and not even allow the user to cross them.
  // Since protocol **v21**.
  hard_limits?: AccountLimit[];

  // Array of limits with a soft threshold of zero
  // that apply to all accounts without KYC.
  // Wallets and merchants are expected to trigger
  // a KYC process before attempting any zero-limited
  // operations.
  // Since protocol **v21**.
  zero_limits?: ZeroLimitedOperation[];

  // Absolute cost offset for the STEFAN curve used
  // to (over) approximate fees payable by amount.
  stefan_abs: AmountString;

  // Factor to multiply the logarithm of the amount
  // with to (over) approximate fees payable by amount.
  // Note that the total to be paid is first to be
  // divided by the smallest denomination to obtain
  // the value that the logarithm is to be taken of.
  stefan_log: AmountString;

  // Linear cost factor for the STEFAN curve used
  // to (over) approximate fees payable by amount.
  //
  // Note that this is a scalar, as it is multiplied
  // with the actual amount.
  stefan_lin: number;

  // List of exchanges that this exchange is partnering
  // with to enable wallet-to-wallet transfers.
  wads: any;

  // Compact EdDSA signature (binary-only) over the
  // contatentation of all of the master_sigs (in reverse
  // chronological order by group) in the arrays under
  // "denominations".  Signature of TALER_ExchangeKeySetPS
  exchange_sig: EddsaSignature;

  // Public EdDSA key of the exchange that was used to generate the signature.
  // Should match one of the exchange's signing keys from signkeys.  It is given
  // explicitly as the client might otherwise be confused by clock skew as to
  // which signing key was used for the exchange_sig.
  exchange_pub: EddsaPublicKey;

  // Optional field with a dictionary of (name, object) pairs defining the
  // supported and enabled extensions, such as age_restriction.
  extensions?: { name: ExtensionManifest };

  // Signature by the exchange master key of the SHA-256 hash of the
  // normalized JSON-object of field extensions, if it was set.
  // The signature has purpose TALER_SIGNATURE_MASTER_EXTENSIONS.
  extensions_sig?: EddsaSignature;

  // Set to true if this exchange has KYC enabled and thus
  // requires KYC auth wire transfers prior to a first deposit.
  // @since in protocol **v24**.
  kyc_enabled?: boolean;

  // Shopping URL where users may find shops that accept
  // digital cash issued by this exchange.
  // @since protocol **v21**.
  shopping_url?: string;

  // Small(est?) amount that can likely be transferred to
  // the exchange. Should be the default amount for KYC
  // authentication wire transfers to this exchange.
  // Optional, not present if not known or not configured.
  // @since protocol **v21**.
  tiny_amount?: Amount;
}

export interface ExchangeMeltRequest {
  coin_pub: CoinPublicKeyString;
  confirm_sig: EddsaSignatureString;
  denom_pub_hash: HashCodeString;
  denom_sig: UnblindedDenominationSignature;
  rc: string;
  value_with_fee: AmountString;
  age_commitment_hash?: HashCodeString;
}

/**
 * Docs name: NewMeltRequest
 */
export interface ExchangeMeltRequestV2 {
  // The old coin's public key
  old_coin_pub: CoinPublicKeyString;

  // Hash of the denomination public key of the old coin, to determine total coin value.
  old_denom_pub_h: HashCodeString;

  // The hash of the age-commitment for the old coin. Only present
  // if the denomination has support for age restriction.
  old_age_commitment_h?: string;

  // Signature over the old coin public key by the denomination.
  old_denom_sig: UnblindedDenominationSignature;

  // Amount of the value of the old coin that should be melted as part of
  // this refresh operation, including melting fee.
  value_with_fee: Amount;

  // Array of n new hash codes of denomination public keys
  // for the new coins to order.
  denoms_h: HashCode[];

  // Seed from which the nonces for the n*κ coin candidates are derived
  // from.
  refresh_seed: HashCode;

  // Master seed for the Clause-Schnorr R-value
  // creation. Must match the /blinding-prepare request.
  // Must not have been used in any prior melt request.
  // Must be present if one of the fresh coin's
  // denominations is of type Clause-Schnorr.
  blinding_seed?: string;

  // kappa arrays of n entries for blinded coin candidates,
  // each matching the respective entries in denoms_h.
  //
  // Note: These are essentially the m_i values in the RefreshDerivePQ
  // function.
  coin_evs: CoinEnvelope[][];

  // Signature by the coin over TALER_RefreshMeltCoinAffirmationPS.
  confirm_sig: EddsaSignatureString;
}

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[];
}

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

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

export type DenomGroup =
  | DenomGroupRsa
  | DenomGroupCs
  | DenomGroupRsaAgeRestricted
  | DenomGroupCsAgeRestricted;

export interface DenomGroupCommon {
  // How much are coins of this denomination worth?
  value: AmountString;

  // Fee charged by the exchange for withdrawing a coin of this denomination.
  fee_withdraw: AmountString;

  // Fee charged by the exchange for depositing a coin of this denomination.
  fee_deposit: AmountString;

  // Fee charged by the exchange for refreshing a coin of this denomination.
  fee_refresh: AmountString;

  // Fee charged by the exchange for refunding a coin of this denomination.
  fee_refund: AmountString;
}

export interface DenomCommon {
  // Signature of TALER_DenominationKeyValidityPS.
  master_sig: EddsaSignatureString;

  // When does the denomination key become valid?
  stamp_start: TalerProtocolTimestamp;

  // When is it no longer possible to deposit coins
  // of this denomination?
  stamp_expire_withdraw: TalerProtocolTimestamp;

  // Timestamp indicating by when legal disputes relating to these coins must
  // be settled, as the exchange will afterwards destroy its evidence relating to
  // transactions involving this coin.
  stamp_expire_legal: TalerProtocolTimestamp;

  stamp_expire_deposit: TalerProtocolTimestamp;

  // Set to 'true' if the exchange somehow "lost"
  // the private key. The denomination was not
  // necessarily revoked, but still cannot be used
  // to withdraw coins at this time (theoretically,
  // the private key could be recovered in the
  // future; coins signed with the private key
  // remain valid).
  lost?: boolean;
}

export interface DenomGroupRsa extends DenomGroupCommon {
  cipher: "RSA";

  denoms: ({
    rsa_pub: RsaPublicKeyString;
  } & DenomCommon)[];
}

export interface DenomGroupRsaAgeRestricted extends DenomGroupCommon {
  cipher: "RSA+age_restricted";
  age_mask: AgeMask;

  denoms: ({
    rsa_pub: RsaPublicKeyString;
  } & DenomCommon)[];
}

export interface DenomGroupCs extends DenomGroupCommon {
  cipher: "CS";
  age_mask: AgeMask;

  denoms: ({
    cs_pub: Cs25519Point;
  } & DenomCommon)[];
}

export interface DenomGroupCsAgeRestricted extends DenomGroupCommon {
  cipher: "CS+age_restricted";
  age_mask: AgeMask;

  denoms: ({
    cs_pub: Cs25519Point;
  } & DenomCommon)[];
}

/**
 * Wire fees as announced by the exchange.
 */
export class WireFeesJson {
  /**
   * Cost of a wire transfer.
   */
  wire_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 ExchangeWireAccount {
  // payto:// URI identifying the account and wire method
  payto_uri: string;

  // URI to convert amounts from or to the currency used by
  // this wire account of the exchange. Missing if no
  // conversion is applicable.
  conversion_url?: string;

  // Restrictions that apply to bank accounts that would send
  // funds to the exchange (crediting this exchange bank account).
  // Optional, empty array for unrestricted.
  credit_restrictions: AccountRestriction[];

  // Restrictions that apply to bank accounts that would receive
  // funds from the exchange (debiting this exchange bank account).
  // Optional, empty array for unrestricted.
  debit_restrictions: AccountRestriction[];

  // Signature using the exchange's offline key over
  // a TALER_MasterWireDetailsPS
  // with purpose TALER_SIGNATURE_MASTER_WIRE_DETAILS.
  master_sig: EddsaSignatureString;

  // Display label wallets should use to show this
  // bank account.
  // Since protocol **v19**.
  bank_label?: string;

  priority?: number;
}

export const codecForExchangeWireAccount = (): Codec<ExchangeWireAccount> =>
  buildCodecForObject<ExchangeWireAccount>()
    .property("conversion_url", codecOptional(codecForStringURL()))
    .property("credit_restrictions", codecForList(codecForAny()))
    .property("debit_restrictions", codecForList(codecForAny()))
    .property("master_sig", codecForEddsaSignature())
    .property("payto_uri", codecForString())
    .property("bank_label", codecOptional(codecForString()))
    .property("priority", codecOptional(codecForNumber()))
    .build("WireAccount");

export interface ExchangeRefundRequest {
  // Amount to be refunded, can be a fraction of the
  // coin's total deposit value (including deposit fee);
  // must be larger than the refund fee.
  refund_amount: AmountString;

  // SHA-512 hash of the contact of the merchant with the customer.
  h_contract_terms: HashCodeString;

  // 64-bit transaction id of the refund transaction between merchant and customer.
  rtransaction_id: number;

  // EdDSA public key of the merchant.
  merchant_pub: EddsaPublicKeyString;

  // EdDSA signature of the merchant over a
  // TALER_RefundRequestPS with purpose
  // TALER_SIGNATURE_MERCHANT_REFUND
  // affirming the refund.
  merchant_sig: EddsaPublicKeyString;
}

export interface ExchangeRefundSuccessResponse {
  // The EdDSA :ref:signature (binary-only) with purpose
  // TALER_SIGNATURE_EXCHANGE_CONFIRM_REFUND over
  // a TALER_RecoupRefreshConfirmationPS
  // 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;
}

export const codecForExchangeRefundSuccessResponse =
  (): Codec<ExchangeRefundSuccessResponse> =>
    buildCodecForObject<ExchangeRefundSuccessResponse>()
      .property("exchange_pub", codecForEddsaPublicKey())
      .property("exchange_sig", codecForEddsaSignature())
      .build("ExchangeRefundSuccessResponse");

export type AccountRestriction =
  | RegexAccountRestriction
  | DenyAllAccountRestriction;

export interface DenyAllAccountRestriction {
  type: "deny";
}

// Accounts interacting with this type of account
// restriction must have a payto://-URI matching
// the given regex.
export interface RegexAccountRestriction {
  type: "regex";

  // Regular expression that the payto://-URI of the
  // partner account must follow.  The regular expression
  // should follow posix-egrep, but without support for character
  // classes, GNU extensions, back-references or intervals. See
  // https://www.gnu.org/software/findutils/manual/html_node/find_html/posix_002degrep-regular-expression-syntax.html
  // for a description of the posix-egrep syntax. Applications
  // may support regexes with additional features, but exchanges
  // must not use such regexes.
  payto_regex: string;

  // Hint for a human to understand the restriction
  // (that is hopefully easier to comprehend than the regex itself).
  human_hint: string;

  // Map from IETF BCP 47 language tags to localized
  // human hints.
  human_hint_i18n?: InternationalizedString;
}

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 interface ExchangeLegacyWithdrawRequest {
  denom_pub_hash: HashCodeString;
  reserve_sig: EddsaSignatureString;
  coin_ev: CoinEnvelope;
}

export interface ExchangeLegacyBatchWithdrawRequest {
  planchets: ExchangeLegacyWithdrawRequest[];
}

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 ExchangeRefreshRevealRequestV2 {
  // The commitment from the /melt/ step,
  // i.e. the SHA512 value  of
  //   1. refresh_seed
  //   2. blinding_seed, if applicable, skip otherwise
  //   4. amount with fee (NBO)
  //   5. kappa*n blinded planchet hashes (which include denomination information),
  //      depths first: [0..n)[0..n)[0..n)
  rc: string;

  // The disclosed kappa-1 signatures by the old coin's private key,
  // over Hash1a("Refresh", Cp, r, i), where Cp is the melted coin's public key,
  // r is the public refresh nonce from the metling step and i runs over the
  // _disclosed_ kappa-1 indices.
  signatures: EddsaSignature[];

  // IFF the denomination of the old coin had support for age restriction,
  // the client MUST provide the original age commitment, i. e. the
  // vector of public keys, or omitted otherwise.
  // The size of the vector MUST be the number of age groups as defined by the
  // Exchange in the field .age_groups of the extension age_restriction.
  age_commitment?: Edx25519PublicKeyEnc[];
}

export const codecForRecoup = (): Codec<Recoup> =>
  buildCodecForObject<Recoup>()
    .property("h_denom_pub", codecForString())
    .build("Recoup");

export const codecForExchangeSigningKey = (): Codec<ExchangeSignKeyJson> =>
  buildCodecForObject<ExchangeSignKeyJson>()
    .property("key", codecForEddsaPublicKey())
    .property("master_sig", codecForEddsaSignature())
    .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("history_fee", codecForAmountString())
    .property("account_fee", codecForAmountString())
    .property("purse_fee", codecForAmountString())
    .property("history_expiration", codecForDuration)
    .property("purse_account_limit", codecForNumber())
    .property("purse_timeout", codecForDuration)
    .property("master_sig", codecForEddsaSignature())
    .build("GlobalFees");

// FIXME: Validate properly!
export const codecForNgDenominations: Codec<DenomGroup> = codecForAny();

export const codecForWireFeesJson = (): Codec<WireFeesJson> =>
  buildCodecForObject<WireFeesJson>()
    .property("wire_fee", codecForAmountString())
    .property("closing_fee", codecForAmountString())
    .property("sig", codecForString())
    .property("start_date", codecForTimestamp)
    .property("end_date", codecForTimestamp)
    .build("WireFeesJson");

export const codecForRecoupConfirmation = (): Codec<RecoupConfirmation> =>
  buildCodecForObject<RecoupConfirmation>()
    .property("reserve_pub", codecOptional(codecForString()))
    .property("old_coin_pub", codecOptional(codecForString()))
    .build("RecoupConfirmation");

export const codecForLegacyWithdrawResponse =
  (): Codec<ExchangeLegacyWithdrawResponse> =>
    buildCodecForObject<ExchangeLegacyWithdrawResponse>()
      .property("ev_sig", codecForBlindedDenominationSignature())
      .build("WithdrawResponse");

export class ExchangeLegacyWithdrawResponse {
  ev_sig: BlindedDenominationSignature;
}

export class ExchangeLegacyWithdrawBatchResponse {
  ev_sigs: ExchangeLegacyWithdrawResponse[];
}

/**
 * Docs name: WithdrawResponse
 */
export interface ExchangeWithdrawResponse {
  /**
   * Array of blinded signatures over each coin_evs,
   * in the same order as was given in the request.
   * The blinded signatures affirm the coin's validity
   * after unblinding.
   */
  ev_sigs: BlindedDenominationSignature[];
}

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 const codecForExchangeLegacyWithdrawBatchResponse =
  (): Codec<ExchangeLegacyWithdrawBatchResponse> =>
    buildCodecForObject<ExchangeLegacyWithdrawBatchResponse>()
      .property("ev_sigs", codecForList(codecForLegacyWithdrawResponse()))
      .build("WithdrawBatchResponse");

export const codecForExchangeWithdrawResponse =
  (): Codec<ExchangeWithdrawResponse> =>
    buildCodecForObject<ExchangeWithdrawResponse>()
      .property("ev_sigs", codecForList(codecForBlindedDenominationSignature()))
      .build("WithdrawResponse");

export const codecForExchangeRevealMeltResponseV2 =
  (): Codec<ExchangeWithdrawResponse> =>
    buildCodecForObject<ExchangeWithdrawResponse>()
      .property("ev_sigs", codecForList(codecForBlindedDenominationSignature()))
      .build("ExchangeRevealMeltResponseV2");

export const codecForExchangeMeltResponse = (): Codec<ExchangeMeltResponse> =>
  buildCodecForObject<ExchangeMeltResponse>()
    .property("exchange_pub", codecForEddsaPublicKey())
    .property("exchange_sig", codecForEddsaSignature())
    .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 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 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: UnblindedDenominationSignature;

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

/**
 * Doc name: api-exchange/MergeSuccess.
 */
export interface ExchangeMergeSuccessResponse {
  // Amount merged (excluding deposit fees).
  merge_amount: Amount;

  // Time at which the merge came into effect.
  // Maximum of the "payment_timestamp" and the
  // "merge_timestamp".
  exchange_timestamp: Timestamp;

  // EdDSA signature of the exchange affirming the merge of
  // purpose TALER_SIGNATURE_PURSE_MERGE_SUCCESS
  // over TALER_PurseMergeSuccessSignaturePS.
  // Signs over the above and the account public key.
  exchange_sig: EddsaSignatureString;

  // public key used to create the signature.
  exchange_pub: EddsaPublicKeyString;
}

export const codecForExchangeMergeSuccessResponse =
  (): Codec<ExchangeMergeSuccessResponse> =>
    buildCodecForObject<ExchangeMergeSuccessResponse>()
      .property("merge_amount", codecForAmountString())
      .property("exchange_timestamp", codecForTimestamp)
      .property("exchange_sig", codecForEddsaSignature())
      .property("exchange_pub", codecForEddsaPublicKey())
      .build("ExchangeMergeSuccessResponse");

/**
 * Doc name: api-exchange/MergeConflict
 */
export interface ExchangeMergeConflictResponse {
  // Client-side timestamp of when the merge request was made.
  merge_timestamp: Timestamp;

  // 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;

  // Base URL of the exchange receiving the payment, only present
  // if the exchange hosting the reserve is not this exchange.
  partner_url?: string;

  // Public key of the reserve that the purse was merged into.
  reserve_pub: EddsaPublicKeyString;
}

export const codecForExchangeMergeConflictResponse =
  (): Codec<ExchangeMergeConflictResponse> =>
    buildCodecForObject<ExchangeMergeConflictResponse>()
      .property("merge_timestamp", codecForTimestamp)
      .property("merge_sig", codecForEddsaSignature())
      .property("reserve_pub", codecForEddsaPublicKey())
      .property("partner_url", codecOptional(codecForString()))
      .build("ExchangeMergeConflictResponse");

/**
 * Contract terms between two wallets (as opposed to a merchant and wallet).
 */
export interface PeerContractTerms {
  amount: AmountString;
  summary: string;
  icon_id?: 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[];
}

/**
 * @deprecated batch deposit should be used.
 */
export interface ExchangeDepositRequest {
  // Amount to be deposited, can be a fraction of the
  // coin's total value.
  contribution: AmountString;

  // The merchant's account details.
  // In case of an auction policy, it refers to the seller.
  merchant_payto_uri: string;

  // The salt is used to hide the payto_uri from customers
  // when computing the h_wire of the merchant.
  wire_salt: string;

  // SHA-512 hash of the contract of the merchant with the customer.  Further
  // details are never disclosed to the exchange.
  h_contract_terms: HashCodeString;

  // 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: UnblindedDenominationSignature;

  // Timestamp when the contract was finalized.
  timestamp: TalerProtocolTimestamp;

  // Indicative time by which the exchange undertakes to transfer the funds to
  // the merchant, in case of successful payment. A wire transfer deadline of 'never'
  // is not allowed.
  wire_transfer_deadline: TalerProtocolTimestamp;

  // EdDSA public key of the merchant, so that the client can identify the
  // merchant for refund requests.
  //
  // THIS FIELD WILL BE DEPRECATED, once the refund mechanism becomes a
  // policy via extension.
  merchant_pub: EddsaPublicKeyString;

  // Date until which the merchant can issue a refund to the customer via the
  // exchange, to be omitted if refunds are not allowed.
  //
  // THIS FIELD WILL BE DEPRECATED, once the refund mechanism becomes a
  // policy via extension.
  refund_deadline?: TalerProtocolTimestamp;

  // CAVEAT: THIS IS WORK IN PROGRESS
  // (Optional) policy for the deposit.
  // This might be a refund, auction or escrow policy.
  //
  // Note that support for policies is an optional feature of the exchange.
  // Optional features are so called "extensions" in Taler. The exchange
  // provides the list of supported extensions, including policies, in the
  // ExtensionsManifestsResponse response to the /keys endpoint.
  policy?: any;

  // Signature over TALER_DepositRequestPS, made by the customer with the
  // coin's private key.
  coin_sig: EddsaSignatureString;

  h_age_commitment?: string;
}

export type TrackTransaction =
  | ({ type: "accepted" } & TrackTransactionAccepted)
  | ({ type: "wired" } & TrackTransactionWired);

export interface BatchDepositSuccess {
  // 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;

  // `Public EdDSA key of the exchange <sign-key-pub>` 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;

  // Array of deposit confirmation signatures from the exchange
  // Entries must be in the same order the coins were given
  // in the batch deposit request.
  exchange_sig: EddsaSignatureString;
}

export const codecForBatchDepositSuccess = (): Codec<BatchDepositSuccess> =>
  buildCodecForObject<BatchDepositSuccess>()
    .property("exchange_pub", codecForEddsaPublicKey())
    .property("exchange_sig", codecForEddsaSignature())
    .property("exchange_timestamp", codecForTimestamp)
    .property("transaction_base_url", codecOptional(codecForString()))
    .build("BatchDepositSuccess");

export interface ExchangePurseStatus {
  // Total amount deposited into the purse so far.
  // If 'total_deposit_amount' minus 'deposit_fees'
  // exceeds 'merge_value_after_fees', and a
  // 'merge_request' exists for the purse, then the
  // purse will (have been) merged with the account.
  balance: AmountString;

  // Time of the merge, missing if "never".
  deposit_timestamp?: TalerProtocolTimestamp;

  // Time of the deposits being complete, missing if "never".
  // Note that this time may not be "stable": once sufficient
  // deposits have been made, is "now" before the purse
  // expiration, and otherwise set to the purse expiration.
  // However, this should also not be relied upon. The key
  // property is that it is either "never" or in the past.
  merge_timestamp?: TalerProtocolTimestamp;
}

export const codecForExchangePurseStatus = (): Codec<ExchangePurseStatus> =>
  buildCodecForObject<ExchangePurseStatus>()
    .property("balance", codecForAmountString())
    .property("deposit_timestamp", codecOptional(codecForTimestamp))
    .property("merge_timestamp", codecOptional(codecForTimestamp))
    .build("ExchangePurseStatus");

export interface TrackTransactionWired {
  // Raw wire transfer identifier of the deposit.
  wtid: Base32String;

  // When was the wire transfer given to the bank.
  execution_time: TalerProtocolTimestamp;

  // The contribution of this coin to the total (without fees)
  coin_contribution: AmountString;

  // Binary-only Signature_ with purpose TALER_SIGNATURE_EXCHANGE_CONFIRM_WIRE
  // over a TALER_ConfirmWirePS
  // whereby the exchange affirms the successful wire transfer.
  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;
}

export const codecForTackTransactionWired = (): Codec<TrackTransactionWired> =>
  buildCodecForObject<TrackTransactionWired>()
    .property("wtid", codecForString())
    .property("execution_time", codecForTimestamp)
    .property("coin_contribution", codecForAmountString())
    .property("exchange_sig", codecForEddsaSignature())
    .property("exchange_pub", codecForEddsaPublicKey())
    .build("TackTransactionWired");

export interface TrackTransactionAccepted {
  // Legitimization target that the merchant should
  // use to check for its KYC status using
  // the /kyc-check/$REQUIREMENT_ROW/... endpoint.
  // Optional, not present if the deposit has not
  // yet been aggregated to the point that a KYC
  // need has been evaluated.
  requirement_row?: number;

  // True if the KYC check for the merchant has been
  // satisfied.  False does not mean that KYC
  // is strictly needed, unless also a
  // legitimization_uuid is provided.
  kyc_ok: boolean;

  // Time by which the exchange currently thinks the deposit will be executed.
  // Actual execution may be later if the KYC check is not satisfied by then.
  execution_time: TalerProtocolTimestamp;

  // Public key associated with the account. The client must sign
  // the initial request for the KYC status using the corresponding
  // private key.  Will be the merchant (instance) public key.
  //
  // Absent if no public key is currently associated
  // with the account and the client MUST thus first
  // credit the exchange via an inbound wire transfer
  // to associate a public key with the debited account.
  // @since protocol **v20**.
  account_pub: EddsaPublicKeyString | undefined;
}

export const codecForTackTransactionAccepted =
  (): Codec<TrackTransactionAccepted> =>
    buildCodecForObject<TrackTransactionAccepted>()
      .property("requirement_row", codecOptional(codecForNumber()))
      .property("kyc_ok", codecForBoolean())
      .property("execution_time", codecForTimestamp)
      .property("account_pub", codecOptional(codecForEddsaPublicKey()))
      .build("TackTransactionAccepted");

export const codecForPeerContractTerms = (): Codec<PeerContractTerms> =>
  buildCodecForObject<PeerContractTerms>()
    .property("summary", codecForString())
    .property("amount", codecForAmountString())
    .property("purse_expiration", codecForTimestamp)
    .property("icon_id", codecOptional(codecForString()))
    .build("PeerContractTerms");

export interface ExchangeBatchDepositRequest {
  // The merchant's account details.
  merchant_payto_uri: string;

  // Merchant's signature over the h_contract_terms.
  // @since v22
  merchant_sig: EddsaSignatureString;

  // The salt is used to hide the ``payto_uri`` from customers
  // when computing the ``h_wire`` of the merchant.
  wire_salt: WireSalt;

  // SHA-512 hash of the contract of the merchant with the customer.  Further
  // details are never disclosed to the exchange.
  h_contract_terms: HashCodeString;

  // The list of coins that are going to be deposited with this Request.
  coins: BatchDepositRequestCoin[];

  // Timestamp when the contract was finalized.
  timestamp: TalerProtocolTimestamp;

  // Indicative time by which the exchange undertakes to transfer the funds to
  // the merchant, in case of successful payment. A wire transfer deadline of 'never'
  // is not allowed.
  wire_transfer_deadline: TalerProtocolTimestamp;

  // EdDSA `public key of the merchant <merchant-pub>`, so that the client can identify the
  // merchant for refund requests.
  merchant_pub: EddsaPublicKeyString;

  // Date until which the merchant can issue a refund to the customer via the
  // exchange, to be omitted if refunds are not allowed.
  //
  // THIS FIELD WILL BE DEPRECATED, once the refund mechanism becomes a
  // policy via extension.
  refund_deadline?: TalerProtocolTimestamp;

  // CAVEAT: THIS IS WORK IN PROGRESS
  // (Optional) policy for the batch-deposit.
  // This might be a refund, auction or escrow policy.
  policy?: any;
}

export interface BatchDepositRequestCoin {
  // EdDSA public key of the coin being deposited.
  coin_pub: EddsaPublicKeyString;

  // 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: UnblindedDenominationSignature;

  // Amount to be deposited, can be a fraction of the
  // coin's total value.
  contribution: AmountString;

  // Signature over `TALER_DepositRequestPS`, made by the customer with the
  // `coin's private key <coin-priv>`.
  coin_sig: EddsaSignatureString;

  h_age_commitment?: string;
}

export interface AvailableMeasureSummary {
  // Available original measures that can be
  // triggered directly by default rules.
  roots: { [measure_name: string]: MeasureInformation };

  // Available AML programs.
  programs: { [prog_name: string]: AmlProgramRequirement };

  // Available KYC checks.
  checks: { [check_name: string]: KycCheckInformation };

  // Default KYC rules. This is the set of KYC rules that
  // applies by default to new "accounts".  Note that some
  // rules only apply to wallets, while others only apply to
  // bank accounts. The returned array is the union of all
  // possible rules, applications should consider the
  // operation_type to filter for rules that actually
  // apply to a specific situation.
  // @since protocol **v28**.
  default_rules: KycRule[];
}

export interface MeasureInformation {
  // Name of a KYC check.
  check_name: string;

  // Name of an AML program.
  prog_name?: string;

  // Context for the check. Optional.
  context?: Object;

  // Operation that this measure relates to.
  // NULL if unknown. Useful as a hint to the
  // user if there are many (voluntary) measures
  // and some related to unlocking certain operations.
  // (and due to zero-amount thresholds, no measure
  // was actually specifically triggered).
  //
  // Must be one of "WITHDRAW", "DEPOSIT",
  // (p2p) "MERGE", (wallet) "BALANCE",
  // (reserve) "CLOSE", "AGGREGATE",
  // "TRANSACTION" or "REFUND".
  // New in protocol **v21**.
  operation_type?: LimitOperationType;

  // Can this measure be undertaken voluntarily?
  // Optional, default is false.
  // Since protocol **vATTEST**.
  voluntary?: boolean;
}

export interface AmlProgramRequirement {
  // Description of what the AML program does.
  description: string;

  // List of required field names in the context to run this
  // AML program. SPA must check that the AML staff is providing
  // adequate CONTEXT when defining a measure using this program.
  context: string[];

  // List of required attribute names in the
  // input of this AML program.  These attributes
  // are the minimum that the check must produce
  // (it may produce more).
  inputs: string[];
}

export interface KycCheckInformation {
  // Description of the KYC check.  Should be shown
  // to the AML staff but will also be shown to the
  // client when they initiate the check in the KYC SPA.
  description: string;

  // Map from IETF BCP 47 language tags to localized
  // description texts.
  description_i18n?: { [lang_tag: string]: string };

  // Names of the fields that the CONTEXT must provide
  // as inputs to this check.
  // SPA must check that the AML staff is providing
  // adequate CONTEXT when defining a measure using
  // this check.
  requires: string[];

  // Names of the attributes the check will output.
  // SPA must check that the outputs match the
  // required inputs when combining a KYC check
  // with an AML program into a measure.
  outputs: string[];

  // Name of a root measure taken when this check fails.
  fallback: string;
}

export interface AmlDecisionDetails {
  // Array of AML decisions made for this account. Possibly
  // contains only the most recent decision if "history" was
  // not set to 'true'.
  aml_history: AmlDecisionDetail[];

  // Array of KYC attributes obtained for this account.
  kyc_attributes: KycDetail[];
}

export interface AmlDecisionDetail {
  // What was the justification given?
  justification: string;

  // What is the new AML state.
  new_state: Integer;

  // When was this decision made?
  decision_time: Timestamp;

  // What is the new AML decision threshold (in monthly transaction volume)?
  new_threshold: AmountString;

  // Who made the decision?
  decider_pub: AmlOfficerPublicKeyP;
}

export interface KycDetail {
  // Name of the configuration section that specifies the provider
  // which was used to collect the KYC details
  provider_section: string;

  // The collected KYC data.  NULL if the attribute data could not
  // be decrypted (internal error of the exchange, likely the
  // attribute key was changed).
  attributes?: Object;

  // Time when the KYC data was collected
  collection_time: Timestamp;

  // Time when the validity of the KYC data will expire
  expiration_time: Timestamp;
}

export type AmlDecisionRequestWithoutSignature = Omit<
  AmlDecisionRequest,
  "officer_sig"
>;

export enum AmlSpaDialect {
  TOPS = "tops",
  GLS = "gls",
  TESTING = "testing",
}

export interface ExchangeVersionResponse {
  // libtool-style representation of the Exchange protocol version, see
  // https://www.gnu.org/software/libtool/manual/html_node/Versioning.html#Versioning
  // The format is "current:revision:age".
  version: string;

  // Name of the protocol.
  name: "taler-exchange";

  // URN of the implementation (needed to interpret 'revision' in version).
  // @since v18, may become mandatory in the future.
  implementation?: string;

  // Currency supported by this exchange, given
  // as a currency code ("USD" or "EUR").
  currency: string;

  // How wallets should render this currency.
  currency_specification: CurrencySpecification;

  // Names of supported KYC requirements.
  supported_kyc_requirements: string[];

  // Bank-specific dialect for the AML SPA. Determines
  // which set of forms is available as well as statistics
  // to show and sets of properties/events to trigger in
  // AML decisions.
  // @since protocol **v24**.
  aml_spa_dialect?: AmlSpaDialect;

  // Open banking gateway base URL where wallets can
  // initiate wire transfers to withdraw
  // digital cash from this exchange.
  // @since protocol **v30**.
  open_banking_gateway?: string;
}

export interface WalletKycRequest {
  // Balance threshold (not necessarily exact balance)
  // to be crossed by the wallet that (may) trigger
  // additional KYC requirements.
  balance: AmountString;

  // EdDSA signature of the wallet affirming the
  // request, must be of purpose
  // TALER_SIGNATURE_WALLET_ACCOUNT_SETUP
  reserve_sig: EddsaSignatureString;

  // long-term wallet reserve-account
  // public key used to create the signature.
  reserve_pub: EddsaPublicKeyString;
}

/**
 * Doc name: api-exchange/WalletKycCheckResponse
 */
export interface WalletKycCheckResponse {
  // Next balance limit above which a KYC check
  // may be required. Optional, not given if no
  // threshold exists (assume infinity).
  next_threshold?: AmountString;

  // When does the current set of AML/KYC rules
  // expire and the wallet needs to check again
  // for updated thresholds.
  expiration_time: Timestamp;
}

// Implemented in this style since exchange
// protocol **v20**.
export interface LegitimizationNeededResponse {
  // Numeric error code unique to the condition.
  // Should always be TALER_EC_EXCHANGE_GENERIC_KYC_REQUIRED.
  code: number;

  // Human-readable description of the error, i.e. "missing parameter",
  // "commitment violation", ...  Should give a human-readable hint
  // about the error's nature. Optional, may change without notice!
  hint?: string;

  // Hash of the payto:// account URI for which KYC
  // is required.
  // The account holder can uses the /kyc-check/$H_PAYTO
  // endpoint to check the KYC status or initiate the KYC process.
  h_payto: PaytoHash;

  // Public key associated with the account. The client must sign
  // the initial request for the KYC status using the corresponding
  // private key.  Will be either a reserve public key or a merchant
  // (instance) public key.
  //
  // Absent if no public key is currently associated
  // with the account and the client MUST thus first
  // credit the exchange via an inbound wire transfer
  // to associate a public key with the debited account.
  account_pub?: EddsaPublicKeyString;

  // Identifies a set of measures that were triggered and that are
  // now preventing this operation from proceeding.  Gives developers
  // a starting point for understanding why the transaction was
  // blocked and how to lift it.
  // Can be zero (which means there is no requirement row),
  // especially if bad_kyc_auth is set.
  requirement_row: Integer;

  // True if the operation was denied because the
  // KYC auth key does not match the merchant public
  // key.  In this case, a KYC auth wire transfer
  // with the merchant public key must be performed
  // first.
  // Since exchange protocol **v21**.
  bad_kyc_auth?: boolean;
}

export interface AccountKycStatus {
  // Current AML state for the target account.  True if
  // operations are not happening due to staff processing
  // paperwork *or* due to legal requirements (so the
  // client cannot do anything but wait).
  //
  // Note that not every AML staff action may be legally
  // exposed to the client, so this is merely a hint that
  // a client should be told that AML staff is currently
  // reviewing the account.  AML staff *may* review
  // accounts without this flag being set!
  aml_review: boolean;

  // Monotonically increasing number identifying the decision.
  // 0 if no decision was taken for this account. Useful for
  // long-polling via min_rule to long-poll for any change
  // to the rules or limits.
  rule_gen: Integer;

  // Access token needed to construct the /kyc-spa/
  // URL that the user should open in a browser to
  // proceed with the KYC process (optional if the status
  // type is 200 Ok, mandatory if the HTTP status
  // is 202 Accepted).
  access_token: AccessToken;

  // Array with limitations that currently apply to this
  // account and that may be increased or lifted if the
  // KYC check is passed.
  // Note that additional limits *may* exist and not be
  // communicated to the client. If such limits are
  // reached, this *may* be indicated by the account
  // going into aml_review state. However, it is
  // also possible that the exchange may legally have
  // to deny operations without being allowed to provide
  // any justification.
  // The limits should be used by the client to
  // possibly structure their operations (e.g. withdraw
  // what is possible below the limit, ask the user to
  // pass KYC checks or withdraw the rest after the time
  // limit is passed, warn the user to not withdraw too
  // much or even prevent the user from generating a
  // request that would cause it to exceed hard limits).
  limits?: AccountLimit[];
}

export enum LimitOperationType {
  withdraw = "WITHDRAW",
  deposit = "DEPOSIT",
  merge = "MERGE",
  aggregate = "AGGREGATE",
  balance = "BALANCE",
  refund = "REFUND",
  close = "CLOSE",
  transaction = "TRANSACTION",
}

export interface AccountLimit {
  // Operation that is limited.
  operation_type: LimitOperationType;

  // Timeframe during which the limit applies.
  timeframe: RelativeTime;

  // Maximum amount allowed during the given timeframe.
  // Zero if the operation is simply forbidden.
  threshold: AmountString;

  // True if this is a soft limit that could be raised
  // by passing KYC checks.  Clients *may* deliberately
  // try to cross limits and trigger measures resulting
  // in 451 responses to begin KYC processes.
  // Clients that are aware of hard limits *should*
  // inform users about the hard limit and prevent flows
  // in the UI that would cause violations of hard limits.
  // Made optional in **v21** with a default of 'false' if missing.
  soft_limit?: boolean;

  // FIXME: undocumented
  rule_name?: string;
}

export interface KycProcessClientInformation {
  // Array of requirements.
  requirements: KycRequirementInformation[];

  // True if the client is expected to eventually satisfy all requirements.
  // Default (if missing) is false.
  is_and_combinator?: boolean;

  // List of available voluntary checks the client could pay for.
  // Since **vATTEST**.
  voluntary_measures?: KycRequirementInformation[];
}

export type KycProcessClientInformationWithEtag =
  KycProcessClientInformation & { etag: string | undefined };

declare const opaque_brand: unique symbol;

declare const opaque_kycReq: unique symbol;

export type KycRequirementInformationId = string & {
  [opaque_brand]?: typeof opaque_kycReq;
};
declare const opaque_formId: unique symbol;
export type KycBuiltInFromId = string & { [opaque_formId]: true };

export interface KycRequirementInformation {
  // Which form should be used? Common values include "INFO"
  // (to just show the descriptions but allow no action),
  // "LINK" (to enable the user to obtain a link via
  // /kyc-start/) or any built-in form name supported
  // by the SPA.
  form: "LINK" | "INFO" | KycBuiltInFromId;

  // English description of the requirement.
  description: string;

  // Object with arbitrary additional context, completely depends on
  // the specific form.
  context?: Object;

  // Map from IETF BCP 47 language tags to localized
  // description texts.
  description_i18n?: { [lang_tag: string]: string };

  // ID of the requirement, useful to construct the
  // /kyc-upload/$ID or /kyc-start/$ID endpoint URLs.
  // Present if and only if "form" is not "INFO".  The
  // $ID value may itself contain / or ? and
  // basically encode any URL path (and optional arguments).
  id?: KycRequirementInformationId;
}

export type ExchangeKycUploadFormRequest = {
  // Which form is being submitted. Further details depend on the form.
  // @since protocol v26.
  // form_id: string;
  [TalerFormAttributes.FORM_ID]: string;
  [TalerFormAttributes.FORM_VERSION]: number;
};

// Since **vATTEST**.
export interface KycCheckPublicInformation {
  // English description of the check.
  description: string;

  // Map from IETF BCP 47 language tags to localized
  // description texts.
  description_i18n?: { [lang_tag: string]: string };

  // FIXME: is the above in any way sufficient
  // to begin the check? Do we not need at least
  // something more??!?
}

export interface AmlStatisticsResponse {
  statistics: EventCounter[];
}
export interface EventCounter {
  // Name of the statistic that is being returned.
  name: string;

  // Number of events of the specified type in
  // the given range.
  counter: Integer;
}

export interface AmlDecisionsResponse {
  // Array of AML decisions matching the query.
  records: AmlDecision[];
}

export interface LegitimizationMeasuresList {
  // Legitimization measures.
  measures: LegitimizationMeasureDetails[];
}

export interface LegitimizationMeasures {
  // Array of legitimization measures that
  // are to be applied.
  measures: MeasureInformation[];

  // True if the client is expected to eventually satisfy all requirements.
  // Default (if missing) is false.
  is_and_combinator?: boolean;

  // True if the requested operation is categorically forbidden.
  // The measures array will be empty in this case.
  verboten: boolean;
}

export interface LegitimizationMeasureDetails {
  // Hash of the normalized payto:// URI of the account the
  // measure applies to.
  h_payto: HashCode;

  // Row of the measure in the exchange database.
  rowid: Integer;

  // When was the measure started?
  start_time: Timestamp;

  // The the actual measures.
  measures: LegitimizationMeasures;

  // Was this measure finished by the customer?
  is_finished: boolean;
}

export interface AmlDecision {
  // Which payto-address is this record about.
  // Identifies a GNU Taler wallet or an affected bank account.
  h_payto: PaytoHash;

  full_payto?: string;

  // True if the underlying payto://-URI is for a wallet
  // Since protocol **v25**.
  is_wallet: boolean;

  // Row ID of the record.  Used to filter by offset.
  rowid: Integer;

  // Justification for the decision. NULL if none
  // is available.
  justification?: string;

  // When was the decision made?
  decision_time: Timestamp;

  // Free-form properties about the account.
  // Can be used to store properties such as PEP,
  // risk category, type of business, hits on
  // sanctions lists, etc.
  properties?: AccountProperties;

  // What are the new rules?
  limits: LegitimizationRuleSet;

  // True if the account is under investigation by AML staff
  // after this decision.
  to_investigate: boolean;

  // True if this is the active decision for the
  // account.
  is_active: boolean;
}

// All fields in this object are optional. The actual
// properties collected depend fully on the discretion
// of the exchange operator;
// however, some common fields are standardized
// and thus described here.
export interface AccountProperties {
  // True if this is a politically exposed account.
  // Rules for classifying accounts as politically
  // exposed are country-dependent.
  pep?: boolean;

  // True if this is a sanctioned account.
  // Rules for classifying accounts as sanctioned
  // are country-dependent.
  sanctioned?: boolean;

  // True if this is a high-risk account.
  // Rules for classifying accounts as at-risk
  // are exchange operator-dependent.
  high_risk?: boolean;

  // Business domain of the account owner.
  // The list of possible business domains is
  // operator- or country-dependent.
  business_domain?: string;

  // Is the client's account currently frozen?
  is_frozen?: boolean;

  // Was the client's account reported to the authorities?
  was_reported?: boolean;

  /**
   * Additional free-form properties.
   */
  [x: string]: any;
}

export interface LegitimizationRuleSet {
  // When does this set of rules expire and
  // we automatically transition to the successor
  // measure?
  expiration_time: Timestamp;

  // Name of the measure to apply when the expiration time is
  // reached.  If not set, we refer to the default
  // set of rules (and the default account state).
  successor_measure?: string;

  // Legitimization rules that are to be applied
  // to this account.
  rules: KycRule[];

  // Custom measures that KYC rules and the
  // successor_measure may refer to.
  custom_measures: { [measure_name: string]: MeasureInformation };
}

export interface AmlDecisionRequest {
  // Human-readable justification for the decision.
  justification: string;

  // Which payto-address is the decision about?
  // Identifies a GNU Taler wallet or an affected bank account.
  h_payto: PaytoHash;

  // Payto address of the account the decision is about.
  // Optional. Must be given if the account is not yet
  // known to the exchange. If given, must match h_payto.
  // New since protocol **v21**.
  payto_uri?: string;

  // What are the new rules?
  // New since protocol **v20**.
  new_rules: LegitimizationRuleSet;

  // What are the new account properties?
  // New since protocol **v20**.
  properties: AccountProperties;

  // Array of AML/KYC events to trigger for statistics.
  // Note that this information is not covered by the signature
  // (which is OK as events are just for statistics).
  // New since protocol **v24**.
  events?: string[];

  // Space-separated list of measures to trigger
  // immediately on the account.
  // Prefixed with a "+" to indicate that the
  // measures should be ANDed.
  // Should typically be used to give the user some
  // information or request additional information.
  // New since protocol **v21**.
  new_measures?: string;

  // True if the account should remain under investigation by AML staff.
  // New since protocol **v20**.
  keep_investigating: boolean;

  /**
   * Signature by the AML officer over a TALER_AmlDecisionPS.
   * Must have purpose TALER_SIGNATURE_MASTER_AML_KEY.
   */
  officer_sig: EddsaSignatureString;

  /**
   * When was the decision made?
   */
  decision_time: Timestamp;

  // KYC attributes uploaded by the AML officer
  // The object *must* contain high-entropy salt,
  // as the hash of the attributes will be
  // stored in plain text.
  attributes?: Object;

  // Expiration timestamp of the attributes.
  // Mandatory if attributes are present.
  attributes_expiration?: Timestamp;
}

export interface KycRule {
  // Type of operation to which the rule applies.
  operation_type: LimitOperationType;

  // The measures will be taken if the given
  // threshold is crossed over the given timeframe.
  threshold: AmountString;

  // Over which duration should the threshold be
  // computed.  All amounts of the respective
  // operation_type will be added up for this
  // duration and the sum compared to the threshold.
  timeframe: RelativeTime;

  // Array of names of measures to apply.
  // Names listed can be original measures or
  // custom measures from the AmlOutcome.
  // A special measure "verboten" is used if the
  // threshold may never be crossed.
  measures: string[];

  // If multiple rules apply to the same account
  // at the same time, the number with the highest
  // rule determines which set of measures will
  // be activated and thus become visible for the
  // user.
  display_priority: Integer;

  // True if the rule (specifically, operation_type,
  // threshold, timeframe) and the general nature of
  // the measures (verboten or approval required)
  // should be exposed to the client.
  // Defaults to "false" if not set.
  exposed?: boolean;

  // True if all the measures will eventually need to
  // be satisfied, false if any of the measures should
  // do.  Primarily used by the SPA to indicate how
  // the measures apply when showing them to the user;
  // in the end, AML programs will decide after each
  // measure what to do next.
  // Default (if missing) is false.
  is_and_combinator?: boolean;

  // Name of the configuration section this rule
  // originates from. Not available for all rules.
  // Primarily informational, but also useful to
  // explicitly manipulate rules by-name in AML programs.
  rule_name?: string;
}

export interface KycAttributes {
  // Matching KYC attribute history of the account.
  details: KycAttributeCollectionEvent[];
}
export interface KycAttributeCollectionEvent {
  // Row ID of the record.  Used to filter by offset.
  rowid: Integer;

  // Name of the provider
  // which was used to collect the attributes. NULL if they were
  // just uploaded via a form by the account owner.
  provider_name?: string;

  // The collected KYC data.  NULL if the attribute data could not
  // be decrypted (internal error of the exchange, likely the
  // attribute key was changed).
  attributes?: { [x: string]: unknown };

  // Time when the KYC data was collected
  collection_time: Timestamp;
}

export enum AmlState {
  normal = 0,
  pending = 1,
  frozen = 2,
}
type Float = number;

export interface ZeroLimitedOperation {
  // Operation that is limited to an amount of
  // zero until the client has passed some KYC check.
  // Must be one of "WITHDRAW", "DEPOSIT",
  // (p2p) "MERGE", (wallet) "BALANCE",
  // (reserve) "CLOSE", "AGGREGATE",
  // "TRANSACTION" or "REFUND".
  operation_type: LimitOperationType;
}

interface ExtensionManifest {
  // The criticality of the extension MUST be provided.  It has the same
  // semantics as "critical" has for extensions in X.509:
  // - if "true", the client must "understand" the extension before
  //   proceeding,
  // - if "false", clients can safely skip extensions they do not
  //   understand.
  // (see https://datatracker.ietf.org/doc/html/rfc5280#section-4.2)
  critical: boolean;

  // The version information MUST be provided in Taler's protocol version
  // ranges notation, see
  // https://docs.taler.net/core/api-common.html#protocol-version-ranges
  version: LibtoolVersionString;

  // Optional configuration object, defined by the feature itself
  config?: object;
}

export interface GlobalFees {
  // What date (inclusive) does these fees go into effect?
  start_date: Timestamp;

  // What date (exclusive) does this fees stop going into effect?
  end_date: Timestamp;

  // 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: RelativeTime;

  // Non-negative number of concurrent purses that any
  // account holder is allowed to create without having
  // to pay the purse_fee.
  purse_account_limit: Integer;

  // 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: RelativeTime;

  // Signature of TALER_GlobalFeesPS.
  master_sig: EddsaSignatureString;
}

export interface AggregateTransferFee {
  // Per transfer wire transfer fee.
  wire_fee: AmountString;

  // Per transfer closing fee.
  closing_fee: AmountString;

  // What date (inclusive) does this fee go into effect?
  // The different fees must cover the full time period in which
  // any of the denomination keys are valid without overlap.
  start_date: Timestamp;

  // What date (exclusive) does this fee stop going into effect?
  // The different fees must cover the full time period in which
  // any of the denomination keys are valid without overlap.
  end_date: Timestamp;

  // Signature of TALER_MasterWireFeePS with
  // purpose TALER_SIGNATURE_MASTER_WIRE_FEES.
  sig: EddsaSignatureString;
}

// Binary representation of the age groups.
// The bits set in the mask mark the edges at the beginning of a next age
// group.  F.e. for the age groups
//     0-7, 8-9, 10-11, 12-13, 14-15, 16-17, 18-21, 21-*
// the following bits are set:
//
//   31     24        16        8         0
//   |      |         |         |         |
//   oooooooo  oo1oo1o1  o1o1o1o1  ooooooo1
//
// A value of 0 means that the exchange does not support the extension for
// age-restriction.
type AgeMask = Integer;

type DenominationKey = RsaDenominationKey | CSDenominationKey;

interface RsaDenominationKey {
  cipher: "RSA";

  // 32-bit age mask.
  age_mask: Integer;

  // RSA public key
  rsa_public_key: RsaPublicKey;
}

interface CSDenominationKey {
  cipher: "CS";

  // 32-bit age mask.
  age_mask: Integer;

  // Public key of the denomination.
  cs_public_key: Cs25519Point;
}

export const codecForAmlSpaDialect = codecForEither(
  codecForConstString(AmlSpaDialect.GLS),
  codecForConstString(AmlSpaDialect.TOPS),
  codecForConstString(AmlSpaDialect.TESTING),
);

export const codecForExchangeConfig = (): Codec<ExchangeVersionResponse> =>
  buildCodecForObject<ExchangeVersionResponse>()
    .property("version", codecForString())
    .property("name", codecForConstString("taler-exchange"))
    .property("implementation", codecOptional(codecForURN()))
    .property("currency", codecForString())
    .property("currency_specification", codecForCurrencySpecificiation())
    .property("supported_kyc_requirements", codecForList(codecForString()))
    .property("aml_spa_dialect", codecOptional(codecForAmlSpaDialect))
    .deprecatedProperty("shopping_url")
    .deprecatedProperty("wallet_balance_limit_without_kyc")
    .build("TalerExchangeApi.ExchangeVersionResponse");

// FIXME: complete the codec to check for valid exchange response
export const codecForExchangeKeysResponse = (): Codec<ExchangeKeysResponse> =>
  buildCodecForObject<ExchangeKeysResponse>()
    .property("version", codecForString())
    .property("base_url", codecForURLString())
    .property("currency", codecForString())
    .property("accounts", codecForAny())
    .property("asset_type", codecForAny())
    .property("auditors", codecForAny())
    .property("currency_specification", codecForAny())
    .property("zero_limits", codecForAny())
    .property("hard_limits", codecForAny())
    .property("denominations", codecForAny())
    .property("exchange_pub", codecForAny())
    .property("exchange_sig", codecForAny())
    .property("extensions", codecForAny())
    .property("extensions_sig", codecForAny())
    .property("global_fees", codecForAny())
    .property("list_issue_date", codecForAny())
    .property("master_public_key", codecForAny())
    .property("recoup", codecForAny())
    .property("reserve_closing_delay", codecForAny())
    .property("signkeys", codecForAny())
    .property("stefan_abs", codecForAny())
    .property("stefan_lin", codecForAny())
    .property("stefan_log", codecForAny())
    .property("wads", codecForAny())
    .property("wallet_balance_limit_without_kyc", codecForAny())
    .property("wire_fees", codecForAny())
    .property("kyc_enabled", codecOptional(codecForBoolean()))
    .property("shopping_url", codecOptional(codecForString()))
    .property("tiny_amount", codecOptional(codecForAmountString()))
    .property("bank_compliance_language", codecOptional(codecForString()))
    .property("open_banking_gateway", codecOptional(codecForURLString()))
    .deprecatedProperty("rewards_allowed")
    .build("TalerExchangeApi.ExchangeKeysResponse");

export const codecForAmlStatisticsResponse = (): Codec<AmlStatisticsResponse> =>
  buildCodecForObject<AmlStatisticsResponse>()
    .property("statistics", codecForList(codecForEventCounter()))
    .build("TalerExchangeApi.AmlStatisticsResponse");

export const codecForEventCounter = (): Codec<EventCounter> =>
  buildCodecForObject<EventCounter>()
    .property("name", codecForString())
    .property("counter", codecForNumber())
    .build("TalerExchangeApi.EventCounter");

export const codecForLegitimizationMeasuresList =
  (): Codec<LegitimizationMeasuresList> =>
    buildCodecForObject<LegitimizationMeasuresList>()
      .property(
        "measures",
        codecForList(codecForLegitimizationMeasureDetails()),
      )
      .build("TalerExchangeApi.LegitimizationMeasuresList");

export const codecForLegitimizationMeasureDetails =
  (): Codec<LegitimizationMeasureDetails> =>
    buildCodecForObject<LegitimizationMeasureDetails>()
      .property("h_payto", codecForAny())
      .property("rowid", codecForAny())
      .property("start_time", codecForAny())
      .property("measures", codecForAny())
      .property("is_finished", codecForAny())
      .build("TalerExchangeApi.LegitimizationMeasureDetails");

export const codecForLegitimizationMeasures =
  (): Codec<LegitimizationMeasures> =>
    buildCodecForObject<LegitimizationMeasures>()
      .property("is_and_combinator", codecForAny())
      .property("verboten", codecForAny())
      .property("measures", codecForList(codecForMeasureInformation()))
      .build("TalerExchangeApi.LegitimizationMeasures");

export const codecForAvailableMeasureSummary =
  (): Codec<AvailableMeasureSummary> =>
    buildCodecForObject<AvailableMeasureSummary>()
      .property("checks", codecForMap(codecForKycCheckInformation()))
      .property("programs", codecForMap(codecForAmlProgramRequirement()))
      .property("roots", codecForMap(codecForMeasureInformation()))
      .property("default_rules", codecForList(codecForKycRules()))
      .build("TalerExchangeApi.AvailableMeasureSummary");

export const codecForAmlProgramRequirement = (): Codec<AmlProgramRequirement> =>
  buildCodecForObject<AmlProgramRequirement>()
    .property("description", codecForString())
    .property("context", codecForList(codecForString()))
    .property("inputs", codecForList(codecForString()))
    .build("TalerExchangeApi.AmlProgramRequirement");

export const codecForKycCheckInformation = (): Codec<KycCheckInformation> =>
  buildCodecForObject<KycCheckInformation>()
    .property("description", codecForString())
    .property(
      "description_i18n",
      codecOptional(codecForInternationalizedString()),
    )
    .property("fallback", codecForString())
    .property("outputs", codecForList(codecForString()))
    .property("requires", codecForList(codecForString()))
    .build("TalerExchangeApi.KycCheckInformation");

export const codecForMeasureInformation = (): Codec<MeasureInformation> =>
  buildCodecForObject<MeasureInformation>()
    .property("prog_name", codecOptional(codecForString()))
    .property("check_name", codecForString())
    .property("context", codecForAny())
    .property("operation_type", codecOptional(codecForOperationType))
    .property("voluntary", codecOptional(codecForBoolean()))
    .build("TalerExchangeApi.MeasureInformation");

export const codecForAmlDecisionsResponse = (): Codec<AmlDecisionsResponse> =>
  buildCodecForObject<AmlDecisionsResponse>()
    .property("records", codecForList(codecForAmlDecision()))
    .build("TalerExchangeApi.AmlDecisionsResponse");

export interface CustomerAccountSummary {
  // Which payto-address is this record about.
  // Identifies a GNU Taler wallet or an affected bank account.
  h_payto: PaytoHash;

  // Full payto URL of the account that the decision is
  // about.
  full_payto: Paytos.FullPaytoString;

  // True if the account was assessed as being high risk.
  high_risk: boolean;

  // Latest comments about the account (if any).
  comments?: string;

  // Row of the account in the exchange tables. Useful to filter
  // by offset.
  rowid: Integer;

  // When was the account opened? "never" if it was never opened.
  open_time: Timestamp;

  // When was the account opened? "never" if it was never closed.
  close_time: Timestamp;

  // True if the account is under investigation by AML staff
  // after this decision.
  to_investigate: boolean;
}
export interface AmlAccountsResponse {
  // Array of customer accounts matching the query.
  accounts: CustomerAccountSummary[];
}
export const codecForAmlCustomerAccountSummary =
  (): Codec<CustomerAccountSummary> =>
    buildCodecForObject<CustomerAccountSummary>()
      .property("h_payto", codecForPaytoHash())
      .property("close_time", codecForTimestamp)
      .property("open_time", codecForTimestamp)
      .property("comments", codecOptional(codecForString()))
      .property("full_payto", codecFullForPaytoString())
      .property("high_risk", codecForBoolean())
      .property("rowid", codecForNumber())
      .property("to_investigate", codecForBoolean())
      .build("TalerExchangeApi.CustomerAccountSummary");

export const codecForAmlDecisionsAccounts = (): Codec<AmlAccountsResponse> =>
  buildCodecForObject<AmlAccountsResponse>()
    .property("accounts", codecForList(codecForAmlCustomerAccountSummary()))
    .build("TalerExchangeApi.AmlAccountsResponse");

// export const codecForAmlDecisionDetails = (): Codec<AmlDecisionDetails> =>
//   buildCodecForObject<AmlDecisionDetails>()
//     .property("aml_history", codecForList(codecForAmlDecisionDetail()))
//     .property("kyc_attributes", codecForList(codecForKycDetail()))
//     .build("TalerExchangeApi.AmlDecisionDetails");

// export const codecForAmlDecisionDetail = (): Codec<AmlDecisionDetail> =>
//   buildCodecForObject<AmlDecisionDetail>()
//     .property("justification", codecForString())
//     .property("new_state", codecForNumber())
//     .property("decision_time", codecForTimestamp)
//     .property("new_threshold", codecForAmountString())
//     .property("decider_pub", codecForString())
//     .build("TalerExchangeApi.AmlDecisionDetail");

export const codecForKycDetail = (): Codec<KycDetail> =>
  buildCodecForObject<KycDetail>()
    .property("provider_section", codecForString())
    .property("attributes", codecOptional(codecForAny()))
    .property("collection_time", codecForTimestamp)
    .property("expiration_time", codecForTimestamp)
    .build("TalerExchangeApi.KycDetail");

export const codecForAmlDecision = (): Codec<AmlDecision> =>
  buildCodecForObject<AmlDecision>()
    .property("h_payto", codecForString())
    .property("full_payto", codecOptional(codecForString()))
    .property("rowid", codecForNumber())
    .property("is_wallet", codecForBoolean())
    .property("justification", codecOptional(codecForString()))
    .property("decision_time", codecForTimestamp)
    .property("properties", codecOptional(codecForAccountProperties()))
    .property("limits", codecForLegitimizationRuleSet())
    .property("to_investigate", codecForBoolean())
    .property("is_active", codecForBoolean())
    .build("TalerExchangeApi.AmlDecision");

export const codecForAccountProperties = (): Codec<AccountProperties> =>
  buildCodecForObject<AccountProperties>()
    .property("pep", codecOptional(codecForBoolean()))
    .property("sanctioned", codecOptional(codecForBoolean()))
    .property("high_risk", codecOptional(codecForBoolean()))
    .property("business_domain", codecOptional(codecForString()))
    .property("is_frozen", codecOptional(codecForBoolean()))
    .property("was_reported", codecOptional(codecForBoolean()))
    .allowExtra()
    .build("TalerExchangeApi.AccountProperties");

export const codecForLegitimizationRuleSet = (): Codec<LegitimizationRuleSet> =>
  buildCodecForObject<LegitimizationRuleSet>()
    .property("expiration_time", codecForTimestamp)
    .property("successor_measure", codecOptional(codecForString()))
    .property("rules", codecForList(codecForKycRules()))
    .property("custom_measures", codecForMap(codecForMeasureInformation()))
    .build("TalerExchangeApi.LegitimizationRuleSet");

export const codecForKycRules = (): Codec<KycRule> =>
  buildCodecForObject<KycRule>()
    .property("operation_type", codecForOperationType)
    .property("threshold", codecForAmountString())
    .property("timeframe", codecForDuration)
    .property("measures", codecForList(codecForString()))
    .property("display_priority", codecForNumber())
    .property("exposed", codecOptional(codecForBoolean()))
    .property("is_and_combinator", codecOptional(codecForBoolean()))
    .property("rule_name", codecOptional(codecForString()))
    .build("TalerExchangeApi.KycRule");

export const codecForAmlKycAttributes = (): Codec<KycAttributes> =>
  buildCodecForObject<KycAttributes>()
    .property("details", codecForList(codecForKycAttributeCollectionEvent()))
    .build("TalerExchangeApi.KycAttributes");

export const codecForKycAttributeCollectionEvent =
  (): Codec<KycAttributeCollectionEvent> =>
    buildCodecForObject<KycAttributeCollectionEvent>()
      .property("rowid", codecForNumber())
      .property("provider_name", codecOptional(codecForString()))
      .property("collection_time", codecForTimestamp)
      .property("attributes", codecOptional(codecForAny()))
      .build("TalerExchangeApi.KycAttributeCollectionEvent");

export const codecForAmlWalletKycCheckResponse =
  (): Codec<WalletKycCheckResponse> =>
    buildCodecForObject<WalletKycCheckResponse>()
      .property("next_threshold", codecOptional(codecForAmountString()))
      .property("expiration_time", codecForTimestamp)
      .build("TalerExchangeApi.WalletKycCheckResponse");

export const codecForLegitimizationNeededResponse =
  (): Codec<LegitimizationNeededResponse> =>
    buildCodecForObject<LegitimizationNeededResponse>()
      .property("code", codecForNumber())
      .property("hint", codecOptional(codecForString()))
      .property("h_payto", codecForString())
      .property("account_pub", codecOptional(codecForEddsaPublicKey()))
      .property("requirement_row", codecForNumber())
      .property("bad_kyc_auth", codecOptional(codecForBoolean()))
      .build("TalerExchangeApi.LegitimizationNeededResponse");

export const codecForAccountKycStatus = (): Codec<AccountKycStatus> =>
  buildCodecForObject<AccountKycStatus>()
    .property("aml_review", codecForBoolean())
    .property("access_token", codecForAccessToken())
    .property("limits", codecOptional(codecForList(codecForAccountLimit())))
    .property("rule_gen", codecForNumber())
    .build("TalerExchangeApi.AccountKycStatus");

export const codecForOperationType = codecForEither(
  codecForConstString(LimitOperationType.withdraw),
  codecForConstString(LimitOperationType.deposit),
  codecForConstString(LimitOperationType.merge),
  codecForConstString(LimitOperationType.balance),
  codecForConstString(LimitOperationType.close),
  codecForConstString(LimitOperationType.aggregate),
  codecForConstString(LimitOperationType.transaction),
  codecForConstString(LimitOperationType.refund),
);

export const codecForAccountLimit = (): Codec<AccountLimit> =>
  buildCodecForObject<AccountLimit>()
    .property("operation_type", codecForOperationType)
    .property("timeframe", codecForDuration)
    .property("threshold", codecForAmountString())
    .property("soft_limit", codecOptional(codecForBoolean()))
    .property("rule_name", codecOptional(codecForString()))
    .build("TalerExchangeApi.AccountLimit");

export const codecForZeroLimitedOperation = (): Codec<ZeroLimitedOperation> =>
  buildCodecForObject<ZeroLimitedOperation>()
    .property("operation_type", codecForOperationType)
    .build("TalerExchangeApi.ZeroLimitedOperation");

export const codecForKycCheckPublicInformation =
  (): Codec<KycCheckPublicInformation> =>
    buildCodecForObject<KycCheckPublicInformation>()
      .property("description", codecForString())
      .property(
        "description_i18n",
        codecOptional(codecForInternationalizedString()),
      )
      .build("TalerExchangeApi.KycCheckPublicInformation");

export const codecForKycRequirementInformationId =
  (): Codec<KycRequirementInformationId> =>
    codecForString() as Codec<KycRequirementInformationId>;
export const codecForKycFormId = (): Codec<KycBuiltInFromId> =>
  codecForString() as Codec<KycBuiltInFromId>;

export const codecForKycRequirementInformation =
  (): Codec<KycRequirementInformation> =>
    buildCodecForObject<KycRequirementInformation>()
      .property(
        "form",
        codecForEither(
          codecForConstString("LINK"),
          codecForConstString("INFO"),
          codecForKycFormId(),
        ),
      )
      .property("description", codecForString())
      .property("context", codecOptional(codecForAny()))
      .property(
        "description_i18n",
        codecOptional(codecForInternationalizedString()),
      )
      .property("id", codecOptional(codecForKycRequirementInformationId()))
      .build("TalerExchangeApi.KycRequirementInformation");

export const codecForKycProcessClientInformation =
  (): Codec<KycProcessClientInformation> =>
    buildCodecForObject<KycProcessClientInformation>()
      .property(
        "requirements",
        codecOptionalDefault(
          codecForList(codecForKycRequirementInformation()),
          [],
        ),
      )
      .property("is_and_combinator", codecOptional(codecForBoolean()))
      .property(
        "voluntary_measures",
        codecOptional(codecForList(codecForKycRequirementInformation())),
      )
      .build("TalerExchangeApi.KycProcessClientInformation");

export const codecForExchangeTransferList = (): Codec<ExchangeTransferList> =>
  buildCodecForObject<ExchangeTransferList>()
    .property("transfers", codecForList(codecForExchangeTransferListEntry()))
    .build("TalerExchangeApi.ExchangeTransferList");

export const codecForExchangeTransferListEntry =
  (): Codec<ExchangeTransferListEntry> =>
    buildCodecForObject<ExchangeTransferListEntry>()
      .property("rowid", codecForNumber())
      .property("payto_uri", codecForPaytoString())
      .property("amount", codecForAmountString())
      .property("execution_time", codecForTimestamp)
      .build("TalerExchangeApi.ExchangeTransferListEntry");

export interface ExchangeTransferList {
  // Matching transaction of the exchange
  transfers: ExchangeTransferListEntry[];
}

export interface ExchangeTransferListEntry {
  // Row ID of the record.  Used to filter by offset.
  rowid: Integer;

  // payto://-URI of the other account.
  payto_uri: string;

  // The amount involved.
  amount: Amount;

  // Time when the transfer was made
  execution_time: Timestamp;
}

export interface KycProcessStartInformation {
  // URL to open.
  redirect_url: string;
}

export const codecForKycProcessStartInformation =
  (): Codec<KycProcessStartInformation> =>
    buildCodecForObject<KycProcessStartInformation>()
      .property("redirect_url", codecForURLString())
      .build("TalerExchangeApi.KycProcessStartInformation");

export interface BatchWithdrawResponse {
  // Array of blinded signatures, in the same order as was
  // given in the request.
  ev_sigs: WithdrawResponse[];
}
export interface WithdrawResponse {
  // The blinded signature over the 'coin_ev', affirms the coin's
  // validity after unblinding.
  ev_sig: BlindedDenominationSignature;
}
export type BlindedDenominationSignature =
  | RsaBlindedDenominationSignature
  | CSBlindedDenominationSignature;

export interface RsaBlindedDenominationSignature {
  cipher: DenomKeyType.Rsa;

  // (blinded) RSA signature
  blinded_rsa_signature: BlindedRsaSignature;
}

export interface CSBlindedDenominationSignature {
  cipher: DenomKeyType.ClauseSchnorr;

  // Signer chosen bit value, 0 or 1, used
  // in Clause Blind Schnorr to make the
  // ROS problem harder.
  b: Integer;

  // Blinded scalar calculated from c_b.
  s: Cs25519Scalar;
}

type BlindedRsaSignature = string;
type Cs25519Scalar = string;
type HashCode = string;
type EddsaSignature = string;
type EddsaPublicKey = string;
type Amount = AmountString;
type Base32 = string;

export interface WithdrawError {
  // Text describing the error.
  hint: string;

  // Detailed error code.
  code: Integer;

  // Amount left in the reserve.
  balance: AmountString;

  // History of the reserve's activity, in the same format
  // as returned by /reserve/$RID/history.
  history: TransactionHistoryItem[];
}
export type TransactionHistoryItem =
  | AccountSetupTransaction
  | ReserveWithdrawTransaction
  | ReserveAgeWithdrawTransaction
  | ReserveCreditTransaction
  | ReserveClosingTransaction
  | ReserveOpenRequestTransaction
  | ReserveCloseRequestTransaction
  | PurseMergeTransaction;

enum TransactionHistoryType {
  setup = "SETUP",
  withdraw = "WITHDRAW",
  ageWithdraw = "AGEWITHDRAW",
  credit = "CREDIT",
  closing = "CLOSING",
  open = "OPEN",
  close = "CLOSE",
  merge = "MERGE",
}
interface AccountSetupTransaction {
  type: TransactionHistoryType.setup;

  // Offset of this entry in the reserve history.
  // Useful to request incremental histories via
  // the "start" query parameter.
  history_offset: Integer;

  // KYC fee agreed to by the reserve owner.
  kyc_fee: AmountString;

  // Time when the KYC was triggered.
  kyc_timestamp: Timestamp;

  // Hash of the wire details of the account.
  // Note that this hash is unsalted and potentially
  // private (as it could be inverted), hence access
  // to this endpoint must be authorized using the
  // private key of the reserve.
  h_wire: HashCode;

  // Signature created with the reserve's private key.
  // Must be of purpose TALER_SIGNATURE_ACCOUNT_SETUP_REQUEST over
  // a TALER_AccountSetupRequestSignaturePS.
  reserve_sig: EddsaSignature;
}
interface ReserveWithdrawTransaction {
  type: "WITHDRAW";

  // Offset of this entry in the reserve history.
  // Useful to request incremental histories via
  // the "start" query parameter.
  history_offset: Integer;

  // Amount withdrawn.
  amount: Amount;

  // Hash of the denomination public key of the coin.
  h_denom_pub: HashCode;

  // Hash of the blinded coin to be signed.
  h_coin_envelope: HashCode;

  // Signature over a TALER_WithdrawRequestPS
  // with purpose TALER_SIGNATURE_WALLET_RESERVE_WITHDRAW
  // created with the reserve's private key.
  reserve_sig: EddsaSignature;

  // Fee that is charged for withdraw.
  withdraw_fee: Amount;
}
interface ReserveAgeWithdrawTransaction {
  type: "AGEWITHDRAW";

  // Offset of this entry in the reserve history.
  // Useful to request incremental histories via
  // the "start" query parameter.
  history_offset: Integer;

  // Total Amount withdrawn.
  amount: Amount;

  // Commitment of all n*kappa blinded coins.
  h_commitment: HashCode;

  // Signature over a TALER_AgeWithdrawRequestPS
  // with purpose TALER_SIGNATURE_WALLET_RESERVE_AGE_WITHDRAW
  // created with the reserve's private key.
  reserve_sig: EddsaSignature;

  // Fee that is charged for withdraw.
  withdraw_fee: Amount;
}
interface ReserveCreditTransaction {
  type: "CREDIT";

  // Offset of this entry in the reserve history.
  // Useful to request incremental histories via
  // the "start" query parameter.
  history_offset: Integer;

  // Amount deposited.
  amount: Amount;

  // Sender account payto:// URL.
  sender_account_url: string;

  // Opaque identifier internal to the exchange that
  // uniquely identifies the wire transfer that credited the reserve.
  wire_reference: Integer;

  // Timestamp of the incoming wire transfer.
  timestamp: Timestamp;
}
interface ReserveClosingTransaction {
  type: "CLOSING";

  // Offset of this entry in the reserve history.
  // Useful to request incremental histories via
  // the "start" query parameter.
  history_offset: Integer;

  // Closing balance.
  amount: Amount;

  // Closing fee charged by the exchange.
  closing_fee: Amount;

  // Wire transfer subject.
  wtid: Base32;

  // payto:// URI of the wire account into which the funds were returned to.
  receiver_account_details: string;

  // This is a signature over a
  // struct TALER_ReserveCloseConfirmationPS with purpose
  // TALER_SIGNATURE_EXCHANGE_RESERVE_CLOSED.
  exchange_sig: EddsaSignature;

  // Public key used to create 'exchange_sig'.
  exchange_pub: EddsaPublicKey;

  // Time when the reserve was closed.
  timestamp: Timestamp;
}
interface ReserveOpenRequestTransaction {
  type: "OPEN";

  // Offset of this entry in the reserve history.
  // Useful to request incremental histories via
  // the "start" query parameter.
  history_offset: Integer;

  // Open fee paid from the reserve.
  open_fee: Amount;

  // This is a signature over
  // a struct TALER_ReserveOpenPS with purpose
  // TALER_SIGNATURE_WALLET_RESERVE_OPEN.
  reserve_sig: EddsaSignature;

  // Timestamp of the open request.
  request_timestamp: Timestamp;

  // Requested expiration.
  requested_expiration: Timestamp;

  // Requested number of free open purses.
  requested_min_purses: Integer;
}
interface ReserveCloseRequestTransaction {
  type: "CLOSE";

  // Offset of this entry in the reserve history.
  // Useful to request incremental histories via
  // the "start" query parameter.
  history_offset: Integer;

  // This is a signature over
  // a struct TALER_ReserveClosePS with purpose
  // TALER_SIGNATURE_WALLET_RESERVE_CLOSE.
  reserve_sig: EddsaSignature;

  // Target account payto://, optional.
  h_payto?: PaytoHash;

  // Timestamp of the close request.
  request_timestamp: Timestamp;
}
interface PurseMergeTransaction {
  type: "MERGE";

  // Offset of this entry in the reserve history.
  // Useful to request incremental histories via
  // the "start" query parameter.
  history_offset: Integer;

  // SHA-512 hash of the contact of the purse.
  h_contract_terms: HashCode;

  // EdDSA public key used to approve merges of this purse.
  merge_pub: EddsaPublicKey;

  // Minimum age required for all coins deposited into the purse.
  min_age: Integer;

  // Number that identifies who created the purse
  // and how it was paid for.
  flags: Integer;

  // Purse public key.
  purse_pub: EddsaPublicKey;

  // EdDSA signature of the account/reserve affirming the merge
  // over a TALER_AccountMergeSignaturePS.
  // Must be of purpose TALER_SIGNATURE_ACCOUNT_MERGE
  reserve_sig: EddsaSignature;

  // Client-side timestamp of when the merge request was made.
  merge_timestamp: Timestamp;

  // Indicative time by which the purse should expire
  // if it has not been merged into an account. At this
  // point, all of the deposits made should be
  // auto-refunded.
  purse_expiration: Timestamp;

  // Purse fee the reserve owner paid for the purse creation.
  purse_fee: Amount;

  // Total amount merged into the reserve.
  // (excludes fees).
  amount: Amount;

  // True if the purse was actually merged.
  // If false, only the purse_fee has an impact
  // on the reserve balance!
  merged: boolean;
}

interface DenominationExpiredMessage {
  // Taler error code.  Note that beyond
  // expiration this message format is also
  // used if the key is not yet valid, or
  // has been revoked.
  code: number;

  // Signature by the exchange over a
  // TALER_DenominationExpiredAffirmationPS.
  // Must have purpose TALER_SIGNATURE_EXCHANGE_AFFIRM_DENOM_EXPIRED.
  exchange_sig: EddsaSignature;

  // Public key of the exchange used to create
  // the 'exchange_sig.
  exchange_pub: EddsaPublicKey;

  // Hash of the denomination public key that is unknown.
  h_denom_pub: HashCode;

  // When was the signature created.
  timestamp: Timestamp;

  // What kind of operation was requested that now
  // failed?
  oper: string;
}

/**
 * Docs name: WithdrawRequest
 */
export interface ExchangeWithdrawRequest {
  // Cipher that is used for the rerserve's signatures.
  // For now, only ed25519 signatures are applicable,
  // but this might change in future versions.
  cipher: "ED25519";

  // The reserve's public key, for the the cipher ED25519,
  // to verify the signature reserve_sig.
  reserve_pub: EddsaPublicKey;

  // Array of n hash codes of denomination public keys to order.
  // The sum of all denomination's values and fees MUST be
  // at most the balance of the reserve. The balance of
  // the reserve will be immediately reduced by that amount.
  // If max_age is set, these denominations MUST support
  // age restriction as defined in the output to /keys.
  denoms_h: HashCode[];

  // If set, the maximum age to commit to. This implies:
  // 1.) it MUST be the same value as the maximum age
  //     of the reserve.
  // 2.) coin_evs MUST be an array of n*kappa
  // 3.) the denominations in denoms_h MUST support
  //      age restriction.
  max_age?: number;

  // Array of blinded coin envelopes of type CoinEnvelope.
  // If max_age is not set, MUST be n entries.
  // If max_age is set, MUST be n*kappa entries,
  // arranged in [0..n)..[0..n), with the first n entries
  // belonging to kappa=0 etc.
  // In case of age restriction, the exchange will
  // respond with an index gamma, which is the index
  // that shall remain undisclosed during the subsequent
  // reveal phase.
  // This hash value along with the reserve's public key
  // will also be used for recoup operations, if needed.
  coin_evs: CoinEnvelope[];

  // Signature of TALER_WithdrawRequestPS created with
  // the reserves's private key.
  reserve_sig: EddsaSignature;
}

export type PurseConflict =
  | DepositDoubleSpendError
  | PurseCreateConflict
  | PurseDepositConflict
  | PurseContractConflict;
export type PurseConflictPartial =
  | PurseCreateConflict
  | PurseDepositConflict
  | PurseContractConflict;

interface DepositDoubleSpendError {
  code: TalerErrorCode.EXCHANGE_GENERIC_INSUFFICIENT_FUNDS;

  // A string explaining that the user tried to
  // double-spend.
  hint: string;

  // EdDSA public key of a coin being double-spent.
  coin_pub: EddsaPublicKey;
}
interface PurseCreateConflict {
  code: TalerErrorCode.EXCHANGE_PURSE_CREATE_CONFLICTING_META_DATA;

  // Total amount to be merged into the reserve.
  // (excludes fees).
  amount: Amount;

  // Minimum age required for all coins deposited into the purse.
  min_age: Integer;

  // Indicative time by which the purse should expire
  // if it has not been merged into an account. At this
  // point, all of the deposits made should be
  // auto-refunded.
  purse_expiration: Timestamp;

  // EdDSA signature of the purse over
  // TALER_PurseMergeSignaturePS of
  // purpose TALER_SIGNATURE_WALLET_PURSE_MERGE
  // confirming that the
  // above details hold for this purse.
  purse_sig: EddsaSignature;

  // SHA-512 hash of the contact of the purse.
  h_contract_terms: HashCode;

  // EdDSA public key used to approve merges of this purse.
  merge_pub: EddsaPublicKey;
}
interface PurseDepositConflict {
  code: TalerErrorCode.EXCHANGE_PURSE_DEPOSIT_CONFLICTING_META_DATA;

  // Public key of the coin being deposited into the purse.
  coin_pub: EddsaPublicKey;

  // Signature over TALER_PurseDepositSignaturePS
  // of purpose TALER_SIGNATURE_WALLET_PURSE_DEPOSIT
  // made by the customer with the
  // coin's private key.
  coin_sig: EddsaSignature;

  // Target exchange URL for the purse. Not present for the
  // same exchange.
  partner_url?: string;

  // Amount to be contributed to the purse by this coin.
  amount: AmountString;
}
interface PurseContractConflict {
  code: TalerErrorCode.EXCHANGE_PURSE_ECONTRACT_CONFLICTING_META_DATA;

  // Hash of the encrypted contract.
  h_econtract: HashCode;

  // Signature over the contract.
  econtract_sig: EddsaSignature;

  // Ephemeral public key for the DH operation to decrypt the contract.
  contract_pub: EddsaPublicKey;
}

export interface PurseCreate {
  // Total value of the purse, excluding fees.
  amount: Amount;

  // Minimum age required for all coins deposited into the purse.
  min_age: Integer;

  // 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: EddsaPublicKey;

  // EdDSA signature of the purse over a
  // TALER_PurseRequestSignaturePS
  // of purpose TALER_SIGNATURE_WALLET_PURSE_CREATE
  // confirming the key
  // invariants associated with the purse.
  // (amount, h_contract_terms, expiration).
  purse_sig: EddsaSignature;

  // SHA-512 hash of the contact of the purse.
  h_contract_terms: HashCode;

  // Array of coins being deposited into the purse.
  // Maximum length is 128.
  deposits: PurseDeposit[];

  // Indicative time by which the purse should expire
  // if it has not been merged into an account. At this
  // point, all of the deposits made will be auto-refunded.
  purse_expiration: Timestamp;
}

export const codecForPurseConflict = () =>
  buildCodecForUnion<PurseConflict>()
    .discriminateOn("code")
    .alternative(
      TalerErrorCode.EXCHANGE_GENERIC_INSUFFICIENT_FUNDS,
      codecForDepositDoubleSpendError(),
    )
    .alternative(
      TalerErrorCode.EXCHANGE_PURSE_CREATE_CONFLICTING_META_DATA,
      codecForPurseCreateConflict(),
    )
    .alternative(
      TalerErrorCode.EXCHANGE_PURSE_DEPOSIT_CONFLICTING_META_DATA,
      codecForPurseDepositConflict(),
    )
    .alternative(
      TalerErrorCode.EXCHANGE_PURSE_ECONTRACT_CONFLICTING_META_DATA,
      codecForPurseContractConflict(),
    )
    .build("PurseConflict");
export const codecForPurseConflictPartial = () =>
  buildCodecForUnion<PurseConflictPartial>()
    .discriminateOn("code")
    .alternative(
      TalerErrorCode.EXCHANGE_PURSE_CREATE_CONFLICTING_META_DATA,
      codecForPurseCreateConflict(),
    )
    .alternative(
      TalerErrorCode.EXCHANGE_PURSE_DEPOSIT_CONFLICTING_META_DATA,
      codecForPurseDepositConflict(),
    )
    .alternative(
      TalerErrorCode.EXCHANGE_PURSE_ECONTRACT_CONFLICTING_META_DATA,
      codecForPurseContractConflict(),
    )
    .build("PurseConflictPartial");
export const codecForDepositDoubleSpendError = () =>
  buildCodecForObject<DepositDoubleSpendError>()
    .property("code", codecForNumber())
    .property("hint", codecForString())
    .property("coin_pub", codecForString())
    .build("DepositDoubleSpendError");
export const codecForPurseCreateConflict = () =>
  buildCodecForObject<PurseCreateConflict>()
    .property("code", codecForNumber())
    .property("amount", codecForAmountString())
    .property("min_age", codecForNumber())
    .property("purse_expiration", codecForTimestamp)
    .property("purse_sig", codecForString())
    .property("h_contract_terms", codecForString())
    .property("merge_pub", codecForString())
    .build("PurseCreateConflict");
export const codecForPurseDepositConflict = () =>
  buildCodecForObject<PurseDepositConflict>()
    .property("code", codecForNumber())
    .property("coin_pub", codecForString())
    .property("partner_url", codecOptional(codecForString()))
    .property("amount", codecForAmountString())
    .build("PurseDepositConflict");
export const codecForPurseContractConflict = () =>
  buildCodecForObject<PurseContractConflict>()
    .property("code", codecForNumber())
    .property("h_econtract", codecForString())
    .property("econtract_sig", codecForString())
    .property("contract_pub", codecForString())
    .build("PurseContractConflict");
