/*
 This file is part of GNU Taler
 (C) 2019-2020 Taler Systems SA

 TALER is free software; you can redistribute it and/or modify it under the
 terms of the GNU General Public License as published by the Free Software
 Foundation; either version 3, or (at your option) any later version.

 TALER is distributed in the hope that it will be useful, but WITHOUT ANY
 WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
 A PARTICULAR PURPOSE.  See the GNU General Public License for more details.

 You should have received a copy of the GNU General Public License along with
 TALER; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
 */

/**
 * Implementation of crypto-related high-level functions for the Taler wallet.
 *
 * @author Florian Dold <dold@taler.net>
 */

/**
 * Imports.
 */

import {
  AgeCommitmentProof,
  AgeRestriction,
  AmountJson,
  Amounts,
  AmountString,
  BlindedDenominationSignature,
  bufferForUint32,
  bufferForUint64,
  bufferFromAmount,
  buildSigPS,
  canonicalJson,
  CoinDepositPermission,
  CoinEnvelope,
  createHashContext,
  decodeCrock,
  decryptContractForDeposit,
  decryptContractForMerge,
  DenomKeyType,
  DepositInfo,
  durationRoundedToBuffer,
  ecdhGetPublic,
  eddsaGetPublic,
  EddsaPrivateKeyString,
  EddsaPublicKeyString,
  eddsaSign,
  EddsaSignatureString,
  eddsaVerify,
  encodeCrock,
  encryptContractForDeposit,
  encryptContractForMerge,
  ExchangeProtocolVersion,
  getRandomBytes,
  GlobalFees,
  hash,
  HashCodeString,
  hashCoinEv,
  hashCoinEvInner,
  hashCoinPub,
  hashDenomPub,
  hashTokenEv,
  hashTokenIssuePub,
  hashTruncate32,
  kdf,
  kdfKw,
  keyExchangeEcdhEddsa,
  Logger,
  MakeSyncSignatureRequest,
  MerchantContractOutputType,
  PayWalletData,
  PlanchetCreationRequest,
  PlanchetUnblindInfo,
  PurseDeposit,
  RecoupRefreshRequest,
  RecoupRequest,
  RefreshPlanchetInfo,
  rsaBlind,
  rsaUnblind,
  rsaVerify,
  SignTokenUseRequest,
  Slate,
  SlateCreationRequest,
  SlateUnblindInfo,
  stringToBytes,
  TalerProtocolTimestamp,
  TalerSignaturePurpose,
  timestampRoundedToBuffer,
  toHexString,
  TokenEnvelope,
  TokenIssueBlindSig,
  UnblindedDenominationSignature,
  WireFee,
  WithdrawalPlanchet,
} from "@gnu-taler/taler-util";
import {
  CreateRecoupRefreshReqRequest,
  CreateRecoupReqRequest,
  DecryptContractForDepositRequest,
  DecryptContractForDepositResponse,
  DecryptContractRequest,
  DecryptContractResponse,
  DerivedRefreshSession,
  DerivedRefreshSessionV2,
  DeriveRefreshSessionRequest,
  DeriveRefreshSessionRequestV2,
  EncryptContractForDepositRequest,
  EncryptContractForDepositResponse,
  EncryptContractRequest,
  EncryptContractResponse,
  SignCoinHistoryRequest,
  SignCoinHistoryResponse,
  SignContractTermsHashRequest,
  SignContractTermsHashResponse,
  SignDeletePurseRequest,
  SignDeletePurseResponse,
  SignPurseMergeRequest,
  SignPurseMergeResponse,
  SignRefundRequest,
  SignRefundResponse,
  SignReservePurseCreateRequest,
  SignReservePurseCreateResponse,
  SignTrackTransactionRequest,
  SignWalletAccountSetupRequest,
  SignWalletAccountSetupResponse,
  SignWalletKycAuthRequest,
  SignWalletKycAuthResponse,
} from "./cryptoTypes.js";

const logger = new Logger("cryptoImplementation.ts");

let debugDumpSigs = false;

/**
 * Enable dumping of signature blobs to stderr.
 *
 * Global option, must only be used for debugging.
 */
export function setDebugDumpSigs(b: boolean) {
  debugDumpSigs = b;
}

/**
 * Interface for (asynchronous) cryptographic operations that
 * Taler uses.
 */
export interface TalerCryptoInterface {
  /**
   * Create a pre-coin of the given denomination to be withdrawn from then given
   * reserve.
   */
  createPlanchet(req: PlanchetCreationRequest): Promise<WithdrawalPlanchet>;

  signWithdrawal(req: SignWithdrawalRequest): Promise<SignWithdrawalResponse>;

  eddsaSign(req: EddsaSignRequest): Promise<EddsaSignResponse>;

  /**
   * Create a pre-token (a.k.a. slate) of a given token family.
   */
  createSlate(req: SlateCreationRequest): Promise<Slate>;

  signTokenUse(req: SignTokenUseRequest): Promise<EddsaSigningResult>;

  signTrackTransaction(
    req: SignTrackTransactionRequest,
  ): Promise<EddsaSigningResult>;

  createRecoupRequest(req: CreateRecoupReqRequest): Promise<RecoupRequest>;

  createRecoupRefreshRequest(
    req: CreateRecoupRefreshReqRequest,
  ): Promise<RecoupRefreshRequest>;

  isValidPaymentSignature(
    req: PaymentSignatureValidationRequest,
  ): Promise<ValidationResult>;

  isValidWireFee(req: WireFeeValidationRequest): Promise<ValidationResult>;

  isValidGlobalFees(
    req: GlobalFeesValidationRequest,
  ): Promise<ValidationResult>;

  isValidDenom(req: DenominationValidationRequest): Promise<ValidationResult>;

  isValidWireAccount(
    req: WireAccountValidationRequest,
  ): Promise<ValidationResult>;

  isValidContractTermsSignature(
    req: ContractTermsValidationRequest,
  ): Promise<ValidationResult>;

  createEddsaKeypair(req: {}): Promise<EddsaKeyPairStrings>;

  eddsaGetPublic(req: EddsaGetPublicRequest): Promise<EddsaGetPublicResponse>;

  unblindDenominationSignature(
    req: UnblindDenominationSignatureRequest,
  ): Promise<UnblindedDenominationSignature>;

  unblindTokenIssueSignature(
    req: UnblindTokenIssueSignatureRequest,
  ): Promise<UnblindedDenominationSignature>;

  rsaUnblind(req: RsaUnblindRequest): Promise<RsaUnblindResponse>;

  rsaVerify(req: RsaVerificationRequest): Promise<ValidationResult>;

  rsaVerifyDirect(req: RsaVerificationRequest): Promise<ValidationResult>;

  rsaBlind(req: RsaBlindRequest): Promise<RsaBlindResponse>;

  signDepositPermission(
    depositInfo: DepositInfo,
  ): Promise<CoinDepositPermission>;

  deriveRefreshSession(
    req: DeriveRefreshSessionRequest,
  ): Promise<DerivedRefreshSession>;

  deriveRefreshSessionV2(
    req: DeriveRefreshSessionRequestV2,
  ): Promise<DerivedRefreshSessionV2>;

  hashString(req: HashStringRequest): Promise<HashStringResult>;

  signCoinLink(req: SignCoinLinkRequest): Promise<EddsaSigningResult>;

  makeSyncSignature(req: MakeSyncSignatureRequest): Promise<EddsaSigningResult>;

  setupRefreshPlanchet(
    req: SetupRefreshPlanchetRequest,
  ): Promise<FreshCoinEncoded>;

  setupRefreshPlanchetV2(
    req: SetupRefreshPlanchetRequestV2,
  ): Promise<FreshCoinEncoded>;

  setupWithdrawalPlanchet(
    req: SetupWithdrawalPlanchetRequest,
  ): Promise<FreshCoinEncoded>;

  keyExchangeEcdheEddsa(
    req: KeyExchangeEcdheEddsaRequest,
  ): Promise<KeyExchangeResult>;

  ecdheGetPublic(req: EddsaGetPublicRequest): Promise<EddsaGetPublicResponse>;

  setupRefreshTransferPub(
    req: SetupRefreshTransferPubRequest,
  ): Promise<TransferPubResponse>;

  signPurseCreation(req: SignPurseCreationRequest): Promise<EddsaSigningResult>;

  signReserveHistoryReq(
    req: SignReserveHistoryReqRequest,
  ): Promise<SignReserveHistoryReqResponse>;

  signPurseDeposits(
    req: SignPurseDepositsRequest,
  ): Promise<SignPurseDepositsResponse>;

  encryptContractForMerge(
    req: EncryptContractRequest,
  ): Promise<EncryptContractResponse>;

  decryptContractForMerge(
    req: DecryptContractRequest,
  ): Promise<DecryptContractResponse>;

  encryptContractForDeposit(
    req: EncryptContractForDepositRequest,
  ): Promise<EncryptContractForDepositResponse>;

  decryptContractForDeposit(
    req: DecryptContractForDepositRequest,
  ): Promise<DecryptContractForDepositResponse>;

  signPurseMerge(req: SignPurseMergeRequest): Promise<SignPurseMergeResponse>;

  signWalletAccountSetup(
    req: SignWalletAccountSetupRequest,
  ): Promise<SignWalletAccountSetupResponse>;

  signReservePurseCreate(
    req: SignReservePurseCreateRequest,
  ): Promise<SignReservePurseCreateResponse>;

  signRefund(req: SignRefundRequest): Promise<SignRefundResponse>;

  signDeletePurse(
    req: SignDeletePurseRequest,
  ): Promise<SignDeletePurseResponse>;

  signCoinHistoryRequest(
    req: SignCoinHistoryRequest,
  ): Promise<SignCoinHistoryResponse>;

  signWalletKycAuth(
    req: SignWalletKycAuthRequest,
  ): Promise<SignWalletKycAuthResponse>;

  signContractTermsHash(
    req: SignContractTermsHashRequest,
  ): Promise<SignContractTermsHashResponse>;
}

