/*
 This file is part of GNU Taler
 (C) 2024 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 {
  Codec,
  buildCodecForObject,
  codecForAny,
  codecForList,
  codecForNumber,
  codecForString,
  codecForStringURL,
  codecOptional,
} from "./codec.js";
import {
  PaytoString,
  buildCodecForUnion,
  codecForAmountString,
  codecForBoolean,
  codecForConstString,
  codecForCurrencySpecificiation,
  codecForMap,
  codecForURN,
  strcmp,
} from "./index.js";
import { Edx25519PublicKeyEnc } from "./taler-crypto.js";
import {
  TalerProtocolDuration,
  TalerProtocolTimestamp,
  codecForDuration,
  codecForTimestamp,
} from "./time.js";
import {
  AmlOfficerPublicKeyP,
  AmountString,
  Base32String,
  CoinPublicKeyString,
  Cs25519Point,
  CurrencySpecification,
  EddsaPublicKeyString,
  EddsaSignatureString,
  HashCodeString,
  Integer,
  InternationalizedString,
  LibtoolVersionString,
  PaytoHash,
  RelativeTime,
  RsaPublicKey,
  RsaPublicKeySring,
  Timestamp,
  WireSalt,
} 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: UnblindedSignature;

  /**
   * Blinding key that was used during withdraw,
   * used to prove that we were actually withdrawing the coin.
   */
  coin_blind_key_secret: string;

  /**
   * Signature of TALER_RecoupRequestPS created with the coin's private key.
   */
  coin_sig: string;

  ewv: ExchangeWithdrawValue;
}

export interface RecoupRefreshRequest {
  /**
   * Hashed enomination public key of the coin we want to get
   * paid back.
   */
  denom_pub_hash: string;

  /**
   * Signature over the coin public key by the denomination.
   *
   * The string variant is for the legacy exchange protocol.
   */
  denom_sig: UnblindedSignature;

  /**
   * Coin's blinding factor.
   */
  coin_blind_key_secret: string;

  /**
   * Signature of TALER_RecoupRefreshRequestPS created with
   * the coin's private key.
   */
  coin_sig: string;

  ewv: ExchangeWithdrawValue;
}

/**
 * Response that we get from the exchange for a payback request.
 */
export interface RecoupConfirmation {
  /**
   * Public key of the reserve that will receive the payback.
   */
  reserve_pub?: string;

  /**
   * Public key of the old coin that will receive the recoup,
   * provided if refreshed was true.
   */
  old_coin_pub?: string;
}

export type UnblindedSignature = RsaUnblindedSignature;

export interface RsaUnblindedSignature {
  cipher: DenomKeyType.Rsa;
  rsa_signature: string;
}

/**
 * Deposit permission for a single coin.
 */
export interface CoinDepositPermission {
  /**
   * Signature by the coin.
   */
  coin_sig: string;

  /**
   * Public key of the coin being spend.
   */
  coin_pub: string;

  /**
   * Signature made by the denomination public key.
   *
   * The string variant is for legacy protocol support.
   */

  ub_sig: UnblindedSignature;

  /**
   * The denomination public key associated with this coin.
   */
  h_denom: string;

  /**
   * The amount that is subtracted from this coin with this payment.
   */
  contribution: string;

  /**
   * URL of the exchange this coin was withdrawn from.
   */
  exchange_url: string;

  minimum_age_sig?: EddsaSignatureString;

