/*
 This file is part of GNU Taler
 (C) 2019-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
 */

/**
 * Types used by clients of the wallet.
 *
 * These types are defined in a separate file make tree shaking easier, since
 * some components use these types (via RPC) but do not depend on the wallet
 * code directly.
 *
 * @author Florian Dold <dold@taler.net>
 */

/**
 * Imports.
 */
import { AmountJson, codecForAmountString } from "./amounts.js";
import {
  Codec,
  Context,
  DecodingError,
  buildCodecForObject,
  buildCodecForUnion,
  codecForAny,
  codecForBoolean,
  codecForConstString,
  codecForEither,
  codecForList,
  codecForMap,
  codecForNumber,
  codecForString,
  codecOptional,
  renderContext,
} from "./codec.js";
import { canonicalizeBaseUrl } from "./helpers.js";
import { PaytoString, codecForPaytoString } from "./payto.js";
import { QrCodeSpec } from "./qr.js";
import { AgeCommitmentProof } from "./taler-crypto.js";
import { TalerErrorCode } from "./taler-error-codes.js";
import { TalerUri, TemplateParams } from "./taleruri.js";
import {
  AbsoluteTime,
  DurationUnitSpec,
  TalerPreciseTimestamp,
  TalerProtocolDuration,
  TalerProtocolTimestamp,
  codecForAbsoluteTime,
  codecForPreciseTimestamp,
  codecForTimestamp,
} from "./time.js";
import { BlindedDonationReceiptKeyPair } from "./types-donau.js";
import { WithdrawalOperationStatusFlag } from "./types-taler-bank-integration.js";
import {
  AmountString,
  CurrencySpecification,
  EddsaPrivateKeyString,
  EddsaPublicKeyString,
  EddsaSignatureString,
  HashCode,
  HashCodeString,
  Timestamp,
  codecForEddsaPrivateKey,
} from "./types-taler-common.js";
import {
  AccountRestriction,
  AuditorDenomSig,
  CoinEnvelope,
  DenomKeyType,
  DenominationPubKey,
  ExchangeAuditor,
  ExchangeRefundRequest,
  ExchangeWireAccount,
  PeerContractTerms,
  UnblindedDenominationSignature,
  codecForExchangeWireAccount,
  codecForPeerContractTerms,
} from "./types-taler-exchange.js";
import {
  MerchantContractTerms,
  MerchantContractTermsV0,
  MerchantContractTermsV1,
  TokenEnvelope,
  TokenIssuePublicKey,
  WalletTemplateDetails,
  codecForMerchantContractTerms,
  codecForMerchantContractTermsV0,
} from "./types-taler-merchant.js";
import { BackupRecovery } from "./types-taler-sync.js";
import {
  TransactionMajorState,
  TransactionMinorState,
  TransactionState,
  TransactionStateWildcard,
} from "./types-taler-wallet-transactions.js";

/**
 * Identifier for a transaction in the wallet.
 */
declare const __txId: unique symbol;
export type TransactionIdStr = `txn:${string}:${string}` & { [__txId]: true };

/**
 * Identifier for a pending task in the wallet.
 */
declare const __pndId: unique symbol;
export type PendingIdStr = `pnd:${string}:${string}` & { [__pndId]: true };

declare const __tmbId: unique symbol;
export type TombstoneIdStr = `tmb:${string}:${string}` & { [__tmbId]: true };

function codecForTransactionIdStr(): Codec<TransactionIdStr> {
  return {
    decode(x: any, c?: Context): TransactionIdStr {
      if (typeof x === "string" && x.startsWith("txn:")) {
        return x as TransactionIdStr;
      }
      throw new DecodingError(
        `expected string starting with "txn:" at ${renderContext(
          c,
        )} but got ${x}`,
      );
    },
  };
}

function codecForPendingIdStr(): Codec<PendingIdStr> {
  return {
    decode(x: any, c?: Context): PendingIdStr {
      if (typeof x === "string" && x.startsWith("txn:")) {
        return x as PendingIdStr;
      }
      throw new DecodingError(
        `expected string starting with "txn:" at ${renderContext(
          c,
        )} but got ${x}`,
      );
    },
  };
}

function codecForTombstoneIdStr(): Codec<TombstoneIdStr> {
  return {
    decode(x: any, c?: Context): TombstoneIdStr {
      if (typeof x === "string" && x.startsWith("tmb:")) {
        return x as TombstoneIdStr;
      }
      throw new DecodingError(
        `expected string starting with "tmb:" at ${renderContext(
          c,
        )} but got ${x}`,
      );
    },
  };
}

export function codecForCanonBaseUrl(): Codec<string> {
  return {
    decode(x: any, c?: Context): string {
      if (typeof x === "string") {
        const canon = canonicalizeBaseUrl(x);
        if (x !== canon) {
          throw new DecodingError(
            `expected canonicalized base URL at ${renderContext(
              c,
            )} but got value '${x}'`,
          );
        }
        return x;
      }
      throw new DecodingError(
        `expected base URL at ${renderContext(c)} but got type ${typeof x}`,
      );
    },
  };
}

export enum ScopeType {
  Global = "global",
  Exchange = "exchange",
  Auditor = "auditor",
}

export type ScopeInfoGlobal = { type: ScopeType.Global; currency: string };

export type ScopeInfoExchange = {
  type: ScopeType.Exchange;
  currency: string;
  url: string;
};

export type ScopeInfoAuditor = {
  type: ScopeType.Auditor;
  currency: string;
  url: string;
};

export type ScopeInfo = ScopeInfoGlobal | ScopeInfoExchange | ScopeInfoAuditor;

export const codecForScopeInfo = (): Codec<ScopeInfo> =>
  buildCodecForUnion<ScopeInfo>()
    .discriminateOn("type")
    .alternative(ScopeType.Global, codecForScopeInfoGlobal())
    .alternative(ScopeType.Exchange, codecForScopeInfoExchange())
    .alternative(ScopeType.Auditor, codecForScopeInfoAuditor())
    .build("ScopeInfo");

/**
 * Response for the create reserve request to the wallet.
 */
export class CreateReserveResponse {
  /**
   * Exchange URL where the bank should create the reserve.
   * The URL is canonicalized in the response.
   */
  exchange: string;

  /**
   * Reserve public key of the newly created reserve.
   */
  reservePub: string;
}

export interface GetBalanceDetailRequest {
  currency: string;
}

export const codecForGetBalanceDetailRequest =
  (): Codec<GetBalanceDetailRequest> =>
    buildCodecForObject<GetBalanceDetailRequest>()
      .property("currency", codecForString())
      .build("GetBalanceDetailRequest");

/**
 * How the amount should be interpreted in a transaction
 * Effective = how the balance is change
 * Raw = effective amount without fee
 *
 * Depending on the transaction, raw can be higher than effective
 */
export enum TransactionAmountMode {
  Effective = "effective",
  Raw = "raw",
}

export interface ConvertAmountRequest {
  amount: AmountString;
  type: TransactionAmountMode;
  depositPaytoUri: PaytoString;
}

export const codecForConvertAmountRequest =
  buildCodecForObject<ConvertAmountRequest>()
    .property("amount", codecForAmountString())
    .property("depositPaytoUri", codecForPaytoString())
    .property(
      "type",
      codecForEither(
        codecForConstString(TransactionAmountMode.Raw),
        codecForConstString(TransactionAmountMode.Effective),
      ),
    )
    .build("ConvertAmountRequest");

export interface GetMaxDepositAmountRequest {
  /**
   * Currency to deposit.
   */
  currency: string;

  /**
   * Target bank account to deposit into.
   */
  depositPaytoUri?: string;

  /**
   * Restrict the deposit to a certain scope.
   */
  restrictScope?: ScopeInfo;
}

export const codecForGetMaxDepositAmountRequest = () =>
  buildCodecForObject<GetMaxDepositAmountRequest>()
    .property("currency", codecForString())
    .property("depositPaytoUri", codecOptional(codecForString()))
    .property("restrictScope", codecOptional(codecForScopeInfo()))
    .build("GetAmountRequest");

export interface GetMaxPeerPushDebitAmountRequest {
  currency: string;
  /**
   * Preferred exchange to use for the p2p payment.
   */
  exchangeBaseUrl?: string;
  restrictScope?: ScopeInfo;
}

export const codecForGetMaxPeerPushDebitAmountRequest =
  (): Codec<GetMaxPeerPushDebitAmountRequest> =>
    buildCodecForObject<GetMaxPeerPushDebitAmountRequest>()
      .property("currency", codecForString())
      .property("exchangeBaseUrl", codecOptional(codecForString()))
      .property("restrictScope", codecOptional(codecForScopeInfo()))
      .build("GetMaxPeerPushDebitRequest");

export interface GetMaxDepositAmountResponse {
  effectiveAmount: AmountString;
  rawAmount: AmountString;

  /**
   * Account restrictions that affect the max deposit amount.
   */
  depositRestrictions?: {
    [exchangeBaseUrl: string]: { [paytoUri: string]: AccountRestriction[] };
  };
}

export interface GetMaxPeerPushDebitAmountResponse {
  effectiveAmount: AmountString;
  rawAmount: AmountString;
  exchangeBaseUrl?: string;
}

export interface AmountResponse {
  effectiveAmount: AmountString;
  rawAmount: AmountString;
}

export const codecForAmountResponse = (): Codec<AmountResponse> =>
  buildCodecForObject<AmountResponse>()
    .property("effectiveAmount", codecForAmountString())
    .property("rawAmount", codecForAmountString())
    .build("AmountResponse");

export enum BalanceFlag {
  IncomingKyc = "incoming-kyc",
  IncomingAml = "incoming-aml",
  IncomingConfirmation = "incoming-confirmation",
  OutgoingKyc = "outgoing-kyc",
}

export interface WalletBalance {
  scopeInfo: ScopeInfo;
  available: AmountString;
  pendingIncoming: AmountString;
  pendingOutgoing: AmountString;

  /**
   * Does the balance for this currency have a pending
   * transaction?
   *
   * @deprecated use flags and pendingIncoming/pendingOutgoing instead
   */
  hasPendingTransactions: boolean;

  /**
   * Is there a transaction that requires user input?
   *
   * @deprecated use flags instead
   */
  requiresUserInput: boolean;

  flags: BalanceFlag[];
}

export const codecForScopeInfoGlobal = (): Codec<ScopeInfoGlobal> =>
  buildCodecForObject<ScopeInfoGlobal>()
    .property("currency", codecForString())
    .property("type", codecForConstString(ScopeType.Global))
    .build("ScopeInfoGlobal");

export const codecForScopeInfoExchange = (): Codec<ScopeInfoExchange> =>
  buildCodecForObject<ScopeInfoExchange>()
    .property("currency", codecForString())
    .property("type", codecForConstString(ScopeType.Exchange))
    .property("url", codecForString())
    .build("ScopeInfoExchange");

export const codecForScopeInfoAuditor = (): Codec<ScopeInfoAuditor> =>
  buildCodecForObject<ScopeInfoAuditor>()
    .property("currency", codecForString())
    .property("type", codecForConstString(ScopeType.Auditor))
    .property("url", codecForString())
    .build("ScopeInfoAuditor");

export interface GetCurrencySpecificationRequest {
  scope: ScopeInfo;
}

export const codecForGetCurrencyInfoRequest =
  (): Codec<GetCurrencySpecificationRequest> =>
    buildCodecForObject<GetCurrencySpecificationRequest>()
      .property("scope", codecForScopeInfo())
      .build("GetCurrencySpecificationRequest");

export interface GetCurrencySpecificationResponse {
  currencySpecification: CurrencySpecification;
}

export interface BuiltinExchange {
  exchangeBaseUrl: string;
  currencyHint: string;
}

export interface PartialWalletRunConfig {
  builtin?: Partial<WalletRunConfig["builtin"]>;
  testing?: Partial<WalletRunConfig["testing"]>;
  features?: Partial<WalletRunConfig["features"]>;
  lazyTaskLoop?: Partial<WalletRunConfig["lazyTaskLoop"]>;
  logLevel?: Partial<WalletRunConfig["logLevel"]>;
}

export interface WalletRunConfig {
  /**
   * Initialization values useful for a complete startup.
   *
   * These are values may be overridden by different wallets
   */
  builtin: {
    exchanges: BuiltinExchange[];
  };

  /**
   * Unsafe options which it should only be used to create
   * testing environment.
   */
  testing: {
    devModeActive: boolean;
    insecureTrustExchange: boolean;
    preventThrottling: boolean;
    skipDefaults: boolean;
    emitObservabilityEvents?: boolean;
  };

  /**
   * Configurations values that may be safe to show to the user
   */
  features: {
    allowHttp: boolean;

    /**
     * If set to true, enable V1 contracts.  Otherwise, emulate v0 contracts
     * to wallet-core clients.
     *
     * Will become enabled by default in the future.
     *
     * Added 2025-08-19.
     */
    enableV1Contracts: boolean;
  };

  /**
   * Start processing tasks only when explicitly required, even after
   * init has been called.
   *
   * Useful when the wallet is started to make single read-only request,
   * as otherwise wallet-core starts making network request and process
   * unrelated pending tasks.
   */
  lazyTaskLoop: boolean;

  /**
   * Global log level.
   */
  logLevel: string;
}

export interface InitRequest {
  config?: PartialWalletRunConfig;
}

export const codecForInitRequest = (): Codec<InitRequest> =>
  buildCodecForObject<InitRequest>()
    .property("config", codecForAny())
    .build("InitRequest");

export interface InitResponse {
  versionInfo: WalletCoreVersion;
}

/**
 * Shorter version of stringifyScopeInfo
 */
export function stringifyScopeInfoShort(si: ScopeInfo): string {
  switch (si.type) {
    case ScopeType.Global:
      return `${si.currency}`;
    case ScopeType.Exchange:
      return `${si.currency}/${encodeURIComponent(si.url)}`;
    case ScopeType.Auditor:
      return `${si.currency}:${encodeURIComponent(si.url)}`;
  }
}
export function parseScopeInfoShort(si: string): ScopeInfo | undefined {
  const indexOfColon = si.indexOf(":");
  const indexOfSlash = si.indexOf("/");
  if (indexOfColon === -1 && indexOfSlash === -1) {
    return {
      type: ScopeType.Global,
      currency: si,
    };
  }
  if (indexOfColon > 0) {
    return {
      type: ScopeType.Auditor,
      currency: si.substring(0, indexOfColon),
      url: decodeURIComponent(si.substring(indexOfColon + 1)),
    };
  }
  if (indexOfSlash > 0) {
    return {
      type: ScopeType.Exchange,
      currency: si.substring(0, indexOfSlash),
      url: decodeURIComponent(si.substring(indexOfSlash + 1)),
    };
  }
  return undefined;
}

/**
 * Encode scope info as a string.
 *
 * Format must be stable as it's used in the database.
 */
export function stringifyScopeInfo(si: ScopeInfo): string {
  switch (si.type) {
    case ScopeType.Global:
      return `taler-si:global/${si.currency}`;
    case ScopeType.Auditor:
      return `taler-si:auditor/${si.currency}/${encodeURIComponent(si.url)}`;
    case ScopeType.Exchange:
      return `taler-si:exchange/${si.currency}/${encodeURIComponent(si.url)}`;
  }
}

export interface DonauSummaryItem {
  /** Base URL of the donau service. */
  donauBaseUrl: string;
  /** Legal domain of the donau service (if available). */
  legalDomain?: string;
  /** Year of the donation(s). */
  year: number;
  /**
   * Sum of donation receipts we received from merchants in the
   * applicable year.
   */
  amountReceiptsAvailable: AmountString;
  /**
   * Sum of donation receipts that were already submitted
   * to the donau in the applicable year.
   */
  amountReceiptsSubmitted: AmountString;
  /**
   * Amount of the latest available statement. Missing if no statement
   * was requested yet.
   */
  amountStatement?: AmountString;
}

/**
 * Response to a getBalances request.
 */
export interface BalancesResponse {
  /** Electronic cash balances, per currency scope. */
  balances: WalletBalance[];
  /* Summary of donations, per donau/year/currency. */
  donauSummary?: DonauSummaryItem[];
}