/**
 * Implementation of the Taler crypto interface where every function
 * always throws.  Only useful in practice as a way to iterate through
 * all possible crypto functions.
 *
 * (This list can be easily auto-generated by your favorite IDE).
 */
export const nullCrypto: TalerCryptoInterface = {
  createPlanchet: function (
    req: PlanchetCreationRequest,
  ): Promise<WithdrawalPlanchet> {
    throw new Error("Function not implemented.");
  },
  eddsaSign: function (req: EddsaSignRequest): Promise<EddsaSignResponse> {
    throw new Error("Function not implemented.");
  },
  createSlate: function (req: SlateCreationRequest): Promise<Slate> {
    throw new Error("Function not implemented.");
  },
  signTokenUse: function (
    req: SignTokenUseRequest,
  ): Promise<EddsaSigningResult> {
    throw new Error("Function not implemented.");
  },
  signTrackTransaction: function (
    req: SignTrackTransactionRequest,
  ): Promise<EddsaSigningResult> {
    throw new Error("Function not implemented.");
  },
  createRecoupRequest: function (
    req: CreateRecoupReqRequest,
  ): Promise<RecoupRequest> {
    throw new Error("Function not implemented.");
  },
  createRecoupRefreshRequest: function (
    req: CreateRecoupRefreshReqRequest,
  ): Promise<RecoupRefreshRequest> {
    throw new Error("Function not implemented.");
  },
  isValidPaymentSignature: function (
    req: PaymentSignatureValidationRequest,
  ): Promise<ValidationResult> {
    throw new Error("Function not implemented.");
  },
  isValidWireFee: function (
    req: WireFeeValidationRequest,
  ): Promise<ValidationResult> {
    throw new Error("Function not implemented.");
  },
  isValidDenom: function (
    req: DenominationValidationRequest,
  ): Promise<ValidationResult> {
    throw new Error("Function not implemented.");
  },
  isValidWireAccount: function (
    req: WireAccountValidationRequest,
  ): Promise<ValidationResult> {
    throw new Error("Function not implemented.");
  },
  isValidGlobalFees: function (
    req: GlobalFeesValidationRequest,
  ): Promise<ValidationResult> {
    throw new Error("Function not implemented.");
  },
  isValidContractTermsSignature: function (
    req: ContractTermsValidationRequest,
  ): Promise<ValidationResult> {
    throw new Error("Function not implemented.");
  },
  createEddsaKeypair: function (req: unknown): Promise<EddsaKeyPairStrings> {
    throw new Error("Function not implemented.");
  },
  eddsaGetPublic: function (
    req: EddsaGetPublicRequest,
  ): Promise<EddsaKeyPairStrings> {
    throw new Error("Function not implemented.");
  },
  unblindDenominationSignature: function (
    req: UnblindDenominationSignatureRequest,
  ): Promise<UnblindedDenominationSignature> {
    throw new Error("Function not implemented.");
  },
  unblindTokenIssueSignature: function (
    req: UnblindTokenIssueSignatureRequest,
  ): Promise<UnblindedDenominationSignature> {
    throw new Error("Function not implemented.");
  },
  rsaUnblind: function (req: RsaUnblindRequest): Promise<RsaUnblindResponse> {
    throw new Error("Function not implemented.");
  },
  rsaVerify: function (req: RsaVerificationRequest): Promise<ValidationResult> {
    throw new Error("Function not implemented.");
  },
  signDepositPermission: function (
    depositInfo: DepositInfo,
  ): Promise<CoinDepositPermission> {
    throw new Error("Function not implemented.");
  },
  deriveRefreshSession: function (
    req: DeriveRefreshSessionRequest,
  ): Promise<DerivedRefreshSession> {
    throw new Error("Function not implemented.");
  },
  hashString: function (req: HashStringRequest): Promise<HashStringResult> {
    throw new Error("Function not implemented.");
  },
  signCoinLink: function (
    req: SignCoinLinkRequest,
  ): Promise<EddsaSigningResult> {
    throw new Error("Function not implemented.");
  },
  makeSyncSignature: function (
    req: MakeSyncSignatureRequest,
  ): Promise<EddsaSigningResult> {
    throw new Error("Function not implemented.");
  },
  setupRefreshPlanchet: function (
    req: SetupRefreshPlanchetRequest,
  ): Promise<FreshCoinEncoded> {
    throw new Error("Function not implemented.");
  },
  setupRefreshPlanchetV2: function (
    req: SetupRefreshPlanchetRequestV2,
  ): Promise<FreshCoinEncoded> {
    throw new Error("Function not implemented.");
  },
  rsaBlind: function (req: RsaBlindRequest): Promise<RsaBlindResponse> {
    throw new Error("Function not implemented.");
  },
  keyExchangeEcdheEddsa: function (
    req: KeyExchangeEcdheEddsaRequest,
  ): Promise<KeyExchangeResult> {
    throw new Error("Function not implemented.");
  },
  setupWithdrawalPlanchet: function (
    req: SetupWithdrawalPlanchetRequest,
  ): Promise<FreshCoinEncoded> {
    throw new Error("Function not implemented.");
  },
  ecdheGetPublic: function (
    req: EddsaGetPublicRequest,
  ): Promise<EddsaGetPublicResponse> {
    throw new Error("Function not implemented.");
  },
  setupRefreshTransferPub: function (
    req: SetupRefreshTransferPubRequest,
  ): Promise<TransferPubResponse> {
    throw new Error("Function not implemented.");
  },
  signPurseCreation: function (
    req: SignPurseCreationRequest,
  ): Promise<EddsaSigningResult> {
    throw new Error("Function not implemented.");
  },
  signPurseDeposits: function (
    req: SignPurseDepositsRequest,
  ): Promise<SignPurseDepositsResponse> {
    throw new Error("Function not implemented.");
  },
  encryptContractForMerge: function (
    req: EncryptContractRequest,
  ): Promise<EncryptContractResponse> {
    throw new Error("Function not implemented.");
  },
  decryptContractForMerge: function (
    req: DecryptContractRequest,
  ): Promise<DecryptContractResponse> {
    throw new Error("Function not implemented.");
  },
  signPurseMerge: function (
    req: SignPurseMergeRequest,
  ): Promise<SignPurseMergeResponse> {
    throw new Error("Function not implemented.");
  },
  encryptContractForDeposit: function (
    req: EncryptContractForDepositRequest,
  ): Promise<EncryptContractForDepositResponse> {
    throw new Error("Function not implemented.");
  },
  decryptContractForDeposit: function (
    req: DecryptContractForDepositRequest,
  ): Promise<DecryptContractForDepositResponse> {
    throw new Error("Function not implemented.");
  },
  signReservePurseCreate: function (
    req: SignReservePurseCreateRequest,
  ): Promise<SignReservePurseCreateResponse> {
    throw new Error("Function not implemented.");
  },
  signRefund: function (req: SignRefundRequest): Promise<SignRefundResponse> {
    throw new Error("Function not implemented.");
  },
  signDeletePurse: function (
    req: SignDeletePurseRequest,
  ): Promise<SignDeletePurseResponse> {
    throw new Error("Function not implemented.");
  },
  signCoinHistoryRequest: function (
    req: SignCoinHistoryRequest,
  ): Promise<SignCoinHistoryResponse> {
    throw new Error("Function not implemented.");
  },
  signReserveHistoryReq: function (
    req: SignReserveHistoryReqRequest,
  ): Promise<SignReserveHistoryReqResponse> {
    throw new Error("Function not implemented.");
  },
  signWalletAccountSetup: function (
    req: SignWalletAccountSetupRequest,
  ): Promise<SignWalletAccountSetupResponse> {
    throw new Error("Function not implemented.");
  },
  signWalletKycAuth: function (
    req: SignWalletKycAuthRequest,
  ): Promise<SignWalletKycAuthResponse> {
    throw new Error("Function not implemented.");
  },
  signContractTermsHash: function (
    req: SignContractTermsHashRequest,
  ): Promise<SignContractTermsHashResponse> {
    throw new Error("Function not implemented.");
  },
  signWithdrawal: function (
    req: SignWithdrawalRequest,
  ): Promise<SignWithdrawalResponse> {
    throw new Error("Function not implemented.");
  },
  deriveRefreshSessionV2: function (
    req: DeriveRefreshSessionRequestV2,
  ): Promise<DerivedRefreshSessionV2> {
    throw new Error("Function not implemented.");
  },
  rsaVerifyDirect: function (
    req: RsaVerificationRequest,
  ): Promise<ValidationResult> {
    throw new Error("Function not implemented.");
  },
};

export type WithArg<X> = X extends (req: infer T) => infer R
  ? (tci: TalerCryptoInterfaceR, req: T) => R
  : never;

export type TalerCryptoInterfaceR = {
  [x in keyof TalerCryptoInterface]: WithArg<TalerCryptoInterface[x]>;
};

export interface SignCoinLinkRequest {
  oldCoinPriv: string;
  newDenomHash: string;
  oldCoinPub: string;
  transferPub: string;
  coinEv: CoinEnvelope;
}

export interface SetupRefreshPlanchetRequest {
  transferSecret: string;
  coinNumber: number;
}

export interface SetupRefreshPlanchetRequestV2 {
  /**
   * Refresh planchet secret, derived from the
   * secret signature made by the old coin.
   */
  refreshPlanchetSecret: string;
  coinNumber: number;
}

export interface SetupWithdrawalPlanchetRequest {
  secretSeed: string;
  coinNumber: number;
}

export interface SignWithdrawalRequest {
  reservePriv: EddsaPrivateKeyString;
  /**
   * Total amount without fees.
   */
  amount: AmountString;

  /**
   * Total withdrawal fee.
   */
  fee: AmountString;

  coinEvs: CoinEnvelope[];

  denomsPubHashes: HashCodeString[];

  // FIXME: Age restriction stuff
}

export interface SignWithdrawalResponse {
  sig: EddsaSignatureString;
}

export interface SignPurseCreationRequest {
  pursePriv: string;
  purseExpiration: TalerProtocolTimestamp;
  purseAmount: AmountString;
  hContractTerms: HashCodeString;
  mergePub: EddsaPublicKeyString;
  minAge: number;
}