  age_commitment?: Edx25519PublicKeyEnc[];

  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 ExchangeKeysJson {
  /**
   * Canonical, public base URL of the exchange.
   */
  base_url: string;

  currency: string;

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

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

export interface ExchangeMeltResponse {
  /**
   * Which of the kappa indices does the client not have to reveal.
   */
  noreveal_index: number;

  /**
   * Signature of TALER_RefreshMeltConfirmationPS whereby the exchange
   * affirms the successful melt and confirming the noreveal_index
   */
  exchange_sig: EddsaSignatureString;

  /*
   * public EdDSA key of the exchange that was used to generate the signature.
   * Should match one of the exchange's signing keys from /keys.  Again given
   * explicitly as the client might otherwise be confused by clock skew as to
   * which signing key was used.
   */
  exchange_pub: EddsaPublicKeyString;

  /*
   * Base URL to use for operations on the refresh context
   * (so the reveal operation).  If not given,
   * the base URL is the same as the one used for this request.
   * Can be used if the base URL for /refreshes/ differs from that
   * for /coins/, i.e. for load balancing.  Clients SHOULD
   * respect the refresh_base_url if provided.  Any HTTP server
   * belonging to an exchange MUST generate a 307 or 308 redirection
   * to the correct base URL should a client uses the wrong base
   * URL, or if the base URL has changed since the melt.
   *
   * When melting the same coin twice (technically allowed
   * as the response might have been lost on the network),
   * the exchange may return different values for the refresh_base_url.
   */
  refresh_base_url?: string;
}

export interface ExchangeRevealItem {
  ev_sig: BlindedDenominationSignature;
}

export interface ExchangeRevealResponse {
  // List of the exchange's blinded RSA signatures on the new coins.
  ev_sigs: ExchangeRevealItem[];
}

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;

  // XOR of all the SHA-512 hash values of the denominations' public keys
  // in this group.  Note that for hashing, the binary format of the
  // public keys is used, and not their base32 encoding.
  hash: HashCodeString;
}

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: RsaPublicKeySring;
  } & DenomCommon)[];
}

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

  denoms: ({
    rsa_pub: RsaPublicKeySring;
  } & 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", codecForString())
    .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", codecForString())
      .property("exchange_sig", codecForString())
      .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 ExchangeWithdrawRequest {
  denom_pub_hash: HashCodeString;
  reserve_sig: EddsaSignatureString;
  coin_ev: CoinEnvelope;
}

export interface ExchangeBatchWithdrawRequest {
  planchets: ExchangeWithdrawRequest[];
}

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 const codecForRecoup = (): Codec<Recoup> =>
  buildCodecForObject<Recoup>()
    .property("h_denom_pub", codecForString())
    .build("Recoup");

export const codecForExchangeSigningKey = (): Codec<ExchangeSignKeyJson> =>
  buildCodecForObject<ExchangeSignKeyJson>()
    .property("key", codecForString())
    .property("master_sig", codecForString())
    .property("stamp_end", codecForTimestamp)
    .property("stamp_start", codecForTimestamp)
    .property("stamp_expire", codecForTimestamp)
    .build("ExchangeSignKeyJson");

export const codecForGlobalFees = (): Codec<GlobalFees> =>
  buildCodecForObject<GlobalFees>()
    .property("start_date", codecForTimestamp)
    .property("end_date", codecForTimestamp)
    .property("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", codecForString())
    .build("GlobalFees");

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

export const codecForExchangeKeysJson = (): Codec<ExchangeKeysJson> =>
  buildCodecForObject<ExchangeKeysJson>()
    .property("base_url", codecForString())
    .property("currency", codecForString())
    .property(
      "currency_specification",
      codecOptional(codecForCurrencySpecificiation()),
    )
    .property("master_public_key", codecForString())
    .property("auditors", codecForList(codecForAuditor()))
    .property("list_issue_date", codecForTimestamp)
    .property("recoup", codecOptional(codecForList(codecForRecoup())))
    .property("signkeys", codecForList(codecForExchangeSigningKey()))
    .property("version", codecForString())
    .property("reserve_closing_delay", codecForDuration)
    .property("global_fees", codecForList(codecForGlobalFees()))
    .property("accounts", codecForList(codecForExchangeWireAccount()))
    .property("wire_fees", codecForMap(codecForList(codecForWireFeesJson())))
    .property("denominations", codecForList(codecForNgDenominations))
    .build("ExchangeKeysJson");

export const codecForWireFeesJson = (): Codec<WireFeesJson> =>
  buildCodecForObject<WireFeesJson>()
    .property("wire_fee", codecForString())
    .property("closing_fee", codecForString())
    .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 codecForWithdrawResponse = (): Codec<ExchangeWithdrawResponse> =>
  buildCodecForObject<ExchangeWithdrawResponse>()
    .property("ev_sig", codecForBlindedDenominationSignature())
    .build("WithdrawResponse");

export class ExchangeWithdrawResponse {
  ev_sig: BlindedDenominationSignature;
}

export class ExchangeWithdrawBatchResponse {
  ev_sigs: ExchangeWithdrawResponse[];
}

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 codecForExchangeWithdrawBatchResponse =
  (): Codec<ExchangeWithdrawBatchResponse> =>
    buildCodecForObject<ExchangeWithdrawBatchResponse>()
      .property("ev_sigs", codecForList(codecForWithdrawResponse()))
      .build("WithdrawBatchResponse");

export const codecForExchangeMeltResponse = (): Codec<ExchangeMeltResponse> =>
  buildCodecForObject<ExchangeMeltResponse>()
    .property("exchange_pub", codecForString())
    .property("exchange_sig", codecForString())
    .property("noreveal_index", codecForNumber())
    .property("refresh_base_url", codecOptional(codecForString()))
    .build("ExchangeMeltResponse");

export const codecForExchangeRevealItem = (): Codec<ExchangeRevealItem> =>
  buildCodecForObject<ExchangeRevealItem>()
    .property("ev_sig", codecForBlindedDenominationSignature())
    .build("ExchangeRevealItem");

export const codecForExchangeRevealResponse =
  (): Codec<ExchangeRevealResponse> =>
    buildCodecForObject<ExchangeRevealResponse>()
      .property("ev_sigs", codecForList(codecForExchangeRevealItem()))
      .build("ExchangeRevealResponse");

export 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: UnblindedSignature;

  /**
   * Age commitment for the coin, if the denomination is age-restricted.
   */
  age_commitment?: string[];

  /**
   * Attestation for the minimum age, if the denomination is age-restricted.
   */
  attest?: string;

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

  /**
   * Public key of the coin being deposited into the purse.
   */
  coin_pub: EddsaPublicKeyString;
}