export const codecForBalance = (): Codec<WalletBalance> =>
  buildCodecForObject<WalletBalance>()
    .property("scopeInfo", codecForAny()) // FIXME
    .property("available", codecForAmountString())
    .property("hasPendingTransactions", codecForBoolean())
    .property("pendingIncoming", codecForAmountString())
    .property("pendingOutgoing", codecForAmountString())
    .property("requiresUserInput", codecForBoolean())
    .property("flags", codecForAny()) // FIXME
    .build("Balance");

export const codecForBalancesResponse = (): Codec<BalancesResponse> =>
  buildCodecForObject<BalancesResponse>()
    .property("balances", codecForList(codecForBalance()))
    .build("BalancesResponse");

/**
 * For terseness.
 */
export function mkAmount(
  value: number,
  fraction: number,
  currency: string,
): AmountJson {
  return { value, fraction, currency };
}

/**
 * Status of a coin.
 */
export enum CoinStatus {
  /**
   * Withdrawn and never shown to anybody.
   */
  Fresh = "fresh",

  /**
   * Coin was lost as the denomination is not usable anymore.
   */
  DenomLoss = "denom-loss",

  /**
   * Fresh, but currently marked as "suspended", thus won't be used
   * for spending.  Used for testing.
   */
  FreshSuspended = "fresh-suspended",

  /**
   * A coin that has been spent and refreshed.
   */
  Dormant = "dormant",
}

export type WalletCoinHistoryItem =
  | {
      type: "withdraw";
      transactionId: TransactionIdStr;
    }
  | {
      type: "spend";
      transactionId: TransactionIdStr;
      amount: AmountString;
    }
  | {
      type: "refresh";
      transactionId: TransactionIdStr;
      amount: AmountString;
    }
  | {
      type: "recoup";
      transactionId: TransactionIdStr;
      amount: AmountString;
    }
  | {
      type: "refund";
      transactionId: TransactionIdStr;
      amount: AmountString;
    };

/**
 * Easy to process format for the public data of coins
 * managed by the wallet.
 */
export interface CoinDumpJson {
  coins: Array<{
    /**
     * The coin's denomination's public key.
     */
    denomPub: DenominationPubKey;
    /**
     * Hash of denom_pub.
     */
    denomPubHash: string;
    /**
     * Value of the denomination (without any fees).
     */
    denomValue: string;
    /**
     * Public key of the coin.
     */
    coinPub: string;
    /**
     * Base URL of the exchange for the coin.
     */
    exchangeBaseUrl: string;
    /**
     * Public key of the parent coin.
     * Only present if this coin was obtained via refreshing.
     */
    refreshParentCoinPub: string | undefined;
    /**
     * Public key of the reserve for this coin.
     * Only present if this coin was obtained via refreshing.
     */
    withdrawalReservePub: string | undefined;
    /**
     * Status of the coin.
     */
    coinStatus: CoinStatus;
    /**
     * Information about the age restriction
     */
    ageCommitmentProof: AgeCommitmentProof | undefined;
    history: WalletCoinHistoryItem[];
  }>;
}

export enum ConfirmPayResultType {
  Done = "done",
  Pending = "pending",
}

/**
 * Result for confirmPay
 */
export interface ConfirmPayResultDone {
  type: ConfirmPayResultType.Done;
  contractTerms: MerchantContractTermsV0;
  transactionId: TransactionIdStr;
}

export interface ConfirmPayResultPending {
  type: ConfirmPayResultType.Pending;
  transactionId: TransactionIdStr;
  lastError: TalerErrorDetail | undefined;
}

export const codecForTalerErrorDetail = (): Codec<TalerErrorDetail> =>
  buildCodecForObject<TalerErrorDetail>()
    .property("code", codecForNumber())
    .property("when", codecOptional(codecForAbsoluteTime))
    .property("hint", codecOptional(codecForString()))
    .build("TalerErrorDetail");

export type ConfirmPayResult = ConfirmPayResultDone | ConfirmPayResultPending;

export const codecForConfirmPayResultPending =
  (): Codec<ConfirmPayResultPending> =>
    buildCodecForObject<ConfirmPayResultPending>()
      .property("lastError", codecOptional(codecForTalerErrorDetail()))
      .property("transactionId", codecForTransactionIdStr())
      .property("type", codecForConstString(ConfirmPayResultType.Pending))
      .build("ConfirmPayResultPending");

export const codecForConfirmPayResultDone = (): Codec<ConfirmPayResultDone> =>
  buildCodecForObject<ConfirmPayResultDone>()
    .property("type", codecForConstString(ConfirmPayResultType.Done))
    .property("transactionId", codecForTransactionIdStr())
    .property("contractTerms", codecForMerchantContractTermsV0())
    .build("ConfirmPayResultDone");

export const codecForConfirmPayResult = (): Codec<ConfirmPayResult> =>
  buildCodecForUnion<ConfirmPayResult>()
    .discriminateOn("type")
    .alternative(
      ConfirmPayResultType.Pending,
      codecForConfirmPayResultPending(),
    )
    .alternative(ConfirmPayResultType.Done, codecForConfirmPayResultDone())
    .build("ConfirmPayResult");

/**
 * Information about all sender wire details known to the wallet,
 * as well as exchanges that accept these wire types.
 */
export interface SenderWireInfos {
  /**
   * Mapping from exchange base url to list of accepted
   * wire types.
   */
  exchangeWireTypes: { [exchangeBaseUrl: string]: string[] };

  /**
   * Sender wire information stored in the wallet.
   */
  senderWires: string[];
}

export enum PreparePayResultType {
  PaymentPossible = "payment-possible",
  InsufficientBalance = "insufficient-balance",
  AlreadyConfirmed = "already-confirmed",
  ChoiceSelection = "choice-selection",
}

export const codecForPreparePayResultPaymentPossible =
  (): Codec<PreparePayResultPaymentPossible> =>
    buildCodecForObject<PreparePayResultPaymentPossible>()
      .property("amountEffective", codecForAmountString())
      .property("amountRaw", codecForAmountString())
      .property("contractTerms", codecForMerchantContractTermsV0())
      .property("transactionId", codecForTransactionIdStr())
      .property("contractTermsHash", codecForString())
      .property("scopes", codecForList(codecForScopeInfo()))
      .property("talerUri", codecForString())
      .property(
        "status",
        codecForConstString(PreparePayResultType.PaymentPossible),
      )
      .build("PreparePayResultPaymentPossible");

export enum InsufficientBalanceHint {
  /**
   *  Merchant doesn't accept money from exchange(s) that the wallet supports.
   */
  MerchantAcceptInsufficient = "merchant-accept-insufficient",

  /**
   * Merchant accepts funds from a matching exchange, but the funds can't be
   * deposited with the wire method.
   */
  MerchantDepositInsufficient = "merchant-deposit-insufficient",

  /**
   * While in principle the balance is sufficient,
   * the age restriction on coins causes the spendable
   * balance to be insufficient.
   */
  AgeRestricted = "age-restricted",

  /**
   * Wallet has enough available funds,
   * but the material funds are insufficient. Usually because there is a
   * pending refresh operation.
   */
  WalletBalanceMaterialInsufficient = "wallet-balance-material-insufficient",

  /**
   * The wallet simply doesn't have enough available funds.
   * This is the "obvious" case of insufficient balance.
   */
  WalletBalanceAvailableInsufficient = "wallet-balance-available-insufficient",

  /**
   * Exchange is missing the global fee configuration, thus fees are unknown
   * and funds from this exchange can't be used for p2p payments.
   */
  ExchangeMissingGlobalFees = "exchange-missing-global-fees",

  /**
   * Even though the balance looks sufficient for the instructed amount,
   * the fees can be covered by neither the merchant nor the remaining wallet
   * balance.
   */
  FeesNotCovered = "fees-not-covered",
}

/**
 * Detailed reason for why the wallet's balance is insufficient.
 */
export interface PaymentInsufficientBalanceDetails {
  /**
   * Amount requested by the merchant.
   */
  amountRequested: AmountString;

  /**
   * Wire method for the requested payment, only applicable
   * for merchant payments.
   */
  wireMethod?: string | undefined;

  /**
   * Hint as to why the balance is insufficient.
   *
   * If this hint is not provided, the balance hints of
   * the individual exchanges should be shown, as the overall
   * reason might be a combination of the reasons for different exchanges.
   */
  causeHint?: InsufficientBalanceHint;

  /**
   * Balance of type "available" (see balance.ts for definition).
   */
  balanceAvailable: AmountString;

  /**
   * Balance of type "material" (see balance.ts for definition).
   */
  balanceMaterial: AmountString;

  /**
   * Balance of type "age-acceptable" (see balance.ts for definition).
   */
  balanceAgeAcceptable: AmountString;

  /**
   * Balance of type "merchant-acceptable" (see balance.ts for definition).
   */
  balanceReceiverAcceptable: AmountString;

  /**
   * Balance of type "merchant-depositable" (see balance.ts for definition).
   */
  balanceReceiverDepositable: AmountString;

  balanceExchangeDepositable: AmountString;

  /**
   * Maximum effective amount that the wallet can spend,
   * when all fees are paid by the wallet.
   */
  maxEffectiveSpendAmount: AmountString;

  perExchange: {
    [url: string]: {
      balanceAvailable: AmountString;
      balanceMaterial: AmountString;
      balanceExchangeDepositable: AmountString;
      balanceAgeAcceptable: AmountString;
      balanceReceiverAcceptable: AmountString;
      balanceReceiverDepositable: AmountString;
      maxEffectiveSpendAmount: AmountString;

      /**
       * Exchange doesn't have global fees configured for the relevant year,
       * p2p payments aren't possible.
       *
       * @deprecated (2025-02-18) use causeHint instead
       */
      missingGlobalFees: boolean;

      /**
       * Hint that UIs should show to explain the insufficient
       * balance.
       */
      causeHint?: InsufficientBalanceHint | undefined;
    };
  };
}

export interface PaymentTokenAvailabilityDetails {
  /**
   * Number of tokens requested by the merchant.
   */
  tokensRequested: number;

  /**
   * Number of tokens available to use.
   */
  tokensAvailable: number;

  /**
   * Number of tokens for which the merchant is unexpected.
   *
   * Can be used to pay (i.e. with forced selection),
   * but a warning should be displayed to the user.
   */
  tokensUnexpected: number;

  /**
   * Number of tokens for which the merchant is untrusted.
   *
   * Cannot be used to pay, so an error should be displayed.
   */
  tokensUntrusted: number;

  perTokenFamily: {
    [slug: string]: {
      causeHint?: TokenAvailabilityHint;
      requested: number;
      available: number;
      unexpected: number;
      untrusted: number;
    };
  };
}

export enum TokenAvailabilityHint {
  WalletTokensAvailableInsufficient = "wallet-tokens-available-insufficient",
  MerchantUnexpected = "merchant-unexpected",
  MerchantUntrusted = "merchant-untrusted",
}

export const codecForPayMerchantInsufficientBalanceDetails =
  (): Codec<PaymentInsufficientBalanceDetails> =>
    buildCodecForObject<PaymentInsufficientBalanceDetails>()
      .property("amountRequested", codecForAmountString())
      .property("wireMethod", codecOptional(codecForString()))
      .property("balanceAgeAcceptable", codecForAmountString())
      .property("balanceAvailable", codecForAmountString())
      .property("balanceMaterial", codecForAmountString())
      .property("balanceReceiverAcceptable", codecForAmountString())
      .property("balanceReceiverDepositable", codecForAmountString())
      .property("balanceExchangeDepositable", codecForAmountString())
      .property("perExchange", codecForAny())
      .property("maxEffectiveSpendAmount", codecForAmountString())
      .build("PayMerchantInsufficientBalanceDetails");

export const codecForPreparePayResultInsufficientBalance =
  (): Codec<PreparePayResultInsufficientBalance> =>
    buildCodecForObject<PreparePayResultInsufficientBalance>()
      .property("amountRaw", codecForAmountString())
      .property("contractTerms", codecForMerchantContractTermsV0())
      .property("talerUri", codecForString())
      .property("transactionId", codecForTransactionIdStr())
      .property(
        "status",
        codecForConstString(PreparePayResultType.InsufficientBalance),
      )
      .property("scopes", codecForList(codecForScopeInfo()))
      .property(
        "balanceDetails",
        codecForPayMerchantInsufficientBalanceDetails(),
      )
      .build("PreparePayResultInsufficientBalance");

export const codecForPreparePayResultAlreadyConfirmed =
  (): Codec<PreparePayResultAlreadyConfirmed> =>
    buildCodecForObject<PreparePayResultAlreadyConfirmed>()
      .property(
        "status",
        codecForConstString(PreparePayResultType.AlreadyConfirmed),
      )
      .property("amountEffective", codecOptional(codecForAmountString()))
      .property("amountRaw", codecForAmountString())
      .property("scopes", codecForList(codecForScopeInfo()))
      .property("paid", codecForBoolean())
      .property("talerUri", codecForString())
      .property("contractTerms", codecForMerchantContractTermsV0())
      .property("contractTermsHash", codecForString())
      .property("transactionId", codecForTransactionIdStr())
      .build("PreparePayResultAlreadyConfirmed");

export const codecForPreparePayResultChoiceSelection =
  (): Codec<PreparePayResultChoiceSelection> =>
    buildCodecForObject<PreparePayResultChoiceSelection>()
      .property(
        "status",
        codecForConstString(PreparePayResultType.ChoiceSelection),
      )
      .property("transactionId", codecForTransactionIdStr())
      .property("contractTerms", codecForMerchantContractTerms())
      .property("contractTermsHash", codecForString())
      .property("talerUri", codecForString())
      .build("PreparePayResultChoiceSelection");

export const codecForPreparePayResult = (): Codec<PreparePayResult> =>
  buildCodecForUnion<PreparePayResult>()
    .discriminateOn("status")
    .alternative(
      PreparePayResultType.AlreadyConfirmed,
      codecForPreparePayResultAlreadyConfirmed(),
    )
    .alternative(
      PreparePayResultType.InsufficientBalance,
      codecForPreparePayResultInsufficientBalance(),
    )
    .alternative(
      PreparePayResultType.PaymentPossible,
      codecForPreparePayResultPaymentPossible(),
    )
    .alternative(
      PreparePayResultType.ChoiceSelection,
      codecForPreparePayResultChoiceSelection(),
    )
    .build("PreparePayResult");

/**
 * Result of a prepare pay operation.
 */
export type PreparePayResult =
  | PreparePayResultInsufficientBalance
  | PreparePayResultAlreadyConfirmed
  | PreparePayResultPaymentPossible
  | PreparePayResultChoiceSelection;

/**
 * Payment is possible.
 *
 * This response is only returned for v0 contracts
 * or when v1 are not enabled yet.
 */
export interface PreparePayResultPaymentPossible {
  status: PreparePayResultType.PaymentPossible;

  transactionId: TransactionIdStr;

  contractTerms: MerchantContractTermsV0;

  /**
   * Scopes involved in this transaction.
   */
  scopes: ScopeInfo[];

  amountRaw: AmountString;

  amountEffective: AmountString;

  /**
   * FIXME: Unclear why this is needed.  Remove?
   */
  contractTermsHash: string;

  /**
   * FIXME: Unclear why this is needed!  Remove?
   */
  talerUri: string;
}

export interface PreparePayResultInsufficientBalance {
  status: PreparePayResultType.InsufficientBalance;
  transactionId: TransactionIdStr;

  /**
   * Scopes involved in this transaction.
   *
   * For the insufficient balance response, contains scopes
   * of *possible* payment providers.
   */
  scopes: ScopeInfo[];

  contractTerms: MerchantContractTermsV0;

  amountRaw: AmountString;

  talerUri: string;

  balanceDetails: PaymentInsufficientBalanceDetails;
}

export interface PreparePayResultAlreadyConfirmed {
  status: PreparePayResultType.AlreadyConfirmed;

  transactionId: TransactionIdStr;

  contractTerms: MerchantContractTerms;

  paid: boolean;

  amountRaw: AmountString;

  amountEffective: AmountString | undefined;

  /**
   * Scopes involved in this transaction.
   */
  scopes: ScopeInfo[];

  contractTermsHash: string;

  talerUri: string;
}

/**
 * Unconfirmed contract v1 payment.
 */
export interface PreparePayResultChoiceSelection {
  status: PreparePayResultType.ChoiceSelection;

  transactionId: TransactionIdStr;

  contractTerms: MerchantContractTerms;

  contractTermsHash: string;

  talerUri: string;
}