export interface SignReserveHistoryReqRequest {
  reservePriv: string;
  startOffset: number;
}

export interface SignReserveHistoryReqResponse {
  sig: string;
}

export interface SpendCoinDetails {
  coinPub: string;
  coinPriv: string;
  contribution: AmountString;
  feeDeposit: AmountString;
  denomPubHash: string;
  denomSig: UnblindedDenominationSignature;
  ageCommitmentProof: AgeCommitmentProof | undefined;
}

export interface SignPurseDepositsRequest {
  pursePub: string;
  exchangeBaseUrl: string;
  coins: SpendCoinDetails[];
}

export interface SignPurseDepositsResponse {
  deposits: PurseDeposit[];
}

export interface RsaVerificationRequest {
  hm: string;
  sig: string;
  pk: string;
}

export interface RsaBlindRequest {
  hm: string;
  bks: string;
  pub: string;
}

export interface EddsaSigningResult {
  sig: string;
}

export interface ValidationResult {
  valid: boolean;
}

export interface HashStringRequest {
  str: string;
}

export interface HashStringResult {
  h: string;
}

export interface WireFeeValidationRequest {
  type: string;
  wf: WireFee;
  masterPub: string;
}

export interface GlobalFeesValidationRequest {
  gf: GlobalFees;
  masterPub: string;
}

export interface DenominationValidationRequest {
  stampStart: TalerProtocolTimestamp;
  stampExpireWithdraw: TalerProtocolTimestamp;
  stampExpireDeposit: TalerProtocolTimestamp;
  stampExpireLegal: TalerProtocolTimestamp;
  value: AmountJson;
  feeWithdraw: AmountJson;
  feeDeposit: AmountJson;
  feeRefresh: AmountJson;
  feeRefund: AmountJson;
  denomPubHash: string;
  masterPub: string;
  masterSig: string;
}

export interface PaymentSignatureValidationRequest {
  sig: string;
  contractHash: string;
  merchantPub: string;
}

export interface ContractTermsValidationRequest {
  contractTermsHash: string;
  sig: string;
  merchantPub: string;
}

export interface WireAccountValidationRequest {
  versionCurrent: ExchangeProtocolVersion;
  paytoUri: string;
  sig: string;
  masterPub: string;
  conversionUrl?: string;
  debitRestrictions?: any[];
  creditRestrictions?: any[];
}

export interface EddsaKeyPairStrings {
  priv: string;
  pub: string;
}

export interface EddsaGetPublicRequest {
  priv: string;
}

export interface EddsaGetPublicResponse {
  pub: string;
}

export interface EcdheGetPublicRequest {
  priv: string;
}

export interface EcdheGetPublicResponse {
  pub: string;
}

export interface UnblindDenominationSignatureRequest {
  planchet: PlanchetUnblindInfo;
  evSig: BlindedDenominationSignature;
}

export interface UnblindTokenIssueSignatureRequest {
  slate: SlateUnblindInfo;
  evSig: TokenIssueBlindSig;
}

export interface FreshCoinEncoded {
  coinPub: string;
  coinPriv: string;
  bks: string;
}

export interface RsaUnblindRequest {
  blindedSig: string;
  bk: string;
  pk: string;
}

export interface RsaBlindResponse {
  blinded: string;
}

export interface RsaUnblindResponse {
  sig: string;
}

export interface KeyExchangeEcdheEddsaRequest {
  ecdhePriv: string;
  eddsaPub: string;
}

export interface KeyExchangeResult {
  h: string;
}

export interface SetupRefreshTransferPubRequest {
  secretSeed: string;
  transferPubIndex: number;
}

export interface TransferPubResponse {
  transferPub: string;
  transferPriv: string;
}

/**
 * JS-native implementation of the Taler crypto worker operations.
 */