export interface ExchangePurseMergeRequest {
  // payto://-URI of the account the purse is to be merged into.
  // Must be of the form: 'payto://taler/$EXCHANGE_URL/$RESERVE_PUB'.
  payto_uri: string;

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

  // EdDSA signature of the purse private key affirming the merge
  // over a TALER_PurseMergeSignaturePS.
  // Must be of purpose TALER_SIGNATURE_PURSE_MERGE.
  merge_sig: EddsaSignatureString;

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

export interface ExchangeGetContractResponse {
  purse_pub: string;
  econtract_sig: string;
  econtract: string;
}

export const codecForExchangeGetContractResponse =
  (): Codec<ExchangeGetContractResponse> =>
    buildCodecForObject<ExchangeGetContractResponse>()
      .property("purse_pub", codecForString())
      .property("econtract_sig", codecForString())
      .property("econtract", codecForString())
      .build("ExchangeGetContractResponse");

/**
 * Contract terms between two wallets (as opposed to a merchant and wallet).
 */
export interface PeerContractTerms {
  amount: AmountString;
  summary: string;
  purse_expiration: TalerProtocolTimestamp;
}

export interface EncryptedContract {
  // Encrypted contract.
  econtract: string;

  // Signature over the (encrypted) contract.
  econtract_sig: string;

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

/**
 * Payload for /reserves/{reserve_pub}/purse
 * endpoint of the exchange.
 */
export interface ExchangeReservePurseRequest {
  /**
   * Minimum amount that must be credited to the reserve, that is
   * the total value of the purse minus the deposit fees.
   * If the deposit fees are lower, the contribution to the
   * reserve can be higher!
   */
  purse_value: AmountString;

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

  // Purse fee the reserve owner is willing to pay
  // for the purse creation. Optional, if not present
  // the purse is to be created from the purse quota
  // of the reserve.
  purse_fee: AmountString;