export interface BankWithdrawDetails {
  status: WithdrawalOperationStatusFlag;
  currency: string;
  amount: AmountJson | undefined;
  editableAmount: boolean;
  maxAmount: AmountJson | undefined;
  wireFee: AmountJson | undefined;
  senderWire?: string;
  exchange?: string;
  editableExchange: boolean;
  confirmTransferUrl?: string;
  wireTypes: string[];
  operationId: string;
  apiBaseUrl: string;
}

export interface AcceptWithdrawalResponse {
  confirmTransferUrl?: string;
  transactionId: TransactionIdStr;
}

/**
 * Details about a purchase, including refund status.
 */
export interface PurchaseDetails {
  contractTerms: Record<string, undefined>;
  hasRefund: boolean;
  totalRefundAmount: AmountJson;
  totalRefundAndRefreshFees: AmountJson;
}

export interface WalletDiagnostics {
  walletManifestVersion: string;
  walletManifestDisplayVersion: string;
  errors: string[];
  firefoxIdbProblem: boolean;
  dbOutdated: boolean;
}

export interface TalerErrorDetail {
  code: TalerErrorCode;
  when?: AbsoluteTime;
  hint?: string;
  [x: string]: unknown;
}

/**
 * Minimal information needed about a planchet for unblinding a signature.
 *
 * Can be a withdrawal/refresh planchet.
 */
export interface PlanchetUnblindInfo {
  denomPub: DenominationPubKey;
  blindingKey: string;
}

export interface WithdrawalPlanchet {
  coinPub: string;
  coinPriv: string;
  reservePub: string;
  denomPubHash: string;
  denomPub: DenominationPubKey;
  blindingKey: string;
  withdrawSig: string;
  coinEv: CoinEnvelope;
  coinValue: AmountJson;
  coinEvHash: string;
  ageCommitmentProof?: AgeCommitmentProof;
}

export interface PlanchetCreationRequest {
  secretSeed: string;
  coinIndex: number;
  value: AmountJson;
  feeWithdraw: AmountJson;
  denomPub: DenominationPubKey;
  reservePub: string;
  reservePriv: string;
  restrictAge?: number;
}

/**
 * Minimal information needed about a slate for unblinding a signature.
 */
export interface SlateUnblindInfo {
  tokenIssuePub: TokenIssuePublicKey;
  blindingKey: string;
}

export interface Slate {
  tokenPub: string;
  tokenPriv: string;
  tokenIssuePub: TokenIssuePublicKey;
  tokenIssuePubHash: string;
  tokenWalletData: PayWalletData;
  tokenEv: TokenEnvelope;
  tokenEvHash: string;
  blindingKey: string;
}

export interface SlateCreationRequest {
  secretSeed: string;
  choiceIndex: number;
  outputIndex: number;
  tokenIssuePub: TokenIssuePublicKey;
  genTokenUseSig: boolean;
  contractTerms: MerchantContractTermsV1;
  contractTermsHash: string;
}

export interface SignTokenUseRequest {
  tokenUsePriv: string;
  walletDataHash: string;
  contractTermsHash: string;
}

/**
 * Reasons for why a coin is being refreshed.
 */
export enum RefreshReason {
  Manual = "manual",
  PayMerchant = "pay-merchant",
  PayDeposit = "pay-deposit",
  PayPeerPush = "pay-peer-push",
  PayPeerPull = "pay-peer-pull",
  Refund = "refund",
  AbortPay = "abort-pay",
  AbortDeposit = "abort-deposit",
  AbortPeerPushDebit = "abort-peer-push-debit",
  AbortPeerPullDebit = "abort-peer-pull-debit",
  Recoup = "recoup",
  BackupRestored = "backup-restored",
  Scheduled = "scheduled",
}

/**
 * Request to refresh a single coin.
 */
export interface CoinRefreshRequest {
  readonly coinPub: string;
  readonly amount: AmountString;
  readonly refundRequest?: ExchangeRefundRequest;
}

/**
 * Private data required to make a deposit permission.
 */
export interface DepositInfo {
  exchangeBaseUrl: string;
  contractTermsHash: string;
  coinPub: string;
  coinPriv: string;
  spendAmount: AmountJson;
  timestamp: TalerProtocolTimestamp;
  refundDeadline: TalerProtocolTimestamp;
  merchantPub: string;
  feeDeposit: AmountJson;
  wireInfoHash: string;
  denomKeyType: DenomKeyType;
  denomPubHash: string;
  denomSig: UnblindedDenominationSignature;

  requiredMinimumAge?: number;

  ageCommitmentProof?: AgeCommitmentProof;

  walletDataHash?: string;
}

export interface ExchangesShortListResponse {
  exchanges: ShortExchangeListItem[];
}

export interface ExchangesListResponse {
  exchanges: ExchangeListItem[];
}

export interface ListExchangesRequest {
  /**
   * Filter results to only include exchanges in the given scope.
   */
  filterByScope?: ScopeInfo;

  filterByExchangeEntryStatus?: ExchangeEntryStatus;
}

export const codecForListExchangesRequest = (): Codec<ListExchangesRequest> =>
  buildCodecForObject<ListExchangesRequest>()
    .property("filterByScope", codecOptional(codecForScopeInfo()))
    .property(
      "filterByExchangeEntryStatus",
      codecOptional(codecForExchangeEntryStatus()),
    )
    .build("ListExchangesRequest");

export interface ExchangeDetailedResponse {
  exchange: ExchangeFullDetails;
}

export interface AddContactRequest {
  contact: ContactEntry;
}

export interface DeleteContactRequest {
  contact: ContactEntry;
}

export interface ContactListResponse {
  contacts: ContactEntry[];
}

export interface MailboxConfiguration {
  mailboxBaseUrl: string;
  privateKey: EddsaPrivateKeyString;
  privateEncryptionKey: string;
  expiration: Timestamp;
  payUri?: TalerUri;
}

export const codecForMailboxConfiguration = (): Codec<MailboxConfiguration> =>
  buildCodecForObject<MailboxConfiguration>()
    .property("mailboxBaseUrl", codecForString())
    .property("privateEncryptionKey", codecForString())
    .property("privateKey", codecForEddsaPrivateKey())
    .property("expiration", codecForTimestamp)
    .build("MailboxConfiguration");

export interface SendTalerUriMailboxMessageRequest {
  contact: ContactEntry;
  talerUri: string;
}

export interface AddMailboxMessageRequest {
  message: MailboxMessageRecord;
}

export interface DeleteMailboxMessageRequest {
  message: MailboxMessageRecord;
}

export interface MailboxMessagesResponse {
  messages: MailboxMessageRecord[];
}

export const codecForContactEntry = (): Codec<ContactEntry> =>
  buildCodecForObject<ContactEntry>()
    .property("alias", codecForString())
    .property("aliasType", codecForString())
    .property("mailboxBaseUri", codecForString())
    .property("mailboxAddress", codecForString())
    .property("source", codecForString())
    .build("ContactListItem");

export const codecForAddContactRequest = (): Codec<AddContactRequest> =>
  buildCodecForObject<AddContactRequest>()
    .property("contact", codecForContactEntry())
    .build("AddContactRequest");

export const codecForDeleteContactRequest = (): Codec<DeleteContactRequest> =>
  buildCodecForObject<DeleteContactRequest>()
    .property("contact", codecForContactEntry())
    .build("DeleteContactRequest");

export const codecForAddMailboxMessageRequest =
  (): Codec<AddMailboxMessageRequest> =>
    buildCodecForObject<AddMailboxMessageRequest>()
      .property("message", codecForAny())
      .build("AddContactRequest");

export const codecForDeleteMailboxMessageRequest =
  (): Codec<DeleteMailboxMessageRequest> =>
    buildCodecForObject<DeleteMailboxMessageRequest>()
      .property("message", codecForAny())
      .build("DeleteContactRequest");

export const codecForSendTalerUriMailboxMessageRequest =
  (): Codec<SendTalerUriMailboxMessageRequest> =>
    buildCodecForObject<SendTalerUriMailboxMessageRequest>()
      .property("contact", codecForContactEntry())
      .property("talerUri", codecForString())
      .build("SendTalerUriMailboxMessageRequest");

export interface WalletCoreVersion {
  implementationSemver: string;
  implementationGitHash: string;

  /**
   * Wallet-core protocol version supported by this implementation
   * of the API ("server" version).
   */
  version: string;
  exchange: string;
  merchant: string;

  bankIntegrationApiRange: string;
  bankConversionApiRange: string;
  corebankApiRange: string;

  /**
   * @deprecated as bank was split into multiple APIs with separate versioning
   */
  bank: string;

  /**
   * @deprecated
   */
  hash: string | undefined;

  /**
   * @deprecated will be removed
   */
  devMode: boolean;
}

export interface WalletBankAccountInfo {
  bankAccountId: string;

  paytoUri: string;

  /**
   * Did we previously complete a KYC process for this bank account?
   *
   * @deprecated no enough information since the kyc can be completed for one exchange but not for another
   * https://bugs.gnunet.org/view.php?id=9696
   */
  kycCompleted: boolean;

  /**
   * Currencies supported by the bank, if known.
   */
  currencies: string[] | undefined;

  label: string | undefined;
}

export interface ListBankAccountsResponse {
  accounts: WalletBankAccountInfo[];
}

export type GetBankAccountByIdResponse = WalletBankAccountInfo;

export interface GetBankAccountByIdRequest {
  bankAccountId: string;
}

export const codecForGetBankAccountByIdRequest =
  (): Codec<GetBankAccountByIdRequest> =>
    buildCodecForObject<GetBankAccountByIdRequest>()
      .property("bankAccountId", codecForString())
      .build("GetBankAccountByIdRequest");

/**
 * Wire fee for one wire method
 */
export interface WireFee {
  /**
   * Fee for wire transfers.
   */
  wireFee: AmountString;

  /**
   * Fees to close and refund a reserve.
   */
  closingFee: AmountString;

  /**
   * Start date of the fee.
   */
  startStamp: TalerProtocolTimestamp;

  /**
   * End date of the fee.
   */
  endStamp: TalerProtocolTimestamp;

  /**
   * Signature made by the exchange master key.
   */
  sig: string;
}

export type WireFeeMap = { [wireMethod: string]: WireFee[] };

export interface WireInfo {
  feesForType: WireFeeMap;
  accounts: ExchangeWireAccount[];
}

export interface ExchangeGlobalFees {
  startDate: TalerProtocolTimestamp;
  endDate: TalerProtocolTimestamp;

  historyFee: AmountString;
  accountFee: AmountString;
  purseFee: AmountString;

  historyTimeout: TalerProtocolDuration;
  purseTimeout: TalerProtocolDuration;

  purseLimit: number;

  signature: string;
}

const codecForWireFee = (): Codec<WireFee> =>
  buildCodecForObject<WireFee>()
    .property("sig", codecForString())
    .property("wireFee", codecForAmountString())
    .property("closingFee", codecForAmountString())
    .property("startStamp", codecForTimestamp)
    .property("endStamp", codecForTimestamp)
    .build("codecForWireFee");

const codecForWireInfo = (): Codec<WireInfo> =>
  buildCodecForObject<WireInfo>()
    .property("feesForType", codecForMap(codecForList(codecForWireFee())))
    .property("accounts", codecForList(codecForExchangeWireAccount()))
    .build("codecForWireInfo");

export interface DenominationInfo {
  /**
   * Value of one coin of the denomination.
   */
  value: AmountString;

  /**
   * Hash of the denomination public key.
   * Stored in the database for faster lookups.
   */
  denomPubHash: string;

  denomPub: DenominationPubKey;

  /**
   * Fee for withdrawing.
   */
  feeWithdraw: AmountString;

  /**
   * Fee for depositing.
   */
  feeDeposit: AmountString;

  /**
   * Fee for refreshing.
   */
  feeRefresh: AmountString;

  /**
   * Fee for refunding.
   */
  feeRefund: AmountString;

  /**
   * Validity start date of the denomination.
   */
  stampStart: TalerProtocolTimestamp;

  /**
   * Date after which the currency can't be withdrawn anymore.
   */
  stampExpireWithdraw: TalerProtocolTimestamp;

  /**
   * Date after the denomination officially doesn't exist anymore.
   */
  stampExpireLegal: TalerProtocolTimestamp;

  /**
   * Data after which coins of this denomination can't be deposited anymore.
   */
  stampExpireDeposit: TalerProtocolTimestamp;

  exchangeBaseUrl: string;
}

export type DenomOperation = "deposit" | "withdraw" | "refresh" | "refund";
export type DenomOperationMap<T> = { [op in DenomOperation]: T };

export interface FeeDescription {
  group: string;
  from: AbsoluteTime;
  until: AbsoluteTime;
  fee?: AmountString;
}

export interface FeeDescriptionPair {
  group: string;
  from: AbsoluteTime;
  until: AbsoluteTime;
  left?: AmountString;
  right?: AmountString;
}

export interface TimePoint<T> {
  id: string;
  group: string;
  fee: AmountString;
  type: "start" | "end";
  moment: AbsoluteTime;
  denom: T;
}

export interface ExchangeFullDetails {
  exchangeBaseUrl: string;
  currency: string;
  paytoUris: string[];
  auditors: ExchangeAuditor[];
  wireInfo: WireInfo;
  denomFees: DenomOperationMap<FeeDescription[]>;
  transferFees: Record<string, FeeDescription[]>;
  globalFees: FeeDescription[];
}

export enum ExchangeTosStatus {
  Pending = "pending",
  Proposed = "proposed",
  Accepted = "accepted",
  MissingTos = "missing-tos",
}

export enum ExchangeEntryStatus {
  Preset = "preset",
  Ephemeral = "ephemeral",
  Used = "used",
}

export const codecForExchangeEntryStatus = (): Codec<ExchangeEntryStatus> =>
  codecForEither(
    codecForConstString(ExchangeEntryStatus.Ephemeral),
    codecForConstString(ExchangeEntryStatus.Preset),
    codecForConstString(ExchangeEntryStatus.Used),
  );

export enum ExchangeUpdateStatus {
  Initial = "initial",
  InitialUpdate = "initial-update",
  Suspended = "suspended",
  UnavailableUpdate = "unavailable-update",
  Ready = "ready",
  ReadyUpdate = "ready-update",
  OutdatedUpdate = "outdated-update",
}

export enum ExchangeWalletKycStatus {
  Done = "done",
  /**
   * Wallet needs to request KYC status.
   */
  LegiInit = "legi-init",
  /**
   * User requires KYC or AML.
   */
  Legi = "legi",
}

export interface OperationErrorInfo {
  error: TalerErrorDetail;
}

export interface ShortExchangeListItem {
  exchangeBaseUrl: string;
}

/**
 * Info about an exchange entry in the wallet.
 */
export interface ExchangeListItem {
  exchangeBaseUrl: string;
  masterPub: string | undefined;
  currency: string;
  paytoUris: string[];
  tosStatus: ExchangeTosStatus;
  exchangeEntryStatus: ExchangeEntryStatus;
  exchangeUpdateStatus: ExchangeUpdateStatus;
  ageRestrictionOptions: number[];

  walletKycStatus?: ExchangeWalletKycStatus;
  walletKycReservePub?: string;
  walletKycAccessToken?: string;
  walletKycUrl?: string;

  /** Threshold that we've requested to satisfy. */
  walletKycRequestedThreshold?: string;

  /**
   * P2P payments are disabled with this exchange
   * (e.g. because no global fees are configured).
   */
  peerPaymentsDisabled: boolean;

  /** Set to true if this exchange doesn't charge any fees. */
  noFees: boolean;

  /** Most general scope that the exchange is a part of. */
  scopeInfo: ScopeInfo;

  /**
   * Instructs wallets to use certain bank-specific
   * language (for buttons) and/or other UI/UX customization
   * for compliance with the rules of that bank.
   */
  bankComplianceLanguage?: string;

  lastUpdateTimestamp: TalerPreciseTimestamp | undefined;

  /**
   * Information about the last error that occurred when trying
   * to update the exchange info.
   */
  lastUpdateErrorInfo?: OperationErrorInfo;

  unavailableReason?: TalerErrorDetail;
}

export interface ContactEntry {
  /**
   * Contact alias
   */
  alias: string;

  /**
   * Alias type
   */
  aliasType: string;

  /**
   * mailbox URI
   */
  mailboxBaseUri: string;

  /**
   * mailbox identity
   */
  mailboxAddress: HashCodeString;