export const nativeCryptoR: TalerCryptoInterfaceR = {
  async eddsaSign(
    tci: TalerCryptoInterfaceR,
    req: EddsaSignRequest,
  ): Promise<EddsaSignResponse> {
    return {
      sig: encodeCrock(eddsaSign(decodeCrock(req.msg), decodeCrock(req.priv))),
    };
  },

  async rsaBlind(
    tci: TalerCryptoInterfaceR,
    req: RsaBlindRequest,
  ): Promise<RsaBlindResponse> {
    const res = rsaBlind(
      decodeCrock(req.hm),
      decodeCrock(req.bks),
      decodeCrock(req.pub),
    );
    return {
      blinded: encodeCrock(res),
    };
  },

  async setupRefreshPlanchet(
    tci: TalerCryptoInterfaceR,
    req: SetupRefreshPlanchetRequest,
  ): Promise<FreshCoinEncoded> {
    const transferSecret = decodeCrock(req.transferSecret);
    const coinNumber = req.coinNumber;
    // See TALER_transfer_secret_to_planchet_secret in C impl
    const planchetMasterSecret = kdfKw({
      ikm: transferSecret,
      outputLength: 32,
      salt: bufferForUint32(coinNumber),
      info: stringToBytes("taler-coin-derivation"),
    });

    const coinPriv = kdfKw({
      ikm: planchetMasterSecret,
      outputLength: 32,
      salt: stringToBytes("coin"),
    });

    const bks = kdfKw({
      ikm: planchetMasterSecret,
      outputLength: 32,
      salt: stringToBytes("bks"),
    });

    const coinPrivEnc = encodeCrock(coinPriv);
    const coinPubRes = await tci.eddsaGetPublic(tci, {
      priv: coinPrivEnc,
    });

    return {
      bks: encodeCrock(bks),
      coinPriv: coinPrivEnc,
      coinPub: coinPubRes.pub,
    };
  },

  async setupRefreshPlanchetV2(
    tci: TalerCryptoInterfaceR,
    req: SetupRefreshPlanchetRequestV2,
  ): Promise<FreshCoinEncoded> {
    const planchetMasterSecret = decodeCrock(req.refreshPlanchetSecret);

    const coinPriv = kdfKw({
      ikm: planchetMasterSecret,
      outputLength: 32,
      salt: stringToBytes("coin"),
    });

    const bks = kdfKw({
      ikm: planchetMasterSecret,
      outputLength: 32,
      salt: stringToBytes("bks"),
    });

    const coinPrivEnc = encodeCrock(coinPriv);
    const coinPubRes = await tci.eddsaGetPublic(tci, {
      priv: coinPrivEnc,
    });

    return {
      bks: encodeCrock(bks),
      coinPriv: coinPrivEnc,
      coinPub: coinPubRes.pub,
    };
  },

  async setupWithdrawalPlanchet(
    tci: TalerCryptoInterfaceR,
    req: SetupWithdrawalPlanchetRequest,
  ): Promise<FreshCoinEncoded> {
    const info = stringToBytes("taler-withdrawal-coin-derivation");
    const saltArrBuf = new ArrayBuffer(4);
    const salt = new Uint8Array(saltArrBuf);
    const saltDataView = new DataView(saltArrBuf);
    saltDataView.setUint32(0, req.coinNumber);
    const secretSeedDec = decodeCrock(req.secretSeed);
    const out = kdf(64, secretSeedDec, salt, info);
    const coinPriv = out.slice(0, 32);
    const bks = out.slice(32, 64);
    const coinPrivEnc = encodeCrock(coinPriv);
    const coinPubRes = await tci.eddsaGetPublic(tci, {
      priv: coinPrivEnc,
    });
    return {
      bks: encodeCrock(bks),
      coinPriv: coinPrivEnc,
      coinPub: coinPubRes.pub,
    };
  },

  async createPlanchet(
    tci: TalerCryptoInterfaceR,
    req: PlanchetCreationRequest,
  ): Promise<WithdrawalPlanchet> {
    const denomPub = req.denomPub;
    if (denomPub.cipher === DenomKeyType.Rsa) {
      const reservePub = decodeCrock(req.reservePub);
      const derivedPlanchet = await tci.setupWithdrawalPlanchet(tci, {
        coinNumber: req.coinIndex,
        secretSeed: req.secretSeed,
      });

      let maybeAcp: AgeCommitmentProof | undefined = undefined;
      let maybeAgeCommitmentHash: string | undefined = undefined;
      if (denomPub.age_mask) {
        const age = req.restrictAge || AgeRestriction.AGE_UNRESTRICTED;
        logger.info(`creating age-restricted planchet (age ${age})`);
        maybeAcp = await AgeRestriction.restrictionCommitSeeded(
          denomPub.age_mask,
          age,
          stringToBytes(req.secretSeed),
        );
        maybeAgeCommitmentHash = AgeRestriction.hashCommitment(
          maybeAcp.commitment,
        );
      }

      const coinPubHash = hashCoinPub(
        derivedPlanchet.coinPub,
        maybeAgeCommitmentHash,
      );

      const blindResp = await tci.rsaBlind(tci, {
        bks: derivedPlanchet.bks,
        hm: encodeCrock(coinPubHash),
        pub: denomPub.rsa_public_key,
      });
      const coinEv: CoinEnvelope = {
        cipher: DenomKeyType.Rsa,
        rsa_blinded_planchet: blindResp.blinded,
      };
      const amountWithFee = Amounts.add(req.value, req.feeWithdraw).amount;
      const denomPubHash = hashDenomPub(req.denomPub);
      const evHash = hashCoinEv(coinEv, encodeCrock(denomPubHash));
      const withdrawRequest = buildSigPS(
        TalerSignaturePurpose.WALLET_RESERVE_WITHDRAW,
      )
        .put(bufferFromAmount(amountWithFee))
        .put(denomPubHash)
        .put(evHash)
        .build();

      const sigResult = await tci.eddsaSign(tci, {
        msg: encodeCrock(withdrawRequest),
        priv: req.reservePriv,
      });

      const planchet: WithdrawalPlanchet = {
        blindingKey: derivedPlanchet.bks,
        coinEv,
        coinPriv: derivedPlanchet.coinPriv,
        coinPub: derivedPlanchet.coinPub,
        coinValue: req.value,
        denomPub,
        denomPubHash: encodeCrock(denomPubHash),
        reservePub: encodeCrock(reservePub),
        withdrawSig: sigResult.sig,
        coinEvHash: encodeCrock(evHash),
        ageCommitmentProof: maybeAcp,
      };
      return planchet;
    } else {
      throw Error("unsupported cipher, unable to create planchet");
    }
  },

  async createSlate(
    tci: TalerCryptoInterfaceR,
    req: SlateCreationRequest,
  ): Promise<Slate> {
    if (req.tokenIssuePub.cipher !== DenomKeyType.Rsa) {
      throw Error(`unsupported cipher (${req.tokenIssuePub.cipher})`);
    }

    // token generation
    const info = stringToBytes("taler-payment-token-derivation");
    const saltArrBuf = new ArrayBuffer(4);
    const salt = new Uint8Array(saltArrBuf);
    const saltDataView = new DataView(saltArrBuf);
    saltDataView.setUint32(0, req.outputIndex);
    const secretSeedDec = decodeCrock(req.secretSeed);
    const out = kdf(64, secretSeedDec, salt, info);
    const tokenPriv = out.slice(0, 32);
    const bks = encodeCrock(out.slice(32, 64));
    const tokenPrivEnc = encodeCrock(tokenPriv);
    const tokenPubRes = await tci.eddsaGetPublic(tci, {
      priv: tokenPrivEnc,
    });

    // token hashing
    const tokenPubHash = hash(decodeCrock(tokenPubRes.pub));

    // token blinding
    const blindResp = await tci.rsaBlind(tci, {
      bks: bks,
      hm: encodeCrock(tokenPubHash),
      pub: req.tokenIssuePub.rsa_pub,
    });

    // token envelope
    const tokenEv: TokenEnvelope = {
      cipher: DenomKeyType.Rsa,
      rsa_blinded_planchet: blindResp.blinded,
    };

    // token issue public key hash
    const tokenIssuePubHash = hashTokenIssuePub(req.tokenIssuePub);
    const evHash = hashTokenEv(tokenEv, encodeCrock(tokenIssuePubHash));

    const choice = req.contractTerms.choices[req.outputIndex];
    const tokenEvs: TokenEnvelope[] = [];
    for (const output of choice.outputs) {
      if (output.type !== MerchantContractOutputType.Token) {
        continue;
      }

      const slug = output.token_family_slug;
      const family = req.contractTerms.token_families[slug];
      tokenEvs.push(
        ...family.keys.map((key) => {
          let tokenEv: TokenEnvelope;
          if (key.cipher === DenomKeyType.Rsa) {
            tokenEv = {
              cipher: DenomKeyType.Rsa,
              rsa_blinded_planchet: key.rsa_pub,
            };
          } else {
            throw Error(`unsupported cipher (${req.tokenIssuePub.cipher})`);
          }
          return tokenEv;
        }),
      );
    }

    // wallet data object with envelopes
    const tokenWalletData: PayWalletData = {
      choice_index: req.choiceIndex,
      tokens_evs: tokenEvs,
    };

    return {
      tokenPub: tokenPubRes.pub,
      tokenPriv: tokenPrivEnc,
      tokenIssuePub: req.tokenIssuePub,
      tokenIssuePubHash: encodeCrock(tokenIssuePubHash),
      tokenWalletData,
      tokenEv,
      tokenEvHash: encodeCrock(evHash),
      blindingKey: bks,
    };
  },

  async signTokenUse(
    tci: TalerCryptoInterfaceR,
    req: SignTokenUseRequest,
  ): Promise<EddsaSigningResult> {
    const tokenUseRequest = buildSigPS(TalerSignaturePurpose.WALLET_TOKEN_USE)
      .put(decodeCrock(req.contractTermsHash))
      .put(decodeCrock(req.walletDataHash))
      .build();

    return await tci.eddsaSign(tci, {
      msg: encodeCrock(tokenUseRequest),
      priv: req.tokenUsePriv,
    });
  },

  async signTrackTransaction(
    tci: TalerCryptoInterfaceR,
    req: SignTrackTransactionRequest,
  ): Promise<EddsaSigningResult> {
    const p = buildSigPS(TalerSignaturePurpose.MERCHANT_TRACK_TRANSACTION)
      .put(decodeCrock(req.contractTermsHash))
      .put(decodeCrock(req.wireHash))
      .put(decodeCrock(req.coinPub))
      .build();
    return { sig: encodeCrock(eddsaSign(p, decodeCrock(req.merchantPriv))) };
  },

  /**
   * Create and sign a message to recoup a coin.
   */
  async createRecoupRequest(
    tci: TalerCryptoInterfaceR,
    req: CreateRecoupReqRequest,
  ): Promise<RecoupRequest> {
    const p = buildSigPS(TalerSignaturePurpose.WALLET_COIN_RECOUP)
      .put(decodeCrock(req.denomPubHash))
      .put(decodeCrock(req.blindingKey))
      .build();

    const coinPriv = decodeCrock(req.coinPriv);
    const coinSig = eddsaSign(p, coinPriv);
    if (req.denomPub.cipher === DenomKeyType.Rsa) {
      const paybackRequest: RecoupRequest = {
        coin_blind_key_secret: req.blindingKey,
        coin_sig: encodeCrock(coinSig),
        denom_pub_hash: req.denomPubHash,
        denom_sig: req.denomSig,
        // FIXME!
        ewv: {
          cipher: "RSA",
        },
      };
      return paybackRequest;
    } else {
      throw new Error();
    }
  },

  /**
   * Create and sign a message to recoup a coin.
   */
  async createRecoupRefreshRequest(
    tci: TalerCryptoInterfaceR,
    req: CreateRecoupRefreshReqRequest,
  ): Promise<RecoupRefreshRequest> {
    const p = buildSigPS(TalerSignaturePurpose.WALLET_COIN_RECOUP_REFRESH)
      .put(decodeCrock(req.denomPubHash))
      .put(decodeCrock(req.blindingKey))
      .build();

    const coinPriv = decodeCrock(req.coinPriv);
    const coinSig = eddsaSign(p, coinPriv);
    if (req.denomPub.cipher === DenomKeyType.Rsa) {
      const recoupRequest: RecoupRefreshRequest = {
        coin_blind_key_secret: req.blindingKey,
        coin_sig: encodeCrock(coinSig),
        denom_pub_hash: req.denomPubHash,
        denom_sig: req.denomSig,
        // FIXME!
        ewv: {
          cipher: "RSA",
        },
      };
      return recoupRequest;
    } else {
      throw new Error();
    }
  },

  /**
   * Check if a payment signature is valid.
   */
  async isValidPaymentSignature(
    tci: TalerCryptoInterfaceR,
    req: PaymentSignatureValidationRequest,
  ): Promise<ValidationResult> {
    const { contractHash, sig, merchantPub } = req;
    const p = buildSigPS(TalerSignaturePurpose.MERCHANT_PAYMENT_OK)
      .put(decodeCrock(contractHash))
      .build();
    const sigBytes = decodeCrock(sig);
    const pubBytes = decodeCrock(merchantPub);
    return { valid: eddsaVerify(p, sigBytes, pubBytes) };
  },

  /**
   * Check if a wire fee is correctly signed.
   */
  async isValidWireFee(
    tci: TalerCryptoInterfaceR,
    req: WireFeeValidationRequest,
  ): Promise<ValidationResult> {
    const { type, wf, masterPub } = req;
    const p = buildSigPS(TalerSignaturePurpose.MASTER_WIRE_FEES)
      .put(hash(stringToBytes(type + "\0")))
      .put(timestampRoundedToBuffer(wf.startStamp))
      .put(timestampRoundedToBuffer(wf.endStamp))
      .put(bufferFromAmount(wf.wireFee))
      .put(bufferFromAmount(wf.closingFee))
      .build();
    const sig = decodeCrock(wf.sig);
    const pub = decodeCrock(masterPub);
    return { valid: eddsaVerify(p, sig, pub) };
  },

  /**
   * Check if a global fee is correctly signed.
   */
  async isValidGlobalFees(
    tci: TalerCryptoInterfaceR,
    req: GlobalFeesValidationRequest,
  ): Promise<ValidationResult> {
    const { gf, masterPub } = req;
    const p = buildSigPS(TalerSignaturePurpose.MASTER_GLOBAL_FEES)
      .put(timestampRoundedToBuffer(gf.start_date))
      .put(timestampRoundedToBuffer(gf.end_date))
      .put(durationRoundedToBuffer(gf.purse_timeout))
      .put(durationRoundedToBuffer(gf.history_expiration))
      .put(bufferFromAmount(Amounts.parseOrThrow(gf.history_fee)))
      .put(bufferFromAmount(Amounts.parseOrThrow(gf.account_fee)))
      .put(bufferFromAmount(Amounts.parseOrThrow(gf.purse_fee)))
      .put(bufferForUint32(gf.purse_account_limit))
      .build();
    const sig = decodeCrock(gf.master_sig);
    const pub = decodeCrock(masterPub);
    return { valid: eddsaVerify(p, sig, pub) };
  },

  /**
   * Check if the signature of a denomination is valid.
   */
  async isValidDenom(
    tci: TalerCryptoInterfaceR,
    req: DenominationValidationRequest,
  ): Promise<ValidationResult> {
    const value: AmountJson = Amounts.parseOrThrow(req.value);
    const p = buildSigPS(TalerSignaturePurpose.MASTER_DENOMINATION_KEY_VALIDITY)
      .put(decodeCrock(req.masterPub))
      .put(timestampRoundedToBuffer(req.stampStart))
      .put(timestampRoundedToBuffer(req.stampExpireWithdraw))
      .put(timestampRoundedToBuffer(req.stampExpireDeposit))
      .put(timestampRoundedToBuffer(req.stampExpireLegal))
      .put(bufferFromAmount(value))
      .put(bufferFromAmount(req.feeWithdraw))
      .put(bufferFromAmount(req.feeDeposit))
      .put(bufferFromAmount(req.feeRefresh))
      .put(bufferFromAmount(req.feeRefund))
      .put(decodeCrock(req.denomPubHash))
      .build();
    const sig = decodeCrock(req.masterSig);
    const pub = decodeCrock(req.masterPub);
    const res = eddsaVerify(p, sig, pub);
    return { valid: res };
  },

  async isValidWireAccount(
    tci: TalerCryptoInterfaceR,
    req: WireAccountValidationRequest,
  ): Promise<ValidationResult> {
    const { sig, masterPub, paytoUri } = req;
    const paytoHash = hashTruncate32(stringToBytes(paytoUri + "\0"));
    const pb = buildSigPS(TalerSignaturePurpose.MASTER_WIRE_DETAILS);
    pb.put(paytoHash);
    if (req.versionCurrent >= 15) {
      let conversionUrlHash;
      if (!req.conversionUrl) {
        conversionUrlHash = new Uint8Array(64);
      } else {
        conversionUrlHash = hash(stringToBytes(req.conversionUrl + "\0"));
      }
      pb.put(conversionUrlHash);
      pb.put(hash(stringToBytes(canonicalJson(req.debitRestrictions) + "\0")));
      pb.put(hash(stringToBytes(canonicalJson(req.creditRestrictions) + "\0")));
    }
    const p = pb.build();
    return { valid: eddsaVerify(p, decodeCrock(sig), decodeCrock(masterPub)) };
  },

  async isValidContractTermsSignature(
    tci: TalerCryptoInterfaceR,
    req: ContractTermsValidationRequest,
  ): Promise<ValidationResult> {
    const cthDec = decodeCrock(req.contractTermsHash);
    const p = buildSigPS(TalerSignaturePurpose.MERCHANT_CONTRACT)
      .put(cthDec)
      .build();
    return {
      valid: eddsaVerify(p, decodeCrock(req.sig), decodeCrock(req.merchantPub)),
    };
  },

  /**
   * Create a new EdDSA key pair.
   */
  async createEddsaKeypair(
    tci: TalerCryptoInterfaceR,
  ): Promise<EddsaKeyPairStrings> {
    const eddsaPriv = encodeCrock(getRandomBytes(32));
    const eddsaPubRes = await tci.eddsaGetPublic(tci, {
      priv: eddsaPriv,
    });
    return {
      priv: eddsaPriv,
      pub: eddsaPubRes.pub,
    };
  },

  async eddsaGetPublic(
    tci: TalerCryptoInterfaceR,
    req: EddsaGetPublicRequest,
  ): Promise<EddsaKeyPairStrings> {
    return {
      priv: req.priv,
      pub: encodeCrock(eddsaGetPublic(decodeCrock(req.priv))),
    };
  },

  async unblindDenominationSignature(
    tci: TalerCryptoInterfaceR,
    req: UnblindDenominationSignatureRequest,
  ): Promise<UnblindedDenominationSignature> {
    if (req.evSig.cipher === DenomKeyType.Rsa) {
      if (req.planchet.denomPub.cipher !== DenomKeyType.Rsa) {
        throw new Error(
          "planchet cipher does not match blind signature cipher",
        );
      }
      const denomSig = rsaUnblind(
        decodeCrock(req.evSig.blinded_rsa_signature),
        decodeCrock(req.planchet.denomPub.rsa_public_key),
        decodeCrock(req.planchet.blindingKey),
      );
      return {
        cipher: DenomKeyType.Rsa,
        rsa_signature: encodeCrock(denomSig),
      };
    } else {
      throw Error(`unblinding for cipher ${req.evSig.cipher} not implemented`);
    }
  },

  async unblindTokenIssueSignature(
    tci: TalerCryptoInterfaceR,
    req: UnblindTokenIssueSignatureRequest,
  ): Promise<UnblindedDenominationSignature> {
    if (req.evSig.cipher === DenomKeyType.Rsa) {
      if (req.slate.tokenIssuePub.cipher !== DenomKeyType.Rsa) {
        throw new Error("slate cipher does not match blind signature cipher");
      }
      const { sig } = await tci.rsaUnblind(tci, {
        bk: req.slate.blindingKey,
        blindedSig: req.evSig.blinded_rsa_signature,
        pk: req.slate.tokenIssuePub.rsa_pub,
      });

      return {
        cipher: DenomKeyType.Rsa,
        rsa_signature: sig,
      };
    } else {
      throw Error(`unblinding for cipher ${req.evSig.cipher} not implemented`);
    }
  },

  /**
   * Unblind a blindly signed value.
   */
  async rsaUnblind(
    tci: TalerCryptoInterfaceR,
    req: RsaUnblindRequest,
  ): Promise<RsaUnblindResponse> {
    const denomSig = rsaUnblind(
      decodeCrock(req.blindedSig),
      decodeCrock(req.pk),
      decodeCrock(req.bk),
    );
    return { sig: encodeCrock(denomSig) };
  },

  /**
   * Verify an RSA signature, where the signature
   * is over the hash of the message.
   */
  async rsaVerify(
    tci: TalerCryptoInterfaceR,
    req: RsaVerificationRequest,
  ): Promise<ValidationResult> {
    return {
      valid: rsaVerify(
        hash(decodeCrock(req.hm)),
        decodeCrock(req.sig),
        decodeCrock(req.pk),
      ),
    };
  },

  /**
   * Verify an RSA signature.
   */
  async rsaVerifyDirect(
    tci: TalerCryptoInterfaceR,
    req: RsaVerificationRequest,
  ): Promise<ValidationResult> {
    return {
      valid: rsaVerify(
        decodeCrock(req.hm),
        decodeCrock(req.sig),
        decodeCrock(req.pk),
      ),
    };
  },

  /**
   * Generate updated coins (to store in the database)
   * and deposit permissions for each given coin.
   */
  async signDepositPermission(
    tci: TalerCryptoInterfaceR,
    depositInfo: DepositInfo,
  ): Promise<CoinDepositPermission> {
    // FIXME: put extensions here if used
    const hExt = new Uint8Array(64);
    let hAgeCommitment: Uint8Array;
    let minimumAgeSig: string | undefined = undefined;
    if (depositInfo.ageCommitmentProof) {
      const ach = AgeRestriction.hashCommitment(
        depositInfo.ageCommitmentProof.commitment,
      );
      hAgeCommitment = decodeCrock(ach);
      if (depositInfo.requiredMinimumAge) {
        minimumAgeSig = encodeCrock(
          AgeRestriction.commitmentAttest(
            depositInfo.ageCommitmentProof,
            depositInfo.requiredMinimumAge,
          ),
        );
      }
    } else {
      // All zeros.
      hAgeCommitment = new Uint8Array(32);
    }
    const walletDataHash = depositInfo.walletDataHash
      ? decodeCrock(depositInfo.walletDataHash)
      : new Uint8Array(64);
    let d: Uint8Array;
    if (depositInfo.denomKeyType === DenomKeyType.Rsa) {
      d = buildSigPS(TalerSignaturePurpose.WALLET_COIN_DEPOSIT)
        .put(decodeCrock(depositInfo.contractTermsHash))
        .put(hAgeCommitment)
        .put(hExt)
        .put(decodeCrock(depositInfo.wireInfoHash))
        .put(decodeCrock(depositInfo.denomPubHash))
        .put(timestampRoundedToBuffer(depositInfo.timestamp))
        .put(timestampRoundedToBuffer(depositInfo.refundDeadline))
        .put(bufferFromAmount(depositInfo.spendAmount))
        .put(bufferFromAmount(depositInfo.feeDeposit))
        .put(decodeCrock(depositInfo.merchantPub))
        .put(walletDataHash)
        .build();
    } else {
      throw Error("unsupported exchange protocol version");
    }
    const coinSigRes = await this.eddsaSign(tci, {
      msg: encodeCrock(d),
      priv: depositInfo.coinPriv,
    });

    if (debugDumpSigs) {
      logger.info(
        `Dumping deposit signature for ct=${depositInfo.contractTermsHash}`,
      );
      logger.info(`Deposit signature: ${toHexString(d)}`);
    }

    if (depositInfo.denomKeyType === DenomKeyType.Rsa) {
      const s: CoinDepositPermission = {
        coin_pub: depositInfo.coinPub,
        coin_sig: coinSigRes.sig,
        contribution: Amounts.stringify(depositInfo.spendAmount),
        h_denom: depositInfo.denomPubHash,
        exchange_url: depositInfo.exchangeBaseUrl,
        ub_sig: {
          cipher: DenomKeyType.Rsa,
          rsa_signature: depositInfo.denomSig.rsa_signature,
        },
      };

      if (depositInfo.requiredMinimumAge) {
        // These are only required by the merchant
        s.minimum_age_sig = minimumAgeSig;
        s.age_commitment =
          depositInfo.ageCommitmentProof?.commitment.publicKeys;
      } else if (depositInfo.ageCommitmentProof) {
        s.h_age_commitment = encodeCrock(hAgeCommitment);
      }

      return s;
    } else {
      throw Error(
        `unsupported denomination cipher (${depositInfo.denomKeyType})`,
      );
    }
  },

  async deriveRefreshSessionV2(
    tci: TalerCryptoInterfaceR,
    req: DeriveRefreshSessionRequestV2,
  ): Promise<DerivedRefreshSessionV2> {
    const {
      newCoinDenoms,
      feeRefresh,
      kappa,
      meltCoinDenomPubHash,
      meltCoinPriv,
      meltCoinPub,
      sessionPublicSeed,
    } = req;

    const currency = Amounts.currencyOf(newCoinDenoms[0].value);
    let valueWithFee = Amounts.zeroOfCurrency(currency);

    // Melt fee of old coin.
    valueWithFee = Amounts.add(valueWithFee, feeRefresh).amount;

    // Coin value and withdrawal fees for new coins.
    for (const ncd of newCoinDenoms) {
      const t = Amounts.add(ncd.value, ncd.feeWithdraw).amount;
      valueWithFee = Amounts.add(
        valueWithFee,
        Amounts.mult(t, ncd.count).amount,
      ).amount;
    }

    const planchetsForGammas: RefreshPlanchetInfo[][] = [];

    const noncesBytes = kdfKw({
      outputLength: 64 * kappa,
      salt: stringToBytes("refresh-kappa-nonces"),
      ikm: decodeCrock(sessionPublicSeed),
    });

    const numDenoms = newCoinDenoms.length;
    let numCoins = 0;
    const coinDenomsHc = createHashContext();
    for (let i = 0; i < numDenoms; i++) {
      for (let j = 0; j < newCoinDenoms[i].count; j++) {
        coinDenomsHc.update(decodeCrock(newCoinDenoms[i].denomPubHash));
        numCoins++;
      }
    }
    const coinDenomsHash = coinDenomsHc.finish();

    const signatures: EddsaSignatureString[] = [];

    const sessionHc = createHashContext();

    sessionHc.update(decodeCrock(sessionPublicSeed));
    const blindingSeed = new Uint8Array(32);
    // For CS, we'd need to also read th real blinding_seed into sessionHc.
    sessionHc.update(blindingSeed);
    sessionHc.update(decodeCrock(meltCoinPub));
    sessionHc.update(bufferFromAmount(valueWithFee));

    if (debugDumpSigs) {
      logger.info(`hashing blinding seed: ${toHexString(blindingSeed)}`);
      logger.info(
        `hashing session seed: ${toHexString(decodeCrock(sessionPublicSeed))}`,
      );
      logger.info(
        `hashing melt coin pub: ${toHexString(decodeCrock(meltCoinPub))}`,
      );
      logger.info(
        `hashing amount: ${toHexString(bufferFromAmount(valueWithFee))}`,
      );
    }

    for (let i = 0; i < kappa; i++) {
      const planchets: RefreshPlanchetInfo[] = [];
      const nonce = noncesBytes.slice(i * 64, i * 64 + 64);

      const planchetsHc = createHashContext();

      const coinLink = buildSigPS(TalerSignaturePurpose.WALLET_COIN_LINK)
        .put(nonce)
        .put(coinDenomsHash)
        .put(bufferForUint32(i))
        .build();
      const sig = await tci.eddsaSign(tci, {
        msg: encodeCrock(coinLink),
        priv: meltCoinPriv,
      });
      signatures.push(sig.sig);

      const planchetSecretsBytes = kdfKw({
        outputLength: 32 * numCoins,
        salt: stringToBytes("refresh-planchet-secret"),
        ikm: decodeCrock(sig.sig),
      });

      for (let j = 0; j < numDenoms; j++) {
        const denomSel = newCoinDenoms[j];
        for (let k = 0; k < newCoinDenoms[j].count; k++) {
          const coinIndex = planchets.length;
          const myPlanchetSecret = planchetSecretsBytes.slice(
            32 * coinIndex,
            32 * coinIndex + 32,
          );
          let fresh: FreshCoinEncoded = await tci.setupRefreshPlanchetV2(tci, {
            coinNumber: coinIndex,
            refreshPlanchetSecret: encodeCrock(myPlanchetSecret),
          });
          let newAc: AgeCommitmentProof | undefined = undefined;
          let newAch: HashCodeString | undefined = undefined;
          if (req.meltCoinAgeCommitmentProof) {
            const ageCommitmentSalt = kdfKw({
              ikm: myPlanchetSecret,
              outputLength: 64,
              salt: stringToBytes("age commitment"),
            });
            newAc = await AgeRestriction.commitmentDerive(
              req.meltCoinAgeCommitmentProof,
              ageCommitmentSalt,
            );
            newAch = AgeRestriction.hashCommitment(newAc.commitment);
          }
          const coinPubHash = hashCoinPub(fresh.coinPub, newAch);
          if (denomSel.denomPub.cipher !== DenomKeyType.Rsa) {
            throw Error("unsupported cipher, can't create refresh session");
          }
          const blindResult = await tci.rsaBlind(tci, {
            bks: fresh.bks,
            hm: encodeCrock(coinPubHash),
            pub: denomSel.denomPub.rsa_public_key,
          });
          const coinEv: CoinEnvelope = {
            cipher: DenomKeyType.Rsa,
            rsa_blinded_planchet: blindResult.blinded,
          };
          const coinEvHash = hashCoinEv(
            coinEv,
            encodeCrock(hashDenomPub(denomSel.denomPub)),
          );
          const planchet: RefreshPlanchetInfo = {
            blindingKey: fresh.bks,
            coinEv,
            coinPriv: fresh.coinPriv,
            coinPub: fresh.coinPub,
            coinEvHash: encodeCrock(coinEvHash),
            maxAge: req.meltCoinMaxAge,
            ageCommitmentProof: newAc,
          };
          planchets.push(planchet);
          planchetsHc.update(coinEvHash);
        }
      }

      const planchetsHash = planchetsHc.finish();
      if (debugDumpSigs) {
        logger.info(`planchets hash ${i}: ${toHexString(planchetsHash)}`);
      }
      sessionHc.update(planchetsHash);

      planchetsForGammas.push(planchets);
    }

    const sessionHash = sessionHc.finish();
    let confirmData: Uint8Array;
    let hAgeCommitment: Uint8Array;
    if (req.meltCoinAgeCommitmentProof) {
      hAgeCommitment = decodeCrock(
        AgeRestriction.hashCommitment(
          req.meltCoinAgeCommitmentProof.commitment,
        ),
      );
    } else {
      hAgeCommitment = new Uint8Array(32);
    }
    confirmData = buildSigPS(TalerSignaturePurpose.WALLET_COIN_MELT)
      .put(sessionHash)
      .put(decodeCrock(meltCoinDenomPubHash))
      .put(hAgeCommitment)
      .put(bufferFromAmount(valueWithFee))
      .put(bufferFromAmount(feeRefresh))
      .build();

    if (debugDumpSigs) {
      logger.info(`melt signature blob:`);
      logger.info(toHexString(confirmData));
      logger.info(`valueWithFee: ${Amounts.stringify(valueWithFee)}`);
      logger.info(`meltFee: ${Amounts.stringify(feeRefresh)}`);
    }

    const confirmSigResp = await tci.eddsaSign(tci, {
      msg: encodeCrock(confirmData),
      priv: meltCoinPriv,
    });

    return {
      signatures,
      planchets: planchetsForGammas,
      confirmSig: confirmSigResp.sig,
      hash: encodeCrock(sessionHash),
      meltCoinPub: meltCoinPub,
      meltValueWithFee: valueWithFee,
    };
  },

  async deriveRefreshSession(
    tci: TalerCryptoInterfaceR,
    req: DeriveRefreshSessionRequest,
  ): Promise<DerivedRefreshSession> {
    const {
      newCoinDenoms,
      feeRefresh: meltFee,
      kappa,
      meltCoinDenomPubHash,
      meltCoinPriv,
      meltCoinPub,
      sessionSecretSeed: refreshSessionSecretSeed,
    } = req;

    const currency = Amounts.currencyOf(newCoinDenoms[0].value);
    let valueWithFee = Amounts.zeroOfCurrency(currency);

    for (const ncd of newCoinDenoms) {
      const t = Amounts.add(ncd.value, ncd.feeWithdraw).amount;
      valueWithFee = Amounts.add(
        valueWithFee,
        Amounts.mult(t, ncd.count).amount,
      ).amount;
    }

    // melt fee
    valueWithFee = Amounts.add(valueWithFee, meltFee).amount;

    const sessionHc = createHashContext();

    const transferPubs: string[] = [];
    const transferPrivs: string[] = [];

    const planchetsForGammas: RefreshPlanchetInfo[][] = [];

    for (let i = 0; i < kappa; i++) {
      const transferKeyPair = await tci.setupRefreshTransferPub(tci, {
        secretSeed: refreshSessionSecretSeed,
        transferPubIndex: i,
      });
      sessionHc.update(decodeCrock(transferKeyPair.transferPub));
      transferPrivs.push(transferKeyPair.transferPriv);
      transferPubs.push(transferKeyPair.transferPub);
    }

    for (const denomSel of newCoinDenoms) {
      for (let i = 0; i < denomSel.count; i++) {
        if (denomSel.denomPub.cipher === DenomKeyType.Rsa) {
          const denomPubHash = hashDenomPub(denomSel.denomPub);
          sessionHc.update(denomPubHash);
        } else {
          throw new Error();
        }
      }
    }

    sessionHc.update(decodeCrock(meltCoinPub));
    sessionHc.update(bufferFromAmount(valueWithFee));

    for (let i = 0; i < kappa; i++) {
      const planchets: RefreshPlanchetInfo[] = [];
      for (let j = 0; j < newCoinDenoms.length; j++) {
        const denomSel = newCoinDenoms[j];
        for (let k = 0; k < denomSel.count; k++) {
          const coinIndex = planchets.length;
          const transferSecretRes = await tci.keyExchangeEcdheEddsa(tci, {
            ecdhePriv: transferPrivs[i],
            eddsaPub: meltCoinPub,
          });
          let coinPub: Uint8Array;
          let coinPriv: Uint8Array;
          let blindingFactor: Uint8Array;
          let fresh: FreshCoinEncoded = await tci.setupRefreshPlanchet(tci, {
            coinNumber: coinIndex,
            transferSecret: transferSecretRes.h,
          });
          let newAc: AgeCommitmentProof | undefined = undefined;
          let newAch: HashCodeString | undefined = undefined;
          if (req.meltCoinAgeCommitmentProof) {
            newAc = await AgeRestriction.commitmentDerive(
              req.meltCoinAgeCommitmentProof,
              decodeCrock(transferSecretRes.h),
            );
            newAch = AgeRestriction.hashCommitment(newAc.commitment);
          }
          coinPriv = decodeCrock(fresh.coinPriv);
          coinPub = decodeCrock(fresh.coinPub);
          blindingFactor = decodeCrock(fresh.bks);
          const coinPubHash = hashCoinPub(fresh.coinPub, newAch);
          if (denomSel.denomPub.cipher !== DenomKeyType.Rsa) {
            throw Error("unsupported cipher, can't create refresh session");
          }
          const blindResult = await tci.rsaBlind(tci, {
            bks: encodeCrock(blindingFactor),
            hm: encodeCrock(coinPubHash),
            pub: denomSel.denomPub.rsa_public_key,
          });
          const coinEv: CoinEnvelope = {
            cipher: DenomKeyType.Rsa,
            rsa_blinded_planchet: blindResult.blinded,
          };
          const coinEvHash = hashCoinEv(
            coinEv,
            encodeCrock(hashDenomPub(denomSel.denomPub)),
          );
          const planchet: RefreshPlanchetInfo = {
            blindingKey: encodeCrock(blindingFactor),
            coinEv,
            coinPriv: encodeCrock(coinPriv),
            coinPub: encodeCrock(coinPub),
            coinEvHash: encodeCrock(coinEvHash),
            maxAge: req.meltCoinMaxAge,
            ageCommitmentProof: newAc,
          };
          planchets.push(planchet);
          hashCoinEvInner(coinEv, sessionHc);
        }
      }
      planchetsForGammas.push(planchets);
    }

    const sessionHash = sessionHc.finish();
    let confirmData: Uint8Array;
    let hAgeCommitment: Uint8Array;
    if (req.meltCoinAgeCommitmentProof) {
      hAgeCommitment = decodeCrock(
        AgeRestriction.hashCommitment(
          req.meltCoinAgeCommitmentProof.commitment,
        ),
      );
    } else {
      hAgeCommitment = new Uint8Array(32);
    }
    confirmData = buildSigPS(TalerSignaturePurpose.WALLET_COIN_MELT)
      .put(sessionHash)
      .put(decodeCrock(meltCoinDenomPubHash))
      .put(hAgeCommitment)
      .put(bufferFromAmount(valueWithFee))
      .put(bufferFromAmount(meltFee))
      .build();

    const confirmSigResp = await tci.eddsaSign(tci, {
      msg: encodeCrock(confirmData),
      priv: meltCoinPriv,
    });

    const refreshSession: DerivedRefreshSession = {
      confirmSig: confirmSigResp.sig,
      hash: encodeCrock(sessionHash),
      meltCoinPub: meltCoinPub,
      planchets: planchetsForGammas,
      transferPrivs,
      transferPubs,
      meltValueWithFee: valueWithFee,
    };

    return refreshSession;
  },

  /**
   * Hash a string including the zero terminator.
   */
  async hashString(
    tci: TalerCryptoInterfaceR,
    req: HashStringRequest,
  ): Promise<HashStringResult> {
    const b = stringToBytes(req.str + "\0");
    return { h: encodeCrock(hash(b)) };
  },

  async signCoinLink(
    tci: TalerCryptoInterfaceR,
    req: SignCoinLinkRequest,
  ): Promise<EddsaSigningResult> {
    const coinEvHash = hashCoinEv(req.coinEv, req.newDenomHash);
    // FIXME: fill in
    const hAgeCommitment = new Uint8Array(32);
    const coinLink = buildSigPS(TalerSignaturePurpose.WALLET_COIN_LINK)
      .put(decodeCrock(req.newDenomHash))
      .put(decodeCrock(req.transferPub))
      .put(hAgeCommitment)
      .put(coinEvHash)
      .build();
    return tci.eddsaSign(tci, {
      msg: encodeCrock(coinLink),
      priv: req.oldCoinPriv,
    });
  },

  async makeSyncSignature(
    tci: TalerCryptoInterfaceR,
    req: MakeSyncSignatureRequest,
  ): Promise<EddsaSigningResult> {
    const hNew = decodeCrock(req.newHash);
    let hOld: Uint8Array;
    if (req.oldHash) {
      hOld = decodeCrock(req.oldHash);
    } else {
      hOld = new Uint8Array(64);
    }
    const sigBlob = buildSigPS(TalerSignaturePurpose.SYNC_BACKUP_UPLOAD)
      .put(hOld)
      .put(hNew)
      .build();
    const uploadSig = eddsaSign(sigBlob, decodeCrock(req.accountPriv));
    return { sig: encodeCrock(uploadSig) };
  },
  async keyExchangeEcdheEddsa(
    tci: TalerCryptoInterfaceR,
    req: KeyExchangeEcdheEddsaRequest,
  ): Promise<KeyExchangeResult> {
    return {
      h: encodeCrock(
        keyExchangeEcdhEddsa(
          decodeCrock(req.ecdhePriv),
          decodeCrock(req.eddsaPub),
        ),
      ),
    };
  },
  async ecdheGetPublic(
    tci: TalerCryptoInterfaceR,
    req: EcdheGetPublicRequest,
  ): Promise<EcdheGetPublicResponse> {
    return {
      pub: encodeCrock(ecdhGetPublic(decodeCrock(req.priv))),
    };
  },
  async setupRefreshTransferPub(
    tci: TalerCryptoInterfaceR,
    req: SetupRefreshTransferPubRequest,
  ): Promise<TransferPubResponse> {
    const info = stringToBytes("taler-transfer-pub-derivation");
    const saltArrBuf = new ArrayBuffer(4);
    const salt = new Uint8Array(saltArrBuf);
    const saltDataView = new DataView(saltArrBuf);
    saltDataView.setUint32(0, req.transferPubIndex);
    const out = kdf(32, decodeCrock(req.secretSeed), salt, info);
    const transferPriv = encodeCrock(out);
    return {
      transferPriv,
      transferPub: (await tci.ecdheGetPublic(tci, { priv: transferPriv })).pub,
    };
  },
  async signPurseCreation(
    tci: TalerCryptoInterfaceR,
    req: SignPurseCreationRequest,
  ): Promise<EddsaSigningResult> {
    const sigBlob = buildSigPS(TalerSignaturePurpose.WALLET_PURSE_CREATE)
      .put(timestampRoundedToBuffer(req.purseExpiration))
      .put(bufferFromAmount(Amounts.parseOrThrow(req.purseAmount)))
      .put(decodeCrock(req.hContractTerms))
      .put(decodeCrock(req.mergePub))
      .put(bufferForUint32(req.minAge))
      .build();
    return await tci.eddsaSign(tci, {
      msg: encodeCrock(sigBlob),
      priv: req.pursePriv,
    });
  },
  async signPurseDeposits(
    tci: TalerCryptoInterfaceR,
    req: SignPurseDepositsRequest,
  ): Promise<SignPurseDepositsResponse> {
    const hExchangeBaseUrl = hash(stringToBytes(req.exchangeBaseUrl + "\0"));
    const deposits: PurseDeposit[] = [];
    for (const c of req.coins) {
      let maybeAch: Uint8Array;
      if (c.ageCommitmentProof) {
        maybeAch = decodeCrock(
          AgeRestriction.hashCommitment(c.ageCommitmentProof.commitment),
        );
      } else {
        maybeAch = new Uint8Array(32);
      }
      const sigBlob = buildSigPS(TalerSignaturePurpose.WALLET_PURSE_DEPOSIT)
        .put(bufferFromAmount(Amounts.parseOrThrow(c.contribution)))
        .put(decodeCrock(c.denomPubHash))
        .put(maybeAch)
        .put(decodeCrock(req.pursePub))
        .put(hExchangeBaseUrl)
        .build();
      const sigResp = await tci.eddsaSign(tci, {
        msg: encodeCrock(sigBlob),
        priv: c.coinPriv,
      });
      deposits.push({
        amount: c.contribution,
        coin_pub: c.coinPub,
        coin_sig: sigResp.sig,
        denom_pub_hash: c.denomPubHash,
        ub_sig: c.denomSig,
        age_commitment: c.ageCommitmentProof
          ? c.ageCommitmentProof.commitment.publicKeys
          : undefined,
      });
    }
    return {
      deposits,
    };
  },
  async encryptContractForMerge(
    tci: TalerCryptoInterfaceR,
    req: EncryptContractRequest,
  ): Promise<EncryptContractResponse> {
    const enc = await encryptContractForMerge(
      decodeCrock(req.pursePub),
      decodeCrock(req.contractPriv),
      decodeCrock(req.mergePriv),
      req.contractTerms,
      decodeCrock(req.nonce),
    );
    const sigBlob = buildSigPS(TalerSignaturePurpose.WALLET_PURSE_ECONTRACT)
      .put(hash(enc))
      .put(decodeCrock(req.contractPub))
      .build();
    const sig = eddsaSign(sigBlob, decodeCrock(req.pursePriv));
    return {
      econtract: {
        contract_pub: req.contractPub,
        econtract: encodeCrock(enc),
        econtract_sig: encodeCrock(sig),
      },
    };
  },
  async decryptContractForMerge(
    tci: TalerCryptoInterfaceR,
    req: DecryptContractRequest,
  ): Promise<DecryptContractResponse> {
    const res = await decryptContractForMerge(
      decodeCrock(req.ciphertext),
      decodeCrock(req.pursePub),
      decodeCrock(req.contractPriv),
    );
    return {
      contractTerms: res.contractTerms,
      mergePriv: encodeCrock(res.mergePriv),
    };
  },
  async encryptContractForDeposit(
    tci: TalerCryptoInterfaceR,
    req: EncryptContractForDepositRequest,
  ): Promise<EncryptContractForDepositResponse> {
    const enc = await encryptContractForDeposit(
      decodeCrock(req.pursePub),
      decodeCrock(req.contractPriv),
      req.contractTerms,
      decodeCrock(req.nonce),
    );
    const sigBlob = buildSigPS(TalerSignaturePurpose.WALLET_PURSE_ECONTRACT)
      .put(hash(enc))
      .put(decodeCrock(req.contractPub))
      .build();
    const sig = eddsaSign(sigBlob, decodeCrock(req.pursePriv));
    return {
      econtract: {
        contract_pub: req.contractPub,
        econtract: encodeCrock(enc),
        econtract_sig: encodeCrock(sig),
      },
    };
  },
  async decryptContractForDeposit(
    tci: TalerCryptoInterfaceR,
    req: DecryptContractForDepositRequest,
  ): Promise<DecryptContractForDepositResponse> {
    const res = await decryptContractForDeposit(
      decodeCrock(req.ciphertext),
      decodeCrock(req.pursePub),
      decodeCrock(req.contractPriv),
    );
    return {
      contractTerms: res.contractTerms,
    };
  },
  async signPurseMerge(
    tci: TalerCryptoInterfaceR,
    req: SignPurseMergeRequest,
  ): Promise<SignPurseMergeResponse> {
    const mergeSigBlob = buildSigPS(TalerSignaturePurpose.WALLET_PURSE_MERGE)
      .put(timestampRoundedToBuffer(req.mergeTimestamp))
      .put(decodeCrock(req.pursePub))
      .put(hashTruncate32(stringToBytes(req.reservePayto + "\0")))
      .build();
    const mergeSigResp = await tci.eddsaSign(tci, {
      msg: encodeCrock(mergeSigBlob),
      priv: req.mergePriv,
    });

    const reserveSigBlob = buildSigPS(
      TalerSignaturePurpose.WALLET_ACCOUNT_MERGE,
    )
      .put(timestampRoundedToBuffer(req.purseExpiration))
      .put(bufferFromAmount(Amounts.parseOrThrow(req.purseAmount)))
      .put(bufferFromAmount(Amounts.parseOrThrow(req.purseFee)))
      .put(decodeCrock(req.contractTermsHash))
      .put(decodeCrock(req.pursePub))
      .put(timestampRoundedToBuffer(req.mergeTimestamp))
      // FIXME: put in min_age
      .put(bufferForUint32(0))
      .put(bufferForUint32(req.flags))
      .build();

    logger.info(
      `signing WALLET_ACCOUNT_MERGE over ${encodeCrock(reserveSigBlob)}`,
    );

    const reserveSigResp = await tci.eddsaSign(tci, {
      msg: encodeCrock(reserveSigBlob),
      priv: req.reservePriv,
    });

    return {
      mergeSig: mergeSigResp.sig,
      accountSig: reserveSigResp.sig,
    };
  },
  async signReservePurseCreate(
    tci: TalerCryptoInterfaceR,
    req: SignReservePurseCreateRequest,
  ): Promise<SignReservePurseCreateResponse> {
    const mergeSigBlob = buildSigPS(TalerSignaturePurpose.WALLET_PURSE_MERGE)
      .put(timestampRoundedToBuffer(req.mergeTimestamp))
      .put(decodeCrock(req.pursePub))
      .put(hashTruncate32(stringToBytes(req.reservePayto + "\0")))
      .build();
    const mergeSigResp = await tci.eddsaSign(tci, {
      msg: encodeCrock(mergeSigBlob),
      priv: req.mergePriv,
    });

    logger.info(`payto URI: ${req.reservePayto}`);
    logger.info(`signing WALLET_PURSE_MERGE over ${encodeCrock(mergeSigBlob)}`);

    const reserveSigBlob = buildSigPS(
      TalerSignaturePurpose.WALLET_ACCOUNT_MERGE,
    )
      .put(timestampRoundedToBuffer(req.purseExpiration))
      .put(bufferFromAmount(Amounts.parseOrThrow(req.purseAmount)))
      .put(bufferFromAmount(Amounts.parseOrThrow(req.purseFee)))
      .put(decodeCrock(req.contractTermsHash))
      .put(decodeCrock(req.pursePub))
      .put(timestampRoundedToBuffer(req.mergeTimestamp))
      // FIXME: put in min_age
      .put(bufferForUint32(0))
      .put(bufferForUint32(req.flags))
      .build();

    logger.info(
      `signing WALLET_ACCOUNT_MERGE over ${encodeCrock(reserveSigBlob)}`,
    );

    const reserveSigResp = await tci.eddsaSign(tci, {
      msg: encodeCrock(reserveSigBlob),
      priv: req.reservePriv,
    });

    const mergePub = encodeCrock(eddsaGetPublic(decodeCrock(req.mergePriv)));

    const purseSigBlob = buildSigPS(TalerSignaturePurpose.WALLET_PURSE_CREATE)
      .put(timestampRoundedToBuffer(req.purseExpiration))
      .put(bufferFromAmount(Amounts.parseOrThrow(req.purseAmount)))
      .put(decodeCrock(req.contractTermsHash))
      .put(decodeCrock(mergePub))
      // FIXME: add age!
      .put(bufferForUint32(0))
      .build();

    const purseSigResp = await tci.eddsaSign(tci, {
      msg: encodeCrock(purseSigBlob),
      priv: req.pursePriv,
    });

    return {
      mergeSig: mergeSigResp.sig,
      accountSig: reserveSigResp.sig,
      purseSig: purseSigResp.sig,
    };
  },
  async signRefund(
    tci: TalerCryptoInterfaceR,
    req: SignRefundRequest,
  ): Promise<SignRefundResponse> {
    const refundSigBlob = buildSigPS(TalerSignaturePurpose.MERCHANT_REFUND)
      .put(decodeCrock(req.contractTermsHash))
      .put(decodeCrock(req.coinPub))
      .put(bufferForUint64(req.rtransactionId))
      .put(bufferFromAmount(req.refundAmount))
      .build();
    const refundSigResp = await tci.eddsaSign(tci, {
      msg: encodeCrock(refundSigBlob),
      priv: req.merchantPriv,
    });
    return {
      sig: refundSigResp.sig,
    };
  },
  async signDeletePurse(
    tci: TalerCryptoInterfaceR,
    req: SignDeletePurseRequest,
  ): Promise<SignDeletePurseResponse> {
    const deleteSigBlob = buildSigPS(
      TalerSignaturePurpose.WALLET_PURSE_DELETE,
    ).build();
    const sigResp = await tci.eddsaSign(tci, {
      msg: encodeCrock(deleteSigBlob),
      priv: req.pursePriv,
    });
    return {
      sig: sigResp.sig,
    };
  },
  async signCoinHistoryRequest(
    tci: TalerCryptoInterfaceR,
    req: SignCoinHistoryRequest,
  ): Promise<SignCoinHistoryResponse> {
    const coinHistorySigBlob = buildSigPS(
      TalerSignaturePurpose.WALLET_COIN_HISTORY,
    )
      .put(bufferForUint64(req.startOffset))
      .build();
    const sigResp = await tci.eddsaSign(tci, {
      msg: encodeCrock(coinHistorySigBlob),
      priv: req.coinPriv,
    });
    return {
      sig: sigResp.sig,
    };
  },
  async signReserveHistoryReq(
    tci: TalerCryptoInterfaceR,
    req: SignReserveHistoryReqRequest,
  ): Promise<SignReserveHistoryReqResponse> {
    const reserveHistoryBlob = buildSigPS(
      TalerSignaturePurpose.WALLET_RESERVE_HISTORY,
    )
      .put(bufferForUint64(req.startOffset))
      .build();
    const sigResp = await tci.eddsaSign(tci, {
      msg: encodeCrock(reserveHistoryBlob),
      priv: req.reservePriv,
    });
    return {
      sig: sigResp.sig,
    };
  },
  async signWalletAccountSetup(
    tci: TalerCryptoInterfaceR,
    req: SignWalletAccountSetupRequest,
  ): Promise<SignWalletAccountSetupResponse> {
    const sigData = buildSigPS(TalerSignaturePurpose.WALLET_ACCOUNT_SETUP)
      .put(bufferFromAmount(req.threshold))
      .build();
    const sigResp = await tci.eddsaSign(tci, {
      msg: encodeCrock(sigData),
      priv: req.reservePriv,
    });
    return {
      sig: sigResp.sig,
    };
  },
  async signWalletKycAuth(
    tci: TalerCryptoInterfaceR,
    req: SignWalletKycAuthRequest,
  ): Promise<SignWalletKycAuthResponse> {
    const sigData = buildSigPS(TalerSignaturePurpose.KYC_AUTH).build();
    const sigResp = await tci.eddsaSign(tci, {
      msg: encodeCrock(sigData),
      priv: req.accountPriv,
    });
    return {
      sig: sigResp.sig,
    };
  },
  async signContractTermsHash(
    tci: TalerCryptoInterfaceR,
    req: SignContractTermsHashRequest,
  ): Promise<SignContractTermsHashResponse> {
    const sigData = buildSigPS(TalerSignaturePurpose.MERCHANT_CONTRACT)
      .put(decodeCrock(req.contractTermsHash))
      .build();
    const sigRes = await tci.eddsaSign(tci, {
      msg: encodeCrock(sigData),
      priv: req.merchantPriv,
    });
    return {
      sig: sigRes.sig,
    };
  },

  async signWithdrawal(
    tci: TalerCryptoInterfaceR,
    req: SignWithdrawalRequest,
  ): Promise<SignWithdrawalResponse> {
    const hc = createHashContext();

    for (let i = 0; i < req.coinEvs.length; i++) {
      hc.update(hashCoinEv(req.coinEvs[i], req.denomsPubHashes[i]));
    }

    const hPlanchets = hc.finish();

    // Clause Schnorr blinding seed
    // Wallet doesn't fully support CS yet.
    const blindingSeed = new Uint8Array(32);

    const withdrawRequest = buildSigPS(
      TalerSignaturePurpose.WALLET_RESERVE_WITHDRAW,
    )
      .put(bufferFromAmount(req.amount))
      .put(bufferFromAmount(req.fee))
      .put(hPlanchets)
      .put(blindingSeed)
      .put(bufferForUint32(0)) // max_age_group
      .put(bufferForUint32(0)) // age mask
      .build();
    const sig = eddsaSign(withdrawRequest, decodeCrock(req.reservePriv));
    return {
      sig: encodeCrock(sig),
    };
  },
};

export interface EddsaSignRequest {
  msg: string;
  priv: string;
}

export interface EddsaSignResponse {
  sig: string;
}

export const nativeCrypto: TalerCryptoInterface = Object.fromEntries(
  Object.keys(nativeCryptoR).map((name) => {
    return [
      name,
      (req: any) =>
        nativeCryptoR[name as keyof TalerCryptoInterfaceR](nativeCryptoR, req),
    ];
  }),
) as any;