  // Optional encrypted contract, in case the buyer is
  // proposing the contract and thus establishing the
  // purse with the payment.
  econtract?: EncryptedContract;

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

  // EdDSA signature of the purse private key affirming the merge
  // over a TALER_PurseMergeSignaturePS.
  // Must be of purpose TALER_SIGNATURE_PURSE_MERGE.
  merge_sig: EddsaSignatureString;

  // EdDSA signature of the account/reserve affirming the merge.
  // Must be of purpose TALER_SIGNATURE_WALLET_ACCOUNT_MERGE
  reserve_sig: EddsaSignatureString;

  // Purse public key.
  purse_pub: EddsaPublicKeyString;

  // EdDSA signature of the purse over
  // TALER_PurseRequestSignaturePS of
  // purpose TALER_SIGNATURE_PURSE_REQUEST
  // confirming that the
  // above details hold for this purse.
  purse_sig: EddsaSignatureString;

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

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

  // Indicative time by which the purse should expire
  // if it has not been paid.
  purse_expiration: TalerProtocolTimestamp;
}

export interface ExchangePurseDeposits {
  // Array of coins to deposit into the purse.
  deposits: PurseDeposit[];
}

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

  // 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", codecForString())
    .property("exchange_sig", codecForString())
    .property("exchange_timestamp", codecForTimestamp)
    .property("transaction_base_url", codecOptional(codecForString()))
    .build("BatchDepositSuccess");

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", codecForString())
    .property("exchange_pub", codecForString())
    .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;
}

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

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

export interface ExchangeBatchDepositRequest {
  // The merchant's account details.
  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: 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: UnblindedSignature;

  // 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 enum AmlState {
  normal = 0,
  pending = 1,
  frozen = 2,
}

export interface AmlRecords {
  // Array of AML records matching the query.
  records: AmlRecord[];
}
export interface AmlRecord {
  // Which payto-address is this record about.
  // Identifies a GNU Taler wallet or an affected bank account.
  h_payto: PaytoHash;

  // What is the current AML state.
  current_state: AmlState;

  // Monthly transaction threshold before a review will be triggered
  threshold: AmountString;

  // RowID of the record.
  rowid: Integer;
}

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 interface AmlDecision {
  // Human-readable justification for the decision.
  justification: string;

  // At what monthly transaction volume should the
  // decision be automatically reviewed?
  new_threshold: AmountString;

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

  // What is the new AML state (e.g. frozen, unfrozen, etc.)
  // Numerical values are defined in AmlDecisionState.
  new_state: Integer;

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

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

  // Optional argument to impose new KYC requirements
  // that the customer has to satisfy to unblock transactions.
  kyc_requirements?: string[];
}

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

export interface WireAccount {
  // payto:// URI identifying the account and wire method
  payto_uri: PaytoString;

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

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

  // The exchange's base URL.
  base_url: string;

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

  /**
   * FIXME: PARTIALLY IMPLEMENTED!!
   */

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

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

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

  // // Array of wire accounts operated by the exchange for
  // // incoming wire transfers.
  // accounts: WireAccount[];

  // // Object mapping names of wire methods (i.e. "iban" or "x-taler-bank")
  // // to wire fees.
  // wire_fees: { method: AggregateTransferFee[] };

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

  // // Set to true if this exchange allows the use
  // // of reserves for rewards.
  // // @deprecated in protocol v18.
  // rewards_allowed: false;

  // // EdDSA master public key of the exchange, used to sign entries
  // // in denoms and signkeys.
  // master_public_key: EddsaPublicKey;

  // // Relative duration until inactive reserves are closed;
  // // not signed (!), can change without notice.
  // reserve_closing_delay: RelativeTime;

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

  // // Denominations offered by this exchange
  // denominations: DenomGroup[];

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

  // // Denominations for which the exchange currently offers/requests recoup.
  // recoup: Recoup[];

  // // Array of globally applicable fees by time range.
  // global_fees: GlobalFees[];