  /**
   * The source of this contact
   * may be a URI
   */
  source: string;
}

/**
 * Record metadata for mailbox messages
 */
export interface MailboxMessageRecord {
  // Origin mailbox
  originMailboxBaseUrl: string;

  // Time of download
  downloadedAt: Timestamp;

  // Taler URI in message
  talerUri: string;
}

const codecForAuditorDenomSig = (): Codec<AuditorDenomSig> =>
  buildCodecForObject<AuditorDenomSig>()
    .property("denom_pub_h", codecForString())
    .property("auditor_sig", codecForString())
    .build("AuditorDenomSig");

const codecForExchangeAuditor = (): Codec<ExchangeAuditor> =>
  buildCodecForObject<ExchangeAuditor>()
    .property("auditor_pub", codecForString())
    .property("auditor_url", codecForString())
    .property("denomination_keys", codecForList(codecForAuditorDenomSig()))
    .build("codecForExchangeAuditor");

export const codecForFeeDescriptionPair = (): Codec<FeeDescriptionPair> =>
  buildCodecForObject<FeeDescriptionPair>()
    .property("group", codecForString())
    .property("from", codecForAbsoluteTime)
    .property("until", codecForAbsoluteTime)
    .property("left", codecOptional(codecForAmountString()))
    .property("right", codecOptional(codecForAmountString()))
    .build("FeeDescriptionPair");

export const codecForFeeDescription = (): Codec<FeeDescription> =>
  buildCodecForObject<FeeDescription>()
    .property("group", codecForString())
    .property("from", codecForAbsoluteTime)
    .property("until", codecForAbsoluteTime)
    .property("fee", codecOptional(codecForAmountString()))
    .build("FeeDescription");

export const codecForFeesByOperations = (): Codec<
  DenomOperationMap<FeeDescription[]>
> =>
  buildCodecForObject<DenomOperationMap<FeeDescription[]>>()
    .property("deposit", codecForList(codecForFeeDescription()))
    .property("withdraw", codecForList(codecForFeeDescription()))
    .property("refresh", codecForList(codecForFeeDescription()))
    .property("refund", codecForList(codecForFeeDescription()))
    .build("DenomOperationMap");

export const codecForExchangeFullDetails = (): Codec<ExchangeFullDetails> =>
  buildCodecForObject<ExchangeFullDetails>()
    .property("currency", codecForString())
    .property("exchangeBaseUrl", codecForCanonBaseUrl())
    .property("paytoUris", codecForList(codecForString()))
    .property("auditors", codecForList(codecForExchangeAuditor()))
    .property("wireInfo", codecForWireInfo())
    .property("denomFees", codecForFeesByOperations())
    .property(
      "transferFees",
      codecForMap(codecForList(codecForFeeDescription())),
    )
    .property("globalFees", codecForList(codecForFeeDescription()))
    .build("ExchangeFullDetails");

export const codecForExchangeListItem = (): Codec<ExchangeListItem> =>
  buildCodecForObject<ExchangeListItem>()
    .property("currency", codecForString())
    .property("exchangeBaseUrl", codecForCanonBaseUrl())
    .property("masterPub", codecOptional(codecForString()))
    .property("paytoUris", codecForList(codecForString()))
    .property("tosStatus", codecForAny())
    .property("exchangeEntryStatus", codecForAny())
    .property("exchangeUpdateStatus", codecForAny())
    .property("ageRestrictionOptions", codecForList(codecForNumber()))
    .property("scopeInfo", codecForScopeInfo())
    .property("lastUpdateErrorInfo", codecForAny())
    .property("lastUpdateTimestamp", codecOptional(codecForPreciseTimestamp))
    .property("noFees", codecForBoolean())
    .property("peerPaymentsDisabled", codecForBoolean())
    .property("bankComplianceLanguage", codecOptional(codecForString()))
    .build("ExchangeListItem");

export const codecForExchangesListResponse = (): Codec<ExchangesListResponse> =>
  buildCodecForObject<ExchangesListResponse>()
    .property("exchanges", codecForList(codecForExchangeListItem()))
    .build("ExchangesListResponse");

export interface AcceptManualWithdrawalResult {
  /**
   * Payto URIs that can be used to fund the withdrawal.
   *
   * @deprecated in favor of withdrawalAccountsList
   */
  exchangePaytoUris: string[];

  /**
   * Public key of the newly created reserve.
   */
  reservePub: string;

  withdrawalAccountsList: WithdrawalExchangeAccountDetails[];

  transactionId: TransactionIdStr;
}

export interface WithdrawalDetailsForAmount {
  /**
   * Exchange base URL for the withdrawal.
   */
  exchangeBaseUrl: string;

  /**
   * Amount that the user will transfer to the exchange.
   */
  amountRaw: AmountString;

  /**
   * Amount that will be added to the user's wallet balance.
   */
  amountEffective: AmountString;

  /**
   * Number of coins that would be used for withdrawal.
   *
   * The UIs should warn if this number is too high (roughly at >100).
   */
  numCoins: number;

  /**
   * Ways to pay the exchange, including accounts that require currency conversion.
   */
  withdrawalAccountsList: WithdrawalExchangeAccountDetails[];

  /**
   * If the exchange supports age-restricted coins it will return
   * the array of ages.
   */
  ageRestrictionOptions?: number[];

  /**
   * Scope info of the currency withdrawn.
   */
  scopeInfo: ScopeInfo;

  /**
   * KYC soft limit.
   *
   * Withdrawals over that amount will require KYC.
   */
  kycSoftLimit?: AmountString;

  /**
   * KYC soft limits.
   *
   * Withdrawals over that amount will be denied.
   */
  kycHardLimit?: AmountString;

  /**
   * Ways to pay the exchange.
   *
   * @deprecated in favor of withdrawalAccountsList
   */
  paytoUris: string[];

  /**
   * Did the user accept the current version of the exchange's
   * terms of service?
   *
   * @deprecated the client should query the exchange entry instead
   */
  tosAccepted: boolean;
}

export interface DenomSelItem {
  denomPubHash: string;
  count: number;
  /**
   * Number of denoms/planchets to skip, because
   * a re-denomination effectively deleted them.
   *
   * For denom revocations, this equals count.
   * But for re-denominations to a smaller withdrawal
   * amounts, skip < count is possible.
   */
  skip?: number;
}

/**
 * Selected denominations with some extra info.
 */
export interface DenomSelectionState {
  totalCoinValue: AmountString;
  totalWithdrawCost: AmountString;
  selectedDenoms: DenomSelItem[];
  hasDenomWithAgeRestriction: boolean;
}

/**
 * Information about what will happen doing a withdrawal.
 *
 * Sent to the wallet frontend to be rendered and shown to the user.
 */
export interface ExchangeWithdrawalDetails {
  exchangePaytoUris: string[];

  /**
   * Filtered wire info to send to the bank.
   */
  exchangeWireAccounts: string[];

  exchangeCreditAccountDetails: WithdrawalExchangeAccountDetails[];

  /**
   * Selected denominations for withdraw.
   */
  selectedDenoms: DenomSelectionState;

  /**
   * Did the user already accept the current terms of service for the exchange?
   */
  termsOfServiceAccepted: boolean;

  /**
   * Amount that will be subtracted from the reserve's balance.
   */
  withdrawalAmountRaw: AmountString;

  /**
   * Amount that will actually be added to the wallet's balance.
   */
  withdrawalAmountEffective: AmountString;

  /**
   * If the exchange supports age-restricted coins it will return
   * the array of ages.
   *
   */
  ageRestrictionOptions?: number[];

  scopeInfo: ScopeInfo;

  /**
   * KYC soft limit.
   *
   * Withdrawals over that amount will require KYC.
   */
  kycSoftLimit?: AmountString;

  /**
   * KYC soft limits.
   *
   * Withdrawals over that amount will be denied.
   */
  kycHardLimit?: AmountString;
}

export interface GetExchangeTosResult {
  /**
   * Markdown version of the current ToS.
   */
  content: string;

  /**
   * Version tag of the current ToS.
   */
  currentEtag: string;

  /**
   * Version tag of the last ToS that the user has accepted,
   * if any.
   */
  acceptedEtag: string | undefined;

  /**
   * Accepted content type
   */
  contentType: string;

  /**
   * Language of the returned content.
   *
   * If missing, language is unknown.
   */
  contentLanguage: string | undefined;

  /**
   * Available languages as advertised by the exchange.
   */
  tosAvailableLanguages: string[];

  tosStatus: ExchangeTosStatus;
}

export interface TestPayArgs {
  merchantBaseUrl: string;
  merchantAuthToken?: string;
  amount: AmountString;
  summary: string;
  forcedCoinSel?: ForcedCoinSel;
}

export const codecForTestPayArgs = (): Codec<TestPayArgs> =>
  buildCodecForObject<TestPayArgs>()
    .property("merchantBaseUrl", codecForCanonBaseUrl())
    .property("merchantAuthToken", codecOptional(codecForString()))
    .property("amount", codecForAmountString())
    .property("summary", codecForString())
    .property("forcedCoinSel", codecForAny())
    .build("TestPayArgs");

export interface IntegrationTestArgs {
  exchangeBaseUrl: string;
  corebankApiBaseUrl: string;
  merchantBaseUrl: string;
  merchantAuthToken?: string;
  amountToWithdraw: AmountString;
  amountToSpend: AmountString;
}

export const codecForIntegrationTestArgs = (): Codec<IntegrationTestArgs> =>
  buildCodecForObject<IntegrationTestArgs>()
    .property("exchangeBaseUrl", codecForCanonBaseUrl())
    .property("merchantBaseUrl", codecForCanonBaseUrl())
    .property("merchantAuthToken", codecOptional(codecForString()))
    .property("amountToSpend", codecForAmountString())
    .property("amountToWithdraw", codecForAmountString())
    .property("corebankApiBaseUrl", codecForCanonBaseUrl())
    .build("IntegrationTestArgs");

export interface IntegrationTestV2Args {
  exchangeBaseUrl: string;
  corebankApiBaseUrl: string;
  merchantBaseUrl: string;
  merchantAuthToken?: string;
}

export const codecForIntegrationTestV2Args = (): Codec<IntegrationTestV2Args> =>
  buildCodecForObject<IntegrationTestV2Args>()
    .property("exchangeBaseUrl", codecForCanonBaseUrl())
    .property("merchantBaseUrl", codecForCanonBaseUrl())
    .property("merchantAuthToken", codecOptional(codecForString()))
    .property("corebankApiBaseUrl", codecForCanonBaseUrl())
    .build("IntegrationTestV2Args");

export interface GetExchangeEntryByUrlRequest {
  exchangeBaseUrl: string;
}

export const codecForGetExchangeDetailedInfoRequest =
  (): Codec<GetExchangeDetailedInfoRequest> =>
    buildCodecForObject<GetExchangeDetailedInfoRequest>()
      .property("exchangeBaseUrl", codecForCanonBaseUrl())
      .build("GetExchangeDetailedInfoRequest");

export interface GetExchangeDetailedInfoRequest {
  exchangeBaseUrl: string;
}

export const codecForGetExchangeEntryByUrlRequest =
  (): Codec<GetExchangeEntryByUrlRequest> =>
    buildCodecForObject<GetExchangeEntryByUrlRequest>()
      .property("exchangeBaseUrl", codecForCanonBaseUrl())
      .build("GetExchangeEntryByUrlRequest");

export type GetExchangeEntryByUrlResponse = ExchangeListItem;

export interface AddExchangeRequest {
  /**
   * Either an http(s) exchange base URL or
   * a taler://add-exchange/ URI.
   */
  uri?: string;

  /**
   * Only ephemerally add the exchange.
   */
  ephemeral?: boolean;

  /**
   * Allow passing incomplete URLs.  The wallet will try to complete
   * the URL and throw an error if completion is not possible.
   */
  allowCompletion?: boolean;

  /**
   * @deprecated use a separate API call to start a forced exchange update instead
   */
  forceUpdate?: boolean;

  /**
   * @deprecated Use {@link uri} instead
   */
  exchangeBaseUrl?: string;
}

export interface AddExchangeResponse {
  /**
   * Base URL of the exchange that was added to the wallet.
   */
  exchangeBaseUrl: string;
}

export const codecForAddExchangeRequest = (): Codec<AddExchangeRequest> =>
  buildCodecForObject<AddExchangeRequest>()
    .property("allowCompletion", codecOptional(codecForBoolean()))
    .property("exchangeBaseUrl", codecOptional(codecForCanonBaseUrl()))
    .property("uri", codecOptional(codecForString()))
    .property("forceUpdate", codecOptional(codecForBoolean()))
    .property("ephemeral", codecOptional(codecForBoolean()))
    .build("AddExchangeRequest");

export interface UpdateExchangeEntryRequest {
  exchangeBaseUrl: string;
  force?: boolean;
}

export const codecForUpdateExchangeEntryRequest =
  (): Codec<UpdateExchangeEntryRequest> =>
    buildCodecForObject<UpdateExchangeEntryRequest>()
      .property("exchangeBaseUrl", codecForCanonBaseUrl())
      .property("force", codecOptional(codecForBoolean()))
      .build("UpdateExchangeEntryRequest");

export interface GetExchangeResourcesRequest {
  exchangeBaseUrl: string;
}

export const codecForGetExchangeResourcesRequest =
  (): Codec<GetExchangeResourcesRequest> =>
    buildCodecForObject<GetExchangeResourcesRequest>()
      .property("exchangeBaseUrl", codecForCanonBaseUrl())
      .build("GetExchangeResourcesRequest");

export interface GetExchangeResourcesResponse {
  hasResources: boolean;
}

export interface DeleteExchangeRequest {
  exchangeBaseUrl: string;

  /**
   * Delete the exchange even if it's in use.
   */
  purge?: boolean;
}

export const codecForDeleteExchangeRequest = (): Codec<DeleteExchangeRequest> =>
  buildCodecForObject<DeleteExchangeRequest>()
    .property("exchangeBaseUrl", codecForCanonBaseUrl())
    .property("purge", codecOptional(codecForBoolean()))
    .build("DeleteExchangeRequest");

export interface ForceExchangeUpdateRequest {
  exchangeBaseUrl: string;
}

export const codecForForceExchangeUpdateRequest =
  (): Codec<AddExchangeRequest> =>
    buildCodecForObject<AddExchangeRequest>()
      .property("exchangeBaseUrl", codecForCanonBaseUrl())
      .build("AddExchangeRequest");

export interface GetExchangeTosRequest {
  exchangeBaseUrl: string;
  acceptedFormat?: string[];
  acceptLanguage?: string;
}

export const codecForGetExchangeTosRequest = (): Codec<GetExchangeTosRequest> =>
  buildCodecForObject<GetExchangeTosRequest>()
    .property("exchangeBaseUrl", codecForCanonBaseUrl())
    .property("acceptedFormat", codecOptional(codecForList(codecForString())))
    .property("acceptLanguage", codecOptional(codecForString()))
    .build("GetExchangeTosRequest");

export interface AcceptManualWithdrawalRequest {
  exchangeBaseUrl: string;
  amount: AmountString;
  restrictAge?: number;

  /**
   * Instead of generating a fresh, random reserve key pair,
   * use the provided reserve private key.
   *
   * Use with caution.  Usage of this field may be restricted
   * to developer mode.
   */
  forceReservePriv?: EddsaPrivateKeyString;
}

export const codecForAcceptManualWithdrawalRequest =
  (): Codec<AcceptManualWithdrawalRequest> =>
    buildCodecForObject<AcceptManualWithdrawalRequest>()
      .property("exchangeBaseUrl", codecForCanonBaseUrl())
      .property("amount", codecForAmountString())
      .property("restrictAge", codecOptional(codecForNumber()))
      .property("forceReservePriv", codecOptional(codecForEddsaPrivateKey()))
      .build("AcceptManualWithdrawalRequest");

export interface GetWithdrawalDetailsForAmountRequest {
  exchangeBaseUrl?: string;

  /**
   * Specify currency scope for the withdrawal.
   *
   * May only be used when exchangeBaseUrl is not specified.
   */
  restrictScope?: ScopeInfo;

  amount: AmountString;

  restrictAge?: number;

  /**
   * ID provided by the client to cancel the request.
   *
   * If the same request is made again with the same clientCancellationId,
   * all previous requests are cancelled.
   *
   * The cancelled request will receive an error response with
   * an error code that indicates the cancellation.
   *
   * The cancellation is best-effort, responses might still arrive.
   */
  clientCancellationId?: string;
}