  // // The date when the denomination keys were last updated.
  // list_issue_date: Timestamp;

  // // Auditors of the exchange.
  // auditors: AuditorKeys[];

  // // The exchange's signing keys.
  // signkeys: SignKey[];

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

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

interface SignKey {
  // The actual exchange's EdDSA signing public key.
  key: EddsaPublicKeyString;

  // Initial validity date for the signing key.
  stamp_start: Timestamp;

  // Date when the exchange will stop using the signing key, allowed to overlap
  // slightly with the next signing key's validity to allow for clock skew.
  stamp_expire: Timestamp;

  // Date when all signatures made by the signing key expire and should
  // henceforth no longer be considered valid in legal disputes.
  stamp_end: Timestamp;

  // Signature over key and stamp_expire by the exchange master key.
  // Signature of TALER_ExchangeSigningKeyValidityPS.
  // Must have purpose TALER_SIGNATURE_MASTER_SIGNING_KEY_VALIDITY.
  master_sig: EddsaSignatureString;
}

interface AuditorKeys {
  // The auditor's EdDSA signing public key.
  auditor_pub: EddsaPublicKeyString;

  // The auditor's URL.
  auditor_url: string;

  // The auditor's name (for humans).
  auditor_name: string;

  // An array of denomination keys the auditor affirms with its signature.
  // Note that the message only includes the hash of the public key, while the
  // signature is actually over the expanded information including expiration
  // times and fees.  The exact format is described below.
  denomination_keys: AuditorDenominationKey[];
}

interface AuditorDenominationKey {
  // Hash of the public RSA key used to sign coins of the respective
  // denomination.  Note that the auditor's signature covers more than just
  // the hash, but this other information is already provided in denoms and
  // thus not repeated here.
  denom_pub_h: HashCodeString;

  // Signature of TALER_ExchangeKeyValidityPS.
  auditor_sig: EddsaSignatureString;
}

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

interface ExchangePartner {
  // Base URL of the partner exchange.
  partner_base_url: string;

  // Public master key of the partner exchange.
  partner_master_pub: EddsaPublicKeyString;

  // Per exchange-to-exchange transfer (wad) fee.
  wad_fee: AmountString;

  // Exchange-to-exchange wad (wire) transfer frequency.
  wad_frequency: RelativeTime;

  // When did this partnership begin (under these conditions)?
  start_date: Timestamp;

  // How long is this partnership expected to last?
  end_date: Timestamp;

  // Signature using the exchange's offline key over
  // TALER_WadPartnerSignaturePS
  // with purpose TALER_SIGNATURE_MASTER_PARTNER_DETAILS.
  master_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 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()))
    .build("TalerExchangeApi.ExchangeVersionResponse");

export const codecForExchangeKeys = (): Codec<ExchangeKeysResponse> =>
  buildCodecForObject<ExchangeKeysResponse>()
    .property("version", codecForString())
    .property("base_url", codecForString())
    .property("currency", codecForString())
    .build("TalerExchangeApi.ExchangeKeysResponse");
export const codecForAmlRecords = (): Codec<AmlRecords> =>
  buildCodecForObject<AmlRecords>()
    .property("records", codecForList(codecForAmlRecord()))
    .build("TalerExchangeApi.AmlRecords");

export const codecForAmlRecord = (): Codec<AmlRecord> =>
  buildCodecForObject<AmlRecord>()
    .property("current_state", codecForNumber())
    .property("h_payto", codecForString())
    .property("rowid", codecForNumber())
    .property("threshold", codecForAmountString())
    .build("TalerExchangeApi.AmlRecord");

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("justification", codecForString())
    .property("new_threshold", codecForAmountString())
    .property("h_payto", codecForString())
    .property("new_state", codecForNumber())
    .property("officer_sig", codecForString())
    .property("decision_time", codecForTimestamp)
    .property("kyc_requirements", codecOptional(codecForList(codecForString())))
    .build("TalerExchangeApi.AmlDecision");