export interface PrepareBankIntegratedWithdrawalRequest {
  talerWithdrawUri: string;
}

export const codecForPrepareBankIntegratedWithdrawalRequest =
  (): Codec<PrepareBankIntegratedWithdrawalRequest> =>
    buildCodecForObject<PrepareBankIntegratedWithdrawalRequest>()
      .property("talerWithdrawUri", codecForString())
      .build("PrepareBankIntegratedWithdrawalRequest");

export interface PrepareBankIntegratedWithdrawalResponse {
  transactionId: TransactionIdStr;
  info: WithdrawUriInfoResponse;
}

export interface ConfirmWithdrawalRequest {
  transactionId: string;
  exchangeBaseUrl: string;
  amount: AmountString | undefined;
  forcedDenomSel?: ForcedDenomSel;
  restrictAge?: number;
}

export const codecForConfirmWithdrawalRequestRequest =
  (): Codec<ConfirmWithdrawalRequest> =>
    buildCodecForObject<ConfirmWithdrawalRequest>()
      .property("transactionId", codecForString())
      .property("amount", codecForAmountString())
      .property("exchangeBaseUrl", codecForCanonBaseUrl())
      .property("forcedDenomSel", codecForAny())
      .property("restrictAge", codecOptional(codecForNumber()))
      .build("ConfirmWithdrawalRequest");

export interface AcceptBankIntegratedWithdrawalRequest {
  talerWithdrawUri: string;
  exchangeBaseUrl: string;
  forcedDenomSel?: ForcedDenomSel;
  /**
   * Amount to withdraw.
   * If the bank's withdrawal operation uses a fixed amount,
   * this field must either be left undefined or its value must match
   * the amount from the withdrawal operation.
   */
  amount?: AmountString;
  restrictAge?: number;
}

export const codecForAcceptBankIntegratedWithdrawalRequest =
  (): Codec<AcceptBankIntegratedWithdrawalRequest> =>
    buildCodecForObject<AcceptBankIntegratedWithdrawalRequest>()
      .property("exchangeBaseUrl", codecForCanonBaseUrl())
      .property("talerWithdrawUri", codecForString())
      .property("forcedDenomSel", codecForAny())
      .property("amount", codecOptional(codecForAmountString()))
      .property("restrictAge", codecOptional(codecForNumber()))
      .build("AcceptBankIntegratedWithdrawalRequest");

export const codecForGetWithdrawalDetailsForAmountRequest =
  (): Codec<GetWithdrawalDetailsForAmountRequest> =>
    buildCodecForObject<GetWithdrawalDetailsForAmountRequest>()
      .property("exchangeBaseUrl", codecOptional(codecForCanonBaseUrl()))
      .property("restrictScope", codecOptional(codecForScopeInfo()))
      .property("amount", codecForAmountString())
      .property("restrictAge", codecOptional(codecForNumber()))
      .property("clientCancellationId", codecOptional(codecForString()))
      .build("GetWithdrawalDetailsForAmountRequest");

export interface AcceptExchangeTosRequest {
  exchangeBaseUrl: string;
}

export const codecForAcceptExchangeTosRequest =
  (): Codec<AcceptExchangeTosRequest> =>
    buildCodecForObject<AcceptExchangeTosRequest>()
      .property("exchangeBaseUrl", codecForCanonBaseUrl())
      .build("AcceptExchangeTosRequest");

export interface ForgetExchangeTosRequest {
  exchangeBaseUrl: string;
}

export const codecForForgetExchangeTosRequest =
  (): Codec<ForgetExchangeTosRequest> =>
    buildCodecForObject<ForgetExchangeTosRequest>()
      .property("exchangeBaseUrl", codecForCanonBaseUrl())
      .build("ForgetExchangeTosRequest");

export interface AcceptRefundRequest {
  transactionId: TransactionIdStr;
}

export const codecForApplyRefundRequest = (): Codec<AcceptRefundRequest> =>
  buildCodecForObject<AcceptRefundRequest>()
    .property("transactionId", codecForTransactionIdStr())
    .build("AcceptRefundRequest");

export interface ApplyRefundFromPurchaseIdRequest {
  purchaseId: string;
}

export const codecForApplyRefundFromPurchaseIdRequest =
  (): Codec<ApplyRefundFromPurchaseIdRequest> =>
    buildCodecForObject<ApplyRefundFromPurchaseIdRequest>()
      .property("purchaseId", codecForString())
      .build("ApplyRefundFromPurchaseIdRequest");

export interface GetWithdrawalDetailsForUriRequest {
  talerWithdrawUri: string;
  /**
   * @deprecated not used
   */
  restrictAge?: number;
}

export const codecForGetWithdrawalDetailsForUri =
  (): Codec<GetWithdrawalDetailsForUriRequest> =>
    buildCodecForObject<GetWithdrawalDetailsForUriRequest>()
      .property("talerWithdrawUri", codecForString())
      .property("restrictAge", codecOptional(codecForNumber()))
      .build("GetWithdrawalDetailsForUriRequest");

export interface ListBankAccountsRequest {
  currency?: string;
}

export const codecForListBankAccounts = (): Codec<ListBankAccountsRequest> =>
  buildCodecForObject<ListBankAccountsRequest>()
    .property("currency", codecOptional(codecForString()))
    .build("ListBankAccountsRequest");

export interface AddBankAccountRequest {
  /**
   * Payto URI of the bank account that should be added.
   */
  paytoUri: string;

  /**
   * Human-readable label for the account.
   */
  label: string;

  /**
   * Currencies supported by the bank (if known).
   */
  currencies?: string[] | undefined;

  /**
   * Bank account that this new account should replace.
   */
  replaceBankAccountId?: string;
}

export interface AddBankAccountResponse {
  /**
   * Identifier of the added bank account.
   */
  bankAccountId: string;
}

export const codecForAddBankAccountRequest = (): Codec<AddBankAccountRequest> =>
  buildCodecForObject<AddBankAccountRequest>()
    .property("replaceBankAccountId", codecOptional(codecForString()))
    .property("paytoUri", codecForString())
    .property("label", codecForString())
    .property("currencies", codecOptional(codecForList(codecForString())))
    .build("AddBankAccountRequest");

export interface ForgetBankAccountRequest {
  bankAccountId: string;
}

export const codecForForgetBankAccount = (): Codec<ForgetBankAccountRequest> =>
  buildCodecForObject<ForgetBankAccountRequest>()
    .property("bankAccountId", codecForString())
    .build("ForgetBankAccountsRequest");

export interface PreparePayRequest {
  talerPayUri: string;
}

export const codecForPreparePayRequest = (): Codec<PreparePayRequest> =>
  buildCodecForObject<PreparePayRequest>()
    .property("talerPayUri", codecForString())
    .build("PreparePay");

export interface GetChoicesForPaymentRequest {
  transactionId: string;
  forcedCoinSel?: ForcedCoinSel;
}

export const codecForGetChoicesForPaymentRequest =
  (): Codec<GetChoicesForPaymentRequest> =>
    buildCodecForObject<GetChoicesForPaymentRequest>()
      .property("transactionId", codecForString())
      .property("forcedCoinSel", codecForAny())
      .build("GetChoicesForPaymentRequest");

export enum ChoiceSelectionDetailType {
  PaymentPossible = "payment-possible",
  InsufficientBalance = "insufficient-balance",
}

export type ChoiceSelectionDetail =
  | ChoiceSelectionDetailPaymentPossible
  | ChoiceSelectionDetailInsufficientBalance;

export interface ChoiceSelectionDetailPaymentPossible {
  status: ChoiceSelectionDetailType.PaymentPossible;
  amountRaw: AmountString;
  amountEffective: AmountString;
  tokenDetails?: PaymentTokenAvailabilityDetails;
}

export interface ChoiceSelectionDetailInsufficientBalance {
  status: ChoiceSelectionDetailType.InsufficientBalance;
  amountRaw: AmountString;
  balanceDetails?: PaymentInsufficientBalanceDetails;
  tokenDetails?: PaymentTokenAvailabilityDetails;
}

export type GetChoicesForPaymentResult = {
  /**
   * Details for all choices in the contract.
   *
   * The index in this array corresponds to the choice
   * index in the original contract v1. For contract v0
   * orders, it will only contain a single choice with no
   * inputs/outputs.
   */
  choices: ChoiceSelectionDetail[];

  /**
   * Index of the choice in @e choices array to present
   * to the user as default.
   *
   * Won´t be set if no default selection is configured
   * or no choice is payable, otherwise, it will always
   * be 0 for v0 orders.
   */
  defaultChoiceIndex?: number;

  /**
   * Whether the choice referenced by @e automaticExecutableIndex
   * should be confirmed automatically without
   * user interaction.
   *
   * If true, the wallet should call `confirmPay'
   * immediately afterwards, if false, the user
   * should be first prompted to select and
   * confirm a choice. Undefined when no choices
   * are payable.
   */
  automaticExecution?: boolean;

  /**
   * Index of the choice that would be set to automatically
   * execute if the choice was payable. When @e automaticExecution
   * is set to true, the payment should be confirmed with this
   * choice index without user interaction.
   */
  automaticExecutableIndex?: number;

  /**
   * Data extracted from the contract terms that
   * is relevant for payment processing in the wallet.
   */
  contractTerms: MerchantContractTerms;
};

export interface SharePaymentRequest {
  merchantBaseUrl: string;
  orderId: string;
}
export const codecForSharePaymentRequest = (): Codec<SharePaymentRequest> =>
  buildCodecForObject<SharePaymentRequest>()
    .property("merchantBaseUrl", codecForCanonBaseUrl())
    .property("orderId", codecForString())
    .build("SharePaymentRequest");

export interface SharePaymentResult {
  privatePayUri: string;
}
export const codecForSharePaymentResult = (): Codec<SharePaymentResult> =>
  buildCodecForObject<SharePaymentResult>()
    .property("privatePayUri", codecForString())
    .build("SharePaymentResult");

export interface CheckPayTemplateRequest {
  talerPayTemplateUri: string;
}

export type CheckPayTemplateReponse = {
  templateDetails: WalletTemplateDetails;
  supportedCurrencies: string[];
};

export const codecForCheckPayTemplateRequest =
  (): Codec<CheckPayTemplateRequest> =>
    buildCodecForObject<CheckPayTemplateRequest>()
      .property("talerPayTemplateUri", codecForString())
      .build("CheckPayTemplateRequest");

export interface PreparePayTemplateRequest {
  talerPayTemplateUri: string;
  templateParams?: TemplateParams;
}

export const codecForPreparePayTemplateRequest =
  (): Codec<PreparePayTemplateRequest> =>
    buildCodecForObject<PreparePayTemplateRequest>()
      .property("talerPayTemplateUri", codecForString())
      .property("templateParams", codecForAny())
      .build("PreparePayTemplate");

export interface ConfirmPayRequest {
  transactionId: TransactionIdStr;
  useDonau?: boolean;
  sessionId?: string;
  forcedCoinSel?: ForcedCoinSel;

  /**
   * Whether token selection should be forced
   * e.g. use tokens with non-matching `expected_domains'
   *
   * Only applies to v1 orders.
   */
  forcedTokenSel?: boolean;

  /**
   * Only applies to v1 orders.
   */
  choiceIndex?: number;
}

export const codecForConfirmPayRequest = (): Codec<ConfirmPayRequest> =>
  buildCodecForObject<ConfirmPayRequest>()
    .property("transactionId", codecForTransactionIdStr())
    .property("sessionId", codecOptional(codecForString()))
    .property("forcedCoinSel", codecForAny())
    .property("forcedTokenSel", codecOptional(codecForBoolean()))
    .property("choiceIndex", codecOptional(codecForNumber()))
    .property("useDonau", codecOptional(codecForBoolean()))
    .build("ConfirmPay");

export interface ListDiscountsRequest {
  /**
   * Filter by hash of token issue public key.
   */
  tokenIssuePubHash?: string;

  /**
   * Filter by merchant base URL.
   */
  merchantBaseUrl?: string;
}

export interface ListDiscountsResponse {
  discounts: DiscountListDetail[];
}

export interface DiscountListDetail {
  /**
   * Hash of token family info.
   */
  tokenFamilyHash: string;

  /**
   * Hash of token issue public key.
   */
  tokenIssuePubHash: string;

  /**
   * URL of the merchant issuing the token.
   */
  merchantBaseUrl: string;

  /**
   * Human-readable name for the token family.
   */
  name: string;

  /**
   * Human-readable description for the token family.
   */
  description: string;

  /**
   * Optional map from IETF BCP 47 language tags to localized descriptions.
   */
  descriptionI18n: any | undefined;

  /**
   * Start time of the token's validity period.
   */
  validityStart: Timestamp;

  /**
   * End time of the token's validity period.
   */
  validityEnd: Timestamp;

  /**
   * Number of tokens available to use.
   */
  tokensAvailable: number;
}

export interface DeleteDiscountRequest {
  /**
   * Hash of token family info.
   */
  tokenFamilyHash: string;
}

export type ListSubscriptionsRequest = ListDiscountsRequest;

export interface ListSubscriptionsResponse {
  subscriptions: SubscriptionListDetail[];
}

export type SubscriptionListDetail = Omit<
  DiscountListDetail,
  "tokensAvailable"
>;

export interface DeleteSubscriptionRequest {
  /**
   * Hash of token family info.
   */
  tokenFamilyHash: string;
}

export const codecForListDiscountsRequest = (): Codec<ListDiscountsRequest> =>
  buildCodecForObject<ListDiscountsRequest>()
    .property("tokenIssuePubHash", codecOptional(codecForString()))
    .property("merchantBaseUrl", codecOptional(codecForCanonBaseUrl()))
    .build("ListDiscounts");

export const codecForDeleteDiscountRequest = (): Codec<DeleteDiscountRequest> =>
  buildCodecForObject<DeleteDiscountRequest>()
    .property("tokenFamilyHash", codecForString())
    .build("DeleteDiscount");

export const codecForListSubscriptionsRequest =
  (): Codec<ListSubscriptionsRequest> =>
    buildCodecForObject<ListSubscriptionsRequest>()
      .property("tokenIssuePubHash", codecOptional(codecForString()))
      .property("merchantBaseUrl", codecOptional(codecForCanonBaseUrl()))
      .build("ListSubscriptions");

export const codecForDeleteSubscriptionRequest =
  (): Codec<DeleteSubscriptionRequest> =>
    buildCodecForObject<DeleteSubscriptionRequest>()
      .property("tokenFamilyHash", codecForString())
      .build("DeleteSubscription");

export interface CoreApiRequestEnvelope {
  id: string;
  operation: string;
  args: unknown;
}

export type CoreApiResponse = CoreApiResponseSuccess | CoreApiResponseError;

export type CoreApiMessageEnvelope = CoreApiResponse | CoreApiNotification;

export interface CoreApiNotification {
  type: "notification";
  payload: unknown;
}

export interface CoreApiResponseSuccess {
  // To distinguish the message from notifications
  type: "response";
  operation: string;
  id: string;
  result: unknown;
}

export interface CoreApiResponseError {
  // To distinguish the message from notifications
  type: "error";
  operation: string;
  id: string;
  error: TalerErrorDetail;
}

export interface WithdrawTestBalanceRequest {
  /**
   * Amount to withdraw.
   */
  amount: AmountString;

  /**
   * Corebank API base URL.
   */
  corebankApiBaseUrl: string;

  /**
   * Exchange to use for withdrawal.
   */
  exchangeBaseUrl: string;

  /**
   * Force the usage of a particular denomination selection.
   *
   * Only useful for testing.
   */
  forcedDenomSel?: ForcedDenomSel;

  /**
   * If set to true, treat the account created during
   * the withdrawal as a foreign withdrawal account.
   */
  useForeignAccount?: boolean;
}

/**
 * Request to the crypto worker to make a sync signature.
 */
export interface MakeSyncSignatureRequest {
  accountPriv: string;
  oldHash: string | undefined;
  newHash: string;
}

/**
 * Planchet for a coin during refresh.
 */
export interface RefreshPlanchetInfo {
  /**
   * Public key for the coin.
   */
  coinPub: string;

  /**
   * Private key for the coin.
   */
  coinPriv: string;

  /**
   * Blinded public key.
   */
  coinEv: CoinEnvelope;

  coinEvHash: string;

  /**
   * Blinding key used.
   */
  blindingKey: string;

  maxAge: number;

  ageCommitmentProof?: AgeCommitmentProof;
}

/**
 * Strategy for loading recovery information.
 */
export enum RecoveryMergeStrategy {
  /**
   * Keep the local wallet root key, import and take over providers.
   */
  Ours = "ours",

  /**
   * Migrate to the wallet root key from the recovery information.
   */
  Theirs = "theirs",
}

/**
 * Load recovery information into the wallet.
 */
export interface RecoveryLoadRequest {
  recovery: BackupRecovery;
  strategy?: RecoveryMergeStrategy;
}

export const codecForWithdrawTestBalance =
  (): Codec<WithdrawTestBalanceRequest> =>
    buildCodecForObject<WithdrawTestBalanceRequest>()
      .property("amount", codecForAmountString())
      .property("exchangeBaseUrl", codecForCanonBaseUrl())
      .property("forcedDenomSel", codecForAny())
      .property("corebankApiBaseUrl", codecForCanonBaseUrl())
      .property("useForeignAccount", codecOptional(codecForBoolean()))
      .build("WithdrawTestBalanceRequest");

export interface SetCoinSuspendedRequest {
  coinPub: string;
  suspended: boolean;
}

export const codecForSetCoinSuspendedRequest =
  (): Codec<SetCoinSuspendedRequest> =>
    buildCodecForObject<SetCoinSuspendedRequest>()
      .property("coinPub", codecForString())
      .property("suspended", codecForBoolean())
      .build("SetCoinSuspendedRequest");

export interface RefreshCoinSpec {
  coinPub: string;
  amount?: AmountString;
}

export const codecForRefreshCoinSpec = (): Codec<RefreshCoinSpec> =>
  buildCodecForObject<RefreshCoinSpec>()
    .property("amount", codecForAmountString())
    .property("coinPub", codecForString())
    .build("ForceRefreshRequest");

export interface ForceRefreshRequest {
  refreshCoinSpecs: RefreshCoinSpec[];
}

export const codecForForceRefreshRequest = (): Codec<ForceRefreshRequest> =>
  buildCodecForObject<ForceRefreshRequest>()
    .property("refreshCoinSpecs", codecForList(codecForRefreshCoinSpec()))
    .build("ForceRefreshRequest");

export interface PrepareRefundRequest {
  talerRefundUri: string;
}

export interface StartRefundQueryForUriResponse {
  /**
   * Transaction id of the *payment* where the refund query was started.
   */
  transactionId: TransactionIdStr;
}

export const codecForPrepareRefundRequest = (): Codec<PrepareRefundRequest> =>
  buildCodecForObject<PrepareRefundRequest>()
    .property("talerRefundUri", codecForString())
    .build("PrepareRefundRequest");

export interface StartRefundQueryRequest {
  transactionId: TransactionIdStr;
}

export const codecForStartRefundQueryRequest =
  (): Codec<StartRefundQueryRequest> =>
    buildCodecForObject<StartRefundQueryRequest>()
      .property("transactionId", codecForTransactionIdStr())
      .build("StartRefundQueryRequest");

export interface FailTransactionRequest {
  transactionId: TransactionIdStr;
}

export const codecForFailTransactionRequest =
  (): Codec<FailTransactionRequest> =>
    buildCodecForObject<FailTransactionRequest>()
      .property("transactionId", codecForTransactionIdStr())
      .build("FailTransactionRequest");

export interface SuspendTransactionRequest {
  transactionId: TransactionIdStr;
}

export const codecForSuspendTransaction =
  (): Codec<SuspendTransactionRequest> =>
    buildCodecForObject<AbortTransactionRequest>()
      .property("transactionId", codecForTransactionIdStr())
      .build("SuspendTransactionRequest");

export interface ResumeTransactionRequest {
  transactionId: TransactionIdStr;
}

export const codecForResumeTransaction = (): Codec<ResumeTransactionRequest> =>
  buildCodecForObject<ResumeTransactionRequest>()
    .property("transactionId", codecForTransactionIdStr())
    .build("ResumeTransactionRequest");

export interface AbortTransactionRequest {
  transactionId: TransactionIdStr;
}

export interface FailTransactionRequest {
  transactionId: TransactionIdStr;
}

export const codecForAbortTransaction = (): Codec<AbortTransactionRequest> =>
  buildCodecForObject<AbortTransactionRequest>()
    .property("transactionId", codecForTransactionIdStr())
    .build("AbortTransactionRequest");

export interface DepositGroupFees {
  coin: AmountString;
  wire: AmountString;
  refresh: AmountString;
}

export interface CreateDepositGroupRequest {
  depositPaytoUri: string;

  /**
   * Amount to deposit (effective amount).
   */
  amount: AmountString;

  /**
   * Restrict the deposit to a certain scope.
   */
  restrictScope?: ScopeInfo;

  /**
   * Use a fixed merchant private key.
   */
  testingFixedPriv?: string;

  /**
   * Pre-allocated transaction ID.
   * Allows clients to easily handle notifications
   * that occur while the operation has been created but
   * before the creation request has returned.
   */
  transactionId?: TransactionIdStr;
}

export interface CheckDepositRequest {
  /**
   * Payto URI to identify the (bank) account that the exchange will wire
   * the money to.
   */
  depositPaytoUri: string;

  /**
   * Amount that should be deposited.
   *
   * Raw amount, fees will be added on top.
   */
  amount: AmountString;

  /**
   * Restrict the deposit to a certain scope.
   */
  restrictScope?: ScopeInfo;

  /**
   * ID provided by the client to cancel the request.
   *
   * If the same request is made again with the same clientCancellationId,
   * all previous requests are cancelled.
   *
   * The cancelled request will receive an error response with
   * an error code that indicates the cancellation.
   *
   * The cancellation is best-effort, responses might still arrive.
   */
  clientCancellationId?: string;
}

export const codecForCheckDepositRequest = (): Codec<CheckDepositRequest> =>
  buildCodecForObject<CheckDepositRequest>()
    .property("restrictScope", codecOptional(codecForScopeInfo()))
    .property("amount", codecForAmountString())
    .property("depositPaytoUri", codecForString())
    .property("clientCancellationId", codecOptional(codecForString()))
    .build("CheckDepositRequest");

export interface CheckDepositResponse {
  totalDepositCost: AmountString;
  effectiveDepositAmount: AmountString;
  fees: DepositGroupFees;

  kycSoftLimit?: AmountString;
  kycHardLimit?: AmountString;

  /**
   * Base URL of exchanges that would likely require soft KYC.
   */
  kycExchanges?: string[];
}

export const codecForCreateDepositGroupRequest =
  (): Codec<CreateDepositGroupRequest> =>
    buildCodecForObject<CreateDepositGroupRequest>()
      .property("restrictScope", codecOptional(codecForScopeInfo()))
      .property("amount", codecForAmountString())
      .property("depositPaytoUri", codecForString())
      .property("transactionId", codecOptional(codecForTransactionIdStr()))
      .property("testingFixedPriv", codecOptional(codecForString()))
      .build("CreateDepositGroupRequest");

/**
 * Response to a createDepositGroup request.
 */
export interface CreateDepositGroupResponse {
  /**
   * Transaction ID of the newly created deposit transaction.
   */
  transactionId: TransactionIdStr;

  /**
   * Current state of the new deposit transaction.
   * Returned as a performance optimization, so that the UI
   * doesn't have to do a separate getTransactionById.
   */
  txState: TransactionState;

  /**
   * @deprecated 2025-06-03, use transactionId instead.
   */
  depositGroupId: string;
}

export interface TxIdResponse {
  transactionId: TransactionIdStr;
}

export interface WithdrawUriInfoResponse {
  operationId: string;
  status: WithdrawalOperationStatusFlag;
  confirmTransferUrl?: string;
  currency: string;
  amount: AmountString | undefined;

  /**
   * Set to true if the user is allowed to edit the amount.
   *
   * Note that even with a non-editable amount, the amount
   * might be undefined at the beginning of the withdrawal
   * process.
   */
  editableAmount: boolean;
  maxAmount: AmountString | undefined;
  wireFee: AmountString | undefined;
  defaultExchangeBaseUrl?: string;
  editableExchange: boolean;
  possibleExchanges: ExchangeListItem[];
}

export const codecForWithdrawUriInfoResponse =
  (): Codec<WithdrawUriInfoResponse> =>
    buildCodecForObject<WithdrawUriInfoResponse>()
      .property("operationId", codecForString())
      .property("confirmTransferUrl", codecOptional(codecForString()))
      .property(
        "status",
        codecForEither(
          codecForConstString("pending"),
          codecForConstString("selected"),
          codecForConstString("aborted"),
          codecForConstString("confirmed"),
        ),
      )
      .property("amount", codecOptional(codecForAmountString()))
      .property("maxAmount", codecOptional(codecForAmountString()))
      .property("wireFee", codecOptional(codecForAmountString()))
      .property("currency", codecForString())
      .property("editableAmount", codecForBoolean())
      .property("editableExchange", codecForBoolean())
      .property("defaultExchangeBaseUrl", codecOptional(codecForCanonBaseUrl()))
      .property("possibleExchanges", codecForList(codecForExchangeListItem()))
      .build("WithdrawUriInfoResponse");

export interface WalletCurrencyInfo {
  trustedAuditors: {
    currency: string;
    auditorPub: string;
    auditorBaseUrl: string;
  }[];
  trustedExchanges: {
    currency: string;
    exchangeMasterPub: string;
    exchangeBaseUrl: string;
  }[];
}

export interface TestingListTasksForTransactionRequest {
  transactionId: TransactionIdStr;
}

export interface TestingListTasksForTransactionsResponse {
  taskIdList: string[];
}

export const codecForTestingListTasksForTransactionRequest =
  (): Codec<TestingListTasksForTransactionRequest> =>
    buildCodecForObject<TestingListTasksForTransactionRequest>()
      .property("transactionId", codecForTransactionIdStr())
      .build("TestingListTasksForTransactionRequest");

export interface DeleteTransactionRequest {
  transactionId: TransactionIdStr;
}

export interface RetryTransactionRequest {
  transactionId: TransactionIdStr;
}

export const codecForDeleteTransactionRequest =
  (): Codec<DeleteTransactionRequest> =>
    buildCodecForObject<DeleteTransactionRequest>()
      .property("transactionId", codecForTransactionIdStr())
      .build("DeleteTransactionRequest");

export const codecForRetryTransactionRequest =
  (): Codec<RetryTransactionRequest> =>
    buildCodecForObject<RetryTransactionRequest>()
      .property("transactionId", codecForTransactionIdStr())
      .build("RetryTransactionRequest");

export interface SetWalletDeviceIdRequest {
  /**
   * New wallet device ID to set.
   */
  walletDeviceId: string;
}

export const codecForSetWalletDeviceIdRequest =
  (): Codec<SetWalletDeviceIdRequest> =>
    buildCodecForObject<SetWalletDeviceIdRequest>()
      .property("walletDeviceId", codecForString())
      .build("SetWalletDeviceIdRequest");

export interface WithdrawFakebankRequest {
  amount: AmountString;
  exchange: string;
  bank: string;
}

export enum AttentionPriority {
  High = "high",
  Medium = "medium",
  Low = "low",
}

export interface UserAttentionByIdRequest {
  entityId: string;
  type: AttentionType;
}

export const codecForUserAttentionByIdRequest =
  (): Codec<UserAttentionByIdRequest> =>
    buildCodecForObject<UserAttentionByIdRequest>()
      .property("type", codecForAny())
      .property("entityId", codecForString())
      .build("UserAttentionByIdRequest");

export const codecForUserAttentionsRequest = (): Codec<UserAttentionsRequest> =>
  buildCodecForObject<UserAttentionsRequest>()
    .property(
      "priority",
      codecOptional(
        codecForEither(
          codecForConstString(AttentionPriority.Low),
          codecForConstString(AttentionPriority.Medium),
          codecForConstString(AttentionPriority.High),
        ),
      ),
    )
    .build("UserAttentionsRequest");

export interface UserAttentionsRequest {
  priority?: AttentionPriority;
}

export type AttentionInfo =
  | AttentionKycWithdrawal
  | AttentionBackupUnpaid
  | AttentionBackupExpiresSoon
  | AttentionMerchantRefund
  | AttentionExchangeTosChanged
  | AttentionExchangeKeyExpired
  | AttentionExchangeDenominationExpired
  | AttentionAuditorTosChanged
  | AttentionAuditorKeyExpires
  | AttentionAuditorDenominationExpires
  | AttentionPullPaymentPaid
  | AttentionPushPaymentReceived;

export enum AttentionType {
  KycWithdrawal = "kyc-withdrawal",

  BackupUnpaid = "backup-unpaid",
  BackupExpiresSoon = "backup-expires-soon",
  MerchantRefund = "merchant-refund",

  ExchangeTosChanged = "exchange-tos-changed",
  ExchangeKeyExpired = "exchange-key-expired",
  ExchangeKeyExpiresSoon = "exchange-key-expires-soon",
  ExchangeDenominationsExpired = "exchange-denominations-expired",
  ExchangeDenominationsExpiresSoon = "exchange-denominations-expires-soon",

  AuditorTosChanged = "auditor-tos-changed",
  AuditorKeyExpires = "auditor-key-expires",
  AuditorDenominationsExpires = "auditor-denominations-expires",

  PullPaymentPaid = "pull-payment-paid",
  PushPaymentReceived = "push-payment-withdrawn",
}

export const UserAttentionPriority: {
  [type in AttentionType]: AttentionPriority;
} = {
  "kyc-withdrawal": AttentionPriority.Medium,

  "backup-unpaid": AttentionPriority.High,
  "backup-expires-soon": AttentionPriority.Medium,
  "merchant-refund": AttentionPriority.Medium,

  "exchange-tos-changed": AttentionPriority.Medium,

  "exchange-key-expired": AttentionPriority.High,
  "exchange-key-expires-soon": AttentionPriority.Medium,
  "exchange-denominations-expired": AttentionPriority.High,
  "exchange-denominations-expires-soon": AttentionPriority.Medium,

  "auditor-tos-changed": AttentionPriority.Medium,
  "auditor-key-expires": AttentionPriority.Medium,
  "auditor-denominations-expires": AttentionPriority.Medium,

  "pull-payment-paid": AttentionPriority.High,
  "push-payment-withdrawn": AttentionPriority.High,
};

interface AttentionBackupExpiresSoon {
  type: AttentionType.BackupExpiresSoon;
  provider_base_url: string;
}
interface AttentionBackupUnpaid {
  type: AttentionType.BackupUnpaid;
  provider_base_url: string;
  talerUri: string;
}

interface AttentionMerchantRefund {
  type: AttentionType.MerchantRefund;
  transactionId: TransactionIdStr;
}

interface AttentionKycWithdrawal {
  type: AttentionType.KycWithdrawal;
  transactionId: TransactionIdStr;
}

interface AttentionExchangeTosChanged {
  type: AttentionType.ExchangeTosChanged;
  exchange_base_url: string;
}
interface AttentionExchangeKeyExpired {
  type: AttentionType.ExchangeKeyExpired;
  exchange_base_url: string;
}
interface AttentionExchangeDenominationExpired {
  type: AttentionType.ExchangeDenominationsExpired;
  exchange_base_url: string;
}
interface AttentionAuditorTosChanged {
  type: AttentionType.AuditorTosChanged;
  auditor_base_url: string;
}

interface AttentionAuditorKeyExpires {
  type: AttentionType.AuditorKeyExpires;
  auditor_base_url: string;
}
interface AttentionAuditorDenominationExpires {
  type: AttentionType.AuditorDenominationsExpires;
  auditor_base_url: string;
}
interface AttentionPullPaymentPaid {
  type: AttentionType.PullPaymentPaid;
  transactionId: TransactionIdStr;
}

interface AttentionPushPaymentReceived {
  type: AttentionType.PushPaymentReceived;
  transactionId: TransactionIdStr;
}

export type UserAttentionUnreadList = Array<{
  info: AttentionInfo;
  when: TalerPreciseTimestamp;
  read: boolean;
}>;

export interface UserAttentionsResponse {
  pending: UserAttentionUnreadList;
}

export interface UserAttentionsCountResponse {
  total: number;
}

export const codecForWithdrawFakebankRequest =
  (): Codec<WithdrawFakebankRequest> =>
    buildCodecForObject<WithdrawFakebankRequest>()
      .property("amount", codecForAmountString())
      .property("bank", codecForString())
      .property("exchange", codecForString())
      .build("WithdrawFakebankRequest");

export interface ActiveTask {
  taskId: string;
  transaction: TransactionIdStr | undefined;
  firstTry: AbsoluteTime | undefined;
  nextTry: AbsoluteTime | undefined;
  retryCounter: number | undefined;
  lastError: TalerErrorDetail | undefined;
}

export interface GetActiveTasksResponse {
  tasks: ActiveTask[];
}

export const codecForActiveTask = (): Codec<ActiveTask> =>
  buildCodecForObject<ActiveTask>()
    .property("taskId", codecForString())
    .property("transaction", codecOptional(codecForTransactionIdStr()))
    .property("retryCounter", codecOptional(codecForNumber()))
    .property("firstTry", codecOptional(codecForAbsoluteTime))
    .property("nextTry", codecOptional(codecForAbsoluteTime))
    .property("lastError", codecOptional(codecForTalerErrorDetail()))
    .build("ActiveTask");

export const codecForGetActiveTasks = (): Codec<GetActiveTasksResponse> =>
  buildCodecForObject<GetActiveTasksResponse>()
    .property("tasks", codecForList(codecForActiveTask()))
    .build("GetActiveTasks");

export interface ImportDbRequest {
  dump: any;
}

export const codecForImportDbRequest = (): Codec<ImportDbRequest> =>
  buildCodecForObject<ImportDbRequest>()
    .property("dump", codecForAny())
    .build("ImportDbRequest");

export interface ForcedDenomSel {
  denoms: {
    value: AmountString;
    count: number;
  }[];
}

/**
 * Forced coin selection for deposits/payments.
 */
export interface ForcedCoinSel {
  coins: {
    value: AmountString;
    contribution: AmountString;
  }[];
}

export interface TestPayResult {
  /**
   * Number of coins used for the payment.
   */
  numCoins: number;
}

export interface SelectedCoin {
  denomPubHash: string;
  coinPub: string;
  contribution: AmountString;
  exchangeBaseUrl: string;
}

export interface SelectedProspectiveCoin {
  denomPubHash: string;
  contribution: AmountString;
  exchangeBaseUrl: string;
}

/**
 * Result of selecting coins, contains the exchange, and selected
 * coins with their denomination.
 */
export interface PayCoinSelection {
  coins: SelectedCoin[];

  /**
   * How much of the wire fees is the customer paying?
   */
  customerWireFees: AmountString;

  /**
   * How much of the deposit fees is the customer paying?
   */
  customerDepositFees: AmountString;

  /**
   * How much of the deposit fees does the exchange charge in total?
   */
  totalDepositFees: AmountString;
}

export interface ProspectivePayCoinSelection {
  prospectiveCoins: SelectedProspectiveCoin[];

  /**
   * How much of the wire fees is the customer paying?
   */
  customerWireFees: AmountString;

  /**
   * How much of the deposit fees is the customer paying?
   */
  customerDepositFees: AmountString;
}

export interface CheckPeerPushDebitRequest {
  /**
   * Preferred exchange to use for the p2p payment.
   */
  exchangeBaseUrl?: string;

  /**
   * Instructed amount.
   *
   * FIXME: Allow specifying the instructed amount type.
   */
  amount: AmountString;

  /**
   * Restrict the scope of funds that can be spent via the given
   * scope info.
   */
  restrictScope?: ScopeInfo;

  /**
   * ID provided by the client to cancel the request.
   *
   * If the same request is made again with the same clientCancellationId,
   * all previous requests are cancelled.
   *
   * The cancelled request will receive an error response with
   * an error code that indicates the cancellation.
   *
   * The cancellation is best-effort, responses might still arrive.
   */
  clientCancellationId?: string;
}

export const codecForCheckPeerPushDebitRequest =
  (): Codec<CheckPeerPushDebitRequest> =>
    buildCodecForObject<CheckPeerPushDebitRequest>()
      .property("exchangeBaseUrl", codecOptional(codecForCanonBaseUrl()))
      .property("restrictScope", codecOptional(codecForScopeInfo()))
      .property("amount", codecForAmountString())
      .property("clientCancellationId", codecOptional(codecForString()))
      .build("CheckPeerPushDebitRequest");

export type CheckPeerPushDebitResponse =
  | CheckPeerPushDebitOkResponse
  | CheckPeerPushDebitInsufficientBalanceResponse;

export interface CheckPeerPushDebitInsufficientBalanceResponse {
  type: "insufficient-balance";

  insufficientBalanceDetails: PaymentInsufficientBalanceDetails;
}

export interface CheckPeerPushDebitOkResponse {
  type: "ok";

  amountRaw: AmountString;

  amountEffective: AmountString;

  /**
   * Exchange base URL.
   */
  exchangeBaseUrl: string;

  /**
   * Maximum expiration date, based on how close the coins
   * used for the payment are to expiry.
   *
   * The value is based on when the wallet would typically
   * automatically refresh the coins on its own, leaving enough
   * time to get a refund for the push payment and refresh the
   * coin.
   */
  maxExpirationDate: TalerProtocolTimestamp;
}

export interface InitiatePeerPushDebitRequest {
  exchangeBaseUrl?: string;

  /**
   * Restrict the scope of funds that can be spent via the given
   * scope info.
   */
  restrictScope?: ScopeInfo;

  partialContractTerms: PeerContractTerms;
}

export interface InitiatePeerPushDebitResponse {
  exchangeBaseUrl: string;
  pursePub: string;
  mergePriv: string;
  contractPriv: string;
  transactionId: TransactionIdStr;
}

export const codecForInitiatePeerPushDebitRequest =
  (): Codec<InitiatePeerPushDebitRequest> =>
    buildCodecForObject<InitiatePeerPushDebitRequest>()
      .property("partialContractTerms", codecForPeerContractTerms())
      .property("exchangeBaseUrl", codecOptional(codecForCanonBaseUrl()))
      .property("restrictScope", codecOptional(codecForScopeInfo()))
      .build("InitiatePeerPushDebitRequest");

export interface PreparePeerPushCreditRequest {
  talerUri: string;
}

export interface PreparePeerPullDebitRequest {
  talerUri: string;
}

export interface PreparePeerPushCreditResponse {
  contractTerms: PeerContractTerms;
  amountRaw: AmountString;
  amountEffective: AmountString;

  transactionId: TransactionIdStr;

  exchangeBaseUrl: string;

  scopeInfo: ScopeInfo;

  /**
   * @deprecated
   */
  amount: AmountString;
}

export interface PreparePeerPullDebitResponse {
  contractTerms: PeerContractTerms;

  amountRaw: AmountString;
  amountEffective: AmountString;

  transactionId: TransactionIdStr;

  exchangeBaseUrl: string;

  scopeInfo: ScopeInfo;

  /**
   * @deprecated Redundant field with bad name, will be removed soon.
   */
  amount: AmountString;
}

export const codecForPreparePeerPushCreditRequest =
  (): Codec<PreparePeerPushCreditRequest> =>
    buildCodecForObject<PreparePeerPushCreditRequest>()
      .property("talerUri", codecForString())
      .build("CheckPeerPushPaymentRequest");

export const codecForCheckPeerPullPaymentRequest =
  (): Codec<PreparePeerPullDebitRequest> =>
    buildCodecForObject<PreparePeerPullDebitRequest>()
      .property("talerUri", codecForString())
      .build("PreparePeerPullDebitRequest");

export interface ConfirmPeerPushCreditRequest {
  transactionId: string;
}
export interface AcceptPeerPushPaymentResponse {
  transactionId: TransactionIdStr;
}

export interface AcceptPeerPullPaymentResponse {
  transactionId: TransactionIdStr;
}

export const codecForConfirmPeerPushPaymentRequest =
  (): Codec<ConfirmPeerPushCreditRequest> =>
    buildCodecForObject<ConfirmPeerPushCreditRequest>()
      .property("transactionId", codecForString())
      .build("ConfirmPeerPushCreditRequest");

export interface ConfirmPeerPullDebitRequest {
  transactionId: TransactionIdStr;
}

export interface ApplyDevExperimentRequest {
  devExperimentUri: string;
}

export const codecForApplyDevExperiment =
  (): Codec<ApplyDevExperimentRequest> =>
    buildCodecForObject<ApplyDevExperimentRequest>()
      .property("devExperimentUri", codecForString())
      .build("ApplyDevExperimentRequest");

export const codecForAcceptPeerPullPaymentRequest =
  (): Codec<ConfirmPeerPullDebitRequest> =>
    buildCodecForObject<ConfirmPeerPullDebitRequest>()
      .property("transactionId", codecForTransactionIdStr())
      .build("ConfirmPeerPullDebitRequest");

export interface CheckPeerPullCreditRequest {
  /**
   * Require using this particular exchange for this operation.
   */
  exchangeBaseUrl?: string;

  restrictScope?: ScopeInfo;

  amount: AmountString;

  /**
   * ID provided by the client to cancel the request.
   *
   * If the same request is made again with the same clientCancellationId,
   * all previous requests are cancelled.
   *
   * The cancelled request will receive an error response with
   * an error code that indicates the cancellation.
   *
   * The cancellation is best-effort, responses might still arrive.
   */
  clientCancellationId?: string;
}

export const codecForPreparePeerPullPaymentRequest =
  (): Codec<CheckPeerPullCreditRequest> =>
    buildCodecForObject<CheckPeerPullCreditRequest>()
      .property("amount", codecForAmountString())
      .property("exchangeBaseUrl", codecOptional(codecForCanonBaseUrl()))
      .property("restrictScope", codecOptional(codecForScopeInfo()))
      .property("clientCancellationId", codecOptional(codecForString()))
      .build("CheckPeerPullCreditRequest");

export interface CheckPeerPullCreditResponse {
  exchangeBaseUrl: string;
  amountRaw: AmountString;
  amountEffective: AmountString;

  /**
   * Number of coins that will be used,
   * can be used by the UI to warn if excessively large.
   */
  numCoins: number;
}

export interface InitiatePeerPullCreditRequest {
  exchangeBaseUrl?: string;
  partialContractTerms: PeerContractTerms;
}

export const codecForInitiatePeerPullPaymentRequest =
  (): Codec<InitiatePeerPullCreditRequest> =>
    buildCodecForObject<InitiatePeerPullCreditRequest>()
      .property("partialContractTerms", codecForPeerContractTerms())
      .property("exchangeBaseUrl", codecOptional(codecForCanonBaseUrl()))
      .build("InitiatePeerPullCreditRequest");

export interface InitiatePeerPullCreditResponse {
  /**
   * Taler URI for the other party to make the payment
   * that was requested.
   *
   * @deprecated since it's not necessarily valid yet until the tx is in the right state
   */
  talerUri: string;

  transactionId: TransactionIdStr;
}

export interface CanonicalizeBaseUrlRequest {
  url: string;
}

export const codecForCanonicalizeBaseUrlRequest =
  (): Codec<CanonicalizeBaseUrlRequest> =>
    buildCodecForObject<CanonicalizeBaseUrlRequest>()
      .property("url", codecForString())
      .build("CanonicalizeBaseUrlRequest");

export interface CanonicalizeBaseUrlResponse {
  url: string;
}

export interface ValidateIbanRequest {
  iban: string;
}

export const codecForValidateIbanRequest = (): Codec<ValidateIbanRequest> =>
  buildCodecForObject<ValidateIbanRequest>()
    .property("iban", codecForString())
    .build("ValidateIbanRequest");

export interface ValidateIbanResponse {
  valid: boolean;
}

export const codecForValidateIbanResponse = (): Codec<ValidateIbanResponse> =>
  buildCodecForObject<ValidateIbanResponse>()
    .property("valid", codecForBoolean())
    .build("ValidateIbanResponse");

export type TransactionStateFilter = "nonfinal";

export interface TransactionRecordFilter {
  onlyState?: TransactionStateFilter;
  onlyCurrency?: string;
}

export interface StoredBackupList {
  storedBackups: {
    name: string;
  }[];
}

export interface CreateStoredBackupResponse {
  name: string;
}

export interface RecoverStoredBackupRequest {
  name: string;
}

export interface DeleteStoredBackupRequest {
  name: string;
}

export const codecForDeleteStoredBackupRequest =
  (): Codec<DeleteStoredBackupRequest> =>
    buildCodecForObject<DeleteStoredBackupRequest>()
      .property("name", codecForString())
      .build("DeleteStoredBackupRequest");

export const codecForRecoverStoredBackupRequest =
  (): Codec<RecoverStoredBackupRequest> =>
    buildCodecForObject<RecoverStoredBackupRequest>()
      .property("name", codecForString())
      .build("RecoverStoredBackupRequest");

export interface TestingSetTimetravelRequest {
  offsetMs: number;
}

export const codecForTestingSetTimetravelRequest =
  (): Codec<TestingSetTimetravelRequest> =>
    buildCodecForObject<TestingSetTimetravelRequest>()
      .property("offsetMs", codecForNumber())
      .build("TestingSetTimetravelRequest");

export interface AllowedAuditorInfo {
  auditorBaseUrl: string;
  auditorPub: string;
}

export interface AllowedExchangeInfo {
  exchangeBaseUrl: string;
  exchangePub: string;
}

/**
 * Data extracted from the contract terms that is relevant for payment
 * processing in the wallet.
 */
export interface DownloadedContractData {
  contractTermsRaw: any;
  contractTerms: MerchantContractTerms;
  contractTermsHash: HashCode;
}

export type PayWalletData = {
  choice_index?: number;
  tokens_evs: TokenEnvelope[];

  // Request for donation receipts to be issued.
  // @since protocol **v21**
  donau?: DonationRequestData;
};

export interface DonationRequestData {
  // Base URL of the selected Donau
  url: string;

  // Year for which the donation receipts are expected.
  // Also determines which keys are used to sign the
  // blinded donation receipts.
  year: number;

  // Array of blinded donation receipts to sign.
  // Must NOT be empty (if no donation receipts
  // are desired, just leave the entire donau
  // argument blank).
  budikeypairs: BlindedDonationReceiptKeyPair[];
}

export interface TestingWaitExchangeStateRequest {
  exchangeBaseUrl: string;
  walletKycStatus?: ExchangeWalletKycStatus;
}

export interface TransactionStatePattern {
  major: TransactionMajorState | TransactionStateWildcard;
  minor?: TransactionMinorState | TransactionStateWildcard;
}

export interface TestingWaitTransactionRequest {
  transactionId: TransactionIdStr;

  /**
   * Additional identifier that is used in the logs
   * to easily find the status of the particular wait
   * request.
   */
  logId?: string;

  /**
   * After the timeout has passed, give up on
   * waiting for the desired state and raise
   * an error instead.
   */
  timeout?: DurationUnitSpec;

  /**
   * If set to true, wait until the desired state
   * is reached with an error.
   */
  requireError?: boolean;

  txState: TransactionStatePattern | TransactionStatePattern[] | number;
}

export interface TestingGetReserveHistoryRequest {
  reservePub: string;
  exchangeBaseUrl: string;
}

export const codecForTestingGetReserveHistoryRequest =
  (): Codec<TestingGetReserveHistoryRequest> =>
    buildCodecForObject<TestingGetReserveHistoryRequest>()
      .property("reservePub", codecForString())
      .property("exchangeBaseUrl", codecForString())
      .build("TestingGetReserveHistoryRequest");

export interface TestingGetDenomStatsRequest {
  exchangeBaseUrl: string;
}

export interface TestingGetDenomStatsResponse {
  numKnown: number;
  numOffered: number;
  numLost: number;
}

export const codecForTestingGetDenomStatsRequest =
  (): Codec<TestingGetDenomStatsRequest> =>
    buildCodecForObject<TestingGetDenomStatsRequest>()
      .property("exchangeBaseUrl", codecForCanonBaseUrl())
      .build("TestingGetDenomStatsRequest");

export interface RunFixupRequest {
  id: string;
}

export const codecForRunFixupRequest = (): Codec<RunFixupRequest> =>
  buildCodecForObject<RunFixupRequest>()
    .property("id", codecForString())
    .build("RunFixupRequest");

export interface WithdrawalExchangeAccountDetails {
  /**
   * Payto URI to credit the exchange.
   *
   * Depending on whether the (manual!) withdrawal is accepted or just
   * being checked, this already includes the subject with the
   * reserve public key.
   */
  paytoUri: string;

  /**
   * Status that indicates whether the account can be used
   * by the user to send funds for a withdrawal.
   *
   * ok: account should be shown to the user
   * error: account should not be shown to the user, UIs might render the error (in conversionError),
   *   especially in dev mode.
   */
  status: "ok" | "error";

  /**
   * Transfer amount. Might be in a different currency than the requested
   * amount for withdrawal.
   *
   * Absent if this is a conversion account and the conversion failed.
   */
  transferAmount?: AmountString;

  /**
   * Currency specification for the external currency.
   *
   * Only included if this account requires a currency conversion.
   */
  currencySpecification?: CurrencySpecification;

  /**
   * Further restrictions for sending money to the
   * exchange.
   */
  creditRestrictions?: AccountRestriction[];

  /**
   * Label given to the account or the account's bank by the exchange.
   */
  bankLabel?: string;

  /*
   * Display priority assigned to this bank account by the exchange.
   */
  priority?: number;

  /**
   * Error that happened when attempting to request the conversion rate.
   */
  conversionError?: TalerErrorDetail;
}

export interface PrepareWithdrawExchangeRequest {
  /**
   * A taler://withdraw-exchange URI.
   */
  talerUri: string;
}

export const codecForPrepareWithdrawExchangeRequest =
  (): Codec<PrepareWithdrawExchangeRequest> =>
    buildCodecForObject<PrepareWithdrawExchangeRequest>()
      .property("talerUri", codecForString())
      .build("PrepareWithdrawExchangeRequest");

export interface PrepareWithdrawExchangeResponse {
  /**
   * Base URL of the exchange that already existed
   * or was ephemerally added as an exchange entry to
   * the wallet.
   */
  exchangeBaseUrl: string;

  /**
   * Amount from the taler://withdraw-exchange URI.
   * Only present if specified in the URI.
   */
  amount?: AmountString;
}

export interface ExchangeEntryState {
  tosStatus: ExchangeTosStatus;
  exchangeEntryStatus: ExchangeEntryStatus;
  exchangeUpdateStatus: ExchangeUpdateStatus;
}

export interface ListGlobalCurrencyAuditorsResponse {
  auditors: {
    currency: string;
    auditorBaseUrl: string;
    auditorPub: string;
  }[];
}

export interface ListGlobalCurrencyExchangesResponse {
  exchanges: {
    currency: string;
    exchangeBaseUrl: string;
    exchangeMasterPub: string;
  }[];
}

export interface AddGlobalCurrencyExchangeRequest {
  currency: string;
  exchangeBaseUrl: string;
  exchangeMasterPub: string;
}

export const codecForAddGlobalCurrencyExchangeRequest =
  (): Codec<AddGlobalCurrencyExchangeRequest> =>
    buildCodecForObject<AddGlobalCurrencyExchangeRequest>()
      .property("currency", codecForString())
      .property("exchangeBaseUrl", codecForCanonBaseUrl())
      .property("exchangeMasterPub", codecForString())
      .build("AddGlobalCurrencyExchangeRequest");

export interface RemoveGlobalCurrencyExchangeRequest {
  currency: string;
  exchangeBaseUrl: string;
  exchangeMasterPub: string;
}

export const codecForRemoveGlobalCurrencyExchangeRequest =
  (): Codec<RemoveGlobalCurrencyExchangeRequest> =>
    buildCodecForObject<RemoveGlobalCurrencyExchangeRequest>()
      .property("currency", codecForString())
      .property("exchangeBaseUrl", codecForCanonBaseUrl())
      .property("exchangeMasterPub", codecForString())
      .build("RemoveGlobalCurrencyExchangeRequest");

export interface AddGlobalCurrencyAuditorRequest {
  currency: string;
  auditorBaseUrl: string;
  auditorPub: string;
}

export const codecForAddGlobalCurrencyAuditorRequest =
  (): Codec<AddGlobalCurrencyAuditorRequest> =>
    buildCodecForObject<AddGlobalCurrencyAuditorRequest>()
      .property("currency", codecForString())
      .property("auditorBaseUrl", codecForCanonBaseUrl())
      .property("auditorPub", codecForString())
      .build("AddGlobalCurrencyAuditorRequest");

export interface RemoveGlobalCurrencyAuditorRequest {
  currency: string;
  auditorBaseUrl: string;
  auditorPub: string;
}

export const codecForRemoveGlobalCurrencyAuditorRequest =
  (): Codec<RemoveGlobalCurrencyAuditorRequest> =>
    buildCodecForObject<RemoveGlobalCurrencyAuditorRequest>()
      .property("currency", codecForString())
      .property("auditorBaseUrl", codecForCanonBaseUrl())
      .property("auditorPub", codecForString())
      .build("RemoveGlobalCurrencyAuditorRequest");

/**
 * Information about one provider.
 *
 * We don't store the account key here,
 * as that's derived from the wallet root key.
 */
export interface ProviderInfo {
  active: boolean;
  syncProviderBaseUrl: string;
  name: string;
  terms?: BackupProviderTerms;
  /**
   * Last communication issue with the provider.
   */
  lastError?: TalerErrorDetail;
  lastSuccessfulBackupTimestamp?: TalerPreciseTimestamp;
  lastAttemptedBackupTimestamp?: TalerPreciseTimestamp;
  paymentProposalIds: string[];
  backupProblem?: BackupProblem;
  paymentStatus: ProviderPaymentStatus;
}

export interface BackupProviderTerms {
  supportedProtocolVersion: string;
  annualFee: AmountString;
  storageLimitInMegabytes: number;
}

export type BackupProblem =
  | BackupUnreadableProblem
  | BackupConflictingDeviceProblem;

export interface BackupUnreadableProblem {
  type: "backup-unreadable";
}

export interface BackupConflictingDeviceProblem {
  type: "backup-conflicting-device";
  otherDeviceId: string;
  myDeviceId: string;
  backupTimestamp: AbsoluteTime;
}

export type ProviderPaymentStatus =
  | ProviderPaymentTermsChanged
  | ProviderPaymentPaid
  | ProviderPaymentInsufficientBalance
  | ProviderPaymentUnpaid
  | ProviderPaymentPending;

export enum ProviderPaymentType {
  Unpaid = "unpaid",
  Pending = "pending",
  InsufficientBalance = "insufficient-balance",
  Paid = "paid",
  TermsChanged = "terms-changed",
}

export interface ProviderPaymentUnpaid {
  type: ProviderPaymentType.Unpaid;
}

export interface ProviderPaymentInsufficientBalance {
  type: ProviderPaymentType.InsufficientBalance;
  amount: AmountString;
}

export interface ProviderPaymentPending {
  type: ProviderPaymentType.Pending;
  talerUri?: string;
}

export interface ProviderPaymentPaid {
  type: ProviderPaymentType.Paid;
  paidUntil: AbsoluteTime;
}

export interface ProviderPaymentTermsChanged {
  type: ProviderPaymentType.TermsChanged;
  paidUntil: AbsoluteTime;
  oldTerms: BackupProviderTerms;
  newTerms: BackupProviderTerms;
}

// FIXME: Does not really belong here, move to sync API
export interface SyncTermsOfServiceResponse {
  // maximum backup size supported
  storage_limit_in_megabytes: number;

  // Fee for an account, per year.
  annual_fee: AmountString;

  // protocol version supported by the server,
  // for now always "0.0".
  version: string;
}

// FIXME: Does not really belong here, move to sync API
export const codecForSyncTermsOfServiceResponse =
  (): Codec<SyncTermsOfServiceResponse> =>
    buildCodecForObject<SyncTermsOfServiceResponse>()
      .property("storage_limit_in_megabytes", codecForNumber())
      .property("annual_fee", codecForAmountString())
      .property("version", codecForString())
      .build("SyncTermsOfServiceResponse");

export interface HintNetworkAvailabilityRequest {
  isNetworkAvailable: boolean;
}

export const codecForHintNetworkAvailabilityRequest =
  (): Codec<HintNetworkAvailabilityRequest> =>
    buildCodecForObject<HintNetworkAvailabilityRequest>()
      .property("isNetworkAvailable", codecForBoolean())
      .build("HintNetworkAvailabilityRequest");

export interface GetDepositWireTypesRequest {
  currency?: string;
  /**
   * Optional scope info to further restrict the result.
   * Currency must match the currency field.
   */
  scopeInfo?: ScopeInfo;
}

export const codecForGetDepositWireTypesRequest =
  (): Codec<GetDepositWireTypesRequest> =>
    buildCodecForObject<GetDepositWireTypesRequest>()
      .property("currency", codecOptional(codecForString()))
      .property("scopeInfo", codecOptional(codecForScopeInfo()))
      .build("GetDepositWireTypesRequest");

export interface GetDepositWireTypesResponse {
  /**
   * Details for each wire type.
   */
  wireTypeDetails: WireTypeDetails[];
}

export interface GetDepositWireTypesForCurrencyRequest {
  currency: string;
  /**
   * Optional scope info to further restrict the result.
   * Currency must match the currency field.
   */
  scopeInfo?: ScopeInfo;
}

export const codecForGetDepositWireTypesForCurrencyRequest =
  (): Codec<GetDepositWireTypesForCurrencyRequest> =>
    buildCodecForObject<GetDepositWireTypesForCurrencyRequest>()
      .property("currency", codecForString())
      .property("scopeInfo", codecOptional(codecForScopeInfo()))
      .build("GetDepositWireTypesForCurrencyRequest");

/**
 * Response with wire types that are supported for a deposit.
 *
 * In the future, we might surface more information here, such as debit restrictions
 * by the exchange, which then can be shown by UIs to the user before they
 * enter their payment information.
 */
export interface GetDepositWireTypesForCurrencyResponse {
  /**
   * @deprecated, use wireTypeDetails instead.
   */
  wireTypes: string[];

  /**
   * Details for each wire type.
   */
  wireTypeDetails: WireTypeDetails[];
}

export interface WireTypeDetails {
  paymentTargetType: string;

  /**
   * Only applicable for payment target type IBAN.
   *
   * Specifies whether the user wants to preferably
   * enter their bank account details as an IBAN
   * or as a BBAN.
   *
   * Mandatory for paymentTargetType="iban".
   */
  preferredEntryType?: "iban" | "bban";

  /**
   * Allowed hostnames for the deposit payto URI.
   * Only applicable to x-taler-bank.
   */
  talerBankHostnames?: string[];
}

export interface GetQrCodesForPaytoRequest {
  paytoUri: string;
}

export const codecForGetQrCodesForPaytoRequest = () =>
  buildCodecForObject<GetQrCodesForPaytoRequest>()
    .property("paytoUri", codecForString())
    .build("GetQrCodesForPaytoRequest");

export interface GetQrCodesForPaytoResponse {
  codes: QrCodeSpec[];
}

export interface GetBankingChoicesForPaytoRequest {
  paytoUri: string;
}

export const codecForGetBankingChoicesForPaytoRequest = () =>
  buildCodecForObject<GetBankingChoicesForPaytoRequest>()
    .property("paytoUri", codecForString())
    .build("GetBankingChoicesForPaytoRequest");

export interface BankingChoiceSpec {
  label: string;
  // FIXME: In the future, we might also have some way to return intents here?
  type: "link";
  uri: string;
}

export interface GetBankingChoicesForPaytoResponse {
  choices: BankingChoiceSpec[];
}

export type EmptyObject = Record<string, never>;

export const codecForEmptyObject = (): Codec<EmptyObject> =>
  buildCodecForObject<EmptyObject>().build("EmptyObject");

export interface TestingWaitWalletKycRequest {
  exchangeBaseUrl: string;
  amount: AmountString;
  /**
   * Do we wait for the KYC to be passed (true),
   * or do we already return if legitimization is
   * required (false).
   */
  passed: boolean;
}

export const codecForTestingWaitWalletKycRequest =
  (): Codec<TestingWaitWalletKycRequest> =>
    buildCodecForObject<TestingWaitWalletKycRequest>()
      .property("exchangeBaseUrl", codecForString())
      .property("amount", codecForAmountString())
      .property("passed", codecForBoolean())
      .build("TestingWaitWalletKycRequest");

export interface TestingPlanMigrateExchangeBaseUrlRequest {
  oldExchangeBaseUrl: string;
  newExchangeBaseUrl: string;
}

export const codecForTestingPlanMigrateExchangeBaseUrlRequest =
  (): Codec<TestingPlanMigrateExchangeBaseUrlRequest> =>
    buildCodecForObject<TestingPlanMigrateExchangeBaseUrlRequest>()
      .property("oldExchangeBaseUrl", codecForString())
      .property("newExchangeBaseUrl", codecForString())
      .build("TestingMigrateExchangeBaseUrlRequest");

export interface StartExchangeWalletKycRequest {
  exchangeBaseUrl: string;
  amount: AmountString;
}

export const codecForStartExchangeWalletKycRequest =
  (): Codec<StartExchangeWalletKycRequest> =>
    buildCodecForObject<StartExchangeWalletKycRequest>()
      .property("exchangeBaseUrl", codecForString())
      .property("amount", codecForAmountString())
      .build("StartExchangeWalletKycRequest");

export interface ExportDbToFileRequest {
  /**
   * Directory that the DB should be exported into.
   */
  directory: string;

  /**
   * Stem of the exported DB filename.
   *
   * The final name will be ${directory}/${stem}.${extension},
   * where the extension depends on the used DB backend.
   */
  stem: string;

  /**
   * Force the format of the export.
   *
   * Currently only "json" is supported as a forced
   * export format.
   */
  forceFormat?: string;
}

export const codecForExportDbToFileRequest = (): Codec<ExportDbToFileRequest> =>
  buildCodecForObject<ExportDbToFileRequest>()
    .property("directory", codecForString())
    .property("stem", codecForString())
    .property("forceFormat", codecOptional(codecForString()))
    .build("ExportDbToFileRequest");

export interface ExportDbToFileResponse {
  /**
   * Full path to the backup.
   */
  path: string;
}

export interface ImportDbFromFileRequest {
  /**
   * Full path to the backup.
   */
  path: string;
}

export const codecForImportDbFromFileRequest =
  (): Codec<ImportDbFromFileRequest> =>
    buildCodecForObject<ImportDbFromFileRequest>()
      .property("path", codecForString())
      .build("ImportDbFromFileRequest");

export interface CompleteBaseUrlRequest {
  url: string;
}

export const codecForCompleteBaseUrlRequest =
  (): Codec<CompleteBaseUrlRequest> =>
    buildCodecForObject<CompleteBaseUrlRequest>()
      .property("url", codecForString())
      .build("CompleteBaseUrlRequest");

export type CompleteBaseUrlResult =
  | {
      /**
       * ok: completion is a proper exchange
       */
      status: "ok";
      /** Completed exchange base URL, if completion was possible */
      completion: string;
    }
  | {
      /**
       * bad-syntax: url is so badly malformed, it can't be completed
       * bad-network: syntax okay, but exchange can't be reached
       * bad-exchange: syntax and network okay, but not talking to an exchange
       */
      status: "bad-syntax" | "bad-network" | "bad-exchange";
      /** Error details in case status is not "ok" */
      error: TalerErrorDetail;
    };

export interface SetDonauRequest {
  donauBaseUrl: string;
  taxPayerId: string;
}

export const codecForSetDonauRequest = (): Codec<SetDonauRequest> =>
  buildCodecForObject<SetDonauRequest>()
    .property("donauBaseUrl", codecForString())
    .property("taxPayerId", codecForString())
    .build("SetDonauRequest");

export interface DonauStatementItem {
  total: AmountString;
  year: number;
  legalDomain: string;
  uri: string;
  donationStatementSig: EddsaSignatureString;
  donauPub: EddsaPublicKeyString;
}

export interface GetDonauStatementsRequest {
  donauBaseUrl?: string;
}

export interface GetDonauStatementsResponse {
  statements: DonauStatementItem[];
}

export interface GetDonauResponse {
  currentDonauInfo:
    | {
        donauBaseUrl: string;
        taxPayerId: string;
      }
    | undefined;
}

export const codecForGetDonauStatementsRequest =
  (): Codec<GetDonauStatementsRequest> =>
    buildCodecForObject<GetDonauStatementsRequest>()
      .property("donauBaseUrl", codecOptional(codecForString()))
      .build("GetDonauStatementsRequest");
