From eec6695be0409669fcad36c6cc7ea01f48d41c97 Mon Sep 17 00:00:00 2001 From: Florian Dold Date: Fri, 14 Oct 2022 22:38:40 +0200 Subject: wallet-core: DB tweaks, consistent file naming convention --- packages/taler-util/src/ReserveStatus.ts | 2 +- packages/taler-util/src/ReserveTransaction.ts | 2 +- packages/taler-util/src/amounts.ts | 2 +- packages/taler-util/src/backup-types.ts | 1279 +++++++++++++++ packages/taler-util/src/backupTypes.ts | 1279 --------------- packages/taler-util/src/bitcoin.ts | 2 +- packages/taler-util/src/contract-terms.test.ts | 127 ++ packages/taler-util/src/contract-terms.ts | 231 +++ packages/taler-util/src/contractTerms.test.ts | 127 -- packages/taler-util/src/contractTerms.ts | 231 --- packages/taler-util/src/index.ts | 12 +- packages/taler-util/src/notifications.ts | 2 +- packages/taler-util/src/taler-crypto.test.ts | 431 +++++ packages/taler-util/src/taler-crypto.ts | 1378 ++++++++++++++++ packages/taler-util/src/taler-types.ts | 2028 ++++++++++++++++++++++++ packages/taler-util/src/talerCrypto.test.ts | 431 ----- packages/taler-util/src/talerCrypto.ts | 1378 ---------------- packages/taler-util/src/talerTypes.ts | 2028 ------------------------ packages/taler-util/src/transactions-types.ts | 568 +++++++ packages/taler-util/src/transactionsTypes.ts | 564 ------- packages/taler-util/src/types-test.ts | 2 +- packages/taler-util/src/wallet-types.ts | 1836 +++++++++++++++++++++ packages/taler-util/src/walletTypes.ts | 1826 --------------------- 23 files changed, 7890 insertions(+), 7876 deletions(-) create mode 100644 packages/taler-util/src/backup-types.ts delete mode 100644 packages/taler-util/src/backupTypes.ts create mode 100644 packages/taler-util/src/contract-terms.test.ts create mode 100644 packages/taler-util/src/contract-terms.ts delete mode 100644 packages/taler-util/src/contractTerms.test.ts delete mode 100644 packages/taler-util/src/contractTerms.ts create mode 100644 packages/taler-util/src/taler-crypto.test.ts create mode 100644 packages/taler-util/src/taler-crypto.ts create mode 100644 packages/taler-util/src/taler-types.ts delete mode 100644 packages/taler-util/src/talerCrypto.test.ts delete mode 100644 packages/taler-util/src/talerCrypto.ts delete mode 100644 packages/taler-util/src/talerTypes.ts create mode 100644 packages/taler-util/src/transactions-types.ts delete mode 100644 packages/taler-util/src/transactionsTypes.ts create mode 100644 packages/taler-util/src/wallet-types.ts delete mode 100644 packages/taler-util/src/walletTypes.ts (limited to 'packages/taler-util') diff --git a/packages/taler-util/src/ReserveStatus.ts b/packages/taler-util/src/ReserveStatus.ts index eb147da2d..be9fa9e8e 100644 --- a/packages/taler-util/src/ReserveStatus.ts +++ b/packages/taler-util/src/ReserveStatus.ts @@ -27,7 +27,7 @@ import { codecForList, Codec, } from "./codec.js"; -import { AmountString } from "./talerTypes.js"; +import { AmountString } from "./taler-types.js"; import { ReserveTransaction, codecForReserveTransaction, diff --git a/packages/taler-util/src/ReserveTransaction.ts b/packages/taler-util/src/ReserveTransaction.ts index 8f3e16da2..5d3f86b1a 100644 --- a/packages/taler-util/src/ReserveTransaction.ts +++ b/packages/taler-util/src/ReserveTransaction.ts @@ -37,7 +37,7 @@ import { EddsaSignatureString, EddsaPublicKeyString, CoinPublicKeyString, -} from "./talerTypes.js"; +} from "./taler-types.js"; import { AbsoluteTime, codecForTimestamp, diff --git a/packages/taler-util/src/amounts.ts b/packages/taler-util/src/amounts.ts index d4de4ca53..337f342a3 100644 --- a/packages/taler-util/src/amounts.ts +++ b/packages/taler-util/src/amounts.ts @@ -27,7 +27,7 @@ import { codecForNumber, Codec, } from "./codec.js"; -import { AmountString } from "./talerTypes.js"; +import { AmountString } from "./taler-types.js"; /** * Number of fractional units that one value unit represents. diff --git a/packages/taler-util/src/backup-types.ts b/packages/taler-util/src/backup-types.ts new file mode 100644 index 000000000..6c7b203b5 --- /dev/null +++ b/packages/taler-util/src/backup-types.ts @@ -0,0 +1,1279 @@ +/* + This file is part of GNU Taler + (C) 2020 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see + */ + +/** + * Type declarations for the backup content format. + * + * Contains some redundancy with the other type declarations, + * as the backup schema must remain very stable and should be self-contained. + * + * Future: + * 1. Ghost spends (coin unexpectedly spent by a wallet with shared data) + * 2. Ghost withdrawals (reserve unexpectedly emptied by another wallet with shared data) + * 3. Track losses through re-denomination of payments/refreshes + * 4. (Feature:) Payments to own bank account and P2P-payments need to be backed up + * 5. Track last/next update time, so on restore we need to do less work + * 6. Currency render preferences? + * + * Questions: + * 1. What happens when two backups are merged that have + * the same coin in different refresh groups? + * => Both are added, one will eventually fail + * 2. Should we make more information forgettable? I.e. is + * the coin selection still relevant for a purchase after the coins + * are legally expired? + * => Yes, still needs to be implemented + * 3. What about re-denominations / re-selection of payment coins? + * Is it enough to store a clock value for the selection? + * => Coin derivation should also consider denom pub hash + * + * General considerations / decisions: + * 1. Information about previously occurring errors and + * retries is never backed up. + * 2. The ToS text of an exchange is never backed up. + * 3. Derived information is never backed up (hashed values, public keys + * when we know the private key). + * + * Problems: + * + * Withdrawal group fork/merging loses money: + * - Before the withdrawal happens, wallet forks into two backups. + * - Both wallets need to re-denominate the withdrawal (unlikely but possible). + * - Because the backup doesn't store planchets where a withdrawal was attempted, + * after merging some money will be list. + * - Fix: backup withdrawal objects also store planchets where withdrawal has been attempted + * + * @author Florian Dold + */ + +/** + * Imports. + */ +import { DenominationPubKey, UnblindedSignature } from "./taler-types.js"; +import { TalerProtocolDuration, TalerProtocolTimestamp } from "./time.js"; + +export const BACKUP_TAG = "gnu-taler-wallet-backup-content" as const; +/** + * Major version. Each increment means a backwards-incompatible change. + * Typically this means that a custom converter needs to be written. + */ +export const BACKUP_VERSION_MAJOR = 1 as const; + +/** + * Minor version. Each increment means that information is added to the backup + * in a backwards-compatible way. + * + * Wallets can always import a smaller minor version than their own backup code version. + * When importing a bigger version, data loss is possible and the user should be urged to + * upgrade their wallet first. + */ +export const BACKUP_VERSION_MINOR = 1 as const; + +/** + * Type alias for strings that are to be treated like amounts. + */ +type BackupAmountString = string; + +/** + * A human-recognizable identifier here that is + * reasonable unique and assigned the first time the wallet is + * started/installed, such as: + * + * `${wallet-implementation} ${os} ${hostname} (${short-uid})` + * => e.g. "GNU Taler Android iceking ABC123" + */ +type DeviceIdString = string; + +/** + * Contract terms JSON. + */ +type RawContractTerms = any; + +/** + * Unique identifier for an operation, used to either (a) reference + * the operation in a tombstone (b) disambiguate conflicting writes. + */ +type OperationUid = string; + +/** + * Content of the backup. + * + * The contents of the wallet must be serialized in a deterministic + * way across implementations, so that the normalized backup content + * JSON is identical when the wallet's content is identical. + */ +export interface WalletBackupContentV1 { + /** + * Magic constant to identify that this is a backup content JSON. + */ + schema_id: typeof BACKUP_TAG; + + /** + * Version of the schema. + */ + schema_version: typeof BACKUP_VERSION_MAJOR; + + minor_version: number; + + /** + * Root public key of the wallet. This field is present as + * a sanity check if the backup content JSON is loaded from file. + */ + wallet_root_pub: string; + + /** + * Current device identifier that "owns" the backup. + * + * This identifier allows one wallet to notice when another + * wallet is "alive" and connected to the same sync provider. + */ + current_device_id: DeviceIdString; + + /** + * Timestamp of the backup. + * + * This timestamp should only be advanced if the content + * of the backup changes. + */ + timestamp: TalerProtocolTimestamp; + + /** + * Per-exchange data sorted by exchange master public key. + * + * Sorted by the exchange public key. + */ + exchanges: BackupExchange[]; + + exchange_details: BackupExchangeDetails[]; + + /** + * Withdrawal groups. + * + * Sorted by the withdrawal group ID. + */ + withdrawal_groups: BackupWithdrawalGroup[]; + + /** + * Grouped refresh sessions. + * + * Sorted by the refresh group ID. + */ + refresh_groups: BackupRefreshGroup[]; + + /** + * Tips. + * + * Sorted by the wallet tip ID. + */ + tips: BackupTip[]; + + /** + * Accepted purchases. + * + * Sorted by the proposal ID. + */ + purchases: BackupPurchase[]; + + /** + * All backup providers. Backup providers + * in this list should be considered "active". + * + * Sorted by the provider base URL. + */ + backup_providers: BackupBackupProvider[]; + + /** + * Recoup groups. + */ + recoup_groups: BackupRecoupGroup[]; + + /** + * Trusted auditors, either for official (3 letter) or local (4-12 letter) + * currencies. + * + * Auditors are sorted by their canonicalized base URL. + */ + trusted_auditors: { [currency: string]: BackupTrustAuditor[] }; + + /** + * Trusted exchange. Only applicable for local currencies (4-12 letter currency code). + * + * Exchanges are sorted by their canonicalized base URL. + */ + trusted_exchanges: { [currency: string]: BackupTrustExchange[] }; + + /** + * Interning table for forgettable values of contract terms. + * + * Used to reduce storage space, as many forgettable items (product image, + * addresses, etc.) might be shared among many contract terms. + */ + intern_table: { [hash: string]: any }; + + /** + * Permanent error reports. + */ + error_reports: BackupErrorReport[]; + + /** + * Deletion tombstones. Lexically sorted. + */ + tombstones: Tombstone[]; +} + +export enum BackupOperationStatus { + Cancelled = "cancelled", + Finished = "finished", + Pending = "pending", +} + +export enum BackupWgType { + BankManual = "bank-manual", + BankIntegrated = "bank-integrated", + PeerPullCredit = "peer-pull-credit", + PeerPushCredit = "peer-push-credit", + Recoup = "recoup", +} + +export type BackupWgInfo = + | { + type: BackupWgType.BankManual; + } + | { + type: BackupWgType.BankIntegrated; + taler_withdraw_uri: string; + + /** + * URL that the user can be redirected to, and allows + * them to confirm (or abort) the bank-integrated withdrawal. + */ + confirm_url?: string; + + /** + * Exchange payto URI that the bank will use to fund the reserve. + */ + exchange_payto_uri: string; + + /** + * Time when the information about this reserve was posted to the bank. + * + * Only applies if bankWithdrawStatusUrl is defined. + * + * Set to undefined if that hasn't happened yet. + */ + timestamp_reserve_info_posted?: TalerProtocolTimestamp; + + /** + * Time when the reserve was confirmed by the bank. + * + * Set to undefined if not confirmed yet. + */ + timestamp_bank_confirmed?: TalerProtocolTimestamp; + } + | { + type: BackupWgType.PeerPullCredit; + contract_terms: any; + contract_priv: string; + } + | { + type: BackupWgType.PeerPushCredit; + contract_terms: any; + } + | { + type: BackupWgType.Recoup; + }; + +/** + * FIXME: Open questions: + * - Do we have to store the denomination selection? Why? + * (If deterministic, amount shouldn't change. Not storing it is simpler.) + */ +export interface BackupWithdrawalGroup { + withdrawal_group_id: string; + + /** + * Detailed info based on the type of withdrawal group. + */ + info: BackupWgInfo; + + secret_seed: string; + + reserve_priv: string; + + exchange_base_url: string; + + timestamp_created: TalerProtocolTimestamp; + + timestamp_finish?: TalerProtocolTimestamp; + + operation_status: BackupOperationStatus; + + instructed_amount: BackupAmountString; + + /** + * Amount including fees (i.e. the amount subtracted from the + * reserve to withdraw all coins in this withdrawal session). + * + * Note that this *includes* the amount remaining in the reserve + * that is too small to be withdrawn, and thus can't be derived + * from selectedDenoms. + */ + raw_withdrawal_amount: BackupAmountString; + + /** + * Restrict withdrawals from this reserve to this age. + */ + restrict_age?: number; + + /** + * Multiset of denominations selected for withdrawal. + */ + selected_denoms: BackupDenomSel; + + selected_denoms_uid: OperationUid; +} + +/** + * Tombstone in the format ":" + */ +export type Tombstone = string; + +/** + * Detailed error report. + * + * For auditor-relevant reports with attached cryptographic proof, + * the error report also should contain the submission status to + * the auditor(s). + */ +interface BackupErrorReport { + // FIXME: specify! +} + +/** + * Trust declaration for an auditor. + * + * The trust applies based on the public key of + * the auditor, irrespective of what base URL the exchange + * is referencing. + */ +export interface BackupTrustAuditor { + /** + * Base URL of the auditor. + */ + auditor_base_url: string; + + /** + * Public key of the auditor. + */ + auditor_pub: string; + + /** + * UIDs for the operation of adding this auditor + * as a trusted auditor. + */ + uids: OperationUid; +} + +/** + * Trust declaration for an exchange. + * + * The trust only applies for the combination of base URL + * and public key. If the master public key changes while the base + * URL stays the same, the exchange has to be re-added by a wallet update + * or by the user. + */ +export interface BackupTrustExchange { + /** + * Canonicalized exchange base URL. + */ + exchange_base_url: string; + + /** + * Master public key of the exchange. + */ + exchange_master_pub: string; + + /** + * UIDs for the operation of adding this exchange + * as trusted. + */ + uids: OperationUid; +} + +export class BackupBackupProviderTerms { + /** + * Last known supported protocol version. + */ + supported_protocol_version: string; + + /** + * Last known annual fee. + */ + annual_fee: BackupAmountString; + + /** + * Last known storage limit. + */ + storage_limit_in_megabytes: number; +} + +/** + * Backup information about one backup storage provider. + */ +export class BackupBackupProvider { + /** + * Canonicalized base URL of the provider. + */ + base_url: string; + + /** + * Last known terms. Might be unavailable in some situations, such + * as directly after restoring form a backup recovery document. + */ + terms?: BackupBackupProviderTerms; + + /** + * Proposal IDs for payments to this provider. + */ + pay_proposal_ids: string[]; + + /** + * UIDs for adding this backup provider. + */ + uids: OperationUid[]; +} + +/** + * Status of recoup operations that were grouped together. + * + * The remaining amount of the corresponding coins must be set to + * zero when the recoup group is created/imported. + */ +export interface BackupRecoupGroup { + /** + * Unique identifier for the recoup group record. + */ + recoup_group_id: string; + + /** + * Timestamp when the recoup was started. + */ + timestamp_created: TalerProtocolTimestamp; + + timestamp_finish?: TalerProtocolTimestamp; + finish_clock?: TalerProtocolTimestamp; + finish_is_failure?: boolean; + + /** + * Information about each coin being recouped. + */ + coins: { + coin_pub: string; + recoup_finished: boolean; + old_amount: BackupAmountString; + }[]; +} + +/** + * Types of coin sources. + */ +export enum BackupCoinSourceType { + Withdraw = "withdraw", + Refresh = "refresh", + Tip = "tip", +} + +/** + * Metadata about a coin obtained via withdrawing. + */ +export interface BackupWithdrawCoinSource { + type: BackupCoinSourceType.Withdraw; + + /** + * Can be the empty string for orphaned coins. + */ + withdrawal_group_id: string; + + /** + * Index of the coin in the withdrawal session. + */ + coin_index: number; + + /** + * Reserve public key for the reserve we got this coin from. + */ + reserve_pub: string; +} + +/** + * Metadata about a coin obtained from refreshing. + * + * FIXME: Currently does not link to the refreshGroupId because + * the wallet DB doesn't do this. Not really necessary, + * but would be more consistent. + */ +export interface BackupRefreshCoinSource { + type: BackupCoinSourceType.Refresh; + + /** + * Public key of the coin that was refreshed into this coin. + */ + old_coin_pub: string; +} + +/** + * Metadata about a coin obtained from a tip. + */ +export interface BackupTipCoinSource { + type: BackupCoinSourceType.Tip; + + /** + * Wallet's identifier for the tip that this coin + * originates from. + */ + wallet_tip_id: string; + + /** + * Index in the tip planchets of the tip. + */ + coin_index: number; +} + +/** + * Metadata about a coin depending on the origin. + */ +export type BackupCoinSource = + | BackupWithdrawCoinSource + | BackupRefreshCoinSource + | BackupTipCoinSource; + +/** + * Backup information about a coin. + * + * (Always part of a BackupExchange/BackupDenom) + */ +export interface BackupCoin { + /** + * Where did the coin come from? Used for recouping coins. + */ + coin_source: BackupCoinSource; + + /** + * Private key to authorize operations on the coin. + */ + coin_priv: string; + + /** + * Unblinded signature by the exchange. + */ + denom_sig: UnblindedSignature; + + /** + * Amount that's left on the coin. + */ + current_amount: BackupAmountString; + + /** + * Blinding key used when withdrawing the coin. + * Potentionally used again during payback. + */ + blinding_key: string; + + /** + * Does the wallet think that the coin is still fresh? + * + * Note that even if a fresh coin is imported, it should still + * be refreshed in most situations. + */ + fresh: boolean; +} + +/** + * Status of a tip we got from a merchant. + */ +export interface BackupTip { + /** + * Tip ID chosen by the wallet. + */ + wallet_tip_id: string; + + /** + * The merchant's identifier for this tip. + */ + merchant_tip_id: string; + + /** + * Secret seed used for the tipping planchets. + */ + secret_seed: string; + + /** + * Has the user accepted the tip? Only after the tip has been accepted coins + * withdrawn from the tip may be used. + */ + timestamp_accepted: TalerProtocolTimestamp | undefined; + + /** + * When was the tip first scanned by the wallet? + */ + timestamp_created: TalerProtocolTimestamp; + + timestamp_finished?: TalerProtocolTimestamp; + finish_is_failure?: boolean; + + /** + * The tipped amount. + */ + tip_amount_raw: BackupAmountString; + + /** + * Timestamp, the tip can't be picked up anymore after this deadline. + */ + timestamp_expiration: TalerProtocolTimestamp; + + /** + * The exchange that will sign our coins, chosen by the merchant. + */ + exchange_base_url: string; + + /** + * Base URL of the merchant that is giving us the tip. + */ + merchant_base_url: string; + + /** + * Selected denominations. Determines the effective tip amount. + */ + selected_denoms: BackupDenomSel; + + /** + * UID for the denomination selection. + * Used to disambiguate when merging. + */ + selected_denoms_uid: OperationUid; +} + +/** + * Reasons for why a coin is being refreshed. + */ +export enum BackupRefreshReason { + Manual = "manual", + Pay = "pay", + Refund = "refund", + AbortPay = "abort-pay", + Recoup = "recoup", + BackupRestored = "backup-restored", + Scheduled = "scheduled", +} + +/** + * Information about one refresh session, always part + * of a refresh group. + * + * (Public key of the old coin is stored in the refresh group.) + */ +export interface BackupRefreshSession { + /** + * Hashed denominations of the newly requested coins. + */ + new_denoms: BackupDenomSel; + + /** + * Seed used to derive the planchets and + * transfer private keys for this refresh session. + */ + session_secret_seed: string; + + /** + * The no-reveal-index after we've done the melting. + */ + noreveal_index?: number; +} + +/** + * Refresh session for one coin inside a refresh group. + */ +export interface BackupRefreshOldCoin { + /** + * Public key of the old coin, + */ + coin_pub: string; + + /** + * Requested amount to refresh. Must be subtracted from the coin's remaining + * amount as soon as the coin is added to the refresh group. + */ + input_amount: BackupAmountString; + + /** + * Estimated output (may change if it takes a long time to create the + * actual session). + */ + estimated_output_amount: BackupAmountString; + + /** + * Did the refresh session finish (or was it unnecessary/impossible to create + * one) + */ + finished: boolean; + + /** + * Refresh session (if created) or undefined it not created yet. + */ + refresh_session: BackupRefreshSession | undefined; +} + +/** + * Information about one refresh group. + * + * May span more than one exchange, but typically doesn't + */ +export interface BackupRefreshGroup { + refresh_group_id: string; + + reason: BackupRefreshReason; + + /** + * Details per old coin. + */ + old_coins: BackupRefreshOldCoin[]; + + timestamp_created: TalerProtocolTimestamp; + + timestamp_finish?: TalerProtocolTimestamp; + finish_is_failure?: boolean; +} + +export enum BackupRefundState { + Failed = "failed", + Applied = "applied", + Pending = "pending", +} + +/** + * Common information about a refund. + */ +export interface BackupRefundItemCommon { + /** + * Execution time as claimed by the merchant + */ + execution_time: TalerProtocolTimestamp; + + /** + * Time when the wallet became aware of the refund. + */ + obtained_time: TalerProtocolTimestamp; + + /** + * Amount refunded for the coin. + */ + refund_amount: BackupAmountString; + + /** + * Coin being refunded. + */ + coin_pub: string; + + /** + * The refund transaction ID for the refund. + */ + rtransaction_id: number; + + /** + * Upper bound on the refresh cost incurred by + * applying this refund. + * + * Might be lower in practice when two refunds on the same + * coin are refreshed in the same refresh operation. + * + * Used to display fees, and stored since it's expensive to recompute + * accurately. + */ + total_refresh_cost_bound: BackupAmountString; +} + +/** + * Failed refund, either because the merchant did + * something wrong or it expired. + */ +export interface BackupRefundFailedItem extends BackupRefundItemCommon { + type: BackupRefundState.Failed; +} + +export interface BackupRefundPendingItem extends BackupRefundItemCommon { + type: BackupRefundState.Pending; +} + +export interface BackupRefundAppliedItem extends BackupRefundItemCommon { + type: BackupRefundState.Applied; +} + +/** + * State of one refund from the merchant, maintained by the wallet. + */ +export type BackupRefundItem = + | BackupRefundFailedItem + | BackupRefundPendingItem + | BackupRefundAppliedItem; + +/** + * Data we store when the payment was accepted. + */ +export interface BackupPayInfo { + pay_coins: { + /** + * Public keys of the coins that were selected. + */ + coin_pub: string; + + /** + * Amount that each coin contributes. + */ + contribution: BackupAmountString; + }[]; + + /** + * Unique ID to disambiguate pay coin selection on merge. + */ + pay_coins_uid: OperationUid; + + /** + * Total cost initially shown to the user. + * + * This includes the amount taken by the merchant, fees (wire/deposit) contributed + * by the customer, refreshing fees, fees for withdraw-after-refresh and "trimmings" + * of coins that are too small to spend. + * + * Note that in rare situations, this cost might not be accurate (e.g. + * when the payment or refresh gets re-denominated). + * We might show adjustments to this later, but currently we don't do so. + */ + total_pay_cost: BackupAmountString; +} + +export interface BackupPurchase { + /** + * Proposal ID for this purchase. Uniquely identifies the + * purchase and the proposal. + */ + proposal_id: string; + + /** + * Status of the proposal. + */ + proposal_status: BackupProposalStatus; + + /** + * Proposal that this one got "redirected" to as part of + * the repurchase detection. + */ + repurchase_proposal_id: string | undefined; + + /** + * Session ID we got when downloading the contract. + */ + download_session_id?: string; + + /** + * Merchant-assigned order ID of the proposal. + */ + order_id: string; + + /** + * Base URL of the merchant that proposed the purchase. + */ + merchant_base_url: string; + + /** + * Claim token initially given by the merchant. + */ + claim_token: string | undefined; + + /** + * Contract terms we got from the merchant. + */ + contract_terms_raw?: RawContractTerms; + + /** + * Signature on the contract terms. + * + * FIXME: Better name needed. + */ + merchant_sig?: string; + + /** + * Private key for the nonce. Might eventually be used + * to prove ownership of the contract. + */ + nonce_priv: string; + + pay_info: BackupPayInfo | undefined; + + /** + * Timestamp of the first time that sending a payment to the merchant + * for this purchase was successful. + */ + timestamp_first_successful_pay: TalerProtocolTimestamp | undefined; + + /** + * Signature by the merchant confirming the payment. + */ + merchant_pay_sig: string | undefined; + + timestamp_proposed: TalerProtocolTimestamp; + + /** + * When was the purchase made? + * Refers to the time that the user accepted. + */ + timestamp_accepted: TalerProtocolTimestamp | undefined; + + /** + * Pending refunds for the purchase. A refund is pending + * when the merchant reports a transient error from the exchange. + */ + refunds: BackupRefundItem[]; + + /** + * Continue querying the refund status until this deadline has expired. + */ + auto_refund_deadline: TalerProtocolTimestamp | undefined; +} + +/** + * Info about one denomination in the backup. + * + * Note that the wallet only backs up validated denominations. + */ +export interface BackupDenomination { + /** + * Value of one coin of the denomination. + */ + value: BackupAmountString; + + /** + * The denomination public key. + */ + denom_pub: DenominationPubKey; + + /** + * Fee for withdrawing. + */ + fee_withdraw: BackupAmountString; + + /** + * Fee for depositing. + */ + fee_deposit: BackupAmountString; + + /** + * Fee for refreshing. + */ + fee_refresh: BackupAmountString; + + /** + * Fee for refunding. + */ + fee_refund: BackupAmountString; + + /** + * Validity start date of the denomination. + */ + stamp_start: TalerProtocolTimestamp; + + /** + * Date after which the currency can't be withdrawn anymore. + */ + stamp_expire_withdraw: TalerProtocolTimestamp; + + /** + * Date after the denomination officially doesn't exist anymore. + */ + stamp_expire_legal: TalerProtocolTimestamp; + + /** + * Data after which coins of this denomination can't be deposited anymore. + */ + stamp_expire_deposit: TalerProtocolTimestamp; + + /** + * Signature by the exchange's master key over the denomination + * information. + */ + master_sig: string; + + /** + * Was this denomination still offered by the exchange the last time + * we checked? + * Only false when the exchange redacts a previously published denomination. + */ + is_offered: boolean; + + /** + * Did the exchange revoke the denomination? + * When this field is set to true in the database, the same transaction + * should also mark all affected coins as revoked. + */ + is_revoked: boolean; + + /** + * Coins of this denomination. + */ + coins: BackupCoin[]; + + /** + * The list issue date of the exchange "/keys" response + * that this denomination was last seen in. + */ + list_issue_date: TalerProtocolTimestamp; +} + +/** + * Denomination selection. + */ +export type BackupDenomSel = { + denom_pub_hash: string; + count: number; +}[]; + +/** + * Wire fee for one wire payment target type as stored in the + * wallet's database. + * + * (Flattened to a list to make the declaration simpler). + */ +export interface BackupExchangeWireFee { + wire_type: string; + + /** + * Fee for wire transfers. + */ + wire_fee: string; + + wad_fee: string; + + /** + * Fees to close and refund a reserve. + */ + closing_fee: string; + + /** + * Start date of the fee. + */ + start_stamp: TalerProtocolTimestamp; + + /** + * End date of the fee. + */ + end_stamp: TalerProtocolTimestamp; + + /** + * Signature made by the exchange master key. + */ + sig: string; +} + +/** + * Global fee as stored in the wallet's database. + * + */ +export interface BackupExchangeGlobalFees { + startDate: TalerProtocolTimestamp; + endDate: TalerProtocolTimestamp; + + kycFee: BackupAmountString; + historyFee: BackupAmountString; + accountFee: BackupAmountString; + purseFee: BackupAmountString; + + historyTimeout: TalerProtocolDuration; + kycTimeout: TalerProtocolDuration; + purseTimeout: TalerProtocolDuration; + + purseLimit: number; + + signature: string; +} +/** + * Structure of one exchange signing key in the /keys response. + */ +export class BackupExchangeSignKey { + stamp_start: TalerProtocolTimestamp; + stamp_expire: TalerProtocolTimestamp; + stamp_end: TalerProtocolTimestamp; + key: string; + master_sig: string; +} + +/** + * Signature by the auditor that a particular denomination key is audited. + */ +export class BackupAuditorDenomSig { + /** + * Denomination public key's hash. + */ + denom_pub_h: string; + + /** + * The signature. + */ + auditor_sig: string; +} + +/** + * Auditor information as given by the exchange in /keys. + */ +export class BackupExchangeAuditor { + /** + * Auditor's public key. + */ + auditor_pub: string; + + /** + * Base URL of the auditor. + */ + auditor_url: string; + + /** + * List of signatures for denominations by the auditor. + */ + denomination_keys: BackupAuditorDenomSig[]; +} + +/** + * Backup information for an exchange. Serves effectively + * as a pointer to the exchange details identified by + * the base URL, master public key and currency. + */ +export interface BackupExchange { + base_url: string; + + master_public_key: string; + + currency: string; + + /** + * Time when the pointer to the exchange details + * was last updated. + * + * Used to facilitate automatic merging. + */ + update_clock: TalerProtocolTimestamp; +} + +/** + * Backup information about an exchange's details. + * + * Note that one base URL can have multiple exchange + * details. The BackupExchange stores a pointer + * to the current exchange details. + */ +export interface BackupExchangeDetails { + /** + * Canonicalized base url of the exchange. + */ + base_url: string; + + /** + * Master public key of the exchange. + */ + master_public_key: string; + + /** + * Auditors (partially) auditing the exchange. + */ + auditors: BackupExchangeAuditor[]; + + /** + * Currency that the exchange offers. + */ + currency: string; + + /** + * Denominations offered by the exchange. + */ + denominations: BackupDenomination[]; + + /** + * Last observed protocol version. + */ + protocol_version: string; + + /** + * Closing delay of reserves. + */ + reserve_closing_delay: TalerProtocolDuration; + + /** + * Signing keys we got from the exchange, can also contain + * older signing keys that are not returned by /keys anymore. + */ + signing_keys: BackupExchangeSignKey[]; + + wire_fees: BackupExchangeWireFee[]; + + global_fees: BackupExchangeGlobalFees[]; + + /** + * Bank accounts offered by the exchange; + */ + accounts: { + payto_uri: string; + master_sig: string; + }[]; + + /** + * ETag for last terms of service download. + */ + tos_accepted_etag: string | undefined; + + /** + * Timestamp when the ToS has been accepted. + */ + tos_accepted_timestamp: TalerProtocolTimestamp | undefined; +} + +export enum BackupProposalStatus { + /** + * Proposed (and either downloaded or not, + * depending on whether contract terms are present), + * but the user needs to accept/reject it. + */ + Proposed = "proposed", + /** + * The user has rejected the proposal. + */ + Refused = "refused", + /** + * Downloading or processing the proposal has failed permanently. + * + * FIXME: Should this be modeled as a "misbehavior report" instead? + */ + PermanentlyFailed = "permanently-failed", + /** + * Downloaded proposal was detected as a re-purchase. + */ + Repurchase = "repurchase", + + Paid = "paid", +} + +export interface BackupRecovery { + walletRootPriv: string; + providers: { + url: string; + }[]; +} diff --git a/packages/taler-util/src/backupTypes.ts b/packages/taler-util/src/backupTypes.ts deleted file mode 100644 index 0270f2586..000000000 --- a/packages/taler-util/src/backupTypes.ts +++ /dev/null @@ -1,1279 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2020 Taler Systems S.A. - - GNU Taler is free software; you can redistribute it and/or modify it under the - terms of the GNU General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - GNU Taler; see the file COPYING. If not, see - */ - -/** - * Type declarations for the backup content format. - * - * Contains some redundancy with the other type declarations, - * as the backup schema must remain very stable and should be self-contained. - * - * Future: - * 1. Ghost spends (coin unexpectedly spent by a wallet with shared data) - * 2. Ghost withdrawals (reserve unexpectedly emptied by another wallet with shared data) - * 3. Track losses through re-denomination of payments/refreshes - * 4. (Feature:) Payments to own bank account and P2P-payments need to be backed up - * 5. Track last/next update time, so on restore we need to do less work - * 6. Currency render preferences? - * - * Questions: - * 1. What happens when two backups are merged that have - * the same coin in different refresh groups? - * => Both are added, one will eventually fail - * 2. Should we make more information forgettable? I.e. is - * the coin selection still relevant for a purchase after the coins - * are legally expired? - * => Yes, still needs to be implemented - * 3. What about re-denominations / re-selection of payment coins? - * Is it enough to store a clock value for the selection? - * => Coin derivation should also consider denom pub hash - * - * General considerations / decisions: - * 1. Information about previously occurring errors and - * retries is never backed up. - * 2. The ToS text of an exchange is never backed up. - * 3. Derived information is never backed up (hashed values, public keys - * when we know the private key). - * - * Problems: - * - * Withdrawal group fork/merging loses money: - * - Before the withdrawal happens, wallet forks into two backups. - * - Both wallets need to re-denominate the withdrawal (unlikely but possible). - * - Because the backup doesn't store planchets where a withdrawal was attempted, - * after merging some money will be list. - * - Fix: backup withdrawal objects also store planchets where withdrawal has been attempted - * - * @author Florian Dold - */ - -/** - * Imports. - */ -import { DenominationPubKey, UnblindedSignature } from "./talerTypes.js"; -import { TalerProtocolDuration, TalerProtocolTimestamp } from "./time.js"; - -export const BACKUP_TAG = "gnu-taler-wallet-backup-content" as const; -/** - * Major version. Each increment means a backwards-incompatible change. - * Typically this means that a custom converter needs to be written. - */ -export const BACKUP_VERSION_MAJOR = 1 as const; - -/** - * Minor version. Each increment means that information is added to the backup - * in a backwards-compatible way. - * - * Wallets can always import a smaller minor version than their own backup code version. - * When importing a bigger version, data loss is possible and the user should be urged to - * upgrade their wallet first. - */ -export const BACKUP_VERSION_MINOR = 1 as const; - -/** - * Type alias for strings that are to be treated like amounts. - */ -type BackupAmountString = string; - -/** - * A human-recognizable identifier here that is - * reasonable unique and assigned the first time the wallet is - * started/installed, such as: - * - * `${wallet-implementation} ${os} ${hostname} (${short-uid})` - * => e.g. "GNU Taler Android iceking ABC123" - */ -type DeviceIdString = string; - -/** - * Contract terms JSON. - */ -type RawContractTerms = any; - -/** - * Unique identifier for an operation, used to either (a) reference - * the operation in a tombstone (b) disambiguate conflicting writes. - */ -type OperationUid = string; - -/** - * Content of the backup. - * - * The contents of the wallet must be serialized in a deterministic - * way across implementations, so that the normalized backup content - * JSON is identical when the wallet's content is identical. - */ -export interface WalletBackupContentV1 { - /** - * Magic constant to identify that this is a backup content JSON. - */ - schema_id: typeof BACKUP_TAG; - - /** - * Version of the schema. - */ - schema_version: typeof BACKUP_VERSION_MAJOR; - - minor_version: number; - - /** - * Root public key of the wallet. This field is present as - * a sanity check if the backup content JSON is loaded from file. - */ - wallet_root_pub: string; - - /** - * Current device identifier that "owns" the backup. - * - * This identifier allows one wallet to notice when another - * wallet is "alive" and connected to the same sync provider. - */ - current_device_id: DeviceIdString; - - /** - * Timestamp of the backup. - * - * This timestamp should only be advanced if the content - * of the backup changes. - */ - timestamp: TalerProtocolTimestamp; - - /** - * Per-exchange data sorted by exchange master public key. - * - * Sorted by the exchange public key. - */ - exchanges: BackupExchange[]; - - exchange_details: BackupExchangeDetails[]; - - /** - * Withdrawal groups. - * - * Sorted by the withdrawal group ID. - */ - withdrawal_groups: BackupWithdrawalGroup[]; - - /** - * Grouped refresh sessions. - * - * Sorted by the refresh group ID. - */ - refresh_groups: BackupRefreshGroup[]; - - /** - * Tips. - * - * Sorted by the wallet tip ID. - */ - tips: BackupTip[]; - - /** - * Accepted purchases. - * - * Sorted by the proposal ID. - */ - purchases: BackupPurchase[]; - - /** - * All backup providers. Backup providers - * in this list should be considered "active". - * - * Sorted by the provider base URL. - */ - backup_providers: BackupBackupProvider[]; - - /** - * Recoup groups. - */ - recoup_groups: BackupRecoupGroup[]; - - /** - * Trusted auditors, either for official (3 letter) or local (4-12 letter) - * currencies. - * - * Auditors are sorted by their canonicalized base URL. - */ - trusted_auditors: { [currency: string]: BackupTrustAuditor[] }; - - /** - * Trusted exchange. Only applicable for local currencies (4-12 letter currency code). - * - * Exchanges are sorted by their canonicalized base URL. - */ - trusted_exchanges: { [currency: string]: BackupTrustExchange[] }; - - /** - * Interning table for forgettable values of contract terms. - * - * Used to reduce storage space, as many forgettable items (product image, - * addresses, etc.) might be shared among many contract terms. - */ - intern_table: { [hash: string]: any }; - - /** - * Permanent error reports. - */ - error_reports: BackupErrorReport[]; - - /** - * Deletion tombstones. Lexically sorted. - */ - tombstones: Tombstone[]; -} - -export enum BackupOperationStatus { - Cancelled = "cancelled", - Finished = "finished", - Pending = "pending", -} - -export enum BackupWgType { - BankManual = "bank-manual", - BankIntegrated = "bank-integrated", - PeerPullCredit = "peer-pull-credit", - PeerPushCredit = "peer-push-credit", - Recoup = "recoup", -} - -export type BackupWgInfo = - | { - type: BackupWgType.BankManual; - } - | { - type: BackupWgType.BankIntegrated; - taler_withdraw_uri: string; - - /** - * URL that the user can be redirected to, and allows - * them to confirm (or abort) the bank-integrated withdrawal. - */ - confirm_url?: string; - - /** - * Exchange payto URI that the bank will use to fund the reserve. - */ - exchange_payto_uri: string; - - /** - * Time when the information about this reserve was posted to the bank. - * - * Only applies if bankWithdrawStatusUrl is defined. - * - * Set to undefined if that hasn't happened yet. - */ - timestamp_reserve_info_posted?: TalerProtocolTimestamp; - - /** - * Time when the reserve was confirmed by the bank. - * - * Set to undefined if not confirmed yet. - */ - timestamp_bank_confirmed?: TalerProtocolTimestamp; - } - | { - type: BackupWgType.PeerPullCredit; - contract_terms: any; - contract_priv: string; - } - | { - type: BackupWgType.PeerPushCredit; - contract_terms: any; - } - | { - type: BackupWgType.Recoup; - }; - -/** - * FIXME: Open questions: - * - Do we have to store the denomination selection? Why? - * (If deterministic, amount shouldn't change. Not storing it is simpler.) - */ -export interface BackupWithdrawalGroup { - withdrawal_group_id: string; - - /** - * Detailed info based on the type of withdrawal group. - */ - info: BackupWgInfo; - - secret_seed: string; - - reserve_priv: string; - - exchange_base_url: string; - - timestamp_created: TalerProtocolTimestamp; - - timestamp_finish?: TalerProtocolTimestamp; - - operation_status: BackupOperationStatus; - - instructed_amount: BackupAmountString; - - /** - * Amount including fees (i.e. the amount subtracted from the - * reserve to withdraw all coins in this withdrawal session). - * - * Note that this *includes* the amount remaining in the reserve - * that is too small to be withdrawn, and thus can't be derived - * from selectedDenoms. - */ - raw_withdrawal_amount: BackupAmountString; - - /** - * Restrict withdrawals from this reserve to this age. - */ - restrict_age?: number; - - /** - * Multiset of denominations selected for withdrawal. - */ - selected_denoms: BackupDenomSel; - - selected_denoms_uid: OperationUid; -} - -/** - * Tombstone in the format ":" - */ -export type Tombstone = string; - -/** - * Detailed error report. - * - * For auditor-relevant reports with attached cryptographic proof, - * the error report also should contain the submission status to - * the auditor(s). - */ -interface BackupErrorReport { - // FIXME: specify! -} - -/** - * Trust declaration for an auditor. - * - * The trust applies based on the public key of - * the auditor, irrespective of what base URL the exchange - * is referencing. - */ -export interface BackupTrustAuditor { - /** - * Base URL of the auditor. - */ - auditor_base_url: string; - - /** - * Public key of the auditor. - */ - auditor_pub: string; - - /** - * UIDs for the operation of adding this auditor - * as a trusted auditor. - */ - uids: OperationUid; -} - -/** - * Trust declaration for an exchange. - * - * The trust only applies for the combination of base URL - * and public key. If the master public key changes while the base - * URL stays the same, the exchange has to be re-added by a wallet update - * or by the user. - */ -export interface BackupTrustExchange { - /** - * Canonicalized exchange base URL. - */ - exchange_base_url: string; - - /** - * Master public key of the exchange. - */ - exchange_master_pub: string; - - /** - * UIDs for the operation of adding this exchange - * as trusted. - */ - uids: OperationUid; -} - -export class BackupBackupProviderTerms { - /** - * Last known supported protocol version. - */ - supported_protocol_version: string; - - /** - * Last known annual fee. - */ - annual_fee: BackupAmountString; - - /** - * Last known storage limit. - */ - storage_limit_in_megabytes: number; -} - -/** - * Backup information about one backup storage provider. - */ -export class BackupBackupProvider { - /** - * Canonicalized base URL of the provider. - */ - base_url: string; - - /** - * Last known terms. Might be unavailable in some situations, such - * as directly after restoring form a backup recovery document. - */ - terms?: BackupBackupProviderTerms; - - /** - * Proposal IDs for payments to this provider. - */ - pay_proposal_ids: string[]; - - /** - * UIDs for adding this backup provider. - */ - uids: OperationUid[]; -} - -/** - * Status of recoup operations that were grouped together. - * - * The remaining amount of the corresponding coins must be set to - * zero when the recoup group is created/imported. - */ -export interface BackupRecoupGroup { - /** - * Unique identifier for the recoup group record. - */ - recoup_group_id: string; - - /** - * Timestamp when the recoup was started. - */ - timestamp_created: TalerProtocolTimestamp; - - timestamp_finish?: TalerProtocolTimestamp; - finish_clock?: TalerProtocolTimestamp; - finish_is_failure?: boolean; - - /** - * Information about each coin being recouped. - */ - coins: { - coin_pub: string; - recoup_finished: boolean; - old_amount: BackupAmountString; - }[]; -} - -/** - * Types of coin sources. - */ -export enum BackupCoinSourceType { - Withdraw = "withdraw", - Refresh = "refresh", - Tip = "tip", -} - -/** - * Metadata about a coin obtained via withdrawing. - */ -export interface BackupWithdrawCoinSource { - type: BackupCoinSourceType.Withdraw; - - /** - * Can be the empty string for orphaned coins. - */ - withdrawal_group_id: string; - - /** - * Index of the coin in the withdrawal session. - */ - coin_index: number; - - /** - * Reserve public key for the reserve we got this coin from. - */ - reserve_pub: string; -} - -/** - * Metadata about a coin obtained from refreshing. - * - * FIXME: Currently does not link to the refreshGroupId because - * the wallet DB doesn't do this. Not really necessary, - * but would be more consistent. - */ -export interface BackupRefreshCoinSource { - type: BackupCoinSourceType.Refresh; - - /** - * Public key of the coin that was refreshed into this coin. - */ - old_coin_pub: string; -} - -/** - * Metadata about a coin obtained from a tip. - */ -export interface BackupTipCoinSource { - type: BackupCoinSourceType.Tip; - - /** - * Wallet's identifier for the tip that this coin - * originates from. - */ - wallet_tip_id: string; - - /** - * Index in the tip planchets of the tip. - */ - coin_index: number; -} - -/** - * Metadata about a coin depending on the origin. - */ -export type BackupCoinSource = - | BackupWithdrawCoinSource - | BackupRefreshCoinSource - | BackupTipCoinSource; - -/** - * Backup information about a coin. - * - * (Always part of a BackupExchange/BackupDenom) - */ -export interface BackupCoin { - /** - * Where did the coin come from? Used for recouping coins. - */ - coin_source: BackupCoinSource; - - /** - * Private key to authorize operations on the coin. - */ - coin_priv: string; - - /** - * Unblinded signature by the exchange. - */ - denom_sig: UnblindedSignature; - - /** - * Amount that's left on the coin. - */ - current_amount: BackupAmountString; - - /** - * Blinding key used when withdrawing the coin. - * Potentionally used again during payback. - */ - blinding_key: string; - - /** - * Does the wallet think that the coin is still fresh? - * - * Note that even if a fresh coin is imported, it should still - * be refreshed in most situations. - */ - fresh: boolean; -} - -/** - * Status of a tip we got from a merchant. - */ -export interface BackupTip { - /** - * Tip ID chosen by the wallet. - */ - wallet_tip_id: string; - - /** - * The merchant's identifier for this tip. - */ - merchant_tip_id: string; - - /** - * Secret seed used for the tipping planchets. - */ - secret_seed: string; - - /** - * Has the user accepted the tip? Only after the tip has been accepted coins - * withdrawn from the tip may be used. - */ - timestamp_accepted: TalerProtocolTimestamp | undefined; - - /** - * When was the tip first scanned by the wallet? - */ - timestamp_created: TalerProtocolTimestamp; - - timestamp_finished?: TalerProtocolTimestamp; - finish_is_failure?: boolean; - - /** - * The tipped amount. - */ - tip_amount_raw: BackupAmountString; - - /** - * Timestamp, the tip can't be picked up anymore after this deadline. - */ - timestamp_expiration: TalerProtocolTimestamp; - - /** - * The exchange that will sign our coins, chosen by the merchant. - */ - exchange_base_url: string; - - /** - * Base URL of the merchant that is giving us the tip. - */ - merchant_base_url: string; - - /** - * Selected denominations. Determines the effective tip amount. - */ - selected_denoms: BackupDenomSel; - - /** - * UID for the denomination selection. - * Used to disambiguate when merging. - */ - selected_denoms_uid: OperationUid; -} - -/** - * Reasons for why a coin is being refreshed. - */ -export enum BackupRefreshReason { - Manual = "manual", - Pay = "pay", - Refund = "refund", - AbortPay = "abort-pay", - Recoup = "recoup", - BackupRestored = "backup-restored", - Scheduled = "scheduled", -} - -/** - * Information about one refresh session, always part - * of a refresh group. - * - * (Public key of the old coin is stored in the refresh group.) - */ -export interface BackupRefreshSession { - /** - * Hashed denominations of the newly requested coins. - */ - new_denoms: BackupDenomSel; - - /** - * Seed used to derive the planchets and - * transfer private keys for this refresh session. - */ - session_secret_seed: string; - - /** - * The no-reveal-index after we've done the melting. - */ - noreveal_index?: number; -} - -/** - * Refresh session for one coin inside a refresh group. - */ -export interface BackupRefreshOldCoin { - /** - * Public key of the old coin, - */ - coin_pub: string; - - /** - * Requested amount to refresh. Must be subtracted from the coin's remaining - * amount as soon as the coin is added to the refresh group. - */ - input_amount: BackupAmountString; - - /** - * Estimated output (may change if it takes a long time to create the - * actual session). - */ - estimated_output_amount: BackupAmountString; - - /** - * Did the refresh session finish (or was it unnecessary/impossible to create - * one) - */ - finished: boolean; - - /** - * Refresh session (if created) or undefined it not created yet. - */ - refresh_session: BackupRefreshSession | undefined; -} - -/** - * Information about one refresh group. - * - * May span more than one exchange, but typically doesn't - */ -export interface BackupRefreshGroup { - refresh_group_id: string; - - reason: BackupRefreshReason; - - /** - * Details per old coin. - */ - old_coins: BackupRefreshOldCoin[]; - - timestamp_created: TalerProtocolTimestamp; - - timestamp_finish?: TalerProtocolTimestamp; - finish_is_failure?: boolean; -} - -export enum BackupRefundState { - Failed = "failed", - Applied = "applied", - Pending = "pending", -} - -/** - * Common information about a refund. - */ -export interface BackupRefundItemCommon { - /** - * Execution time as claimed by the merchant - */ - execution_time: TalerProtocolTimestamp; - - /** - * Time when the wallet became aware of the refund. - */ - obtained_time: TalerProtocolTimestamp; - - /** - * Amount refunded for the coin. - */ - refund_amount: BackupAmountString; - - /** - * Coin being refunded. - */ - coin_pub: string; - - /** - * The refund transaction ID for the refund. - */ - rtransaction_id: number; - - /** - * Upper bound on the refresh cost incurred by - * applying this refund. - * - * Might be lower in practice when two refunds on the same - * coin are refreshed in the same refresh operation. - * - * Used to display fees, and stored since it's expensive to recompute - * accurately. - */ - total_refresh_cost_bound: BackupAmountString; -} - -/** - * Failed refund, either because the merchant did - * something wrong or it expired. - */ -export interface BackupRefundFailedItem extends BackupRefundItemCommon { - type: BackupRefundState.Failed; -} - -export interface BackupRefundPendingItem extends BackupRefundItemCommon { - type: BackupRefundState.Pending; -} - -export interface BackupRefundAppliedItem extends BackupRefundItemCommon { - type: BackupRefundState.Applied; -} - -/** - * State of one refund from the merchant, maintained by the wallet. - */ -export type BackupRefundItem = - | BackupRefundFailedItem - | BackupRefundPendingItem - | BackupRefundAppliedItem; - -/** - * Data we store when the payment was accepted. - */ -export interface BackupPayInfo { - pay_coins: { - /** - * Public keys of the coins that were selected. - */ - coin_pub: string; - - /** - * Amount that each coin contributes. - */ - contribution: BackupAmountString; - }[]; - - /** - * Unique ID to disambiguate pay coin selection on merge. - */ - pay_coins_uid: OperationUid; - - /** - * Total cost initially shown to the user. - * - * This includes the amount taken by the merchant, fees (wire/deposit) contributed - * by the customer, refreshing fees, fees for withdraw-after-refresh and "trimmings" - * of coins that are too small to spend. - * - * Note that in rare situations, this cost might not be accurate (e.g. - * when the payment or refresh gets re-denominated). - * We might show adjustments to this later, but currently we don't do so. - */ - total_pay_cost: BackupAmountString; -} - -export interface BackupPurchase { - /** - * Proposal ID for this purchase. Uniquely identifies the - * purchase and the proposal. - */ - proposal_id: string; - - /** - * Status of the proposal. - */ - proposal_status: BackupProposalStatus; - - /** - * Proposal that this one got "redirected" to as part of - * the repurchase detection. - */ - repurchase_proposal_id: string | undefined; - - /** - * Session ID we got when downloading the contract. - */ - download_session_id?: string; - - /** - * Merchant-assigned order ID of the proposal. - */ - order_id: string; - - /** - * Base URL of the merchant that proposed the purchase. - */ - merchant_base_url: string; - - /** - * Claim token initially given by the merchant. - */ - claim_token: string | undefined; - - /** - * Contract terms we got from the merchant. - */ - contract_terms_raw?: RawContractTerms; - - /** - * Signature on the contract terms. - * - * FIXME: Better name needed. - */ - merchant_sig?: string; - - /** - * Private key for the nonce. Might eventually be used - * to prove ownership of the contract. - */ - nonce_priv: string; - - pay_info: BackupPayInfo | undefined; - - /** - * Timestamp of the first time that sending a payment to the merchant - * for this purchase was successful. - */ - timestamp_first_successful_pay: TalerProtocolTimestamp | undefined; - - /** - * Signature by the merchant confirming the payment. - */ - merchant_pay_sig: string | undefined; - - timestamp_proposed: TalerProtocolTimestamp; - - /** - * When was the purchase made? - * Refers to the time that the user accepted. - */ - timestamp_accepted: TalerProtocolTimestamp | undefined; - - /** - * Pending refunds for the purchase. A refund is pending - * when the merchant reports a transient error from the exchange. - */ - refunds: BackupRefundItem[]; - - /** - * Continue querying the refund status until this deadline has expired. - */ - auto_refund_deadline: TalerProtocolTimestamp | undefined; -} - -/** - * Info about one denomination in the backup. - * - * Note that the wallet only backs up validated denominations. - */ -export interface BackupDenomination { - /** - * Value of one coin of the denomination. - */ - value: BackupAmountString; - - /** - * The denomination public key. - */ - denom_pub: DenominationPubKey; - - /** - * Fee for withdrawing. - */ - fee_withdraw: BackupAmountString; - - /** - * Fee for depositing. - */ - fee_deposit: BackupAmountString; - - /** - * Fee for refreshing. - */ - fee_refresh: BackupAmountString; - - /** - * Fee for refunding. - */ - fee_refund: BackupAmountString; - - /** - * Validity start date of the denomination. - */ - stamp_start: TalerProtocolTimestamp; - - /** - * Date after which the currency can't be withdrawn anymore. - */ - stamp_expire_withdraw: TalerProtocolTimestamp; - - /** - * Date after the denomination officially doesn't exist anymore. - */ - stamp_expire_legal: TalerProtocolTimestamp; - - /** - * Data after which coins of this denomination can't be deposited anymore. - */ - stamp_expire_deposit: TalerProtocolTimestamp; - - /** - * Signature by the exchange's master key over the denomination - * information. - */ - master_sig: string; - - /** - * Was this denomination still offered by the exchange the last time - * we checked? - * Only false when the exchange redacts a previously published denomination. - */ - is_offered: boolean; - - /** - * Did the exchange revoke the denomination? - * When this field is set to true in the database, the same transaction - * should also mark all affected coins as revoked. - */ - is_revoked: boolean; - - /** - * Coins of this denomination. - */ - coins: BackupCoin[]; - - /** - * The list issue date of the exchange "/keys" response - * that this denomination was last seen in. - */ - list_issue_date: TalerProtocolTimestamp; -} - -/** - * Denomination selection. - */ -export type BackupDenomSel = { - denom_pub_hash: string; - count: number; -}[]; - -/** - * Wire fee for one wire payment target type as stored in the - * wallet's database. - * - * (Flattened to a list to make the declaration simpler). - */ -export interface BackupExchangeWireFee { - wire_type: string; - - /** - * Fee for wire transfers. - */ - wire_fee: string; - - wad_fee: string; - - /** - * Fees to close and refund a reserve. - */ - closing_fee: string; - - /** - * Start date of the fee. - */ - start_stamp: TalerProtocolTimestamp; - - /** - * End date of the fee. - */ - end_stamp: TalerProtocolTimestamp; - - /** - * Signature made by the exchange master key. - */ - sig: string; -} - -/** - * Global fee as stored in the wallet's database. - * - */ -export interface BackupExchangeGlobalFees { - startDate: TalerProtocolTimestamp; - endDate: TalerProtocolTimestamp; - - kycFee: BackupAmountString; - historyFee: BackupAmountString; - accountFee: BackupAmountString; - purseFee: BackupAmountString; - - historyTimeout: TalerProtocolDuration; - kycTimeout: TalerProtocolDuration; - purseTimeout: TalerProtocolDuration; - - purseLimit: number; - - signature: string; -} -/** - * Structure of one exchange signing key in the /keys response. - */ -export class BackupExchangeSignKey { - stamp_start: TalerProtocolTimestamp; - stamp_expire: TalerProtocolTimestamp; - stamp_end: TalerProtocolTimestamp; - key: string; - master_sig: string; -} - -/** - * Signature by the auditor that a particular denomination key is audited. - */ -export class BackupAuditorDenomSig { - /** - * Denomination public key's hash. - */ - denom_pub_h: string; - - /** - * The signature. - */ - auditor_sig: string; -} - -/** - * Auditor information as given by the exchange in /keys. - */ -export class BackupExchangeAuditor { - /** - * Auditor's public key. - */ - auditor_pub: string; - - /** - * Base URL of the auditor. - */ - auditor_url: string; - - /** - * List of signatures for denominations by the auditor. - */ - denomination_keys: BackupAuditorDenomSig[]; -} - -/** - * Backup information for an exchange. Serves effectively - * as a pointer to the exchange details identified by - * the base URL, master public key and currency. - */ -export interface BackupExchange { - base_url: string; - - master_public_key: string; - - currency: string; - - /** - * Time when the pointer to the exchange details - * was last updated. - * - * Used to facilitate automatic merging. - */ - update_clock: TalerProtocolTimestamp; -} - -/** - * Backup information about an exchange's details. - * - * Note that one base URL can have multiple exchange - * details. The BackupExchange stores a pointer - * to the current exchange details. - */ -export interface BackupExchangeDetails { - /** - * Canonicalized base url of the exchange. - */ - base_url: string; - - /** - * Master public key of the exchange. - */ - master_public_key: string; - - /** - * Auditors (partially) auditing the exchange. - */ - auditors: BackupExchangeAuditor[]; - - /** - * Currency that the exchange offers. - */ - currency: string; - - /** - * Denominations offered by the exchange. - */ - denominations: BackupDenomination[]; - - /** - * Last observed protocol version. - */ - protocol_version: string; - - /** - * Closing delay of reserves. - */ - reserve_closing_delay: TalerProtocolDuration; - - /** - * Signing keys we got from the exchange, can also contain - * older signing keys that are not returned by /keys anymore. - */ - signing_keys: BackupExchangeSignKey[]; - - wire_fees: BackupExchangeWireFee[]; - - global_fees: BackupExchangeGlobalFees[]; - - /** - * Bank accounts offered by the exchange; - */ - accounts: { - payto_uri: string; - master_sig: string; - }[]; - - /** - * ETag for last terms of service download. - */ - tos_accepted_etag: string | undefined; - - /** - * Timestamp when the ToS has been accepted. - */ - tos_accepted_timestamp: TalerProtocolTimestamp | undefined; -} - -export enum BackupProposalStatus { - /** - * Proposed (and either downloaded or not, - * depending on whether contract terms are present), - * but the user needs to accept/reject it. - */ - Proposed = "proposed", - /** - * The user has rejected the proposal. - */ - Refused = "refused", - /** - * Downloading or processing the proposal has failed permanently. - * - * FIXME: Should this be modeled as a "misbehavior report" instead? - */ - PermanentlyFailed = "permanently-failed", - /** - * Downloaded proposal was detected as a re-purchase. - */ - Repurchase = "repurchase", - - Paid = "paid", -} - -export interface BackupRecovery { - walletRootPriv: string; - providers: { - url: string; - }[]; -} diff --git a/packages/taler-util/src/bitcoin.ts b/packages/taler-util/src/bitcoin.ts index ede3cbcdc..8c22ba522 100644 --- a/packages/taler-util/src/bitcoin.ts +++ b/packages/taler-util/src/bitcoin.ts @@ -23,7 +23,7 @@ * Imports. */ import { AmountJson, Amounts } from "./amounts.js"; -import { decodeCrock } from "./talerCrypto.js"; +import { decodeCrock } from "./taler-crypto.js"; import * as segwit from "./segwit_addr.js"; function buf2hex(buffer: Uint8Array) { diff --git a/packages/taler-util/src/contract-terms.test.ts b/packages/taler-util/src/contract-terms.test.ts new file mode 100644 index 000000000..fc0920501 --- /dev/null +++ b/packages/taler-util/src/contract-terms.test.ts @@ -0,0 +1,127 @@ +/* + This file is part of GNU Taler + (C) 2021 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see + */ + +/** + * Imports. + */ +import test from "ava"; +import { initNodePrng } from "./prng-node.js"; +import { ContractTermsUtil } from "./contract-terms.js"; + +// Since we import nacl-fast directly (and not via index.node.ts), we need to +// init the PRNG manually. +initNodePrng(); + +test("contract terms canon hashing", (t) => { + const cReq = { + foo: 42, + bar: "hello", + $forgettable: { + foo: true, + }, + }; + + const c1 = ContractTermsUtil.saltForgettable(cReq); + const c2 = ContractTermsUtil.saltForgettable(cReq); + t.assert(typeof cReq.$forgettable.foo === "boolean"); + t.assert(typeof c1.$forgettable.foo === "string"); + t.assert(c1.$forgettable.foo !== c2.$forgettable.foo); + + const h1 = ContractTermsUtil.hashContractTerms(c1); + + const c3 = ContractTermsUtil.scrub(JSON.parse(JSON.stringify(c1))); + + t.assert(c3.foo === undefined); + t.assert(c3.bar === cReq.bar); + + const h2 = ContractTermsUtil.hashContractTerms(c3); + + t.deepEqual(h1, h2); +}); + +test("contract terms canon hashing (nested)", (t) => { + const cReq = { + foo: 42, + bar: { + prop1: "hello, world", + $forgettable: { + prop1: true, + }, + }, + $forgettable: { + bar: true, + }, + }; + + const c1 = ContractTermsUtil.saltForgettable(cReq); + + t.is(typeof c1.$forgettable.bar, "string"); + t.is(typeof c1.bar.$forgettable.prop1, "string"); + + const forgetPath = (x: any, s: string) => + ContractTermsUtil.forgetAll(x, (p) => p.join(".") === s); + + // Forget bar first + const c2 = forgetPath(c1, "bar"); + + // Forget bar.prop1 first + const c3 = forgetPath(forgetPath(c1, "bar.prop1"), "bar"); + + // Forget everything + const c4 = ContractTermsUtil.scrub(c1); + + const h1 = ContractTermsUtil.hashContractTerms(c1); + const h2 = ContractTermsUtil.hashContractTerms(c2); + const h3 = ContractTermsUtil.hashContractTerms(c3); + const h4 = ContractTermsUtil.hashContractTerms(c4); + + t.is(h1, h2); + t.is(h1, h3); + t.is(h1, h4); + + // Doesn't contain salt + t.false(ContractTermsUtil.validateForgettable(cReq)); + + t.true(ContractTermsUtil.validateForgettable(c1)); + t.true(ContractTermsUtil.validateForgettable(c2)); + t.true(ContractTermsUtil.validateForgettable(c3)); + t.true(ContractTermsUtil.validateForgettable(c4)); +}); + +test("contract terms reference vector", (t) => { + const j = { + k1: 1, + $forgettable: { + k1: "SALT", + }, + k2: { + n1: true, + $forgettable: { + n1: "salt", + }, + }, + k3: { + n1: "string", + }, + }; + + const h = ContractTermsUtil.hashContractTerms(j); + + t.deepEqual( + h, + "VDE8JPX0AEEE3EX1K8E11RYEWSZQKGGZCV6BWTE4ST1C8711P7H850Z7F2Q2HSSYETX87ERC2JNHWB7GTDWTDWMM716VKPSRBXD7SRR", + ); +}); diff --git a/packages/taler-util/src/contract-terms.ts b/packages/taler-util/src/contract-terms.ts new file mode 100644 index 000000000..3567b50d8 --- /dev/null +++ b/packages/taler-util/src/contract-terms.ts @@ -0,0 +1,231 @@ +/* + This file is part of GNU Taler + (C) 2021 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see + */ + +import { canonicalJson } from "./helpers.js"; +import { Logger } from "./logging.js"; +import { kdf } from "./kdf.js"; +import { + decodeCrock, + encodeCrock, + getRandomBytes, + hash, + stringToBytes, +} from "./taler-crypto.js"; + +const logger = new Logger("contractTerms.ts"); + +export namespace ContractTermsUtil { + export function forgetAllImpl( + anyJson: any, + path: string[], + pred: PathPredicate, + ): any { + const dup = JSON.parse(JSON.stringify(anyJson)); + if (Array.isArray(dup)) { + for (let i = 0; i < dup.length; i++) { + dup[i] = forgetAllImpl(dup[i], [...path, `${i}`], pred); + } + } else if (typeof dup === "object" && dup != null) { + if (typeof dup.$forgettable === "object") { + for (const x of Object.keys(dup.$forgettable)) { + if (!pred([...path, x])) { + continue; + } + if (!dup.$forgotten) { + dup.$forgotten = {}; + } + if (!dup.$forgotten[x]) { + const membValCanon = stringToBytes( + canonicalJson(scrub(dup[x])) + "\0", + ); + const membSalt = stringToBytes(dup.$forgettable[x] + "\0"); + const h = kdf(64, membValCanon, membSalt, new Uint8Array([])); + dup.$forgotten[x] = encodeCrock(h); + } + delete dup[x]; + delete dup.$forgettable[x]; + } + if (Object.keys(dup.$forgettable).length === 0) { + delete dup.$forgettable; + } + } + for (const x of Object.keys(dup)) { + if (x.startsWith("$")) { + continue; + } + dup[x] = forgetAllImpl(dup[x], [...path, x], pred); + } + } + return dup; + } + + export type PathPredicate = (path: string[]) => boolean; + + /** + * Scrub all forgettable members from an object. + */ + export function scrub(anyJson: any): any { + return forgetAllImpl(anyJson, [], () => true); + } + + /** + * Recursively forget all forgettable members of an object, + * where the path matches a predicate. + */ + export function forgetAll(anyJson: any, pred: PathPredicate): any { + return forgetAllImpl(anyJson, [], pred); + } + + /** + * Generate a salt for all members marked as forgettable, + * but which don't have an actual salt yet. + */ + export function saltForgettable(anyJson: any): any { + const dup = JSON.parse(JSON.stringify(anyJson)); + if (Array.isArray(dup)) { + for (let i = 0; i < dup.length; i++) { + dup[i] = saltForgettable(dup[i]); + } + } else if (typeof dup === "object" && dup !== null) { + if (typeof dup.$forgettable === "object") { + for (const k of Object.keys(dup.$forgettable)) { + if (dup.$forgettable[k] === true) { + dup.$forgettable[k] = encodeCrock(getRandomBytes(32)); + } + } + } + for (const x of Object.keys(dup)) { + if (x.startsWith("$")) { + continue; + } + dup[x] = saltForgettable(dup[x]); + } + } + return dup; + } + + const nameRegex = /^[0-9A-Za-z_]+$/; + + /** + * Check that the given JSON object is well-formed with regards + * to forgettable fields and other restrictions for forgettable JSON. + */ + export function validateForgettable(anyJson: any): boolean { + if (typeof anyJson === "string") { + return true; + } + if (typeof anyJson === "number") { + return ( + Number.isInteger(anyJson) && + anyJson >= Number.MIN_SAFE_INTEGER && + anyJson <= Number.MAX_SAFE_INTEGER + ); + } + if (typeof anyJson === "boolean") { + return true; + } + if (anyJson === null) { + return true; + } + if (Array.isArray(anyJson)) { + return anyJson.every((x) => validateForgettable(x)); + } + if (typeof anyJson === "object") { + for (const k of Object.keys(anyJson)) { + if (k.match(nameRegex)) { + if (validateForgettable(anyJson[k])) { + continue; + } else { + return false; + } + } + if (k === "$forgettable") { + const fga = anyJson.$forgettable; + if (!fga || typeof fga !== "object") { + return false; + } + for (const fk of Object.keys(fga)) { + if (!fk.match(nameRegex)) { + return false; + } + if (!(fk in anyJson)) { + return false; + } + const fv = anyJson.$forgettable[fk]; + if (typeof fv !== "string") { + return false; + } + } + } else if (k === "$forgotten") { + const fgo = anyJson.$forgotten; + if (!fgo || typeof fgo !== "object") { + return false; + } + for (const fk of Object.keys(fgo)) { + if (!fk.match(nameRegex)) { + return false; + } + // Check that the value has actually been forgotten. + if (fk in anyJson) { + return false; + } + const fv = anyJson.$forgotten[fk]; + if (typeof fv !== "string") { + return false; + } + try { + const decFv = decodeCrock(fv); + if (decFv.length != 64) { + return false; + } + } catch (e) { + return false; + } + // Check that salt has been deleted after forgetting. + if (anyJson.$forgettable?.[k] !== undefined) { + return false; + } + } + } else { + return false; + } + } + return true; + } + return false; + } + + /** + * Check that no forgettable information has been forgotten. + * + * Must only be called on an object already validated with validateForgettable. + */ + export function validateNothingForgotten(contractTerms: any): boolean { + throw Error("not implemented yet"); + } + + /** + * Hash a contract terms object. Forgettable fields + * are scrubbed and JSON canonicalization is applied + * before hashing. + */ + export function hashContractTerms(contractTerms: unknown): string { + const cleaned = scrub(contractTerms); + const canon = canonicalJson(cleaned) + "\0"; + const bytes = stringToBytes(canon); + return encodeCrock(hash(bytes)); + } +} diff --git a/packages/taler-util/src/contractTerms.test.ts b/packages/taler-util/src/contractTerms.test.ts deleted file mode 100644 index d021495d0..000000000 --- a/packages/taler-util/src/contractTerms.test.ts +++ /dev/null @@ -1,127 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2021 Taler Systems S.A. - - GNU Taler is free software; you can redistribute it and/or modify it under the - terms of the GNU General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - GNU Taler; see the file COPYING. If not, see - */ - -/** - * Imports. - */ -import test from "ava"; -import { initNodePrng } from "./prng-node.js"; -import { ContractTermsUtil } from "./contractTerms.js"; - -// Since we import nacl-fast directly (and not via index.node.ts), we need to -// init the PRNG manually. -initNodePrng(); - -test("contract terms canon hashing", (t) => { - const cReq = { - foo: 42, - bar: "hello", - $forgettable: { - foo: true, - }, - }; - - const c1 = ContractTermsUtil.saltForgettable(cReq); - const c2 = ContractTermsUtil.saltForgettable(cReq); - t.assert(typeof cReq.$forgettable.foo === "boolean"); - t.assert(typeof c1.$forgettable.foo === "string"); - t.assert(c1.$forgettable.foo !== c2.$forgettable.foo); - - const h1 = ContractTermsUtil.hashContractTerms(c1); - - const c3 = ContractTermsUtil.scrub(JSON.parse(JSON.stringify(c1))); - - t.assert(c3.foo === undefined); - t.assert(c3.bar === cReq.bar); - - const h2 = ContractTermsUtil.hashContractTerms(c3); - - t.deepEqual(h1, h2); -}); - -test("contract terms canon hashing (nested)", (t) => { - const cReq = { - foo: 42, - bar: { - prop1: "hello, world", - $forgettable: { - prop1: true, - }, - }, - $forgettable: { - bar: true, - }, - }; - - const c1 = ContractTermsUtil.saltForgettable(cReq); - - t.is(typeof c1.$forgettable.bar, "string"); - t.is(typeof c1.bar.$forgettable.prop1, "string"); - - const forgetPath = (x: any, s: string) => - ContractTermsUtil.forgetAll(x, (p) => p.join(".") === s); - - // Forget bar first - const c2 = forgetPath(c1, "bar"); - - // Forget bar.prop1 first - const c3 = forgetPath(forgetPath(c1, "bar.prop1"), "bar"); - - // Forget everything - const c4 = ContractTermsUtil.scrub(c1); - - const h1 = ContractTermsUtil.hashContractTerms(c1); - const h2 = ContractTermsUtil.hashContractTerms(c2); - const h3 = ContractTermsUtil.hashContractTerms(c3); - const h4 = ContractTermsUtil.hashContractTerms(c4); - - t.is(h1, h2); - t.is(h1, h3); - t.is(h1, h4); - - // Doesn't contain salt - t.false(ContractTermsUtil.validateForgettable(cReq)); - - t.true(ContractTermsUtil.validateForgettable(c1)); - t.true(ContractTermsUtil.validateForgettable(c2)); - t.true(ContractTermsUtil.validateForgettable(c3)); - t.true(ContractTermsUtil.validateForgettable(c4)); -}); - -test("contract terms reference vector", (t) => { - const j = { - k1: 1, - $forgettable: { - k1: "SALT", - }, - k2: { - n1: true, - $forgettable: { - n1: "salt", - }, - }, - k3: { - n1: "string", - }, - }; - - const h = ContractTermsUtil.hashContractTerms(j); - - t.deepEqual( - h, - "VDE8JPX0AEEE3EX1K8E11RYEWSZQKGGZCV6BWTE4ST1C8711P7H850Z7F2Q2HSSYETX87ERC2JNHWB7GTDWTDWMM716VKPSRBXD7SRR", - ); -}); diff --git a/packages/taler-util/src/contractTerms.ts b/packages/taler-util/src/contractTerms.ts deleted file mode 100644 index aa6bf7baf..000000000 --- a/packages/taler-util/src/contractTerms.ts +++ /dev/null @@ -1,231 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2021 Taler Systems S.A. - - GNU Taler is free software; you can redistribute it and/or modify it under the - terms of the GNU General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - GNU Taler; see the file COPYING. If not, see - */ - -import { canonicalJson } from "./helpers.js"; -import { Logger } from "./logging.js"; -import { kdf } from "./kdf.js"; -import { - decodeCrock, - encodeCrock, - getRandomBytes, - hash, - stringToBytes, -} from "./talerCrypto.js"; - -const logger = new Logger("contractTerms.ts"); - -export namespace ContractTermsUtil { - export function forgetAllImpl( - anyJson: any, - path: string[], - pred: PathPredicate, - ): any { - const dup = JSON.parse(JSON.stringify(anyJson)); - if (Array.isArray(dup)) { - for (let i = 0; i < dup.length; i++) { - dup[i] = forgetAllImpl(dup[i], [...path, `${i}`], pred); - } - } else if (typeof dup === "object" && dup != null) { - if (typeof dup.$forgettable === "object") { - for (const x of Object.keys(dup.$forgettable)) { - if (!pred([...path, x])) { - continue; - } - if (!dup.$forgotten) { - dup.$forgotten = {}; - } - if (!dup.$forgotten[x]) { - const membValCanon = stringToBytes( - canonicalJson(scrub(dup[x])) + "\0", - ); - const membSalt = stringToBytes(dup.$forgettable[x] + "\0"); - const h = kdf(64, membValCanon, membSalt, new Uint8Array([])); - dup.$forgotten[x] = encodeCrock(h); - } - delete dup[x]; - delete dup.$forgettable[x]; - } - if (Object.keys(dup.$forgettable).length === 0) { - delete dup.$forgettable; - } - } - for (const x of Object.keys(dup)) { - if (x.startsWith("$")) { - continue; - } - dup[x] = forgetAllImpl(dup[x], [...path, x], pred); - } - } - return dup; - } - - export type PathPredicate = (path: string[]) => boolean; - - /** - * Scrub all forgettable members from an object. - */ - export function scrub(anyJson: any): any { - return forgetAllImpl(anyJson, [], () => true); - } - - /** - * Recursively forget all forgettable members of an object, - * where the path matches a predicate. - */ - export function forgetAll(anyJson: any, pred: PathPredicate): any { - return forgetAllImpl(anyJson, [], pred); - } - - /** - * Generate a salt for all members marked as forgettable, - * but which don't have an actual salt yet. - */ - export function saltForgettable(anyJson: any): any { - const dup = JSON.parse(JSON.stringify(anyJson)); - if (Array.isArray(dup)) { - for (let i = 0; i < dup.length; i++) { - dup[i] = saltForgettable(dup[i]); - } - } else if (typeof dup === "object" && dup !== null) { - if (typeof dup.$forgettable === "object") { - for (const k of Object.keys(dup.$forgettable)) { - if (dup.$forgettable[k] === true) { - dup.$forgettable[k] = encodeCrock(getRandomBytes(32)); - } - } - } - for (const x of Object.keys(dup)) { - if (x.startsWith("$")) { - continue; - } - dup[x] = saltForgettable(dup[x]); - } - } - return dup; - } - - const nameRegex = /^[0-9A-Za-z_]+$/; - - /** - * Check that the given JSON object is well-formed with regards - * to forgettable fields and other restrictions for forgettable JSON. - */ - export function validateForgettable(anyJson: any): boolean { - if (typeof anyJson === "string") { - return true; - } - if (typeof anyJson === "number") { - return ( - Number.isInteger(anyJson) && - anyJson >= Number.MIN_SAFE_INTEGER && - anyJson <= Number.MAX_SAFE_INTEGER - ); - } - if (typeof anyJson === "boolean") { - return true; - } - if (anyJson === null) { - return true; - } - if (Array.isArray(anyJson)) { - return anyJson.every((x) => validateForgettable(x)); - } - if (typeof anyJson === "object") { - for (const k of Object.keys(anyJson)) { - if (k.match(nameRegex)) { - if (validateForgettable(anyJson[k])) { - continue; - } else { - return false; - } - } - if (k === "$forgettable") { - const fga = anyJson.$forgettable; - if (!fga || typeof fga !== "object") { - return false; - } - for (const fk of Object.keys(fga)) { - if (!fk.match(nameRegex)) { - return false; - } - if (!(fk in anyJson)) { - return false; - } - const fv = anyJson.$forgettable[fk]; - if (typeof fv !== "string") { - return false; - } - } - } else if (k === "$forgotten") { - const fgo = anyJson.$forgotten; - if (!fgo || typeof fgo !== "object") { - return false; - } - for (const fk of Object.keys(fgo)) { - if (!fk.match(nameRegex)) { - return false; - } - // Check that the value has actually been forgotten. - if (fk in anyJson) { - return false; - } - const fv = anyJson.$forgotten[fk]; - if (typeof fv !== "string") { - return false; - } - try { - const decFv = decodeCrock(fv); - if (decFv.length != 64) { - return false; - } - } catch (e) { - return false; - } - // Check that salt has been deleted after forgetting. - if (anyJson.$forgettable?.[k] !== undefined) { - return false; - } - } - } else { - return false; - } - } - return true; - } - return false; - } - - /** - * Check that no forgettable information has been forgotten. - * - * Must only be called on an object already validated with validateForgettable. - */ - export function validateNothingForgotten(contractTerms: any): boolean { - throw Error("not implemented yet"); - } - - /** - * Hash a contract terms object. Forgettable fields - * are scrubbed and JSON canonicalization is applied - * before hashing. - */ - export function hashContractTerms(contractTerms: unknown): string { - const cleaned = scrub(contractTerms); - const canon = canonicalJson(cleaned) + "\0"; - const bytes = stringToBytes(canon); - return encodeCrock(hash(bytes)); - } -} diff --git a/packages/taler-util/src/index.ts b/packages/taler-util/src/index.ts index cf48ba803..1d55da068 100644 --- a/packages/taler-util/src/index.ts +++ b/packages/taler-util/src/index.ts @@ -3,7 +3,7 @@ import { TalerErrorCode } from "./taler-error-codes.js"; export { TalerErrorCode }; export * from "./amounts.js"; -export * from "./backupTypes.js"; +export * from "./backup-types.js"; export * from "./codec.js"; export * from "./helpers.js"; export * from "./libtool-version.js"; @@ -11,17 +11,17 @@ export * from "./notifications.js"; export * from "./payto.js"; export * from "./ReserveStatus.js"; export * from "./ReserveTransaction.js"; -export * from "./talerTypes.js"; +export * from "./taler-types.js"; export * from "./taleruri.js"; export * from "./time.js"; -export * from "./transactionsTypes.js"; -export * from "./walletTypes.js"; +export * from "./transactions-types.js"; +export * from "./wallet-types.js"; export * from "./i18n.js"; export * from "./logging.js"; export * from "./url.js"; export { fnutil } from "./fnutils.js"; export * from "./kdf.js"; -export * from "./talerCrypto.js"; +export * from "./taler-crypto.js"; export * from "./http-status-codes.js"; export * from "./bitcoin.js"; export { @@ -32,4 +32,4 @@ export { } from "./nacl-fast.js"; export { RequestThrottler } from "./RequestThrottler.js"; export * from "./CancellationToken.js"; -export * from "./contractTerms.js"; +export * from "./contract-terms.js"; diff --git a/packages/taler-util/src/notifications.ts b/packages/taler-util/src/notifications.ts index b3d9ad1dc..17019237a 100644 --- a/packages/taler-util/src/notifications.ts +++ b/packages/taler-util/src/notifications.ts @@ -22,7 +22,7 @@ /** * Imports. */ -import { TalerErrorDetail } from "./walletTypes.js"; +import { TalerErrorDetail } from "./wallet-types.js"; export enum NotificationType { CoinWithdrawn = "coin-withdrawn", diff --git a/packages/taler-util/src/taler-crypto.test.ts b/packages/taler-util/src/taler-crypto.test.ts new file mode 100644 index 000000000..e90516cc4 --- /dev/null +++ b/packages/taler-util/src/taler-crypto.test.ts @@ -0,0 +1,431 @@ +/* + This file is part of GNU Taler + (C) 2019 GNUnet e.V. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see + */ + +/** + * Imports + */ +import test from "ava"; +import { + encodeCrock, + decodeCrock, + ecdheGetPublic, + eddsaGetPublic, + keyExchangeEddsaEcdhe, + keyExchangeEcdheEddsa, + stringToBytes, + bytesToString, + deriveBSeed, + csBlind, + csUnblind, + csVerify, + scalarMultBase25519, + deriveSecrets, + calcRBlind, + Edx25519, + getRandomBytes, + bigintToNaclArr, + bigintFromNaclArr, +} from "./taler-crypto.js"; +import { sha512, kdf } from "./kdf.js"; +import * as nacl from "./nacl-fast.js"; +import { initNodePrng } from "./prng-node.js"; + +// Since we import nacl-fast directly (and not via index.node.ts), we need to +// init the PRNG manually. +initNodePrng(); +import bigint from "big-integer"; +import { AssertionError } from "assert"; +import BigInteger from "big-integer"; + +test("encoding", (t) => { + const s = "Hello, World"; + const encStr = encodeCrock(stringToBytes(s)); + const outBuf = decodeCrock(encStr); + const sOut = bytesToString(outBuf); + t.deepEqual(s, sOut); +}); + +test("taler-exchange-tvg hash code", (t) => { + const input = "91JPRV3F5GG4EKJN41A62V35E8"; + const output = + "CW96WR74JS8T53EC8GKSGD49QKH4ZNFTZXDAWMMV5GJ1E4BM6B8GPN5NVHDJ8ZVXNCW7Q4WBYCV61HCA3PZC2YJD850DT29RHHN7ESR"; + + const myOutput = encodeCrock(sha512(decodeCrock(input))); + + t.deepEqual(myOutput, output); +}); + +test("taler-exchange-tvg ecdhe key", (t) => { + const priv1 = "X4T4N0M8PVQXQEBW2BA7049KFSM7J437NSDFC6GDNM3N5J9367A0"; + const pub1 = "M997P494MS6A95G1P0QYWW2VNPSHSX5Q6JBY5B9YMNYWP0B50X3G"; + const priv2 = "14A0MMQ64DCV8HE0CS3WBC9DHFJAHXRGV7NEARFJPC5R5E1697E0"; + const skm = + "NXRY2YCY7H9B6KM928ZD55WG964G59YR0CPX041DYXKBZZ85SAWNPQ8B30QRM5FMHYCXJAN0EAADJYWEF1X3PAC2AJN28626TR5A6AR"; + + const myPub1 = nacl.scalarMult_base(decodeCrock(priv1)); + t.deepEqual(encodeCrock(myPub1), pub1); + + const mySkm = nacl.hash( + nacl.scalarMult(decodeCrock(priv2), decodeCrock(pub1)), + ); + t.deepEqual(encodeCrock(mySkm), skm); +}); + +test("taler-exchange-tvg eddsa key", (t) => { + const priv = "9TM70AKDTS57AWY9JK2J4TMBTMW6K62WHHGZWYDG0VM5ABPZKD40"; + const pub = "8GSJZ649T2PXMKZC01Y4ANNBE7MF14QVK9SQEC4E46ZHKCVG8AS0"; + + const pair = nacl.crypto_sign_keyPair_fromSeed(decodeCrock(priv)); + t.deepEqual(encodeCrock(pair.publicKey), pub); +}); + +test("taler-exchange-tvg kdf", (t) => { + const salt = "94KPT83PCNS7J83KC5P78Y8"; + const ikm = "94KPT83MD1JJ0WV5CDS6AX10D5Q70XBM41NPAY90DNGQ8SBJD5GPR"; + const ctx = + "94KPT83141HPYVKMCNW78833D1TPWTSC41GPRWVF41NPWVVQDRG62WS04XMPWSKF4WG6JVH0EHM6A82J8S1G"; + const outLen = 64; + const out = + "GTMR4QT05Z9WF5HKVG0WK9RPXGHSMHJNW377G9GJXCA8B0FEKPF4D27RJMSJZYWSQNTBJ5EYVV7ZW18B48Z0JVJJ80RHB706Y96Q358"; + + const myOut = kdf( + outLen, + decodeCrock(ikm), + decodeCrock(salt), + decodeCrock(ctx), + ); + + t.deepEqual(encodeCrock(myOut), out); +}); + +test("taler-exchange-tvg eddsa_ecdh", (t) => { + const priv_ecdhe = "4AFZWMSGTVCHZPQ0R81NWXDCK4N58G7SDBBE5KXE080Y50370JJG"; + const pub_ecdhe = "FXFN5GPAFTKVPWJDPVXQ87167S8T82T5ZV8CDYC0NH2AE14X0M30"; + const priv_eddsa = "1KG54M8T3X8BSFSZXCR3SQBSR7Y9P53NX61M864S7TEVMJ2XVPF0"; + const pub_eddsa = "7BXWKG6N224C57RTDV8XEAHR108HG78NMA995BE8QAT5GC1S7E80"; + const key_material = + "PKZ42Z56SVK2796HG1QYBRJ6ZQM2T9QGA3JA4AAZ8G7CWK9FPX175Q9JE5P0ZAX3HWWPHAQV4DPCK10R9X3SAXHRV0WF06BHEC2ZTKR"; + + const myEcdhePub = ecdheGetPublic(decodeCrock(priv_ecdhe)); + t.deepEqual(encodeCrock(myEcdhePub), pub_ecdhe); + + const myEddsaPub = eddsaGetPublic(decodeCrock(priv_eddsa)); + t.deepEqual(encodeCrock(myEddsaPub), pub_eddsa); + + const myKm1 = keyExchangeEddsaEcdhe( + decodeCrock(priv_eddsa), + decodeCrock(pub_ecdhe), + ); + t.deepEqual(encodeCrock(myKm1), key_material); + + const myKm2 = keyExchangeEcdheEddsa( + decodeCrock(priv_ecdhe), + decodeCrock(pub_eddsa), + ); + t.deepEqual(encodeCrock(myKm2), key_material); +}); + +test("incremental hashing #1", (t) => { + const n = 1024; + const d = nacl.randomBytes(n); + + const h1 = nacl.hash(d); + const h2 = new nacl.HashState().update(d).finish(); + + const s = new nacl.HashState(); + for (let i = 0; i < n; i++) { + const b = new Uint8Array(1); + b[0] = d[i]; + s.update(b); + } + + const h3 = s.finish(); + + t.deepEqual(encodeCrock(h1), encodeCrock(h2)); + t.deepEqual(encodeCrock(h1), encodeCrock(h3)); +}); + +test("incremental hashing #2", (t) => { + const n = 10; + const d = nacl.randomBytes(n); + + const h1 = nacl.hash(d); + const h2 = new nacl.HashState().update(d).finish(); + const s = new nacl.HashState(); + for (let i = 0; i < n; i++) { + const b = new Uint8Array(1); + b[0] = d[i]; + s.update(b); + } + + const h3 = s.finish(); + + t.deepEqual(encodeCrock(h1), encodeCrock(h3)); + t.deepEqual(encodeCrock(h1), encodeCrock(h2)); +}); + +test("taler-exchange-tvg eddsa_ecdh #2", (t) => { + const priv_ecdhe = "W5FH9CFS3YPGSCV200GE8TH6MAACPKKGEG2A5JTFSD1HZ5RYT7Q0"; + const pub_ecdhe = "FER9CRS2T8783TAANPZ134R704773XT0ZT1XPFXZJ9D4QX67ZN00"; + const priv_eddsa = "MSZ1TBKC6YQ19ZFP3NTJVKWNVGFP35BBRW8FTAQJ9Z2B96VC9P4G"; + const pub_eddsa = "Y7MKG85PBT8ZEGHF08JBVZXEV70TS0PY5Y2CMEN1WXEDN63KP1A0"; + const key_material = + "G6RA58N61K7MT3WA13Q7VRTE1FQS6H43RX9HK8Z5TGAB61601GEGX51JRHHQMNKNM2R9AVC1STSGQDRHGKWVYP584YGBCTVMMJYQF30"; + + const myEcdhePub = ecdheGetPublic(decodeCrock(priv_ecdhe)); + t.deepEqual(encodeCrock(myEcdhePub), pub_ecdhe); + + const myEddsaPub = eddsaGetPublic(decodeCrock(priv_eddsa)); + t.deepEqual(encodeCrock(myEddsaPub), pub_eddsa); + + const myKm1 = keyExchangeEddsaEcdhe( + decodeCrock(priv_eddsa), + decodeCrock(pub_ecdhe), + ); + t.deepEqual(encodeCrock(myKm1), key_material); + + const myKm2 = keyExchangeEcdheEddsa( + decodeCrock(priv_ecdhe), + decodeCrock(pub_eddsa), + ); + t.deepEqual(encodeCrock(myKm2), key_material); +}); + +test("taler CS blind c", async (t) => { + /**$ + * Test Vectors: + { + "operation": "cs_blind_signing", + "message_hash": "KZ7540050MWFPPPJ6C0910TC15AWD6KN6GMK4YH8PY5Z2RKP7NQMHZ1NDD7JHD9CA2CZXDKYN7XRX521YERAF6N50VJZMHWPH18TCFG", + "cs_public_key": "1903SZ7QE1K8T4BHTJ32KDJ153SBXT22DGNQDY5NKJE535J72H2G", + "cs_private_key": "K43QAMEPE9KJJTX6AJZD6N4SN1N3ARVAXZ2MRNPT85FHD4QD2C60", + "cs_nonce": "GWPVFP9160XNADYQZ4T6S7RACB2482KG1JCY0X2Z5R52W74YXY3G", + "cs_r_priv_0": "B01FJCRCST8JM10K17SJXY7S7HH7T65JMFQ03H6PNYY9Z167Q1T0", + "cs_r_priv_1": "N3GW5X6VYSB8PY83CYNHJ3PN6TCA5N5BCS4WT2WEEQH7MTK915P0", + "cs_r_pub_0": "J5XFBKFP9T6BM02H6ZV6Y568PQ2K398MD339036F25XTSP1A7T3G", + "cs_r_pub_1": "GA2CZKJ6CWFS81ZN1T5R4GQFHF7XJV6HWHDR1JA9VATKKXQN89J0", + "cs_bs_alpha_0": "R06FWJ4XEK4JKKKA03JARGD0PD5JAX8DK2N6J0K8CAZZMVQEJ1T0", + "cs_bs_alpha_1": "13NXE2FEHJS0Q5XCWNRF4V1NC3BSAHN6BW02WZ07PG6967156HYG", + "cs_bs_beta_0": "T3EZP42RJQXRTJ4FTDWF18Z422VX7KFGN8GJ3QCCM1QV3N456HD0", + "cs_bs_beta_1": "P3MECYGCCR58QVEDSW443699CDXVT8C8W5ZT22PPNRJ363M72H6G", + "cs_r_pub_blind_0": "CHK7JC4SXZ4Y9RDA3881S82F7BP99H35Q361WR6RBXN5YN2ZM1M0", + "cs_r_pub_blind_1": "4C65R74GA9PPDX4DC2B948W96T3Z6QEENK2NDJQPNB9QBTKCT590", + "cs_c_0": "F288QXT67TR36E6DHE399G8J24RM6C3DP16HGMH74B6WZ1DETR10", + "cs_c_1": "EFK5WTN01NCVS3DZCG20MQDHRHBATRG8589BA0XSZDZ6D0HFR470", + "cs_blind_s": "6KZF904YZA8KK4C8X5JV57E7B84SR8TDDN9GDC8QTRRSNTHJTM4G", + "cs_b": "0000000", + "cs_sig_s": "F4ZKMFW3Q7DFN0N94KAMG2JFFHAC362T0QZ6ZCVZ73RS8P91CR70", + "cs_sig_R": "CHK7JC4SXZ4Y9RDA3881S82F7BP99H35Q361WR6RBXN5YN2ZM1M0", + "cs_c_blind_0": "6TN5454DZCHBDXFAGQFXQY37FNX6YRKW0MPFEX4TG5EHXC98M840", + "cs_c_blind_1": "EX6MYRZX6EC93YB4EE3M7AR3PQDYYG4092917YF29HD36X58NG0G", + "cs_prehash_0": "D29BBP762HEN6ZHZ5T2T6S4VMV400K9Y659M1QQZYZ0WJS3V3EJSF0FVXSCD1E99JJJMW295EY8TEE97YEGSGEQ0Q0A9DDMS2NCAG9R", + "cs_prehash_1": "9BYD02BC29ZF26BG88DWFCCENCS8CD8VZN76XP8JPWKTN9JS73MBCD0F36N0JSM223MRNJZACNYPMW23SGRHYVSP6BTT79GSSK5R228" + } + */ + + type CsBlindSignature = { + sBlind: Uint8Array; + rPubBlind: Uint8Array; + }; + /** + * CS denomination keypair + */ + const priv = "K43QAMEPE9KJJTX6AJZD6N4SN1N3ARVAXZ2MRNPT85FHD4QD2C60"; + const pub_cmp = "1903SZ7QE1K8T4BHTJ32KDJ153SBXT22DGNQDY5NKJE535J72H2G"; + const pub = await scalarMultBase25519(decodeCrock(priv)); + t.deepEqual(decodeCrock(pub_cmp), pub); + + const nonce = "GWPVFP9160XNADYQZ4T6S7RACB2482KG1JCY0X2Z5R52W74YXY3G"; + const msg_hash = + "KZ7540050MWFPPPJ6C0910TC15AWD6KN6GMK4YH8PY5Z2RKP7NQMHZ1NDD7JHD9CA2CZXDKYN7XRX521YERAF6N50VJZMHWPH18TCFG"; + + /** + * rPub is returned from the exchange's new /csr API + */ + const rPriv0 = "B01FJCRCST8JM10K17SJXY7S7HH7T65JMFQ03H6PNYY9Z167Q1T0"; + const rPriv1 = "N3GW5X6VYSB8PY83CYNHJ3PN6TCA5N5BCS4WT2WEEQH7MTK915P0"; + const rPub0 = await scalarMultBase25519(decodeCrock(rPriv0)); + const rPub1 = await scalarMultBase25519(decodeCrock(rPriv1)); + + const rPub: [Uint8Array, Uint8Array] = [rPub0, rPub1]; + + t.deepEqual( + rPub[0], + decodeCrock("J5XFBKFP9T6BM02H6ZV6Y568PQ2K398MD339036F25XTSP1A7T3G"), + ); + t.deepEqual( + rPub[1], + decodeCrock("GA2CZKJ6CWFS81ZN1T5R4GQFHF7XJV6HWHDR1JA9VATKKXQN89J0"), + ); + + /** + * Test if blinding seed derivation is deterministic + * In the wallet the b-seed MUST be different from the Withdraw-Nonce or Refresh Nonce! + * (Eg. derive two different values from coin priv) -> See CS protocols for details + */ + const priv_eddsa = "1KG54M8T3X8BSFSZXCR3SQBSR7Y9P53NX61M864S7TEVMJ2XVPF0"; + // const pub_eddsa = eddsaGetPublic(decodeCrock(priv_eddsa)); + const bseed1 = deriveBSeed(decodeCrock(priv_eddsa), rPub); + const bseed2 = deriveBSeed(decodeCrock(priv_eddsa), rPub); + t.deepEqual(bseed1, bseed2); + + /** + * In this scenario the nonce from the test vectors is used as b-seed and refresh. + * This is only used in testing to test functionality. + * DO NOT USE the same values for blinding-seed and nonce anywhere else. + * + * Tests whether the blinding secrets are derived as in the exchange implementation + */ + const bseed = decodeCrock(nonce); + const secrets = deriveSecrets(bseed); + t.deepEqual( + secrets.alpha[0], + decodeCrock("R06FWJ4XEK4JKKKA03JARGD0PD5JAX8DK2N6J0K8CAZZMVQEJ1T0"), + ); + t.deepEqual( + secrets.alpha[1], + decodeCrock("13NXE2FEHJS0Q5XCWNRF4V1NC3BSAHN6BW02WZ07PG6967156HYG"), + ); + t.deepEqual( + secrets.beta[0], + decodeCrock("T3EZP42RJQXRTJ4FTDWF18Z422VX7KFGN8GJ3QCCM1QV3N456HD0"), + ); + t.deepEqual( + secrets.beta[1], + decodeCrock("P3MECYGCCR58QVEDSW443699CDXVT8C8W5ZT22PPNRJ363M72H6G"), + ); + + const rBlind = await calcRBlind(pub, secrets, rPub); + t.deepEqual( + rBlind[0], + decodeCrock("CHK7JC4SXZ4Y9RDA3881S82F7BP99H35Q361WR6RBXN5YN2ZM1M0"), + ); + t.deepEqual( + rBlind[1], + decodeCrock("4C65R74GA9PPDX4DC2B948W96T3Z6QEENK2NDJQPNB9QBTKCT590"), + ); + + const c = await csBlind(bseed, rPub, pub, decodeCrock(msg_hash)); + t.deepEqual( + c[0], + decodeCrock("F288QXT67TR36E6DHE399G8J24RM6C3DP16HGMH74B6WZ1DETR10"), + ); + t.deepEqual( + c[1], + decodeCrock("EFK5WTN01NCVS3DZCG20MQDHRHBATRG8589BA0XSZDZ6D0HFR470"), + ); + + const lMod = Array.from( + new Uint8Array([ + 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x14, 0xde, 0xf9, 0xde, 0xa2, 0xf7, 0x9c, 0xd6, + 0x58, 0x12, 0x63, 0x1a, 0x5c, 0xf5, 0xd3, 0xed, + ]), + ); + const L = bigint.fromArray(lMod, 256, false).toString(); + //Lmod needs to be 2^252+27742317777372353535851937790883648493 + if (!L.startsWith("723700")) { + throw new AssertionError({ message: L }); + } + + const b = 0; + const blindsig: CsBlindSignature = { + sBlind: decodeCrock("6KZF904YZA8KK4C8X5JV57E7B84SR8TDDN9GDC8QTRRSNTHJTM4G"), + rPubBlind: rPub[b], + }; + + const sig = await csUnblind(bseed, rPub, pub, b, blindsig); + t.deepEqual( + sig.s, + decodeCrock("F4ZKMFW3Q7DFN0N94KAMG2JFFHAC362T0QZ6ZCVZ73RS8P91CR70"), + ); + t.deepEqual( + sig.rPub, + decodeCrock("CHK7JC4SXZ4Y9RDA3881S82F7BP99H35Q361WR6RBXN5YN2ZM1M0"), + ); + + const res = await csVerify(decodeCrock(msg_hash), sig, pub); + t.deepEqual(res, true); +}); + +test("bigint/nacl conversion", async (t) => { + const b1 = BigInteger(42); + const n1 = bigintToNaclArr(b1, 32); + t.is(n1[0], 42); + t.is(n1.length, 32); + const b2 = bigintFromNaclArr(n1); + t.true(b1.eq(b2)); +}); + +test("taler age restriction crypto", async (t) => { + const priv1 = await Edx25519.keyCreate(); + const pub1 = await Edx25519.getPublic(priv1); + + const seed = getRandomBytes(32); + + const priv2 = await Edx25519.privateKeyDerive(priv1, seed); + const pub2 = await Edx25519.publicKeyDerive(pub1, seed); + + const pub2Ref = await Edx25519.getPublic(priv2); + + t.deepEqual(pub2, pub2Ref); +}); + +test("edx signing", async (t) => { + const priv1 = await Edx25519.keyCreate(); + const pub1 = await Edx25519.getPublic(priv1); + + const msg = stringToBytes("hello world"); + + const sig = nacl.crypto_edx25519_sign_detached(msg, priv1, pub1); + + t.true(nacl.crypto_edx25519_sign_detached_verify(msg, sig, pub1)); + + sig[0]++; + + t.false(nacl.crypto_edx25519_sign_detached_verify(msg, sig, pub1)); +}); + +test("edx test vector", async (t) => { + // Generated by gnunet-crypto-tvg + const tv = { + operation: "edx25519_derive", + priv1_edx: + "P0JAQ53G66M7TSGQTCFVFMPCBC7WHBRYDZGQXM8VD88C72NJANR07V1DQRAE7KSH92HZ3B62PJVRYFTVFTQM43K5AQD8R4A7HWJ3P7G", + pub1_edx: "4YZ6D5MGWTWCTKY4W931V4S5SW0XG7AD4A60J2Z9CSEB9WE05WB0", + seed: "SQ3YAVGNZ2GYER9VQAJB2M1Z903Y458HYXWBSF9S2A9YKF85R4DHYJX35YXXX82CBGFW2TRBCR1ZCWSQ7A87QW5SHC8WP9JH48P8KK8", + priv2_edx: + "GQ7NCSVNKY0QS7GQVFP2TSG6P4YN1NCK303K5TYXXBKSZ61M3R4XFZ0KA42JND6GBZRXRSJY9EX3HMMY160VQ6Y6H2NZ8H0WVQRCG1R", + pub2_edx: "F5X6379F0FSY87MN9210FAN84PR8KYDJQ5G5784H1N3FY12ZKAPG", + }; + + { + const pub1Prime = await Edx25519.getPublic(decodeCrock(tv.priv1_edx)); + t.deepEqual(pub1Prime, decodeCrock(tv.pub1_edx)); + } + + const pub2Prime = await Edx25519.publicKeyDerive( + decodeCrock(tv.pub1_edx), + decodeCrock(tv.seed), + ); + t.deepEqual(pub2Prime, decodeCrock(tv.pub2_edx)); + + const priv2Prime = await Edx25519.privateKeyDerive( + decodeCrock(tv.priv1_edx), + decodeCrock(tv.seed), + ); + t.deepEqual(priv2Prime, decodeCrock(tv.priv2_edx)); +}); diff --git a/packages/taler-util/src/taler-crypto.ts b/packages/taler-util/src/taler-crypto.ts new file mode 100644 index 000000000..d7e9a0c06 --- /dev/null +++ b/packages/taler-util/src/taler-crypto.ts @@ -0,0 +1,1378 @@ +/* + This file is part of GNU Taler + (C) 2019 GNUnet e.V. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see + */ + +/** + * Native implementation of GNU Taler crypto. + */ + +/** + * Imports. + */ +import * as nacl from "./nacl-fast.js"; +import { kdf, kdfKw } from "./kdf.js"; +import bigint from "big-integer"; +import { + CoinEnvelope, + CoinPublicKeyString, + DenominationPubKey, + DenomKeyType, + HashCodeString, +} from "./taler-types.js"; +import { Logger } from "./logging.js"; +import { secretbox } from "./nacl-fast.js"; +import * as fflate from "fflate"; +import { canonicalJson } from "./helpers.js"; + +export type Flavor = T & { + _flavor?: `taler.${FlavorT}`; +}; + +export type FlavorP = T & { + _flavor?: `taler.${FlavorT}`; + _size?: S; +}; + +export function getRandomBytes(n: number): Uint8Array { + return nacl.randomBytes(n); +} + +export function getRandomBytesF( + n: T, +): FlavorP { + return nacl.randomBytes(n); +} + +const encTable = "0123456789ABCDEFGHJKMNPQRSTVWXYZ"; + +class EncodingError extends Error { + constructor() { + super("Encoding error"); + Object.setPrototypeOf(this, EncodingError.prototype); + } +} + +function getValue(chr: string): number { + let a = chr; + switch (chr) { + case "O": + case "o": + a = "0;"; + break; + case "i": + case "I": + case "l": + case "L": + a = "1"; + break; + case "u": + case "U": + a = "V"; + } + + if (a >= "0" && a <= "9") { + return a.charCodeAt(0) - "0".charCodeAt(0); + } + + if (a >= "a" && a <= "z") a = a.toUpperCase(); + let dec = 0; + if (a >= "A" && a <= "Z") { + if ("I" < a) dec++; + if ("L" < a) dec++; + if ("O" < a) dec++; + if ("U" < a) dec++; + return a.charCodeAt(0) - "A".charCodeAt(0) + 10 - dec; + } + throw new EncodingError(); +} + +export function encodeCrock(data: ArrayBuffer): string { + const dataBytes = new Uint8Array(data); + let sb = ""; + const size = data.byteLength; + let bitBuf = 0; + let numBits = 0; + let pos = 0; + while (pos < size || numBits > 0) { + if (pos < size && numBits < 5) { + const d = dataBytes[pos++]; + bitBuf = (bitBuf << 8) | d; + numBits += 8; + } + if (numBits < 5) { + // zero-padding + bitBuf = bitBuf << (5 - numBits); + numBits = 5; + } + const v = (bitBuf >>> (numBits - 5)) & 31; + sb += encTable[v]; + numBits -= 5; + } + return sb; +} + +export function decodeCrock(encoded: string): Uint8Array { + const size = encoded.length; + let bitpos = 0; + let bitbuf = 0; + let readPosition = 0; + const outLen = Math.floor((size * 5) / 8); + const out = new Uint8Array(outLen); + let outPos = 0; + + while (readPosition < size || bitpos > 0) { + if (readPosition < size) { + const v = getValue(encoded[readPosition++]); + bitbuf = (bitbuf << 5) | v; + bitpos += 5; + } + while (bitpos >= 8) { + const d = (bitbuf >>> (bitpos - 8)) & 0xff; + out[outPos++] = d; + bitpos -= 8; + } + if (readPosition == size && bitpos > 0) { + bitbuf = (bitbuf << (8 - bitpos)) & 0xff; + bitpos = bitbuf == 0 ? 0 : 8; + } + } + return out; +} + +export function eddsaGetPublic(eddsaPriv: Uint8Array): Uint8Array { + const pair = nacl.crypto_sign_keyPair_fromSeed(eddsaPriv); + return pair.publicKey; +} + +export function ecdheGetPublic(ecdhePriv: Uint8Array): Uint8Array { + return nacl.scalarMult_base(ecdhePriv); +} + +export function keyExchangeEddsaEcdhe( + eddsaPriv: Uint8Array, + ecdhePub: Uint8Array, +): Uint8Array { + const ph = nacl.hash(eddsaPriv); + const a = new Uint8Array(32); + for (let i = 0; i < 32; i++) { + a[i] = ph[i]; + } + const x = nacl.scalarMult(a, ecdhePub); + return nacl.hash(x); +} + +export function keyExchangeEcdheEddsa( + ecdhePriv: Uint8Array & MaterialEcdhePriv, + eddsaPub: Uint8Array & MaterialEddsaPub, +): Uint8Array { + const curve25519Pub = nacl.sign_ed25519_pk_to_curve25519(eddsaPub); + const x = nacl.scalarMult(ecdhePriv, curve25519Pub); + return nacl.hash(x); +} + +interface RsaPub { + N: bigint.BigInteger; + e: bigint.BigInteger; +} + +/** + * KDF modulo a big integer. + */ +function kdfMod( + n: bigint.BigInteger, + ikm: Uint8Array, + salt: Uint8Array, + info: Uint8Array, +): bigint.BigInteger { + const nbits = n.bitLength().toJSNumber(); + const buflen = Math.floor((nbits - 1) / 8 + 1); + const mask = (1 << (8 - (buflen * 8 - nbits))) - 1; + let counter = 0; + while (true) { + const ctx = new Uint8Array(info.byteLength + 2); + ctx.set(info, 0); + ctx[ctx.length - 2] = (counter >>> 8) & 0xff; + ctx[ctx.length - 1] = counter & 0xff; + const buf = kdf(buflen, ikm, salt, ctx); + const arr = Array.from(buf); + arr[0] = arr[0] & mask; + const r = bigint.fromArray(arr, 256, false); + if (r.lt(n)) { + return r; + } + counter++; + } +} + +function csKdfMod( + n: bigint.BigInteger, + ikm: Uint8Array, + salt: Uint8Array, + info: Uint8Array, +): Uint8Array { + const nbits = n.bitLength().toJSNumber(); + const buflen = Math.floor((nbits - 1) / 8 + 1); + const mask = (1 << (8 - (buflen * 8 - nbits))) - 1; + let counter = 0; + while (true) { + const ctx = new Uint8Array(info.byteLength + 2); + ctx.set(info, 0); + ctx[ctx.length - 2] = (counter >>> 8) & 0xff; + ctx[ctx.length - 1] = counter & 0xff; + const buf = kdf(buflen, ikm, salt, ctx); + const arr = Array.from(buf); + arr[0] = arr[0] & mask; + const r = bigint.fromArray(arr, 256, false); + if (r.lt(n)) { + return new Uint8Array(arr); + } + counter++; + } +} + +// Newer versions of node have TextEncoder and TextDecoder as a global, +// just like modern browsers. +// In older versions of node or environments that do not have these +// globals, they must be polyfilled (by adding them to globa/globalThis) +// before stringToBytes or bytesToString is called the first time. + +let encoder: any; +let decoder: any; + +export function stringToBytes(s: string): Uint8Array { + if (!encoder) { + // @ts-ignore + encoder = new TextEncoder(); + } + return encoder.encode(s); +} + +export function bytesToString(b: Uint8Array): string { + if (!decoder) { + // @ts-ignore + decoder = new TextDecoder(); + } + return decoder.decode(b); +} + +function loadBigInt(arr: Uint8Array): bigint.BigInteger { + return bigint.fromArray(Array.from(arr), 256, false); +} + +function rsaBlindingKeyDerive( + rsaPub: RsaPub, + bks: Uint8Array, +): bigint.BigInteger { + const salt = stringToBytes("Blinding KDF extractor HMAC key"); + const info = stringToBytes("Blinding KDF"); + return kdfMod(rsaPub.N, bks, salt, info); +} + +/* + * Test for malicious RSA key. + * + * Assuming n is an RSA modulous and r is generated using a call to + * GNUNET_CRYPTO_kdf_mod_mpi, if gcd(r,n) != 1 then n must be a + * malicious RSA key designed to deanomize the user. + * + * @param r KDF result + * @param n RSA modulus of the public key + */ +function rsaGcdValidate(r: bigint.BigInteger, n: bigint.BigInteger): void { + const t = bigint.gcd(r, n); + if (!t.equals(bigint.one)) { + throw Error("malicious RSA public key"); + } +} + +function rsaFullDomainHash(hm: Uint8Array, rsaPub: RsaPub): bigint.BigInteger { + const info = stringToBytes("RSA-FDA FTpsW!"); + const salt = rsaPubEncode(rsaPub); + const r = kdfMod(rsaPub.N, hm, salt, info); + rsaGcdValidate(r, rsaPub.N); + return r; +} + +function rsaPubDecode(rsaPub: Uint8Array): RsaPub { + const modulusLength = (rsaPub[0] << 8) | rsaPub[1]; + const exponentLength = (rsaPub[2] << 8) | rsaPub[3]; + if (4 + exponentLength + modulusLength != rsaPub.length) { + throw Error("invalid RSA public key (format wrong)"); + } + const modulus = rsaPub.slice(4, 4 + modulusLength); + const exponent = rsaPub.slice( + 4 + modulusLength, + 4 + modulusLength + exponentLength, + ); + const res = { + N: loadBigInt(modulus), + e: loadBigInt(exponent), + }; + return res; +} + +function rsaPubEncode(rsaPub: RsaPub): Uint8Array { + const mb = rsaPub.N.toArray(256).value; + const eb = rsaPub.e.toArray(256).value; + const out = new Uint8Array(4 + mb.length + eb.length); + out[0] = (mb.length >>> 8) & 0xff; + out[1] = mb.length & 0xff; + out[2] = (eb.length >>> 8) & 0xff; + out[3] = eb.length & 0xff; + out.set(mb, 4); + out.set(eb, 4 + mb.length); + return out; +} + +export function rsaBlind( + hm: Uint8Array, + bks: Uint8Array, + rsaPubEnc: Uint8Array, +): Uint8Array { + const rsaPub = rsaPubDecode(rsaPubEnc); + const data = rsaFullDomainHash(hm, rsaPub); + const r = rsaBlindingKeyDerive(rsaPub, bks); + const r_e = r.modPow(rsaPub.e, rsaPub.N); + const bm = r_e.multiply(data).mod(rsaPub.N); + return new Uint8Array(bm.toArray(256).value); +} + +export function rsaUnblind( + sig: Uint8Array, + rsaPubEnc: Uint8Array, + bks: Uint8Array, +): Uint8Array { + const rsaPub = rsaPubDecode(rsaPubEnc); + const blinded_s = loadBigInt(sig); + const r = rsaBlindingKeyDerive(rsaPub, bks); + const r_inv = r.modInv(rsaPub.N); + const s = blinded_s.multiply(r_inv).mod(rsaPub.N); + return new Uint8Array(s.toArray(256).value); +} + +export function rsaVerify( + hm: Uint8Array, + rsaSig: Uint8Array, + rsaPubEnc: Uint8Array, +): boolean { + const rsaPub = rsaPubDecode(rsaPubEnc); + const d = rsaFullDomainHash(hm, rsaPub); + const sig = loadBigInt(rsaSig); + const sig_e = sig.modPow(rsaPub.e, rsaPub.N); + return sig_e.equals(d); +} + +export type CsSignature = { + s: Uint8Array; + rPub: Uint8Array; +}; + +export type CsBlindSignature = { + sBlind: Uint8Array; + rPubBlind: Uint8Array; +}; + +export type CsBlindingSecrets = { + alpha: [Uint8Array, Uint8Array]; + beta: [Uint8Array, Uint8Array]; +}; + +export function typedArrayConcat(chunks: Uint8Array[]): Uint8Array { + let payloadLen = 0; + for (const c of chunks) { + payloadLen += c.byteLength; + } + const buf = new ArrayBuffer(payloadLen); + const u8buf = new Uint8Array(buf); + let p = 0; + for (const c of chunks) { + u8buf.set(c, p); + p += c.byteLength; + } + return u8buf; +} + +/** + * Map to scalar subgroup function + * perform clamping as described in RFC7748 + * @param scalar + */ +function mtoSS(scalar: Uint8Array): Uint8Array { + scalar[0] &= 248; + scalar[31] &= 127; + scalar[31] |= 64; + return scalar; +} + +/** + * The function returns the CS blinding secrets from a seed + * @param bseed seed to derive blinding secrets + * @returns blinding secrets + */ +export function deriveSecrets(bseed: Uint8Array): CsBlindingSecrets { + const outLen = 130; + const salt = stringToBytes("alphabeta"); + const rndout = kdf(outLen, bseed, salt); + const secrets: CsBlindingSecrets = { + alpha: [mtoSS(rndout.slice(0, 32)), mtoSS(rndout.slice(64, 96))], + beta: [mtoSS(rndout.slice(32, 64)), mtoSS(rndout.slice(96, 128))], + }; + return secrets; +} + +/** + * Used for testing, simple scalar multiplication with base point of Ed25519 + * @param s scalar + * @returns new point sG + */ +export async function scalarMultBase25519(s: Uint8Array): Promise { + return nacl.crypto_scalarmult_ed25519_base_noclamp(s); +} + +/** + * calculation of the blinded public point R in CS + * @param csPub denomination publik key + * @param secrets client blinding secrets + * @param rPub public R received from /csr API + */ +export async function calcRBlind( + csPub: Uint8Array, + secrets: CsBlindingSecrets, + rPub: [Uint8Array, Uint8Array], +): Promise<[Uint8Array, Uint8Array]> { + const aG0 = nacl.crypto_scalarmult_ed25519_base_noclamp(secrets.alpha[0]); + const aG1 = nacl.crypto_scalarmult_ed25519_base_noclamp(secrets.alpha[1]); + + const bDp0 = nacl.crypto_scalarmult_ed25519_noclamp(secrets.beta[0], csPub); + const bDp1 = nacl.crypto_scalarmult_ed25519_noclamp(secrets.beta[1], csPub); + + const res0 = nacl.crypto_core_ed25519_add(aG0, bDp0); + const res1 = nacl.crypto_core_ed25519_add(aG1, bDp1); + return [ + nacl.crypto_core_ed25519_add(rPub[0], res0), + nacl.crypto_core_ed25519_add(rPub[1], res1), + ]; +} + +/** + * FDH function used in CS + * @param hm message hash + * @param rPub public R included in FDH + * @param csPub denomination public key as context + * @returns mapped Curve25519 scalar + */ +function csFDH( + hm: Uint8Array, + rPub: Uint8Array, + csPub: Uint8Array, +): Uint8Array { + const lMod = Array.from( + new Uint8Array([ + 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x14, 0xde, 0xf9, 0xde, 0xa2, 0xf7, 0x9c, 0xd6, + 0x58, 0x12, 0x63, 0x1a, 0x5c, 0xf5, 0xd3, 0xed, + ]), + ); + const L = bigint.fromArray(lMod, 256, false); + + const info = stringToBytes("Curve25519FDH"); + const preshash = nacl.hash(typedArrayConcat([rPub, hm])); + return csKdfMod(L, preshash, csPub, info).reverse(); +} + +/** + * blinding seed derived from coin private key + * @param coinPriv private key of the corresponding coin + * @param rPub public R received from /csr API + * @returns blinding seed + */ +export function deriveBSeed( + coinPriv: Uint8Array, + rPub: [Uint8Array, Uint8Array], +): Uint8Array { + const outLen = 32; + const salt = stringToBytes("b-seed"); + const ikm = typedArrayConcat([coinPriv, rPub[0], rPub[1]]); + return kdf(outLen, ikm, salt); +} + +/** + * Derive withdraw nonce, used in /csr request + * Note: In withdraw protocol, the nonce is chosen randomly + * @param coinPriv coin private key + * @returns nonce + */ +export function deriveWithdrawNonce(coinPriv: Uint8Array): Uint8Array { + const outLen = 32; + const salt = stringToBytes("n"); + return kdf(outLen, coinPriv, salt); +} + +/** + * Blind operation for CS signatures, used after /csr call + * @param bseed blinding seed to derive blinding secrets + * @param rPub public R received from /csr + * @param csPub denomination public key + * @param hm message to blind + * @returns two blinded c + */ +export async function csBlind( + bseed: Uint8Array, + rPub: [Uint8Array, Uint8Array], + csPub: Uint8Array, + hm: Uint8Array, +): Promise<[Uint8Array, Uint8Array]> { + const secrets = deriveSecrets(bseed); + const rPubBlind = await calcRBlind(csPub, secrets, rPub); + const c_0 = csFDH(hm, rPubBlind[0], csPub); + const c_1 = csFDH(hm, rPubBlind[1], csPub); + return [ + nacl.crypto_core_ed25519_scalar_add(c_0, secrets.beta[0]), + nacl.crypto_core_ed25519_scalar_add(c_1, secrets.beta[1]), + ]; +} + +/** + * Unblind operation to unblind the signature + * @param bseed seed to derive secrets + * @param rPub public R received from /csr + * @param csPub denomination publick key + * @param b returned from exchange to select c + * @param csSig blinded signature + * @returns unblinded signature + */ +export async function csUnblind( + bseed: Uint8Array, + rPub: [Uint8Array, Uint8Array], + csPub: Uint8Array, + b: number, + csSig: CsBlindSignature, +): Promise { + if (b != 0 && b != 1) { + throw new Error(); + } + const secrets = deriveSecrets(bseed); + const rPubDash = (await calcRBlind(csPub, secrets, rPub))[b]; + const sig: CsSignature = { + s: nacl.crypto_core_ed25519_scalar_add(csSig.sBlind, secrets.alpha[b]), + rPub: rPubDash, + }; + return sig; +} + +/** + * Verification algorithm for CS signatures + * @param hm message signed + * @param csSig unblinded signature + * @param csPub denomination publick key + * @returns true if valid, false if invalid + */ +export async function csVerify( + hm: Uint8Array, + csSig: CsSignature, + csPub: Uint8Array, +): Promise { + const cDash = csFDH(hm, csSig.rPub, csPub); + const sG = nacl.crypto_scalarmult_ed25519_base_noclamp(csSig.s); + const cbDp = nacl.crypto_scalarmult_ed25519_noclamp(cDash, csPub); + const sGeq = nacl.crypto_core_ed25519_add(csSig.rPub, cbDp); + return nacl.verify(sG, sGeq); +} + +export interface EddsaKeyPair { + eddsaPub: Uint8Array; + eddsaPriv: Uint8Array; +} + +export interface EcdheKeyPair { + ecdhePub: Uint8Array; + ecdhePriv: Uint8Array; +} + +export interface Edx25519Keypair { + edxPub: string; + edxPriv: string; +} + +export function createEddsaKeyPair(): EddsaKeyPair { + const eddsaPriv = nacl.randomBytes(32); + const eddsaPub = eddsaGetPublic(eddsaPriv); + return { eddsaPriv, eddsaPub }; +} + +export function createEcdheKeyPair(): EcdheKeyPair { + const ecdhePriv = nacl.randomBytes(32); + const ecdhePub = ecdheGetPublic(ecdhePriv); + return { ecdhePriv, ecdhePub }; +} + +export function hash(d: Uint8Array): Uint8Array { + return nacl.hash(d); +} + +/** + * Hash the input with SHA-512 and truncate the result + * to 32 bytes. + */ +export function hashTruncate32(d: Uint8Array): Uint8Array { + const sha512HashCode = nacl.hash(d); + return sha512HashCode.subarray(0, 32); +} + +export function hashCoinEv( + coinEv: CoinEnvelope, + denomPubHash: HashCodeString, +): Uint8Array { + const hashContext = createHashContext(); + hashContext.update(decodeCrock(denomPubHash)); + hashCoinEvInner(coinEv, hashContext); + return hashContext.finish(); +} + +const logger = new Logger("talerCrypto.ts"); + +export function hashCoinEvInner( + coinEv: CoinEnvelope, + hashState: nacl.HashState, +): void { + const hashInputBuf = new ArrayBuffer(4); + const uint8ArrayBuf = new Uint8Array(hashInputBuf); + const dv = new DataView(hashInputBuf); + dv.setUint32(0, DenomKeyType.toIntTag(coinEv.cipher)); + hashState.update(uint8ArrayBuf); + switch (coinEv.cipher) { + case DenomKeyType.Rsa: + hashState.update(decodeCrock(coinEv.rsa_blinded_planchet)); + return; + default: + throw new Error(); + } +} + +export function hashCoinPub( + coinPub: CoinPublicKeyString, + ach?: HashCodeString, +): Uint8Array { + if (!ach) { + return hash(decodeCrock(coinPub)); + } + + return hash(typedArrayConcat([decodeCrock(coinPub), decodeCrock(ach)])); +} + +/** + * Hash a denomination public key. + */ +export function hashDenomPub(pub: DenominationPubKey): Uint8Array { + if (pub.cipher === DenomKeyType.Rsa) { + const pubBuf = decodeCrock(pub.rsa_public_key); + const hashInputBuf = new ArrayBuffer(pubBuf.length + 4 + 4); + const uint8ArrayBuf = new Uint8Array(hashInputBuf); + const dv = new DataView(hashInputBuf); + dv.setUint32(0, pub.age_mask ?? 0); + dv.setUint32(4, DenomKeyType.toIntTag(pub.cipher)); + uint8ArrayBuf.set(pubBuf, 8); + return nacl.hash(uint8ArrayBuf); + } else if (pub.cipher === DenomKeyType.ClauseSchnorr) { + const pubBuf = decodeCrock(pub.cs_public_key); + const hashInputBuf = new ArrayBuffer(pubBuf.length + 4 + 4); + const uint8ArrayBuf = new Uint8Array(hashInputBuf); + const dv = new DataView(hashInputBuf); + dv.setUint32(0, pub.age_mask ?? 0); + dv.setUint32(4, DenomKeyType.toIntTag(pub.cipher)); + uint8ArrayBuf.set(pubBuf, 8); + return nacl.hash(uint8ArrayBuf); + } else { + throw Error( + `unsupported cipher (${ + (pub as DenominationPubKey).cipher + }), unable to hash`, + ); + } +} + +export function eddsaSign(msg: Uint8Array, eddsaPriv: Uint8Array): Uint8Array { + const pair = nacl.crypto_sign_keyPair_fromSeed(eddsaPriv); + return nacl.sign_detached(msg, pair.secretKey); +} + +export function eddsaVerify( + msg: Uint8Array, + sig: Uint8Array, + eddsaPub: Uint8Array, +): boolean { + return nacl.sign_detached_verify(msg, sig, eddsaPub); +} + +export function createHashContext(): nacl.HashState { + return new nacl.HashState(); +} + +export interface FreshCoin { + coinPub: Uint8Array; + coinPriv: Uint8Array; + bks: Uint8Array; + maxAge: number; + ageCommitmentProof: AgeCommitmentProof | undefined; +} + +export function bufferForUint32(n: number): Uint8Array { + const arrBuf = new ArrayBuffer(4); + const buf = new Uint8Array(arrBuf); + const dv = new DataView(arrBuf); + dv.setUint32(0, n); + return buf; +} + +export function bufferForUint8(n: number): Uint8Array { + const arrBuf = new ArrayBuffer(1); + const buf = new Uint8Array(arrBuf); + const dv = new DataView(arrBuf); + dv.setUint8(0, n); + return buf; +} + +export async function setupTipPlanchet( + secretSeed: Uint8Array, + denomPub: DenominationPubKey, + coinNumber: number, +): Promise { + const info = stringToBytes("taler-tip-coin-derivation"); + const saltArrBuf = new ArrayBuffer(4); + const salt = new Uint8Array(saltArrBuf); + const saltDataView = new DataView(saltArrBuf); + saltDataView.setUint32(0, coinNumber); + const out = kdf(64, secretSeed, salt, info); + const coinPriv = out.slice(0, 32); + const bks = out.slice(32, 64); + let maybeAcp: AgeCommitmentProof | undefined; + if (denomPub.age_mask != 0) { + maybeAcp = await AgeRestriction.restrictionCommitSeeded( + denomPub.age_mask, + AgeRestriction.AGE_UNRESTRICTED, + secretSeed, + ); + } + return { + bks, + coinPriv, + coinPub: eddsaGetPublic(coinPriv), + maxAge: AgeRestriction.AGE_UNRESTRICTED, + ageCommitmentProof: maybeAcp, + }; +} +/** + * + * @param paytoUri + * @param salt 16-byte salt + * @returns + */ +export function hashWire(paytoUri: string, salt: string): string { + const r = kdf( + 64, + stringToBytes(paytoUri + "\0"), + decodeCrock(salt), + stringToBytes("merchant-wire-signature"), + ); + return encodeCrock(r); +} + +export enum TalerSignaturePurpose { + MERCHANT_TRACK_TRANSACTION = 1103, + WALLET_RESERVE_WITHDRAW = 1200, + WALLET_COIN_DEPOSIT = 1201, + GLOBAL_FEES = 1022, + MASTER_DENOMINATION_KEY_VALIDITY = 1025, + MASTER_WIRE_FEES = 1028, + MASTER_WIRE_DETAILS = 1030, + WALLET_COIN_MELT = 1202, + TEST = 4242, + MERCHANT_PAYMENT_OK = 1104, + MERCHANT_CONTRACT = 1101, + WALLET_COIN_RECOUP = 1203, + WALLET_COIN_LINK = 1204, + WALLET_COIN_RECOUP_REFRESH = 1206, + WALLET_AGE_ATTESTATION = 1207, + WALLET_PURSE_CREATE = 1210, + WALLET_PURSE_DEPOSIT = 1211, + WALLET_PURSE_MERGE = 1213, + WALLET_ACCOUNT_MERGE = 1214, + WALLET_PURSE_ECONTRACT = 1216, + EXCHANGE_CONFIRM_RECOUP = 1039, + EXCHANGE_CONFIRM_RECOUP_REFRESH = 1041, + ANASTASIS_POLICY_UPLOAD = 1400, + ANASTASIS_POLICY_DOWNLOAD = 1401, + SYNC_BACKUP_UPLOAD = 1450, +} + +export const enum WalletAccountMergeFlags { + /** + * Not a legal mode! + */ + None = 0, + + /** + * We are merging a fully paid-up purse into a reserve. + */ + MergeFullyPaidPurse = 1, + + CreateFromPurseQuota = 2, + + CreateWithPurseFee = 3, +} + +export class SignaturePurposeBuilder { + private chunks: Uint8Array[] = []; + + constructor(private purposeNum: number) {} + + put(bytes: Uint8Array): SignaturePurposeBuilder { + this.chunks.push(Uint8Array.from(bytes)); + return this; + } + + build(): Uint8Array { + let payloadLen = 0; + for (const c of this.chunks) { + payloadLen += c.byteLength; + } + const buf = new ArrayBuffer(4 + 4 + payloadLen); + const u8buf = new Uint8Array(buf); + let p = 8; + for (const c of this.chunks) { + u8buf.set(c, p); + p += c.byteLength; + } + const dvbuf = new DataView(buf); + dvbuf.setUint32(0, payloadLen + 4 + 4); + dvbuf.setUint32(4, this.purposeNum); + return u8buf; + } +} + +export function buildSigPS(purposeNum: number): SignaturePurposeBuilder { + return new SignaturePurposeBuilder(purposeNum); +} + +export type OpaqueData = Flavor; +export type Edx25519PublicKey = FlavorP; +export type Edx25519PrivateKey = FlavorP; +export type Edx25519Signature = FlavorP; + +export type Edx25519PublicKeyEnc = FlavorP; +export type Edx25519PrivateKeyEnc = FlavorP< + string, + "Edx25519PrivateKeyEnc", + 64 +>; + +/** + * Convert a big integer to a fixed-size, little-endian array. + */ +export function bigintToNaclArr( + x: bigint.BigInteger, + size: number, +): Uint8Array { + const byteArr = new Uint8Array(size); + const arr = x.toArray(256).value.reverse(); + byteArr.set(arr, 0); + return byteArr; +} + +export function bigintFromNaclArr(arr: Uint8Array): bigint.BigInteger { + let rev = new Uint8Array(arr); + rev = rev.reverse(); + return bigint.fromArray(Array.from(rev), 256, false); +} + +export namespace Edx25519 { + const revL = [ + 0xed, 0xd3, 0xf5, 0x5c, 0x1a, 0x63, 0x12, 0x58, 0xd6, 0x9c, 0xf7, 0xa2, + 0xde, 0xf9, 0xde, 0x14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x10, + ]; + + const L = bigint.fromArray(revL.reverse(), 256, false); + + export async function keyCreateFromSeed( + seed: OpaqueData, + ): Promise { + return nacl.crypto_edx25519_private_key_create_from_seed(seed); + } + + export async function keyCreate(): Promise { + return nacl.crypto_edx25519_private_key_create(); + } + + export async function getPublic( + priv: Edx25519PrivateKey, + ): Promise { + return nacl.crypto_edx25519_get_public(priv); + } + + export function sign( + msg: OpaqueData, + key: Edx25519PrivateKey, + ): Promise { + throw Error("not implemented"); + } + + async function deriveFactor( + pub: Edx25519PublicKey, + seed: OpaqueData, + ): Promise { + const res = kdfKw({ + outputLength: 64, + salt: seed, + ikm: pub, + info: stringToBytes("edx25519-derivation"), + }); + + return res; + } + + export async function privateKeyDerive( + priv: Edx25519PrivateKey, + seed: OpaqueData, + ): Promise { + const pub = await getPublic(priv); + const privDec = priv; + const a = bigintFromNaclArr(privDec.subarray(0, 32)); + const factorEnc = await deriveFactor(pub, seed); + const factorModL = bigintFromNaclArr(factorEnc).mod(L); + + const aPrime = a.divide(8).multiply(factorModL).mod(L).multiply(8).mod(L); + const bPrime = nacl + .hash(typedArrayConcat([privDec.subarray(32, 64), factorEnc])) + .subarray(0, 32); + + const newPriv = typedArrayConcat([bigintToNaclArr(aPrime, 32), bPrime]); + + return newPriv; + } + + export async function publicKeyDerive( + pub: Edx25519PublicKey, + seed: OpaqueData, + ): Promise { + const factorEnc = await deriveFactor(pub, seed); + const factorReduced = nacl.crypto_core_ed25519_scalar_reduce(factorEnc); + const res = nacl.crypto_scalarmult_ed25519_noclamp(factorReduced, pub); + return res; + } +} + +export interface AgeCommitment { + mask: number; + + /** + * Public keys, one for each age group specified in the age mask. + */ + publicKeys: Edx25519PublicKeyEnc[]; +} + +export interface AgeProof { + /** + * Private keys. Typically smaller than the number of public keys, + * because we drop private keys from age groups that are restricted. + */ + privateKeys: Edx25519PrivateKeyEnc[]; +} + +export interface AgeCommitmentProof { + commitment: AgeCommitment; + proof: AgeProof; +} + +function invariant(cond: boolean): asserts cond { + if (!cond) { + throw Error("invariant failed"); + } +} + +export namespace AgeRestriction { + /** + * Smallest age value that the protocol considers "unrestricted". + */ + export const AGE_UNRESTRICTED = 32; + + export function hashCommitment(ac: AgeCommitment): HashCodeString { + const hc = new nacl.HashState(); + for (const pub of ac.publicKeys) { + hc.update(decodeCrock(pub)); + } + return encodeCrock(hc.finish().subarray(0, 32)); + } + + export function countAgeGroups(mask: number): number { + let count = 0; + let m = mask; + while (m > 0) { + count += m & 1; + m = m >> 1; + } + return count; + } + + export function getAgeGroupIndex(mask: number, age: number): number { + invariant((mask & 1) === 1); + let i = 0; + let m = mask; + let a = age; + while (m > 0) { + if (a <= 0) { + break; + } + m = m >> 1; + i += m & 1; + a--; + } + return i; + } + + export function ageGroupSpecToMask(ageGroupSpec: string): number { + throw Error("not implemented"); + } + + export async function restrictionCommit( + ageMask: number, + age: number, + ): Promise { + invariant((ageMask & 1) === 1); + const numPubs = countAgeGroups(ageMask) - 1; + const numPrivs = getAgeGroupIndex(ageMask, age); + + const pubs: Edx25519PublicKey[] = []; + const privs: Edx25519PrivateKey[] = []; + + for (let i = 0; i < numPubs; i++) { + const priv = await Edx25519.keyCreate(); + const pub = await Edx25519.getPublic(priv); + pubs.push(pub); + if (i < numPrivs) { + privs.push(priv); + } + } + + return { + commitment: { + mask: ageMask, + publicKeys: pubs.map((x) => encodeCrock(x)), + }, + proof: { + privateKeys: privs.map((x) => encodeCrock(x)), + }, + }; + } + + export async function restrictionCommitSeeded( + ageMask: number, + age: number, + seed: Uint8Array, + ): Promise { + invariant((ageMask & 1) === 1); + const numPubs = countAgeGroups(ageMask) - 1; + const numPrivs = getAgeGroupIndex(ageMask, age); + + const pubs: Edx25519PublicKey[] = []; + const privs: Edx25519PrivateKey[] = []; + + for (let i = 0; i < numPubs; i++) { + const privSeed = await kdfKw({ + outputLength: 32, + ikm: seed, + info: stringToBytes("age-restriction-commit"), + salt: bufferForUint32(i), + }); + const priv = await Edx25519.keyCreateFromSeed(privSeed); + const pub = await Edx25519.getPublic(priv); + pubs.push(pub); + if (i < numPrivs) { + privs.push(priv); + } + } + + return { + commitment: { + mask: ageMask, + publicKeys: pubs.map((x) => encodeCrock(x)), + }, + proof: { + privateKeys: privs.map((x) => encodeCrock(x)), + }, + }; + } + + /** + * Check that c1 = c2*salt + */ + export async function commitCompare( + c1: AgeCommitment, + c2: AgeCommitment, + salt: OpaqueData, + ): Promise { + if (c1.publicKeys.length != c2.publicKeys.length) { + return false; + } + for (let i = 0; i < c1.publicKeys.length; i++) { + const k1 = decodeCrock(c1.publicKeys[i]); + const k2 = await Edx25519.publicKeyDerive( + decodeCrock(c2.publicKeys[i]), + salt, + ); + if (k1 != k2) { + return false; + } + } + return true; + } + + export async function commitmentDerive( + commitmentProof: AgeCommitmentProof, + salt: OpaqueData, + ): Promise { + const newPrivs: Edx25519PrivateKey[] = []; + const newPubs: Edx25519PublicKey[] = []; + + for (const oldPub of commitmentProof.commitment.publicKeys) { + newPubs.push(await Edx25519.publicKeyDerive(decodeCrock(oldPub), salt)); + } + + for (const oldPriv of commitmentProof.proof.privateKeys) { + newPrivs.push( + await Edx25519.privateKeyDerive(decodeCrock(oldPriv), salt), + ); + } + + return { + commitment: { + mask: commitmentProof.commitment.mask, + publicKeys: newPubs.map((x) => encodeCrock(x)), + }, + proof: { + privateKeys: newPrivs.map((x) => encodeCrock(x)), + }, + }; + } + + export function commitmentAttest( + commitmentProof: AgeCommitmentProof, + age: number, + ): Edx25519Signature { + const d = buildSigPS(TalerSignaturePurpose.WALLET_AGE_ATTESTATION) + .put(bufferForUint32(commitmentProof.commitment.mask)) + .put(bufferForUint32(age)) + .build(); + const group = getAgeGroupIndex(commitmentProof.commitment.mask, age); + if (group === 0) { + // No attestation required. + return new Uint8Array(64); + } + const priv = commitmentProof.proof.privateKeys[group - 1]; + const pub = commitmentProof.commitment.publicKeys[group - 1]; + const sig = nacl.crypto_edx25519_sign_detached( + d, + decodeCrock(priv), + decodeCrock(pub), + ); + return sig; + } + + export function commitmentVerify( + commitment: AgeCommitment, + sig: string, + age: number, + ): boolean { + const d = buildSigPS(TalerSignaturePurpose.WALLET_AGE_ATTESTATION) + .put(bufferForUint32(commitment.mask)) + .put(bufferForUint32(age)) + .build(); + const group = getAgeGroupIndex(commitment.mask, age); + if (group === 0) { + // No attestation required. + return true; + } + const pub = commitment.publicKeys[group - 1]; + return nacl.crypto_edx25519_sign_detached_verify( + d, + decodeCrock(sig), + decodeCrock(pub), + ); + } +} + +// FIXME: make it a branded type! +type EncryptionNonce = FlavorP; + +async function deriveKey( + keySeed: OpaqueData, + nonce: EncryptionNonce, + salt: string, +): Promise { + return kdfKw({ + outputLength: 32, + salt: nonce, + ikm: keySeed, + info: stringToBytes(salt), + }); +} + +async function encryptWithDerivedKey( + nonce: EncryptionNonce, + keySeed: OpaqueData, + plaintext: OpaqueData, + salt: string, +): Promise { + const key = await deriveKey(keySeed, nonce, salt); + const cipherText = secretbox(plaintext, nonce, key); + return typedArrayConcat([nonce, cipherText]); +} + +const nonceSize = 24; + +async function decryptWithDerivedKey( + ciphertext: OpaqueData, + keySeed: OpaqueData, + salt: string, +): Promise { + const ctBuf = ciphertext; + const nonceBuf = ctBuf.slice(0, nonceSize); + const enc = ctBuf.slice(nonceSize); + const key = await deriveKey(keySeed, nonceBuf, salt); + const clearText = nacl.secretbox_open(enc, nonceBuf, key); + if (!clearText) { + throw Error("could not decrypt"); + } + return clearText; +} + +enum ContractFormatTag { + PaymentOffer = 0, + PaymentRequest = 1, +} + +type MaterialEddsaPub = { + _materialType?: "eddsa-pub"; + _size?: 32; +}; + +type MaterialEddsaPriv = { + _materialType?: "ecdhe-priv"; + _size?: 32; +}; + +type MaterialEcdhePub = { + _materialType?: "ecdhe-pub"; + _size?: 32; +}; + +type MaterialEcdhePriv = { + _materialType?: "ecdhe-priv"; + _size?: 32; +}; + +type PursePublicKey = FlavorP & + MaterialEddsaPub; + +type ContractPrivateKey = FlavorP & + MaterialEcdhePriv; + +type MergePrivateKey = FlavorP & + MaterialEddsaPriv; + +const mergeSalt = "p2p-merge-contract"; +const depositSalt = "p2p-deposit-contract"; + +export function encryptContractForMerge( + pursePub: PursePublicKey, + contractPriv: ContractPrivateKey, + mergePriv: MergePrivateKey, + contractTerms: any, +): Promise { + const contractTermsCanon = canonicalJson(contractTerms) + "\0"; + const contractTermsBytes = stringToBytes(contractTermsCanon); + const contractTermsCompressed = fflate.zlibSync(contractTermsBytes); + const data = typedArrayConcat([ + bufferForUint32(ContractFormatTag.PaymentOffer), + bufferForUint32(contractTermsBytes.length), + mergePriv, + contractTermsCompressed, + ]); + const key = keyExchangeEcdheEddsa(contractPriv, pursePub); + return encryptWithDerivedKey(getRandomBytesF(24), key, data, mergeSalt); +} + +export function encryptContractForDeposit( + pursePub: PursePublicKey, + contractPriv: ContractPrivateKey, + contractTerms: any, +): Promise { + const contractTermsCanon = canonicalJson(contractTerms) + "\0"; + const contractTermsBytes = stringToBytes(contractTermsCanon); + const contractTermsCompressed = fflate.zlibSync(contractTermsBytes); + const data = typedArrayConcat([ + bufferForUint32(ContractFormatTag.PaymentRequest), + bufferForUint32(contractTermsBytes.length), + contractTermsCompressed, + ]); + const key = keyExchangeEcdheEddsa(contractPriv, pursePub); + return encryptWithDerivedKey(getRandomBytesF(24), key, data, depositSalt); +} + +export interface DecryptForMergeResult { + contractTerms: any; + mergePriv: Uint8Array; +} + +export interface DecryptForDepositResult { + contractTerms: any; +} + +export async function decryptContractForMerge( + enc: OpaqueData, + pursePub: PursePublicKey, + contractPriv: ContractPrivateKey, +): Promise { + const key = keyExchangeEcdheEddsa(contractPriv, pursePub); + const dec = await decryptWithDerivedKey(enc, key, mergeSalt); + const mergePriv = dec.slice(8, 8 + 32); + const contractTermsCompressed = dec.slice(8 + 32); + const contractTermsBuf = fflate.unzlibSync(contractTermsCompressed); + // Slice of the '\0' at the end and decode to a string + const contractTermsString = bytesToString( + contractTermsBuf.slice(0, contractTermsBuf.length - 1), + ); + return { + mergePriv: mergePriv, + contractTerms: JSON.parse(contractTermsString), + }; +} + +export async function decryptContractForDeposit( + enc: OpaqueData, + pursePub: PursePublicKey, + contractPriv: ContractPrivateKey, +): Promise { + const key = keyExchangeEcdheEddsa(contractPriv, pursePub); + const dec = await decryptWithDerivedKey(enc, key, depositSalt); + const contractTermsCompressed = dec.slice(8); + const contractTermsBuf = fflate.unzlibSync(contractTermsCompressed); + // Slice of the '\0' at the end and decode to a string + const contractTermsString = bytesToString( + contractTermsBuf.slice(0, contractTermsBuf.length - 1), + ); + return { + contractTerms: JSON.parse(contractTermsString), + }; +} diff --git a/packages/taler-util/src/taler-types.ts b/packages/taler-util/src/taler-types.ts new file mode 100644 index 000000000..de88fef69 --- /dev/null +++ b/packages/taler-util/src/taler-types.ts @@ -0,0 +1,2028 @@ +/* + This file is part of GNU Taler + (C) 2019 GNUnet e.V. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see + */ + +/** + * Type and schema definitions and helpers for the core GNU Taler protocol. + * + * Even though the rest of the wallet uses camelCase for fields, use snake_case + * here, since that's the convention for the Taler JSON+HTTP API. + */ + +/** + * Imports. + */ + +import { codecForAmountString } from "./amounts.js"; +import { + buildCodecForObject, + buildCodecForUnion, + Codec, + codecForAny, + codecForBoolean, + codecForConstNumber, + codecForConstString, + codecForList, + codecForMap, + codecForNumber, + codecForString, + codecOptional, +} from "./codec.js"; +import { strcmp } from "./helpers.js"; +import { AgeCommitmentProof, Edx25519PublicKeyEnc } from "./taler-crypto.js"; +import { + codecForAbsoluteTime, + codecForDuration, + codecForTimestamp, + TalerProtocolDuration, + TalerProtocolTimestamp, +} from "./time.js"; + +/** + * Denomination as found in the /keys response from the exchange. + */ +export class ExchangeDenomination { + /** + * Value of one coin of the denomination. + */ + value: string; + + /** + * Public signing key of the denomination. + */ + denom_pub: DenominationPubKey; + + /** + * Fee for withdrawing. + */ + fee_withdraw: string; + + /** + * Fee for depositing. + */ + fee_deposit: string; + + /** + * Fee for refreshing. + */ + fee_refresh: string; + + /** + * Fee for refunding. + */ + fee_refund: string; + + /** + * Start date from which withdraw is allowed. + */ + stamp_start: TalerProtocolTimestamp; + + /** + * End date for withdrawing. + */ + stamp_expire_withdraw: TalerProtocolTimestamp; + + /** + * Expiration date after which the exchange can forget about + * the currency. + */ + stamp_expire_legal: TalerProtocolTimestamp; + + /** + * Date after which the coins of this denomination can't be + * deposited anymore. + */ + stamp_expire_deposit: TalerProtocolTimestamp; + + /** + * Signature over the denomination information by the exchange's master + * signing key. + */ + master_sig: string; +} + +/** + * Signature by the auditor that a particular denomination key is audited. + */ +export class AuditorDenomSig { + /** + * Denomination public key's hash. + */ + denom_pub_h: string; + + /** + * The signature. + */ + auditor_sig: string; +} + +/** + * Auditor information as given by the exchange in /keys. + */ +export class ExchangeAuditor { + /** + * Auditor's public key. + */ + auditor_pub: string; + + /** + * Base URL of the auditor. + */ + auditor_url: string; + + /** + * List of signatures for denominations by the auditor. + */ + denomination_keys: AuditorDenomSig[]; +} + +export type ExchangeWithdrawValue = + | ExchangeRsaWithdrawValue + | ExchangeCsWithdrawValue; + +export interface ExchangeRsaWithdrawValue { + cipher: "RSA"; +} + +export interface ExchangeCsWithdrawValue { + cipher: "CS"; + + /** + * CSR R0 value + */ + r_pub_0: string; + + /** + * CSR R1 value + */ + r_pub_1: string; +} + +export interface RecoupRequest { + /** + * Hashed denomination public key of the coin we want to get + * paid back. + */ + denom_pub_hash: string; + + /** + * Signature over the coin public key by the denomination. + * + * The string variant is for the legacy exchange protocol. + */ + denom_sig: UnblindedSignature; + + /** + * Blinding key that was used during withdraw, + * used to prove that we were actually withdrawing the coin. + */ + coin_blind_key_secret: string; + + /** + * Signature of TALER_RecoupRequestPS created with the coin's private key. + */ + coin_sig: string; + + ewv: ExchangeWithdrawValue; +} + +export interface RecoupRefreshRequest { + /** + * Hashed enomination public key of the coin we want to get + * paid back. + */ + denom_pub_hash: string; + + /** + * Signature over the coin public key by the denomination. + * + * The string variant is for the legacy exchange protocol. + */ + denom_sig: UnblindedSignature; + + /** + * Coin's blinding factor. + */ + coin_blind_key_secret: string; + + /** + * Signature of TALER_RecoupRefreshRequestPS created with + * the coin's private key. + */ + coin_sig: string; + + ewv: ExchangeWithdrawValue; +} + +/** + * Response that we get from the exchange for a payback request. + */ +export interface RecoupConfirmation { + /** + * Public key of the reserve that will receive the payback. + */ + reserve_pub?: string; + + /** + * Public key of the old coin that will receive the recoup, + * provided if refreshed was true. + */ + old_coin_pub?: string; +} + +export type UnblindedSignature = RsaUnblindedSignature; + +export interface RsaUnblindedSignature { + cipher: DenomKeyType.Rsa; + rsa_signature: string; +} + +/** + * Deposit permission for a single coin. + */ +export interface CoinDepositPermission { + /** + * Signature by the coin. + */ + coin_sig: string; + + /** + * Public key of the coin being spend. + */ + coin_pub: string; + + /** + * Signature made by the denomination public key. + * + * The string variant is for legacy protocol support. + */ + + ub_sig: UnblindedSignature; + + /** + * The denomination public key associated with this coin. + */ + h_denom: string; + + /** + * The amount that is subtracted from this coin with this payment. + */ + contribution: string; + + /** + * URL of the exchange this coin was withdrawn from. + */ + exchange_url: string; + + minimum_age_sig?: EddsaSignatureString; + + age_commitment?: Edx25519PublicKeyEnc[]; +} + +/** + * Information about an exchange as stored inside a + * merchant's contract terms. + */ +export interface ExchangeHandle { + /** + * Master public signing key of the exchange. + */ + master_pub: string; + + /** + * Base URL of the exchange. + */ + url: string; +} + +export interface AuditorHandle { + /** + * Official name of the auditor. + */ + name: string; + + /** + * Master public signing key of the auditor. + */ + auditor_pub: string; + + /** + * Base URL of the auditor. + */ + url: string; +} + +// Delivery location, loosely modeled as a subset of +// ISO20022's PostalAddress25. +export interface Location { + // Nation with its own government. + country?: string; + + // Identifies a subdivision of a country such as state, region, county. + country_subdivision?: string; + + // Identifies a subdivision within a country sub-division. + district?: string; + + // Name of a built-up area, with defined boundaries, and a local government. + town?: string; + + // Specific location name within the town. + town_location?: string; + + // Identifier consisting of a group of letters and/or numbers that + // is added to a postal address to assist the sorting of mail. + post_code?: string; + + // Name of a street or thoroughfare. + street?: string; + + // Name of the building or house. + building_name?: string; + + // Number that identifies the position of a building on a street. + building_number?: string; + + // Free-form address lines, should not exceed 7 elements. + address_lines?: string[]; +} + +export interface MerchantInfo { + name: string; + jurisdiction?: Location; + address?: Location; + logo?: string; + website?: string; + email?: string; +} + +export interface Tax { + // the name of the tax + name: string; + + // amount paid in tax + tax: AmountString; +} + +export interface Product { + // merchant-internal identifier for the product. + product_id?: string; + + // Human-readable product description. + description: string; + + // Map from IETF BCP 47 language tags to localized descriptions + description_i18n?: { [lang_tag: string]: string }; + + // The number of units of the product to deliver to the customer. + quantity?: number; + + // The unit in which the product is measured (liters, kilograms, packages, etc.) + unit?: string; + + // The price of the product; this is the total price for quantity times unit of this product. + price?: AmountString; + + // An optional base64-encoded product image + image?: string; + + // a list of taxes paid by the merchant for this product. Can be empty. + taxes?: Tax[]; + + // time indicating when this product should be delivered + delivery_date?: TalerProtocolTimestamp; +} + +export interface InternationalizedString { + [lang_tag: string]: string; +} + +/** + * Contract terms from a merchant. + */ +export interface ContractTerms { + /** + * Hash of the merchant's wire details. + */ + h_wire: string; + + /** + * Hash of the merchant's wire details. + */ + auto_refund?: TalerProtocolDuration; + + /** + * Wire method the merchant wants to use. + */ + wire_method: string; + + /** + * Human-readable short summary of the contract. + */ + summary: string; + + summary_i18n?: InternationalizedString; + + /** + * Nonce used to ensure freshness. + */ + nonce: string; + + /** + * Total amount payable. + */ + amount: string; + + /** + * Auditors accepted by the merchant. + */ + auditors: AuditorHandle[]; + + /** + * Deadline to pay for the contract. + */ + pay_deadline: TalerProtocolTimestamp; + + /** + * Maximum deposit fee covered by the merchant. + */ + max_fee: string; + + /** + * Information about the merchant. + */ + merchant: MerchantInfo; + + /** + * Public key of the merchant. + */ + merchant_pub: string; + + /** + * Time indicating when the order should be delivered. + * May be overwritten by individual products. + */ + delivery_date?: TalerProtocolTimestamp; + + /** + * Delivery location for (all!) products. + */ + delivery_location?: Location; + + /** + * List of accepted exchanges. + */ + exchanges: ExchangeHandle[]; + + /** + * Products that are sold in this contract. + */ + products?: Product[]; + + /** + * Deadline for refunds. + */ + refund_deadline: TalerProtocolTimestamp; + + /** + * Deadline for the wire transfer. + */ + wire_transfer_deadline: TalerProtocolTimestamp; + + /** + * Time when the contract was generated by the merchant. + */ + timestamp: TalerProtocolTimestamp; + + /** + * Order id to uniquely identify the purchase within + * one merchant instance. + */ + order_id: string; + + /** + * Base URL of the merchant's backend. + */ + merchant_base_url: string; + + /** + * Fulfillment URL to view the product or + * delivery status. + */ + fulfillment_url?: string; + + /** + * URL meant to share the shopping cart. + */ + public_reorder_url?: string; + + /** + * Plain text fulfillment message in the merchant's default language. + */ + fulfillment_message?: string; + + /** + * Internationalized fulfillment messages. + */ + fulfillment_message_i18n?: InternationalizedString; + + /** + * Share of the wire fee that must be settled with one payment. + */ + wire_fee_amortization?: number; + + /** + * Maximum wire fee that the merchant agrees to pay for. + */ + max_wire_fee?: string; + + minimum_age?: number; + + /** + * Extra data, interpreted by the mechant only. + */ + extra?: any; +} + +/** + * Refund permission in the format that the merchant gives it to us. + */ +export interface MerchantAbortPayRefundDetails { + /** + * Amount to be refunded. + */ + refund_amount: string; + + /** + * Fee for the refund. + */ + refund_fee: string; + + /** + * Public key of the coin being refunded. + */ + coin_pub: string; + + /** + * Refund transaction ID between merchant and exchange. + */ + rtransaction_id: number; + + /** + * Exchange's key used for the signature. + */ + exchange_pub?: string; + + /** + * Exchange's signature to confirm the refund. + */ + exchange_sig?: string; + + /** + * Error replay from the exchange (if any). + */ + exchange_reply?: any; + + /** + * Error code from the exchange (if any). + */ + exchange_code?: number; + + /** + * HTTP status code of the exchange's response + * to the merchant's refund request. + */ + exchange_http_status: number; +} + +/** + * Response for a refund pickup or a /pay in abort mode. + */ +export interface MerchantRefundResponse { + /** + * Public key of the merchant + */ + merchant_pub: string; + + /** + * Contract terms hash of the contract that + * is being refunded. + */ + h_contract_terms: string; + + /** + * The signed refund permissions, to be sent to the exchange. + */ + refunds: MerchantAbortPayRefundDetails[]; +} + +/** + * Planchet detail sent to the merchant. + */ +export interface TipPlanchetDetail { + /** + * Hashed denomination public key. + */ + denom_pub_hash: string; + + /** + * Coin's blinded public key. + */ + coin_ev: CoinEnvelope; +} + +/** + * Request sent to the merchant to pick up a tip. + */ +export interface TipPickupRequest { + /** + * Identifier of the tip. + */ + tip_id: string; + + /** + * List of planchets the wallet wants to use for the tip. + */ + planchets: TipPlanchetDetail[]; +} + +/** + * Reserve signature, defined as separate class to facilitate + * schema validation. + */ +export interface MerchantBlindSigWrapperV1 { + /** + * Reserve signature. + */ + blind_sig: string; +} + +/** + * Response of the merchant + * to the TipPickupRequest. + */ +export interface MerchantTipResponseV1 { + /** + * The order of the signatures matches the planchets list. + */ + blind_sigs: MerchantBlindSigWrapperV1[]; +} + +export interface MerchantBlindSigWrapperV2 { + blind_sig: BlindedDenominationSignature; +} + +/** + * Response of the merchant + * to the TipPickupRequest. + */ +export interface MerchantTipResponseV2 { + /** + * The order of the signatures matches the planchets list. + */ + blind_sigs: MerchantBlindSigWrapperV2[]; +} + +/** + * Element of the payback list that the + * exchange gives us in /keys. + */ +export class Recoup { + /** + * The hash of the denomination public key for which the payback is offered. + */ + h_denom_pub: string; +} + +/** + * Structure of one exchange signing key in the /keys response. + */ +export class ExchangeSignKeyJson { + stamp_start: TalerProtocolTimestamp; + stamp_expire: TalerProtocolTimestamp; + stamp_end: TalerProtocolTimestamp; + key: EddsaPublicKeyString; + master_sig: EddsaSignatureString; +} + +/** + * Structure that the exchange gives us in /keys. + */ +export class ExchangeKeysJson { + /** + * List of offered denominations. + */ + denoms: ExchangeDenomination[]; + + /** + * The exchange's master public key. + */ + master_public_key: string; + + /** + * The list of auditors (partially) auditing the exchange. + */ + auditors: ExchangeAuditor[]; + + /** + * Timestamp when this response was issued. + */ + list_issue_date: TalerProtocolTimestamp; + + /** + * List of revoked denominations. + */ + recoup?: Recoup[]; + + /** + * Short-lived signing keys used to sign online + * responses. + */ + signkeys: ExchangeSignKeyJson[]; + + /** + * Protocol version. + */ + version: string; + + reserve_closing_delay: TalerProtocolDuration; + + global_fees: GlobalFees[]; +} + +export interface GlobalFees { + // What date (inclusive) does these fees go into effect? + start_date: TalerProtocolTimestamp; + + // What date (exclusive) does this fees stop going into effect? + end_date: TalerProtocolTimestamp; + + // KYC fee, charged when a user wants to create an account. + // The first year of the account_annual_fee after the KYC is + // always included. + kyc_fee: AmountString; + + // Account history fee, charged when a user wants to + // obtain a reserve/account history. + history_fee: AmountString; + + // Annual fee charged for having an open account at the + // exchange. Charged to the account. If the account + // balance is insufficient to cover this fee, the account + // is automatically deleted/closed. (Note that the exchange + // will keep the account history around for longer for + // regulatory reasons.) + account_fee: AmountString; + + // Purse fee, charged only if a purse is abandoned + // and was not covered by the account limit. + purse_fee: AmountString; + + // How long will the exchange preserve the account history? + // After an account was deleted/closed, the exchange will + // retain the account history for legal reasons until this time. + history_expiration: TalerProtocolDuration; + + // How long does the exchange promise to keep funds + // an account for which the KYC has never happened + // after a purse was merged into an account? Basically, + // after this time funds in an account without KYC are + // forfeit. + account_kyc_timeout: TalerProtocolDuration; + + // Non-negative number of concurrent purses that any + // account holder is allowed to create without having + // to pay the purse_fee. + purse_account_limit: number; + + // How long does an exchange keep a purse around after a purse + // has expired (or been successfully merged)? A 'GET' request + // for a purse will succeed until the purse expiration time + // plus this value. + purse_timeout: TalerProtocolDuration; + + // Signature of TALER_GlobalFeesPS. + master_sig: string; +} +/** + * Wire fees as announced by the exchange. + */ +export class WireFeesJson { + /** + * Cost of a wire transfer. + */ + wire_fee: string; + + wad_fee: string; + + /** + * Cost of clising a reserve. + */ + closing_fee: string; + + /** + * Signature made with the exchange's master key. + */ + sig: string; + + /** + * Date from which the fee applies. + */ + start_date: TalerProtocolTimestamp; + + /** + * Data after which the fee doesn't apply anymore. + */ + end_date: TalerProtocolTimestamp; +} + +export interface AccountInfo { + payto_uri: string; + master_sig: string; +} + +export interface ExchangeWireJson { + accounts: AccountInfo[]; + fees: { [methodName: string]: WireFeesJson[] }; +} + +/** + * Proposal returned from the contract URL. + */ +export class Proposal { + /** + * Contract terms for the propoal. + * Raw, un-decoded JSON object. + */ + contract_terms: any; + + /** + * Signature over contract, made by the merchant. The public key used for signing + * must be contract_terms.merchant_pub. + */ + sig: string; +} + +/** + * Response from the internal merchant API. + */ +export class CheckPaymentResponse { + order_status: string; + refunded: boolean | undefined; + refunded_amount: string | undefined; + contract_terms: any | undefined; + taler_pay_uri: string | undefined; + contract_url: string | undefined; +} + +/** + * Response from the bank. + */ +export class WithdrawOperationStatusResponse { + selection_done: boolean; + + transfer_done: boolean; + + aborted: boolean; + + amount: string; + + sender_wire?: string; + + suggested_exchange?: string; + + confirm_transfer_url?: string; + + wire_types: string[]; +} + +/** + * Response from the merchant. + */ +export class TipPickupGetResponse { + tip_amount: string; + + exchange_url: string; + + expiration: TalerProtocolTimestamp; +} + +export enum DenomKeyType { + Rsa = "RSA", + ClauseSchnorr = "CS", +} + +export namespace DenomKeyType { + export function toIntTag(t: DenomKeyType): number { + switch (t) { + case DenomKeyType.Rsa: + return 1; + case DenomKeyType.ClauseSchnorr: + return 2; + } + } +} + +export interface RsaBlindedDenominationSignature { + cipher: DenomKeyType.Rsa; + blinded_rsa_signature: string; +} + +export interface CSBlindedDenominationSignature { + cipher: DenomKeyType.ClauseSchnorr; +} + +export type BlindedDenominationSignature = + | RsaBlindedDenominationSignature + | CSBlindedDenominationSignature; + +export const codecForRsaBlindedDenominationSignature = () => + buildCodecForObject() + .property("cipher", codecForConstString(DenomKeyType.Rsa)) + .property("blinded_rsa_signature", codecForString()) + .build("RsaBlindedDenominationSignature"); + +export const codecForBlindedDenominationSignature = () => + buildCodecForUnion() + .discriminateOn("cipher") + .alternative(DenomKeyType.Rsa, codecForRsaBlindedDenominationSignature()) + .build("BlindedDenominationSignature"); + +export class WithdrawResponse { + ev_sig: BlindedDenominationSignature; +} + +export class WithdrawBatchResponse { + ev_sigs: WithdrawResponse[]; +} + +/** + * Easy to process format for the public data of coins + * managed by the wallet. + */ +export interface CoinDumpJson { + coins: Array<{ + /** + * The coin's denomination's public key. + */ + denom_pub: DenominationPubKey; + /** + * Hash of denom_pub. + */ + denom_pub_hash: string; + /** + * Value of the denomination (without any fees). + */ + denom_value: string; + /** + * Public key of the coin. + */ + coin_pub: string; + /** + * Base URL of the exchange for the coin. + */ + exchange_base_url: string; + /** + * Remaining value on the coin, to the knowledge of + * the wallet. + */ + remaining_value: string; + /** + * Public key of the parent coin. + * Only present if this coin was obtained via refreshing. + */ + refresh_parent_coin_pub: string | undefined; + /** + * Public key of the reserve for this coin. + * Only present if this coin was obtained via refreshing. + */ + withdrawal_reserve_pub: string | undefined; + /** + * Is the coin suspended? + * Suspended coins are not considered for payments. + */ + coin_suspended: boolean; + + /** + * Information about the age restriction + */ + ageCommitmentProof: AgeCommitmentProof | undefined; + }>; +} + +export interface MerchantPayResponse { + sig: string; +} + +export interface ExchangeMeltRequest { + coin_pub: CoinPublicKeyString; + confirm_sig: EddsaSignatureString; + denom_pub_hash: HashCodeString; + denom_sig: UnblindedSignature; + rc: string; + value_with_fee: AmountString; + age_commitment_hash?: HashCodeString; +} + +export interface ExchangeMeltResponse { + /** + * Which of the kappa indices does the client not have to reveal. + */ + noreveal_index: number; + + /** + * Signature of TALER_RefreshMeltConfirmationPS whereby the exchange + * affirms the successful melt and confirming the noreveal_index + */ + exchange_sig: EddsaSignatureString; + + /* + * public EdDSA key of the exchange that was used to generate the signature. + * Should match one of the exchange's signing keys from /keys. Again given + * explicitly as the client might otherwise be confused by clock skew as to + * which signing key was used. + */ + exchange_pub: EddsaPublicKeyString; + + /* + * Base URL to use for operations on the refresh context + * (so the reveal operation). If not given, + * the base URL is the same as the one used for this request. + * Can be used if the base URL for /refreshes/ differs from that + * for /coins/, i.e. for load balancing. Clients SHOULD + * respect the refresh_base_url if provided. Any HTTP server + * belonging to an exchange MUST generate a 307 or 308 redirection + * to the correct base URL should a client uses the wrong base + * URL, or if the base URL has changed since the melt. + * + * When melting the same coin twice (technically allowed + * as the response might have been lost on the network), + * the exchange may return different values for the refresh_base_url. + */ + refresh_base_url?: string; +} + +export interface ExchangeRevealItem { + ev_sig: BlindedDenominationSignature; +} + +export interface ExchangeRevealResponse { + // List of the exchange's blinded RSA signatures on the new coins. + ev_sigs: ExchangeRevealItem[]; +} + +interface MerchantOrderStatusPaid { + // Was the payment refunded (even partially, via refund or abort)? + refunded: boolean; + + // Is any amount of the refund still waiting to be picked up (even partially)? + refund_pending: boolean; + + // Amount that was refunded in total. + refund_amount: AmountString; + + // Amount that already taken by the wallet. + refund_taken: AmountString; +} + +interface MerchantOrderRefundResponse { + /** + * Amount that was refunded in total. + */ + refund_amount: AmountString; + + /** + * Successful refunds for this payment, empty array for none. + */ + refunds: MerchantCoinRefundStatus[]; + + /** + * Public key of the merchant. + */ + merchant_pub: EddsaPublicKeyString; +} + +export type MerchantCoinRefundStatus = + | MerchantCoinRefundSuccessStatus + | MerchantCoinRefundFailureStatus; + +export interface MerchantCoinRefundSuccessStatus { + type: "success"; + + // HTTP status of the exchange request, 200 (integer) required for refund confirmations. + exchange_status: 200; + + // the EdDSA :ref:signature (binary-only) with purpose + // TALER_SIGNATURE_EXCHANGE_CONFIRM_REFUND using a current signing key of the + // exchange affirming the successful refund + exchange_sig: EddsaSignatureString; + + // public EdDSA key of the exchange that was used to generate the signature. + // Should match one of the exchange's signing keys from /keys. It is given + // explicitly as the client might otherwise be confused by clock skew as to + // which signing key was used. + exchange_pub: EddsaPublicKeyString; + + // Refund transaction ID. + rtransaction_id: number; + + // public key of a coin that was refunded + coin_pub: EddsaPublicKeyString; + + // Amount that was refunded, including refund fee charged by the exchange + // to the customer. + refund_amount: AmountString; + + execution_time: TalerProtocolTimestamp; +} + +export interface MerchantCoinRefundFailureStatus { + type: "failure"; + + // HTTP status of the exchange request, must NOT be 200. + exchange_status: number; + + // Taler error code from the exchange reply, if available. + exchange_code?: number; + + // If available, HTTP reply from the exchange. + exchange_reply?: any; + + // Refund transaction ID. + rtransaction_id: number; + + // public key of a coin that was refunded + coin_pub: EddsaPublicKeyString; + + // Amount that was refunded, including refund fee charged by the exchange + // to the customer. + refund_amount: AmountString; + + execution_time: TalerProtocolTimestamp; +} + +export interface MerchantOrderStatusUnpaid { + /** + * URI that the wallet must process to complete the payment. + */ + taler_pay_uri: string; + + /** + * Alternative order ID which was paid for already in the same session. + * + * Only given if the same product was purchased before in the same session. + */ + already_paid_order_id?: string; +} + +/** + * Response body for the following endpoint: + * + * POST {talerBankIntegrationApi}/withdrawal-operation/{wopid} + */ +export interface BankWithdrawalOperationPostResponse { + transfer_done: boolean; +} + +export type DenominationPubKey = RsaDenominationPubKey | CsDenominationPubKey; + +export interface RsaDenominationPubKey { + readonly cipher: DenomKeyType.Rsa; + readonly rsa_public_key: string; + readonly age_mask: number; +} + +export interface CsDenominationPubKey { + readonly cipher: DenomKeyType.ClauseSchnorr; + readonly age_mask: number; + readonly cs_public_key: string; +} + +export namespace DenominationPubKey { + export function cmp( + p1: DenominationPubKey, + p2: DenominationPubKey, + ): -1 | 0 | 1 { + if (p1.cipher < p2.cipher) { + return -1; + } else if (p1.cipher > p2.cipher) { + return +1; + } else if ( + p1.cipher === DenomKeyType.Rsa && + p2.cipher === DenomKeyType.Rsa + ) { + if ((p1.age_mask ?? 0) < (p2.age_mask ?? 0)) { + return -1; + } else if ((p1.age_mask ?? 0) > (p2.age_mask ?? 0)) { + return 1; + } + return strcmp(p1.rsa_public_key, p2.rsa_public_key); + } else if ( + p1.cipher === DenomKeyType.ClauseSchnorr && + p2.cipher === DenomKeyType.ClauseSchnorr + ) { + if ((p1.age_mask ?? 0) < (p2.age_mask ?? 0)) { + return -1; + } else if ((p1.age_mask ?? 0) > (p2.age_mask ?? 0)) { + return 1; + } + return strcmp(p1.cs_public_key, p2.cs_public_key); + } else { + throw Error("unsupported cipher"); + } + } +} + +export const codecForRsaDenominationPubKey = () => + buildCodecForObject() + .property("cipher", codecForConstString(DenomKeyType.Rsa)) + .property("rsa_public_key", codecForString()) + .property("age_mask", codecForNumber()) + .build("DenominationPubKey"); + +export const codecForCsDenominationPubKey = () => + buildCodecForObject() + .property("cipher", codecForConstString(DenomKeyType.ClauseSchnorr)) + .property("cs_public_key", codecForString()) + .property("age_mask", codecForNumber()) + .build("CsDenominationPubKey"); + +export const codecForDenominationPubKey = () => + buildCodecForUnion() + .discriminateOn("cipher") + .alternative(DenomKeyType.Rsa, codecForRsaDenominationPubKey()) + .alternative(DenomKeyType.ClauseSchnorr, codecForCsDenominationPubKey()) + .build("DenominationPubKey"); + +export const codecForBankWithdrawalOperationPostResponse = + (): Codec => + buildCodecForObject() + .property("transfer_done", codecForBoolean()) + .build("BankWithdrawalOperationPostResponse"); + +export type AmountString = string; +export type Base32String = string; +export type EddsaSignatureString = string; +export type EddsaPublicKeyString = string; +export type CoinPublicKeyString = string; + +export const codecForDenomination = (): Codec => + buildCodecForObject() + .property("value", codecForString()) + .property("denom_pub", codecForDenominationPubKey()) + .property("fee_withdraw", codecForString()) + .property("fee_deposit", codecForString()) + .property("fee_refresh", codecForString()) + .property("fee_refund", codecForString()) + .property("stamp_start", codecForTimestamp) + .property("stamp_expire_withdraw", codecForTimestamp) + .property("stamp_expire_legal", codecForTimestamp) + .property("stamp_expire_deposit", codecForTimestamp) + .property("master_sig", codecForString()) + .build("Denomination"); + +export const codecForAuditorDenomSig = (): Codec => + buildCodecForObject() + .property("denom_pub_h", codecForString()) + .property("auditor_sig", codecForString()) + .build("AuditorDenomSig"); + +export const codecForAuditor = (): Codec => + buildCodecForObject() + .property("auditor_pub", codecForString()) + .property("auditor_url", codecForString()) + .property("denomination_keys", codecForList(codecForAuditorDenomSig())) + .build("Auditor"); + +export const codecForExchangeHandle = (): Codec => + buildCodecForObject() + .property("master_pub", codecForString()) + .property("url", codecForString()) + .build("ExchangeHandle"); + +export const codecForAuditorHandle = (): Codec => + buildCodecForObject() + .property("name", codecForString()) + .property("auditor_pub", codecForString()) + .property("url", codecForString()) + .build("AuditorHandle"); + +export const codecForLocation = (): Codec => + buildCodecForObject() + .property("country", codecOptional(codecForString())) + .property("country_subdivision", codecOptional(codecForString())) + .property("building_name", codecOptional(codecForString())) + .property("building_number", codecOptional(codecForString())) + .property("district", codecOptional(codecForString())) + .property("street", codecOptional(codecForString())) + .property("post_code", codecOptional(codecForString())) + .property("town", codecOptional(codecForString())) + .property("town_location", codecOptional(codecForString())) + .property("address_lines", codecOptional(codecForList(codecForString()))) + .build("Location"); + +export const codecForMerchantInfo = (): Codec => + buildCodecForObject() + .property("name", codecForString()) + .property("address", codecOptional(codecForLocation())) + .property("jurisdiction", codecOptional(codecForLocation())) + .build("MerchantInfo"); + +export const codecForTax = (): Codec => + buildCodecForObject() + .property("name", codecForString()) + .property("tax", codecForString()) + .build("Tax"); + +export const codecForInternationalizedString = + (): Codec => codecForMap(codecForString()); + +export const codecForProduct = (): Codec => + buildCodecForObject() + .property("product_id", codecOptional(codecForString())) + .property("description", codecForString()) + .property( + "description_i18n", + codecOptional(codecForInternationalizedString()), + ) + .property("quantity", codecOptional(codecForNumber())) + .property("unit", codecOptional(codecForString())) + .property("price", codecOptional(codecForString())) + .build("Tax"); + +export const codecForContractTerms = (): Codec => + buildCodecForObject() + .property("order_id", codecForString()) + .property("fulfillment_url", codecOptional(codecForString())) + .property("fulfillment_message", codecOptional(codecForString())) + .property( + "fulfillment_message_i18n", + codecOptional(codecForInternationalizedString()), + ) + .property("merchant_base_url", codecForString()) + .property("h_wire", codecForString()) + .property("auto_refund", codecOptional(codecForDuration)) + .property("wire_method", codecForString()) + .property("summary", codecForString()) + .property("summary_i18n", codecOptional(codecForInternationalizedString())) + .property("nonce", codecForString()) + .property("amount", codecForString()) + .property("auditors", codecForList(codecForAuditorHandle())) + .property("pay_deadline", codecForTimestamp) + .property("refund_deadline", codecForTimestamp) + .property("wire_transfer_deadline", codecForTimestamp) + .property("timestamp", codecForTimestamp) + .property("delivery_location", codecOptional(codecForLocation())) + .property("delivery_date", codecOptional(codecForTimestamp)) + .property("max_fee", codecForString()) + .property("max_wire_fee", codecOptional(codecForString())) + .property("merchant", codecForMerchantInfo()) + .property("merchant_pub", codecForString()) + .property("exchanges", codecForList(codecForExchangeHandle())) + .property("products", codecOptional(codecForList(codecForProduct()))) + .property("extra", codecForAny()) + .property("minimum_age", codecOptional(codecForNumber())) + .build("ContractTerms"); + +export const codecForMerchantRefundPermission = + (): Codec => + buildCodecForObject() + .property("refund_amount", codecForAmountString()) + .property("refund_fee", codecForAmountString()) + .property("coin_pub", codecForString()) + .property("rtransaction_id", codecForNumber()) + .property("exchange_http_status", codecForNumber()) + .property("exchange_code", codecOptional(codecForNumber())) + .property("exchange_reply", codecOptional(codecForAny())) + .property("exchange_sig", codecOptional(codecForString())) + .property("exchange_pub", codecOptional(codecForString())) + .build("MerchantRefundPermission"); + +export const codecForMerchantRefundResponse = + (): Codec => + buildCodecForObject() + .property("merchant_pub", codecForString()) + .property("h_contract_terms", codecForString()) + .property("refunds", codecForList(codecForMerchantRefundPermission())) + .build("MerchantRefundResponse"); + +export const codecForBlindSigWrapperV2 = (): Codec => + buildCodecForObject() + .property("blind_sig", codecForBlindedDenominationSignature()) + .build("MerchantBlindSigWrapperV2"); + +export const codecForMerchantTipResponseV2 = (): Codec => + buildCodecForObject() + .property("blind_sigs", codecForList(codecForBlindSigWrapperV2())) + .build("MerchantTipResponseV2"); + +export const codecForRecoup = (): Codec => + buildCodecForObject() + .property("h_denom_pub", codecForString()) + .build("Recoup"); + +export const codecForExchangeSigningKey = (): Codec => + buildCodecForObject() + .property("key", codecForString()) + .property("master_sig", codecForString()) + .property("stamp_end", codecForTimestamp) + .property("stamp_start", codecForTimestamp) + .property("stamp_expire", codecForTimestamp) + .build("ExchangeSignKeyJson"); + +export const codecForGlobalFees = (): Codec => + buildCodecForObject() + .property("start_date", codecForTimestamp) + .property("end_date", codecForTimestamp) + .property("kyc_fee", codecForAmountString()) + .property("history_fee", codecForAmountString()) + .property("account_fee", codecForAmountString()) + .property("purse_fee", codecForAmountString()) + .property("history_expiration", codecForDuration) + .property("account_kyc_timeout", codecForDuration) + .property("purse_account_limit", codecForNumber()) + .property("purse_timeout", codecForDuration) + .property("master_sig", codecForString()) + .build("GlobalFees"); + +export const codecForExchangeKeysJson = (): Codec => + buildCodecForObject() + .property("denoms", codecForList(codecForDenomination())) + .property("master_public_key", codecForString()) + .property("auditors", codecForList(codecForAuditor())) + .property("list_issue_date", codecForTimestamp) + .property("recoup", codecOptional(codecForList(codecForRecoup()))) + .property("signkeys", codecForList(codecForExchangeSigningKey())) + .property("version", codecForString()) + .property("reserve_closing_delay", codecForDuration) + .property("global_fees", codecForList(codecForGlobalFees())) + .build("ExchangeKeysJson"); + +export const codecForWireFeesJson = (): Codec => + buildCodecForObject() + .property("wire_fee", codecForString()) + .property("closing_fee", codecForString()) + .property("wad_fee", codecForString()) + .property("sig", codecForString()) + .property("start_date", codecForTimestamp) + .property("end_date", codecForTimestamp) + .build("WireFeesJson"); + +export const codecForAccountInfo = (): Codec => + buildCodecForObject() + .property("payto_uri", codecForString()) + .property("master_sig", codecForString()) + .build("AccountInfo"); + +export const codecForExchangeWireJson = (): Codec => + buildCodecForObject() + .property("accounts", codecForList(codecForAccountInfo())) + .property("fees", codecForMap(codecForList(codecForWireFeesJson()))) + .build("ExchangeWireJson"); + +export const codecForProposal = (): Codec => + buildCodecForObject() + .property("contract_terms", codecForAny()) + .property("sig", codecForString()) + .build("Proposal"); + +export const codecForCheckPaymentResponse = (): Codec => + buildCodecForObject() + .property("order_status", codecForString()) + .property("refunded", codecOptional(codecForBoolean())) + .property("refunded_amount", codecOptional(codecForString())) + .property("contract_terms", codecOptional(codecForAny())) + .property("taler_pay_uri", codecOptional(codecForString())) + .property("contract_url", codecOptional(codecForString())) + .build("CheckPaymentResponse"); + +export const codecForWithdrawOperationStatusResponse = + (): Codec => + buildCodecForObject() + .property("selection_done", codecForBoolean()) + .property("transfer_done", codecForBoolean()) + .property("aborted", codecForBoolean()) + .property("amount", codecForString()) + .property("sender_wire", codecOptional(codecForString())) + .property("suggested_exchange", codecOptional(codecForString())) + .property("confirm_transfer_url", codecOptional(codecForString())) + .property("wire_types", codecForList(codecForString())) + .build("WithdrawOperationStatusResponse"); + +export const codecForTipPickupGetResponse = (): Codec => + buildCodecForObject() + .property("tip_amount", codecForString()) + .property("exchange_url", codecForString()) + .property("expiration", codecForTimestamp) + .build("TipPickupGetResponse"); + +export const codecForRecoupConfirmation = (): Codec => + buildCodecForObject() + .property("reserve_pub", codecOptional(codecForString())) + .property("old_coin_pub", codecOptional(codecForString())) + .build("RecoupConfirmation"); + +export const codecForWithdrawResponse = (): Codec => + buildCodecForObject() + .property("ev_sig", codecForBlindedDenominationSignature()) + .build("WithdrawResponse"); + +export const codecForWithdrawBatchResponse = (): Codec => + buildCodecForObject() + .property("ev_sigs", codecForList(codecForWithdrawResponse())) + .build("WithdrawBatchResponse"); + +export const codecForMerchantPayResponse = (): Codec => + buildCodecForObject() + .property("sig", codecForString()) + .build("MerchantPayResponse"); + +export const codecForExchangeMeltResponse = (): Codec => + buildCodecForObject() + .property("exchange_pub", codecForString()) + .property("exchange_sig", codecForString()) + .property("noreveal_index", codecForNumber()) + .property("refresh_base_url", codecOptional(codecForString())) + .build("ExchangeMeltResponse"); + +export const codecForExchangeRevealItem = (): Codec => + buildCodecForObject() + .property("ev_sig", codecForBlindedDenominationSignature()) + .build("ExchangeRevealItem"); + +export const codecForExchangeRevealResponse = + (): Codec => + buildCodecForObject() + .property("ev_sigs", codecForList(codecForExchangeRevealItem())) + .build("ExchangeRevealResponse"); + +export const codecForMerchantCoinRefundSuccessStatus = + (): Codec => + buildCodecForObject() + .property("type", codecForConstString("success")) + .property("coin_pub", codecForString()) + .property("exchange_status", codecForConstNumber(200)) + .property("exchange_sig", codecForString()) + .property("rtransaction_id", codecForNumber()) + .property("refund_amount", codecForString()) + .property("exchange_pub", codecForString()) + .property("execution_time", codecForTimestamp) + .build("MerchantCoinRefundSuccessStatus"); + +export const codecForMerchantCoinRefundFailureStatus = + (): Codec => + buildCodecForObject() + .property("type", codecForConstString("failure")) + .property("coin_pub", codecForString()) + .property("exchange_status", codecForNumber()) + .property("rtransaction_id", codecForNumber()) + .property("refund_amount", codecForString()) + .property("exchange_code", codecOptional(codecForNumber())) + .property("exchange_reply", codecOptional(codecForAny())) + .property("execution_time", codecForTimestamp) + .build("MerchantCoinRefundFailureStatus"); + +export const codecForMerchantCoinRefundStatus = + (): Codec => + buildCodecForUnion() + .discriminateOn("type") + .alternative("success", codecForMerchantCoinRefundSuccessStatus()) + .alternative("failure", codecForMerchantCoinRefundFailureStatus()) + .build("MerchantCoinRefundStatus"); + +export const codecForMerchantOrderStatusPaid = + (): Codec => + buildCodecForObject() + .property("refund_amount", codecForString()) + .property("refund_taken", codecForString()) + .property("refund_pending", codecForBoolean()) + .property("refunded", codecForBoolean()) + .build("MerchantOrderStatusPaid"); + +export const codecForMerchantOrderRefundPickupResponse = + (): Codec => + buildCodecForObject() + .property("merchant_pub", codecForString()) + .property("refund_amount", codecForString()) + .property("refunds", codecForList(codecForMerchantCoinRefundStatus())) + .build("MerchantOrderRefundPickupResponse"); + +export const codecForMerchantOrderStatusUnpaid = + (): Codec => + buildCodecForObject() + .property("taler_pay_uri", codecForString()) + .property("already_paid_order_id", codecOptional(codecForString())) + .build("MerchantOrderStatusUnpaid"); + +export interface AbortRequest { + // hash of the order's contract terms (this is used to authenticate the + // wallet/customer in case $ORDER_ID is guessable). + h_contract: string; + + // List of coins the wallet would like to see refunds for. + // (Should be limited to the coins for which the original + // payment succeeded, as far as the wallet knows.) + coins: AbortingCoin[]; +} + +export interface AbortingCoin { + // Public key of a coin for which the wallet is requesting an abort-related refund. + coin_pub: EddsaPublicKeyString; + + // The amount to be refunded (matches the original contribution) + contribution: AmountString; + + // URL of the exchange this coin was withdrawn from. + exchange_url: string; +} + +export interface AbortResponse { + // List of refund responses about the coins that the wallet + // requested an abort for. In the same order as the 'coins' + // from the original request. + // The rtransaction_id is implied to be 0. + refunds: MerchantAbortPayRefundStatus[]; +} + +export const codecForMerchantAbortPayRefundSuccessStatus = + (): Codec => + buildCodecForObject() + .property("exchange_pub", codecForString()) + .property("exchange_sig", codecForString()) + .property("exchange_status", codecForConstNumber(200)) + .property("type", codecForConstString("success")) + .build("MerchantAbortPayRefundSuccessStatus"); + +export const codecForMerchantAbortPayRefundFailureStatus = + (): Codec => + buildCodecForObject() + .property("exchange_code", codecForNumber()) + .property("exchange_reply", codecForAny()) + .property("exchange_status", codecForNumber()) + .property("type", codecForConstString("failure")) + .build("MerchantAbortPayRefundFailureStatus"); + +export const codecForMerchantAbortPayRefundStatus = + (): Codec => + buildCodecForUnion() + .discriminateOn("type") + .alternative("success", codecForMerchantAbortPayRefundSuccessStatus()) + .alternative("failure", codecForMerchantAbortPayRefundFailureStatus()) + .build("MerchantAbortPayRefundStatus"); + +export const codecForAbortResponse = (): Codec => + buildCodecForObject() + .property("refunds", codecForList(codecForMerchantAbortPayRefundStatus())) + .build("AbortResponse"); + +export type MerchantAbortPayRefundStatus = + | MerchantAbortPayRefundSuccessStatus + | MerchantAbortPayRefundFailureStatus; + +// Details about why a refund failed. +export interface MerchantAbortPayRefundFailureStatus { + // Used as tag for the sum type RefundStatus sum type. + type: "failure"; + + // HTTP status of the exchange request, must NOT be 200. + exchange_status: number; + + // Taler error code from the exchange reply, if available. + exchange_code?: number; + + // If available, HTTP reply from the exchange. + exchange_reply?: unknown; +} + +// Additional details needed to verify the refund confirmation signature +// (h_contract_terms and merchant_pub) are already known +// to the wallet and thus not included. +export interface MerchantAbortPayRefundSuccessStatus { + // Used as tag for the sum type MerchantCoinRefundStatus sum type. + type: "success"; + + // HTTP status of the exchange request, 200 (integer) required for refund confirmations. + exchange_status: 200; + + // the EdDSA :ref:signature (binary-only) with purpose + // TALER_SIGNATURE_EXCHANGE_CONFIRM_REFUND using a current signing key of the + // exchange affirming the successful refund + exchange_sig: string; + + // public EdDSA key of the exchange that was used to generate the signature. + // Should match one of the exchange's signing keys from /keys. It is given + // explicitly as the client might otherwise be confused by clock skew as to + // which signing key was used. + exchange_pub: string; +} + +export interface TalerConfigResponse { + name: string; + version: string; + currency?: string; +} + +export const codecForTalerConfigResponse = (): Codec => + buildCodecForObject() + .property("name", codecForString()) + .property("version", codecForString()) + .property("currency", codecOptional(codecForString())) + .build("TalerConfigResponse"); + +export interface FutureKeysResponse { + future_denoms: any[]; + + future_signkeys: any[]; + + master_pub: string; + + denom_secmod_public_key: string; + + // Public key of the signkey security module. + signkey_secmod_public_key: string; +} + +export const codecForKeysManagementResponse = (): Codec => + buildCodecForObject() + .property("master_pub", codecForString()) + .property("future_signkeys", codecForList(codecForAny())) + .property("future_denoms", codecForList(codecForAny())) + .property("denom_secmod_public_key", codecForAny()) + .property("signkey_secmod_public_key", codecForAny()) + .build("FutureKeysResponse"); + +export interface MerchantConfigResponse { + currency: string; + name: string; + version: string; +} + +export const codecForMerchantConfigResponse = + (): Codec => + buildCodecForObject() + .property("currency", codecForString()) + .property("name", codecForString()) + .property("version", codecForString()) + .build("MerchantConfigResponse"); + +export enum ExchangeProtocolVersion { + /** + * Current version supported by the wallet. + */ + V12 = 12, +} + +export enum MerchantProtocolVersion { + /** + * Current version supported by the wallet. + */ + V3 = 3, +} + +export type CoinEnvelope = CoinEnvelopeRsa | CoinEnvelopeCs; + +export interface CoinEnvelopeRsa { + cipher: DenomKeyType.Rsa; + rsa_blinded_planchet: string; +} + +export interface CoinEnvelopeCs { + cipher: DenomKeyType.ClauseSchnorr; + // FIXME: add remaining fields +} + +export type HashCodeString = string; + +export interface ExchangeWithdrawRequest { + denom_pub_hash: HashCodeString; + reserve_sig: EddsaSignatureString; + coin_ev: CoinEnvelope; +} + +export interface ExchangeRefreshRevealRequest { + new_denoms_h: HashCodeString[]; + coin_evs: CoinEnvelope[]; + /** + * kappa - 1 transfer private keys (ephemeral ECDHE keys). + */ + transfer_privs: string[]; + + transfer_pub: EddsaPublicKeyString; + + link_sigs: EddsaSignatureString[]; + + /** + * Iff the corresponding denomination has support for age restriction, + * the client MUST provide the original age commitment, i.e. the vector + * of public keys. + */ + old_age_commitment?: Edx25519PublicKeyEnc[]; +} + +export interface DepositSuccess { + // Optional base URL of the exchange for looking up wire transfers + // associated with this transaction. If not given, + // the base URL is the same as the one used for this request. + // Can be used if the base URL for /transactions/ differs from that + // for /coins/, i.e. for load balancing. Clients SHOULD + // respect the transaction_base_url if provided. Any HTTP server + // belonging to an exchange MUST generate a 307 or 308 redirection + // to the correct base URL should a client uses the wrong base + // URL, or if the base URL has changed since the deposit. + transaction_base_url?: string; + + // timestamp when the deposit was received by the exchange. + exchange_timestamp: TalerProtocolTimestamp; + + // the EdDSA signature of TALER_DepositConfirmationPS using a current + // signing key of the exchange affirming the successful + // deposit and that the exchange will transfer the funds after the refund + // deadline, or as soon as possible if the refund deadline is zero. + exchange_sig: string; + + // public EdDSA key of the exchange that was used to + // generate the signature. + // Should match one of the exchange's signing keys from /keys. It is given + // explicitly as the client might otherwise be confused by clock skew as to + // which signing key was used. + exchange_pub: string; +} + +export const codecForDepositSuccess = (): Codec => + buildCodecForObject() + .property("exchange_pub", codecForString()) + .property("exchange_sig", codecForString()) + .property("exchange_timestamp", codecForTimestamp) + .property("transaction_base_url", codecOptional(codecForString())) + .build("DepositSuccess"); + +export interface PurseDeposit { + /** + * Amount to be deposited, can be a fraction of the + * coin's total value. + */ + amount: AmountString; + + /** + * Hash of denomination RSA key with which the coin is signed. + */ + denom_pub_hash: HashCodeString; + + /** + * Exchange's unblinded RSA signature of the coin. + */ + ub_sig: UnblindedSignature; + + /** + * Age commitment for the coin, if the denomination is age-restricted. + */ + age_commitment?: string[]; + + /** + * Attestation for the minimum age, if the denomination is age-restricted. + */ + attest?: string; + + /** + * Signature over TALER_PurseDepositSignaturePS + * of purpose TALER_SIGNATURE_WALLET_PURSE_DEPOSIT + * made by the customer with the + * coin's private key. + */ + coin_sig: EddsaSignatureString; + + /** + * Public key of the coin being deposited into the purse. + */ + coin_pub: EddsaPublicKeyString; +} + +export interface ExchangePurseMergeRequest { + // payto://-URI of the account the purse is to be merged into. + // Must be of the form: 'payto://taler/$EXCHANGE_URL/$RESERVE_PUB'. + payto_uri: string; + + // EdDSA signature of the account/reserve affirming the merge + // over a TALER_AccountMergeSignaturePS. + // Must be of purpose TALER_SIGNATURE_ACCOUNT_MERGE + reserve_sig: EddsaSignatureString; + + // EdDSA signature of the purse private key affirming the merge + // over a TALER_PurseMergeSignaturePS. + // Must be of purpose TALER_SIGNATURE_PURSE_MERGE. + merge_sig: EddsaSignatureString; + + // Client-side timestamp of when the merge request was made. + merge_timestamp: TalerProtocolTimestamp; +} + +export interface ExchangeGetContractResponse { + purse_pub: string; + econtract_sig: string; + econtract: string; +} + +export const codecForExchangeGetContractResponse = + (): Codec => + buildCodecForObject() + .property("purse_pub", codecForString()) + .property("econtract_sig", codecForString()) + .property("econtract", codecForString()) + .build("ExchangeGetContractResponse"); + +/** + * Contract terms between two wallets (as opposed to a merchant and wallet). + */ +export interface PeerContractTerms { + amount: AmountString; + summary: string; + purse_expiration: TalerProtocolTimestamp; +} + +export interface EncryptedContract { + // Encrypted contract. + econtract: string; + + // Signature over the (encrypted) contract. + econtract_sig: string; + + // Ephemeral public key for the DH operation to decrypt the encrypted contract. + contract_pub: string; +} + +/** + * Payload for /reserves/{reserve_pub}/purse + * endpoint of the exchange. + */ +export interface ExchangeReservePurseRequest { + /** + * Minimum amount that must be credited to the reserve, that is + * the total value of the purse minus the deposit fees. + * If the deposit fees are lower, the contribution to the + * reserve can be higher! + */ + purse_value: AmountString; + + // Minimum age required for all coins deposited into the purse. + min_age: number; + + // Purse fee the reserve owner is willing to pay + // for the purse creation. Optional, if not present + // the purse is to be created from the purse quota + // of the reserve. + purse_fee: AmountString; + + // Optional encrypted contract, in case the buyer is + // proposing the contract and thus establishing the + // purse with the payment. + econtract?: EncryptedContract; + + // EdDSA public key used to approve merges of this purse. + merge_pub: EddsaPublicKeyString; + + // EdDSA signature of the purse private key affirming the merge + // over a TALER_PurseMergeSignaturePS. + // Must be of purpose TALER_SIGNATURE_PURSE_MERGE. + merge_sig: EddsaSignatureString; + + // EdDSA signature of the account/reserve affirming the merge. + // Must be of purpose TALER_SIGNATURE_WALLET_ACCOUNT_MERGE + reserve_sig: EddsaSignatureString; + + // Purse public key. + purse_pub: EddsaPublicKeyString; + + // EdDSA signature of the purse over + // TALER_PurseRequestSignaturePS of + // purpose TALER_SIGNATURE_PURSE_REQUEST + // confirming that the + // above details hold for this purse. + purse_sig: EddsaSignatureString; + + // SHA-512 hash of the contact of the purse. + h_contract_terms: HashCodeString; + + // Client-side timestamp of when the merge request was made. + merge_timestamp: TalerProtocolTimestamp; + + // Indicative time by which the purse should expire + // if it has not been paid. + purse_expiration: TalerProtocolTimestamp; +} + +export interface ExchangePurseDeposits { + // Array of coins to deposit into the purse. + deposits: PurseDeposit[]; +} diff --git a/packages/taler-util/src/talerCrypto.test.ts b/packages/taler-util/src/talerCrypto.test.ts deleted file mode 100644 index 29458ac37..000000000 --- a/packages/taler-util/src/talerCrypto.test.ts +++ /dev/null @@ -1,431 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2019 GNUnet e.V. - - GNU Taler is free software; you can redistribute it and/or modify it under the - terms of the GNU General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - GNU Taler; see the file COPYING. If not, see - */ - -/** - * Imports - */ -import test from "ava"; -import { - encodeCrock, - decodeCrock, - ecdheGetPublic, - eddsaGetPublic, - keyExchangeEddsaEcdhe, - keyExchangeEcdheEddsa, - stringToBytes, - bytesToString, - deriveBSeed, - csBlind, - csUnblind, - csVerify, - scalarMultBase25519, - deriveSecrets, - calcRBlind, - Edx25519, - getRandomBytes, - bigintToNaclArr, - bigintFromNaclArr, -} from "./talerCrypto.js"; -import { sha512, kdf } from "./kdf.js"; -import * as nacl from "./nacl-fast.js"; -import { initNodePrng } from "./prng-node.js"; - -// Since we import nacl-fast directly (and not via index.node.ts), we need to -// init the PRNG manually. -initNodePrng(); -import bigint from "big-integer"; -import { AssertionError } from "assert"; -import BigInteger from "big-integer"; - -test("encoding", (t) => { - const s = "Hello, World"; - const encStr = encodeCrock(stringToBytes(s)); - const outBuf = decodeCrock(encStr); - const sOut = bytesToString(outBuf); - t.deepEqual(s, sOut); -}); - -test("taler-exchange-tvg hash code", (t) => { - const input = "91JPRV3F5GG4EKJN41A62V35E8"; - const output = - "CW96WR74JS8T53EC8GKSGD49QKH4ZNFTZXDAWMMV5GJ1E4BM6B8GPN5NVHDJ8ZVXNCW7Q4WBYCV61HCA3PZC2YJD850DT29RHHN7ESR"; - - const myOutput = encodeCrock(sha512(decodeCrock(input))); - - t.deepEqual(myOutput, output); -}); - -test("taler-exchange-tvg ecdhe key", (t) => { - const priv1 = "X4T4N0M8PVQXQEBW2BA7049KFSM7J437NSDFC6GDNM3N5J9367A0"; - const pub1 = "M997P494MS6A95G1P0QYWW2VNPSHSX5Q6JBY5B9YMNYWP0B50X3G"; - const priv2 = "14A0MMQ64DCV8HE0CS3WBC9DHFJAHXRGV7NEARFJPC5R5E1697E0"; - const skm = - "NXRY2YCY7H9B6KM928ZD55WG964G59YR0CPX041DYXKBZZ85SAWNPQ8B30QRM5FMHYCXJAN0EAADJYWEF1X3PAC2AJN28626TR5A6AR"; - - const myPub1 = nacl.scalarMult_base(decodeCrock(priv1)); - t.deepEqual(encodeCrock(myPub1), pub1); - - const mySkm = nacl.hash( - nacl.scalarMult(decodeCrock(priv2), decodeCrock(pub1)), - ); - t.deepEqual(encodeCrock(mySkm), skm); -}); - -test("taler-exchange-tvg eddsa key", (t) => { - const priv = "9TM70AKDTS57AWY9JK2J4TMBTMW6K62WHHGZWYDG0VM5ABPZKD40"; - const pub = "8GSJZ649T2PXMKZC01Y4ANNBE7MF14QVK9SQEC4E46ZHKCVG8AS0"; - - const pair = nacl.crypto_sign_keyPair_fromSeed(decodeCrock(priv)); - t.deepEqual(encodeCrock(pair.publicKey), pub); -}); - -test("taler-exchange-tvg kdf", (t) => { - const salt = "94KPT83PCNS7J83KC5P78Y8"; - const ikm = "94KPT83MD1JJ0WV5CDS6AX10D5Q70XBM41NPAY90DNGQ8SBJD5GPR"; - const ctx = - "94KPT83141HPYVKMCNW78833D1TPWTSC41GPRWVF41NPWVVQDRG62WS04XMPWSKF4WG6JVH0EHM6A82J8S1G"; - const outLen = 64; - const out = - "GTMR4QT05Z9WF5HKVG0WK9RPXGHSMHJNW377G9GJXCA8B0FEKPF4D27RJMSJZYWSQNTBJ5EYVV7ZW18B48Z0JVJJ80RHB706Y96Q358"; - - const myOut = kdf( - outLen, - decodeCrock(ikm), - decodeCrock(salt), - decodeCrock(ctx), - ); - - t.deepEqual(encodeCrock(myOut), out); -}); - -test("taler-exchange-tvg eddsa_ecdh", (t) => { - const priv_ecdhe = "4AFZWMSGTVCHZPQ0R81NWXDCK4N58G7SDBBE5KXE080Y50370JJG"; - const pub_ecdhe = "FXFN5GPAFTKVPWJDPVXQ87167S8T82T5ZV8CDYC0NH2AE14X0M30"; - const priv_eddsa = "1KG54M8T3X8BSFSZXCR3SQBSR7Y9P53NX61M864S7TEVMJ2XVPF0"; - const pub_eddsa = "7BXWKG6N224C57RTDV8XEAHR108HG78NMA995BE8QAT5GC1S7E80"; - const key_material = - "PKZ42Z56SVK2796HG1QYBRJ6ZQM2T9QGA3JA4AAZ8G7CWK9FPX175Q9JE5P0ZAX3HWWPHAQV4DPCK10R9X3SAXHRV0WF06BHEC2ZTKR"; - - const myEcdhePub = ecdheGetPublic(decodeCrock(priv_ecdhe)); - t.deepEqual(encodeCrock(myEcdhePub), pub_ecdhe); - - const myEddsaPub = eddsaGetPublic(decodeCrock(priv_eddsa)); - t.deepEqual(encodeCrock(myEddsaPub), pub_eddsa); - - const myKm1 = keyExchangeEddsaEcdhe( - decodeCrock(priv_eddsa), - decodeCrock(pub_ecdhe), - ); - t.deepEqual(encodeCrock(myKm1), key_material); - - const myKm2 = keyExchangeEcdheEddsa( - decodeCrock(priv_ecdhe), - decodeCrock(pub_eddsa), - ); - t.deepEqual(encodeCrock(myKm2), key_material); -}); - -test("incremental hashing #1", (t) => { - const n = 1024; - const d = nacl.randomBytes(n); - - const h1 = nacl.hash(d); - const h2 = new nacl.HashState().update(d).finish(); - - const s = new nacl.HashState(); - for (let i = 0; i < n; i++) { - const b = new Uint8Array(1); - b[0] = d[i]; - s.update(b); - } - - const h3 = s.finish(); - - t.deepEqual(encodeCrock(h1), encodeCrock(h2)); - t.deepEqual(encodeCrock(h1), encodeCrock(h3)); -}); - -test("incremental hashing #2", (t) => { - const n = 10; - const d = nacl.randomBytes(n); - - const h1 = nacl.hash(d); - const h2 = new nacl.HashState().update(d).finish(); - const s = new nacl.HashState(); - for (let i = 0; i < n; i++) { - const b = new Uint8Array(1); - b[0] = d[i]; - s.update(b); - } - - const h3 = s.finish(); - - t.deepEqual(encodeCrock(h1), encodeCrock(h3)); - t.deepEqual(encodeCrock(h1), encodeCrock(h2)); -}); - -test("taler-exchange-tvg eddsa_ecdh #2", (t) => { - const priv_ecdhe = "W5FH9CFS3YPGSCV200GE8TH6MAACPKKGEG2A5JTFSD1HZ5RYT7Q0"; - const pub_ecdhe = "FER9CRS2T8783TAANPZ134R704773XT0ZT1XPFXZJ9D4QX67ZN00"; - const priv_eddsa = "MSZ1TBKC6YQ19ZFP3NTJVKWNVGFP35BBRW8FTAQJ9Z2B96VC9P4G"; - const pub_eddsa = "Y7MKG85PBT8ZEGHF08JBVZXEV70TS0PY5Y2CMEN1WXEDN63KP1A0"; - const key_material = - "G6RA58N61K7MT3WA13Q7VRTE1FQS6H43RX9HK8Z5TGAB61601GEGX51JRHHQMNKNM2R9AVC1STSGQDRHGKWVYP584YGBCTVMMJYQF30"; - - const myEcdhePub = ecdheGetPublic(decodeCrock(priv_ecdhe)); - t.deepEqual(encodeCrock(myEcdhePub), pub_ecdhe); - - const myEddsaPub = eddsaGetPublic(decodeCrock(priv_eddsa)); - t.deepEqual(encodeCrock(myEddsaPub), pub_eddsa); - - const myKm1 = keyExchangeEddsaEcdhe( - decodeCrock(priv_eddsa), - decodeCrock(pub_ecdhe), - ); - t.deepEqual(encodeCrock(myKm1), key_material); - - const myKm2 = keyExchangeEcdheEddsa( - decodeCrock(priv_ecdhe), - decodeCrock(pub_eddsa), - ); - t.deepEqual(encodeCrock(myKm2), key_material); -}); - -test("taler CS blind c", async (t) => { - /**$ - * Test Vectors: - { - "operation": "cs_blind_signing", - "message_hash": "KZ7540050MWFPPPJ6C0910TC15AWD6KN6GMK4YH8PY5Z2RKP7NQMHZ1NDD7JHD9CA2CZXDKYN7XRX521YERAF6N50VJZMHWPH18TCFG", - "cs_public_key": "1903SZ7QE1K8T4BHTJ32KDJ153SBXT22DGNQDY5NKJE535J72H2G", - "cs_private_key": "K43QAMEPE9KJJTX6AJZD6N4SN1N3ARVAXZ2MRNPT85FHD4QD2C60", - "cs_nonce": "GWPVFP9160XNADYQZ4T6S7RACB2482KG1JCY0X2Z5R52W74YXY3G", - "cs_r_priv_0": "B01FJCRCST8JM10K17SJXY7S7HH7T65JMFQ03H6PNYY9Z167Q1T0", - "cs_r_priv_1": "N3GW5X6VYSB8PY83CYNHJ3PN6TCA5N5BCS4WT2WEEQH7MTK915P0", - "cs_r_pub_0": "J5XFBKFP9T6BM02H6ZV6Y568PQ2K398MD339036F25XTSP1A7T3G", - "cs_r_pub_1": "GA2CZKJ6CWFS81ZN1T5R4GQFHF7XJV6HWHDR1JA9VATKKXQN89J0", - "cs_bs_alpha_0": "R06FWJ4XEK4JKKKA03JARGD0PD5JAX8DK2N6J0K8CAZZMVQEJ1T0", - "cs_bs_alpha_1": "13NXE2FEHJS0Q5XCWNRF4V1NC3BSAHN6BW02WZ07PG6967156HYG", - "cs_bs_beta_0": "T3EZP42RJQXRTJ4FTDWF18Z422VX7KFGN8GJ3QCCM1QV3N456HD0", - "cs_bs_beta_1": "P3MECYGCCR58QVEDSW443699CDXVT8C8W5ZT22PPNRJ363M72H6G", - "cs_r_pub_blind_0": "CHK7JC4SXZ4Y9RDA3881S82F7BP99H35Q361WR6RBXN5YN2ZM1M0", - "cs_r_pub_blind_1": "4C65R74GA9PPDX4DC2B948W96T3Z6QEENK2NDJQPNB9QBTKCT590", - "cs_c_0": "F288QXT67TR36E6DHE399G8J24RM6C3DP16HGMH74B6WZ1DETR10", - "cs_c_1": "EFK5WTN01NCVS3DZCG20MQDHRHBATRG8589BA0XSZDZ6D0HFR470", - "cs_blind_s": "6KZF904YZA8KK4C8X5JV57E7B84SR8TDDN9GDC8QTRRSNTHJTM4G", - "cs_b": "0000000", - "cs_sig_s": "F4ZKMFW3Q7DFN0N94KAMG2JFFHAC362T0QZ6ZCVZ73RS8P91CR70", - "cs_sig_R": "CHK7JC4SXZ4Y9RDA3881S82F7BP99H35Q361WR6RBXN5YN2ZM1M0", - "cs_c_blind_0": "6TN5454DZCHBDXFAGQFXQY37FNX6YRKW0MPFEX4TG5EHXC98M840", - "cs_c_blind_1": "EX6MYRZX6EC93YB4EE3M7AR3PQDYYG4092917YF29HD36X58NG0G", - "cs_prehash_0": "D29BBP762HEN6ZHZ5T2T6S4VMV400K9Y659M1QQZYZ0WJS3V3EJSF0FVXSCD1E99JJJMW295EY8TEE97YEGSGEQ0Q0A9DDMS2NCAG9R", - "cs_prehash_1": "9BYD02BC29ZF26BG88DWFCCENCS8CD8VZN76XP8JPWKTN9JS73MBCD0F36N0JSM223MRNJZACNYPMW23SGRHYVSP6BTT79GSSK5R228" - } - */ - - type CsBlindSignature = { - sBlind: Uint8Array; - rPubBlind: Uint8Array; - }; - /** - * CS denomination keypair - */ - const priv = "K43QAMEPE9KJJTX6AJZD6N4SN1N3ARVAXZ2MRNPT85FHD4QD2C60"; - const pub_cmp = "1903SZ7QE1K8T4BHTJ32KDJ153SBXT22DGNQDY5NKJE535J72H2G"; - const pub = await scalarMultBase25519(decodeCrock(priv)); - t.deepEqual(decodeCrock(pub_cmp), pub); - - const nonce = "GWPVFP9160XNADYQZ4T6S7RACB2482KG1JCY0X2Z5R52W74YXY3G"; - const msg_hash = - "KZ7540050MWFPPPJ6C0910TC15AWD6KN6GMK4YH8PY5Z2RKP7NQMHZ1NDD7JHD9CA2CZXDKYN7XRX521YERAF6N50VJZMHWPH18TCFG"; - - /** - * rPub is returned from the exchange's new /csr API - */ - const rPriv0 = "B01FJCRCST8JM10K17SJXY7S7HH7T65JMFQ03H6PNYY9Z167Q1T0"; - const rPriv1 = "N3GW5X6VYSB8PY83CYNHJ3PN6TCA5N5BCS4WT2WEEQH7MTK915P0"; - const rPub0 = await scalarMultBase25519(decodeCrock(rPriv0)); - const rPub1 = await scalarMultBase25519(decodeCrock(rPriv1)); - - const rPub: [Uint8Array, Uint8Array] = [rPub0, rPub1]; - - t.deepEqual( - rPub[0], - decodeCrock("J5XFBKFP9T6BM02H6ZV6Y568PQ2K398MD339036F25XTSP1A7T3G"), - ); - t.deepEqual( - rPub[1], - decodeCrock("GA2CZKJ6CWFS81ZN1T5R4GQFHF7XJV6HWHDR1JA9VATKKXQN89J0"), - ); - - /** - * Test if blinding seed derivation is deterministic - * In the wallet the b-seed MUST be different from the Withdraw-Nonce or Refresh Nonce! - * (Eg. derive two different values from coin priv) -> See CS protocols for details - */ - const priv_eddsa = "1KG54M8T3X8BSFSZXCR3SQBSR7Y9P53NX61M864S7TEVMJ2XVPF0"; - // const pub_eddsa = eddsaGetPublic(decodeCrock(priv_eddsa)); - const bseed1 = deriveBSeed(decodeCrock(priv_eddsa), rPub); - const bseed2 = deriveBSeed(decodeCrock(priv_eddsa), rPub); - t.deepEqual(bseed1, bseed2); - - /** - * In this scenario the nonce from the test vectors is used as b-seed and refresh. - * This is only used in testing to test functionality. - * DO NOT USE the same values for blinding-seed and nonce anywhere else. - * - * Tests whether the blinding secrets are derived as in the exchange implementation - */ - const bseed = decodeCrock(nonce); - const secrets = deriveSecrets(bseed); - t.deepEqual( - secrets.alpha[0], - decodeCrock("R06FWJ4XEK4JKKKA03JARGD0PD5JAX8DK2N6J0K8CAZZMVQEJ1T0"), - ); - t.deepEqual( - secrets.alpha[1], - decodeCrock("13NXE2FEHJS0Q5XCWNRF4V1NC3BSAHN6BW02WZ07PG6967156HYG"), - ); - t.deepEqual( - secrets.beta[0], - decodeCrock("T3EZP42RJQXRTJ4FTDWF18Z422VX7KFGN8GJ3QCCM1QV3N456HD0"), - ); - t.deepEqual( - secrets.beta[1], - decodeCrock("P3MECYGCCR58QVEDSW443699CDXVT8C8W5ZT22PPNRJ363M72H6G"), - ); - - const rBlind = await calcRBlind(pub, secrets, rPub); - t.deepEqual( - rBlind[0], - decodeCrock("CHK7JC4SXZ4Y9RDA3881S82F7BP99H35Q361WR6RBXN5YN2ZM1M0"), - ); - t.deepEqual( - rBlind[1], - decodeCrock("4C65R74GA9PPDX4DC2B948W96T3Z6QEENK2NDJQPNB9QBTKCT590"), - ); - - const c = await csBlind(bseed, rPub, pub, decodeCrock(msg_hash)); - t.deepEqual( - c[0], - decodeCrock("F288QXT67TR36E6DHE399G8J24RM6C3DP16HGMH74B6WZ1DETR10"), - ); - t.deepEqual( - c[1], - decodeCrock("EFK5WTN01NCVS3DZCG20MQDHRHBATRG8589BA0XSZDZ6D0HFR470"), - ); - - const lMod = Array.from( - new Uint8Array([ - 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x14, 0xde, 0xf9, 0xde, 0xa2, 0xf7, 0x9c, 0xd6, - 0x58, 0x12, 0x63, 0x1a, 0x5c, 0xf5, 0xd3, 0xed, - ]), - ); - const L = bigint.fromArray(lMod, 256, false).toString(); - //Lmod needs to be 2^252+27742317777372353535851937790883648493 - if (!L.startsWith("723700")) { - throw new AssertionError({ message: L }); - } - - const b = 0; - const blindsig: CsBlindSignature = { - sBlind: decodeCrock("6KZF904YZA8KK4C8X5JV57E7B84SR8TDDN9GDC8QTRRSNTHJTM4G"), - rPubBlind: rPub[b], - }; - - const sig = await csUnblind(bseed, rPub, pub, b, blindsig); - t.deepEqual( - sig.s, - decodeCrock("F4ZKMFW3Q7DFN0N94KAMG2JFFHAC362T0QZ6ZCVZ73RS8P91CR70"), - ); - t.deepEqual( - sig.rPub, - decodeCrock("CHK7JC4SXZ4Y9RDA3881S82F7BP99H35Q361WR6RBXN5YN2ZM1M0"), - ); - - const res = await csVerify(decodeCrock(msg_hash), sig, pub); - t.deepEqual(res, true); -}); - -test("bigint/nacl conversion", async (t) => { - const b1 = BigInteger(42); - const n1 = bigintToNaclArr(b1, 32); - t.is(n1[0], 42); - t.is(n1.length, 32); - const b2 = bigintFromNaclArr(n1); - t.true(b1.eq(b2)); -}); - -test("taler age restriction crypto", async (t) => { - const priv1 = await Edx25519.keyCreate(); - const pub1 = await Edx25519.getPublic(priv1); - - const seed = getRandomBytes(32); - - const priv2 = await Edx25519.privateKeyDerive(priv1, seed); - const pub2 = await Edx25519.publicKeyDerive(pub1, seed); - - const pub2Ref = await Edx25519.getPublic(priv2); - - t.deepEqual(pub2, pub2Ref); -}); - -test("edx signing", async (t) => { - const priv1 = await Edx25519.keyCreate(); - const pub1 = await Edx25519.getPublic(priv1); - - const msg = stringToBytes("hello world"); - - const sig = nacl.crypto_edx25519_sign_detached(msg, priv1, pub1); - - t.true(nacl.crypto_edx25519_sign_detached_verify(msg, sig, pub1)); - - sig[0]++; - - t.false(nacl.crypto_edx25519_sign_detached_verify(msg, sig, pub1)); -}); - -test("edx test vector", async (t) => { - // Generated by gnunet-crypto-tvg - const tv = { - operation: "edx25519_derive", - priv1_edx: - "P0JAQ53G66M7TSGQTCFVFMPCBC7WHBRYDZGQXM8VD88C72NJANR07V1DQRAE7KSH92HZ3B62PJVRYFTVFTQM43K5AQD8R4A7HWJ3P7G", - pub1_edx: "4YZ6D5MGWTWCTKY4W931V4S5SW0XG7AD4A60J2Z9CSEB9WE05WB0", - seed: "SQ3YAVGNZ2GYER9VQAJB2M1Z903Y458HYXWBSF9S2A9YKF85R4DHYJX35YXXX82CBGFW2TRBCR1ZCWSQ7A87QW5SHC8WP9JH48P8KK8", - priv2_edx: - "GQ7NCSVNKY0QS7GQVFP2TSG6P4YN1NCK303K5TYXXBKSZ61M3R4XFZ0KA42JND6GBZRXRSJY9EX3HMMY160VQ6Y6H2NZ8H0WVQRCG1R", - pub2_edx: "F5X6379F0FSY87MN9210FAN84PR8KYDJQ5G5784H1N3FY12ZKAPG", - }; - - { - const pub1Prime = await Edx25519.getPublic(decodeCrock(tv.priv1_edx)); - t.deepEqual(pub1Prime, decodeCrock(tv.pub1_edx)); - } - - const pub2Prime = await Edx25519.publicKeyDerive( - decodeCrock(tv.pub1_edx), - decodeCrock(tv.seed), - ); - t.deepEqual(pub2Prime, decodeCrock(tv.pub2_edx)); - - const priv2Prime = await Edx25519.privateKeyDerive( - decodeCrock(tv.priv1_edx), - decodeCrock(tv.seed), - ); - t.deepEqual(priv2Prime, decodeCrock(tv.priv2_edx)); -}); diff --git a/packages/taler-util/src/talerCrypto.ts b/packages/taler-util/src/talerCrypto.ts deleted file mode 100644 index 84842a69f..000000000 --- a/packages/taler-util/src/talerCrypto.ts +++ /dev/null @@ -1,1378 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2019 GNUnet e.V. - - GNU Taler is free software; you can redistribute it and/or modify it under the - terms of the GNU General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - GNU Taler; see the file COPYING. If not, see - */ - -/** - * Native implementation of GNU Taler crypto. - */ - -/** - * Imports. - */ -import * as nacl from "./nacl-fast.js"; -import { kdf, kdfKw } from "./kdf.js"; -import bigint from "big-integer"; -import { - CoinEnvelope, - CoinPublicKeyString, - DenominationPubKey, - DenomKeyType, - HashCodeString, -} from "./talerTypes.js"; -import { Logger } from "./logging.js"; -import { secretbox } from "./nacl-fast.js"; -import * as fflate from "fflate"; -import { canonicalJson } from "./helpers.js"; - -export type Flavor = T & { - _flavor?: `taler.${FlavorT}`; -}; - -export type FlavorP = T & { - _flavor?: `taler.${FlavorT}`; - _size?: S; -}; - -export function getRandomBytes(n: number): Uint8Array { - return nacl.randomBytes(n); -} - -export function getRandomBytesF( - n: T, -): FlavorP { - return nacl.randomBytes(n); -} - -const encTable = "0123456789ABCDEFGHJKMNPQRSTVWXYZ"; - -class EncodingError extends Error { - constructor() { - super("Encoding error"); - Object.setPrototypeOf(this, EncodingError.prototype); - } -} - -function getValue(chr: string): number { - let a = chr; - switch (chr) { - case "O": - case "o": - a = "0;"; - break; - case "i": - case "I": - case "l": - case "L": - a = "1"; - break; - case "u": - case "U": - a = "V"; - } - - if (a >= "0" && a <= "9") { - return a.charCodeAt(0) - "0".charCodeAt(0); - } - - if (a >= "a" && a <= "z") a = a.toUpperCase(); - let dec = 0; - if (a >= "A" && a <= "Z") { - if ("I" < a) dec++; - if ("L" < a) dec++; - if ("O" < a) dec++; - if ("U" < a) dec++; - return a.charCodeAt(0) - "A".charCodeAt(0) + 10 - dec; - } - throw new EncodingError(); -} - -export function encodeCrock(data: ArrayBuffer): string { - const dataBytes = new Uint8Array(data); - let sb = ""; - const size = data.byteLength; - let bitBuf = 0; - let numBits = 0; - let pos = 0; - while (pos < size || numBits > 0) { - if (pos < size && numBits < 5) { - const d = dataBytes[pos++]; - bitBuf = (bitBuf << 8) | d; - numBits += 8; - } - if (numBits < 5) { - // zero-padding - bitBuf = bitBuf << (5 - numBits); - numBits = 5; - } - const v = (bitBuf >>> (numBits - 5)) & 31; - sb += encTable[v]; - numBits -= 5; - } - return sb; -} - -export function decodeCrock(encoded: string): Uint8Array { - const size = encoded.length; - let bitpos = 0; - let bitbuf = 0; - let readPosition = 0; - const outLen = Math.floor((size * 5) / 8); - const out = new Uint8Array(outLen); - let outPos = 0; - - while (readPosition < size || bitpos > 0) { - if (readPosition < size) { - const v = getValue(encoded[readPosition++]); - bitbuf = (bitbuf << 5) | v; - bitpos += 5; - } - while (bitpos >= 8) { - const d = (bitbuf >>> (bitpos - 8)) & 0xff; - out[outPos++] = d; - bitpos -= 8; - } - if (readPosition == size && bitpos > 0) { - bitbuf = (bitbuf << (8 - bitpos)) & 0xff; - bitpos = bitbuf == 0 ? 0 : 8; - } - } - return out; -} - -export function eddsaGetPublic(eddsaPriv: Uint8Array): Uint8Array { - const pair = nacl.crypto_sign_keyPair_fromSeed(eddsaPriv); - return pair.publicKey; -} - -export function ecdheGetPublic(ecdhePriv: Uint8Array): Uint8Array { - return nacl.scalarMult_base(ecdhePriv); -} - -export function keyExchangeEddsaEcdhe( - eddsaPriv: Uint8Array, - ecdhePub: Uint8Array, -): Uint8Array { - const ph = nacl.hash(eddsaPriv); - const a = new Uint8Array(32); - for (let i = 0; i < 32; i++) { - a[i] = ph[i]; - } - const x = nacl.scalarMult(a, ecdhePub); - return nacl.hash(x); -} - -export function keyExchangeEcdheEddsa( - ecdhePriv: Uint8Array & MaterialEcdhePriv, - eddsaPub: Uint8Array & MaterialEddsaPub, -): Uint8Array { - const curve25519Pub = nacl.sign_ed25519_pk_to_curve25519(eddsaPub); - const x = nacl.scalarMult(ecdhePriv, curve25519Pub); - return nacl.hash(x); -} - -interface RsaPub { - N: bigint.BigInteger; - e: bigint.BigInteger; -} - -/** - * KDF modulo a big integer. - */ -function kdfMod( - n: bigint.BigInteger, - ikm: Uint8Array, - salt: Uint8Array, - info: Uint8Array, -): bigint.BigInteger { - const nbits = n.bitLength().toJSNumber(); - const buflen = Math.floor((nbits - 1) / 8 + 1); - const mask = (1 << (8 - (buflen * 8 - nbits))) - 1; - let counter = 0; - while (true) { - const ctx = new Uint8Array(info.byteLength + 2); - ctx.set(info, 0); - ctx[ctx.length - 2] = (counter >>> 8) & 0xff; - ctx[ctx.length - 1] = counter & 0xff; - const buf = kdf(buflen, ikm, salt, ctx); - const arr = Array.from(buf); - arr[0] = arr[0] & mask; - const r = bigint.fromArray(arr, 256, false); - if (r.lt(n)) { - return r; - } - counter++; - } -} - -function csKdfMod( - n: bigint.BigInteger, - ikm: Uint8Array, - salt: Uint8Array, - info: Uint8Array, -): Uint8Array { - const nbits = n.bitLength().toJSNumber(); - const buflen = Math.floor((nbits - 1) / 8 + 1); - const mask = (1 << (8 - (buflen * 8 - nbits))) - 1; - let counter = 0; - while (true) { - const ctx = new Uint8Array(info.byteLength + 2); - ctx.set(info, 0); - ctx[ctx.length - 2] = (counter >>> 8) & 0xff; - ctx[ctx.length - 1] = counter & 0xff; - const buf = kdf(buflen, ikm, salt, ctx); - const arr = Array.from(buf); - arr[0] = arr[0] & mask; - const r = bigint.fromArray(arr, 256, false); - if (r.lt(n)) { - return new Uint8Array(arr); - } - counter++; - } -} - -// Newer versions of node have TextEncoder and TextDecoder as a global, -// just like modern browsers. -// In older versions of node or environments that do not have these -// globals, they must be polyfilled (by adding them to globa/globalThis) -// before stringToBytes or bytesToString is called the first time. - -let encoder: any; -let decoder: any; - -export function stringToBytes(s: string): Uint8Array { - if (!encoder) { - // @ts-ignore - encoder = new TextEncoder(); - } - return encoder.encode(s); -} - -export function bytesToString(b: Uint8Array): string { - if (!decoder) { - // @ts-ignore - decoder = new TextDecoder(); - } - return decoder.decode(b); -} - -function loadBigInt(arr: Uint8Array): bigint.BigInteger { - return bigint.fromArray(Array.from(arr), 256, false); -} - -function rsaBlindingKeyDerive( - rsaPub: RsaPub, - bks: Uint8Array, -): bigint.BigInteger { - const salt = stringToBytes("Blinding KDF extractor HMAC key"); - const info = stringToBytes("Blinding KDF"); - return kdfMod(rsaPub.N, bks, salt, info); -} - -/* - * Test for malicious RSA key. - * - * Assuming n is an RSA modulous and r is generated using a call to - * GNUNET_CRYPTO_kdf_mod_mpi, if gcd(r,n) != 1 then n must be a - * malicious RSA key designed to deanomize the user. - * - * @param r KDF result - * @param n RSA modulus of the public key - */ -function rsaGcdValidate(r: bigint.BigInteger, n: bigint.BigInteger): void { - const t = bigint.gcd(r, n); - if (!t.equals(bigint.one)) { - throw Error("malicious RSA public key"); - } -} - -function rsaFullDomainHash(hm: Uint8Array, rsaPub: RsaPub): bigint.BigInteger { - const info = stringToBytes("RSA-FDA FTpsW!"); - const salt = rsaPubEncode(rsaPub); - const r = kdfMod(rsaPub.N, hm, salt, info); - rsaGcdValidate(r, rsaPub.N); - return r; -} - -function rsaPubDecode(rsaPub: Uint8Array): RsaPub { - const modulusLength = (rsaPub[0] << 8) | rsaPub[1]; - const exponentLength = (rsaPub[2] << 8) | rsaPub[3]; - if (4 + exponentLength + modulusLength != rsaPub.length) { - throw Error("invalid RSA public key (format wrong)"); - } - const modulus = rsaPub.slice(4, 4 + modulusLength); - const exponent = rsaPub.slice( - 4 + modulusLength, - 4 + modulusLength + exponentLength, - ); - const res = { - N: loadBigInt(modulus), - e: loadBigInt(exponent), - }; - return res; -} - -function rsaPubEncode(rsaPub: RsaPub): Uint8Array { - const mb = rsaPub.N.toArray(256).value; - const eb = rsaPub.e.toArray(256).value; - const out = new Uint8Array(4 + mb.length + eb.length); - out[0] = (mb.length >>> 8) & 0xff; - out[1] = mb.length & 0xff; - out[2] = (eb.length >>> 8) & 0xff; - out[3] = eb.length & 0xff; - out.set(mb, 4); - out.set(eb, 4 + mb.length); - return out; -} - -export function rsaBlind( - hm: Uint8Array, - bks: Uint8Array, - rsaPubEnc: Uint8Array, -): Uint8Array { - const rsaPub = rsaPubDecode(rsaPubEnc); - const data = rsaFullDomainHash(hm, rsaPub); - const r = rsaBlindingKeyDerive(rsaPub, bks); - const r_e = r.modPow(rsaPub.e, rsaPub.N); - const bm = r_e.multiply(data).mod(rsaPub.N); - return new Uint8Array(bm.toArray(256).value); -} - -export function rsaUnblind( - sig: Uint8Array, - rsaPubEnc: Uint8Array, - bks: Uint8Array, -): Uint8Array { - const rsaPub = rsaPubDecode(rsaPubEnc); - const blinded_s = loadBigInt(sig); - const r = rsaBlindingKeyDerive(rsaPub, bks); - const r_inv = r.modInv(rsaPub.N); - const s = blinded_s.multiply(r_inv).mod(rsaPub.N); - return new Uint8Array(s.toArray(256).value); -} - -export function rsaVerify( - hm: Uint8Array, - rsaSig: Uint8Array, - rsaPubEnc: Uint8Array, -): boolean { - const rsaPub = rsaPubDecode(rsaPubEnc); - const d = rsaFullDomainHash(hm, rsaPub); - const sig = loadBigInt(rsaSig); - const sig_e = sig.modPow(rsaPub.e, rsaPub.N); - return sig_e.equals(d); -} - -export type CsSignature = { - s: Uint8Array; - rPub: Uint8Array; -}; - -export type CsBlindSignature = { - sBlind: Uint8Array; - rPubBlind: Uint8Array; -}; - -export type CsBlindingSecrets = { - alpha: [Uint8Array, Uint8Array]; - beta: [Uint8Array, Uint8Array]; -}; - -export function typedArrayConcat(chunks: Uint8Array[]): Uint8Array { - let payloadLen = 0; - for (const c of chunks) { - payloadLen += c.byteLength; - } - const buf = new ArrayBuffer(payloadLen); - const u8buf = new Uint8Array(buf); - let p = 0; - for (const c of chunks) { - u8buf.set(c, p); - p += c.byteLength; - } - return u8buf; -} - -/** - * Map to scalar subgroup function - * perform clamping as described in RFC7748 - * @param scalar - */ -function mtoSS(scalar: Uint8Array): Uint8Array { - scalar[0] &= 248; - scalar[31] &= 127; - scalar[31] |= 64; - return scalar; -} - -/** - * The function returns the CS blinding secrets from a seed - * @param bseed seed to derive blinding secrets - * @returns blinding secrets - */ -export function deriveSecrets(bseed: Uint8Array): CsBlindingSecrets { - const outLen = 130; - const salt = stringToBytes("alphabeta"); - const rndout = kdf(outLen, bseed, salt); - const secrets: CsBlindingSecrets = { - alpha: [mtoSS(rndout.slice(0, 32)), mtoSS(rndout.slice(64, 96))], - beta: [mtoSS(rndout.slice(32, 64)), mtoSS(rndout.slice(96, 128))], - }; - return secrets; -} - -/** - * Used for testing, simple scalar multiplication with base point of Ed25519 - * @param s scalar - * @returns new point sG - */ -export async function scalarMultBase25519(s: Uint8Array): Promise { - return nacl.crypto_scalarmult_ed25519_base_noclamp(s); -} - -/** - * calculation of the blinded public point R in CS - * @param csPub denomination publik key - * @param secrets client blinding secrets - * @param rPub public R received from /csr API - */ -export async function calcRBlind( - csPub: Uint8Array, - secrets: CsBlindingSecrets, - rPub: [Uint8Array, Uint8Array], -): Promise<[Uint8Array, Uint8Array]> { - const aG0 = nacl.crypto_scalarmult_ed25519_base_noclamp(secrets.alpha[0]); - const aG1 = nacl.crypto_scalarmult_ed25519_base_noclamp(secrets.alpha[1]); - - const bDp0 = nacl.crypto_scalarmult_ed25519_noclamp(secrets.beta[0], csPub); - const bDp1 = nacl.crypto_scalarmult_ed25519_noclamp(secrets.beta[1], csPub); - - const res0 = nacl.crypto_core_ed25519_add(aG0, bDp0); - const res1 = nacl.crypto_core_ed25519_add(aG1, bDp1); - return [ - nacl.crypto_core_ed25519_add(rPub[0], res0), - nacl.crypto_core_ed25519_add(rPub[1], res1), - ]; -} - -/** - * FDH function used in CS - * @param hm message hash - * @param rPub public R included in FDH - * @param csPub denomination public key as context - * @returns mapped Curve25519 scalar - */ -function csFDH( - hm: Uint8Array, - rPub: Uint8Array, - csPub: Uint8Array, -): Uint8Array { - const lMod = Array.from( - new Uint8Array([ - 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x14, 0xde, 0xf9, 0xde, 0xa2, 0xf7, 0x9c, 0xd6, - 0x58, 0x12, 0x63, 0x1a, 0x5c, 0xf5, 0xd3, 0xed, - ]), - ); - const L = bigint.fromArray(lMod, 256, false); - - const info = stringToBytes("Curve25519FDH"); - const preshash = nacl.hash(typedArrayConcat([rPub, hm])); - return csKdfMod(L, preshash, csPub, info).reverse(); -} - -/** - * blinding seed derived from coin private key - * @param coinPriv private key of the corresponding coin - * @param rPub public R received from /csr API - * @returns blinding seed - */ -export function deriveBSeed( - coinPriv: Uint8Array, - rPub: [Uint8Array, Uint8Array], -): Uint8Array { - const outLen = 32; - const salt = stringToBytes("b-seed"); - const ikm = typedArrayConcat([coinPriv, rPub[0], rPub[1]]); - return kdf(outLen, ikm, salt); -} - -/** - * Derive withdraw nonce, used in /csr request - * Note: In withdraw protocol, the nonce is chosen randomly - * @param coinPriv coin private key - * @returns nonce - */ -export function deriveWithdrawNonce(coinPriv: Uint8Array): Uint8Array { - const outLen = 32; - const salt = stringToBytes("n"); - return kdf(outLen, coinPriv, salt); -} - -/** - * Blind operation for CS signatures, used after /csr call - * @param bseed blinding seed to derive blinding secrets - * @param rPub public R received from /csr - * @param csPub denomination public key - * @param hm message to blind - * @returns two blinded c - */ -export async function csBlind( - bseed: Uint8Array, - rPub: [Uint8Array, Uint8Array], - csPub: Uint8Array, - hm: Uint8Array, -): Promise<[Uint8Array, Uint8Array]> { - const secrets = deriveSecrets(bseed); - const rPubBlind = await calcRBlind(csPub, secrets, rPub); - const c_0 = csFDH(hm, rPubBlind[0], csPub); - const c_1 = csFDH(hm, rPubBlind[1], csPub); - return [ - nacl.crypto_core_ed25519_scalar_add(c_0, secrets.beta[0]), - nacl.crypto_core_ed25519_scalar_add(c_1, secrets.beta[1]), - ]; -} - -/** - * Unblind operation to unblind the signature - * @param bseed seed to derive secrets - * @param rPub public R received from /csr - * @param csPub denomination publick key - * @param b returned from exchange to select c - * @param csSig blinded signature - * @returns unblinded signature - */ -export async function csUnblind( - bseed: Uint8Array, - rPub: [Uint8Array, Uint8Array], - csPub: Uint8Array, - b: number, - csSig: CsBlindSignature, -): Promise { - if (b != 0 && b != 1) { - throw new Error(); - } - const secrets = deriveSecrets(bseed); - const rPubDash = (await calcRBlind(csPub, secrets, rPub))[b]; - const sig: CsSignature = { - s: nacl.crypto_core_ed25519_scalar_add(csSig.sBlind, secrets.alpha[b]), - rPub: rPubDash, - }; - return sig; -} - -/** - * Verification algorithm for CS signatures - * @param hm message signed - * @param csSig unblinded signature - * @param csPub denomination publick key - * @returns true if valid, false if invalid - */ -export async function csVerify( - hm: Uint8Array, - csSig: CsSignature, - csPub: Uint8Array, -): Promise { - const cDash = csFDH(hm, csSig.rPub, csPub); - const sG = nacl.crypto_scalarmult_ed25519_base_noclamp(csSig.s); - const cbDp = nacl.crypto_scalarmult_ed25519_noclamp(cDash, csPub); - const sGeq = nacl.crypto_core_ed25519_add(csSig.rPub, cbDp); - return nacl.verify(sG, sGeq); -} - -export interface EddsaKeyPair { - eddsaPub: Uint8Array; - eddsaPriv: Uint8Array; -} - -export interface EcdheKeyPair { - ecdhePub: Uint8Array; - ecdhePriv: Uint8Array; -} - -export interface Edx25519Keypair { - edxPub: string; - edxPriv: string; -} - -export function createEddsaKeyPair(): EddsaKeyPair { - const eddsaPriv = nacl.randomBytes(32); - const eddsaPub = eddsaGetPublic(eddsaPriv); - return { eddsaPriv, eddsaPub }; -} - -export function createEcdheKeyPair(): EcdheKeyPair { - const ecdhePriv = nacl.randomBytes(32); - const ecdhePub = ecdheGetPublic(ecdhePriv); - return { ecdhePriv, ecdhePub }; -} - -export function hash(d: Uint8Array): Uint8Array { - return nacl.hash(d); -} - -/** - * Hash the input with SHA-512 and truncate the result - * to 32 bytes. - */ -export function hashTruncate32(d: Uint8Array): Uint8Array { - const sha512HashCode = nacl.hash(d); - return sha512HashCode.subarray(0, 32); -} - -export function hashCoinEv( - coinEv: CoinEnvelope, - denomPubHash: HashCodeString, -): Uint8Array { - const hashContext = createHashContext(); - hashContext.update(decodeCrock(denomPubHash)); - hashCoinEvInner(coinEv, hashContext); - return hashContext.finish(); -} - -const logger = new Logger("talerCrypto.ts"); - -export function hashCoinEvInner( - coinEv: CoinEnvelope, - hashState: nacl.HashState, -): void { - const hashInputBuf = new ArrayBuffer(4); - const uint8ArrayBuf = new Uint8Array(hashInputBuf); - const dv = new DataView(hashInputBuf); - dv.setUint32(0, DenomKeyType.toIntTag(coinEv.cipher)); - hashState.update(uint8ArrayBuf); - switch (coinEv.cipher) { - case DenomKeyType.Rsa: - hashState.update(decodeCrock(coinEv.rsa_blinded_planchet)); - return; - default: - throw new Error(); - } -} - -export function hashCoinPub( - coinPub: CoinPublicKeyString, - ach?: HashCodeString, -): Uint8Array { - if (!ach) { - return hash(decodeCrock(coinPub)); - } - - return hash(typedArrayConcat([decodeCrock(coinPub), decodeCrock(ach)])); -} - -/** - * Hash a denomination public key. - */ -export function hashDenomPub(pub: DenominationPubKey): Uint8Array { - if (pub.cipher === DenomKeyType.Rsa) { - const pubBuf = decodeCrock(pub.rsa_public_key); - const hashInputBuf = new ArrayBuffer(pubBuf.length + 4 + 4); - const uint8ArrayBuf = new Uint8Array(hashInputBuf); - const dv = new DataView(hashInputBuf); - dv.setUint32(0, pub.age_mask ?? 0); - dv.setUint32(4, DenomKeyType.toIntTag(pub.cipher)); - uint8ArrayBuf.set(pubBuf, 8); - return nacl.hash(uint8ArrayBuf); - } else if (pub.cipher === DenomKeyType.ClauseSchnorr) { - const pubBuf = decodeCrock(pub.cs_public_key); - const hashInputBuf = new ArrayBuffer(pubBuf.length + 4 + 4); - const uint8ArrayBuf = new Uint8Array(hashInputBuf); - const dv = new DataView(hashInputBuf); - dv.setUint32(0, pub.age_mask ?? 0); - dv.setUint32(4, DenomKeyType.toIntTag(pub.cipher)); - uint8ArrayBuf.set(pubBuf, 8); - return nacl.hash(uint8ArrayBuf); - } else { - throw Error( - `unsupported cipher (${ - (pub as DenominationPubKey).cipher - }), unable to hash`, - ); - } -} - -export function eddsaSign(msg: Uint8Array, eddsaPriv: Uint8Array): Uint8Array { - const pair = nacl.crypto_sign_keyPair_fromSeed(eddsaPriv); - return nacl.sign_detached(msg, pair.secretKey); -} - -export function eddsaVerify( - msg: Uint8Array, - sig: Uint8Array, - eddsaPub: Uint8Array, -): boolean { - return nacl.sign_detached_verify(msg, sig, eddsaPub); -} - -export function createHashContext(): nacl.HashState { - return new nacl.HashState(); -} - -export interface FreshCoin { - coinPub: Uint8Array; - coinPriv: Uint8Array; - bks: Uint8Array; - maxAge: number; - ageCommitmentProof: AgeCommitmentProof | undefined; -} - -export function bufferForUint32(n: number): Uint8Array { - const arrBuf = new ArrayBuffer(4); - const buf = new Uint8Array(arrBuf); - const dv = new DataView(arrBuf); - dv.setUint32(0, n); - return buf; -} - -export function bufferForUint8(n: number): Uint8Array { - const arrBuf = new ArrayBuffer(1); - const buf = new Uint8Array(arrBuf); - const dv = new DataView(arrBuf); - dv.setUint8(0, n); - return buf; -} - -export async function setupTipPlanchet( - secretSeed: Uint8Array, - denomPub: DenominationPubKey, - coinNumber: number, -): Promise { - const info = stringToBytes("taler-tip-coin-derivation"); - const saltArrBuf = new ArrayBuffer(4); - const salt = new Uint8Array(saltArrBuf); - const saltDataView = new DataView(saltArrBuf); - saltDataView.setUint32(0, coinNumber); - const out = kdf(64, secretSeed, salt, info); - const coinPriv = out.slice(0, 32); - const bks = out.slice(32, 64); - let maybeAcp: AgeCommitmentProof | undefined; - if (denomPub.age_mask != 0) { - maybeAcp = await AgeRestriction.restrictionCommitSeeded( - denomPub.age_mask, - AgeRestriction.AGE_UNRESTRICTED, - secretSeed, - ); - } - return { - bks, - coinPriv, - coinPub: eddsaGetPublic(coinPriv), - maxAge: AgeRestriction.AGE_UNRESTRICTED, - ageCommitmentProof: maybeAcp, - }; -} -/** - * - * @param paytoUri - * @param salt 16-byte salt - * @returns - */ -export function hashWire(paytoUri: string, salt: string): string { - const r = kdf( - 64, - stringToBytes(paytoUri + "\0"), - decodeCrock(salt), - stringToBytes("merchant-wire-signature"), - ); - return encodeCrock(r); -} - -export enum TalerSignaturePurpose { - MERCHANT_TRACK_TRANSACTION = 1103, - WALLET_RESERVE_WITHDRAW = 1200, - WALLET_COIN_DEPOSIT = 1201, - GLOBAL_FEES = 1022, - MASTER_DENOMINATION_KEY_VALIDITY = 1025, - MASTER_WIRE_FEES = 1028, - MASTER_WIRE_DETAILS = 1030, - WALLET_COIN_MELT = 1202, - TEST = 4242, - MERCHANT_PAYMENT_OK = 1104, - MERCHANT_CONTRACT = 1101, - WALLET_COIN_RECOUP = 1203, - WALLET_COIN_LINK = 1204, - WALLET_COIN_RECOUP_REFRESH = 1206, - WALLET_AGE_ATTESTATION = 1207, - WALLET_PURSE_CREATE = 1210, - WALLET_PURSE_DEPOSIT = 1211, - WALLET_PURSE_MERGE = 1213, - WALLET_ACCOUNT_MERGE = 1214, - WALLET_PURSE_ECONTRACT = 1216, - EXCHANGE_CONFIRM_RECOUP = 1039, - EXCHANGE_CONFIRM_RECOUP_REFRESH = 1041, - ANASTASIS_POLICY_UPLOAD = 1400, - ANASTASIS_POLICY_DOWNLOAD = 1401, - SYNC_BACKUP_UPLOAD = 1450, -} - -export const enum WalletAccountMergeFlags { - /** - * Not a legal mode! - */ - None = 0, - - /** - * We are merging a fully paid-up purse into a reserve. - */ - MergeFullyPaidPurse = 1, - - CreateFromPurseQuota = 2, - - CreateWithPurseFee = 3, -} - -export class SignaturePurposeBuilder { - private chunks: Uint8Array[] = []; - - constructor(private purposeNum: number) {} - - put(bytes: Uint8Array): SignaturePurposeBuilder { - this.chunks.push(Uint8Array.from(bytes)); - return this; - } - - build(): Uint8Array { - let payloadLen = 0; - for (const c of this.chunks) { - payloadLen += c.byteLength; - } - const buf = new ArrayBuffer(4 + 4 + payloadLen); - const u8buf = new Uint8Array(buf); - let p = 8; - for (const c of this.chunks) { - u8buf.set(c, p); - p += c.byteLength; - } - const dvbuf = new DataView(buf); - dvbuf.setUint32(0, payloadLen + 4 + 4); - dvbuf.setUint32(4, this.purposeNum); - return u8buf; - } -} - -export function buildSigPS(purposeNum: number): SignaturePurposeBuilder { - return new SignaturePurposeBuilder(purposeNum); -} - -export type OpaqueData = Flavor; -export type Edx25519PublicKey = FlavorP; -export type Edx25519PrivateKey = FlavorP; -export type Edx25519Signature = FlavorP; - -export type Edx25519PublicKeyEnc = FlavorP; -export type Edx25519PrivateKeyEnc = FlavorP< - string, - "Edx25519PrivateKeyEnc", - 64 ->; - -/** - * Convert a big integer to a fixed-size, little-endian array. - */ -export function bigintToNaclArr( - x: bigint.BigInteger, - size: number, -): Uint8Array { - const byteArr = new Uint8Array(size); - const arr = x.toArray(256).value.reverse(); - byteArr.set(arr, 0); - return byteArr; -} - -export function bigintFromNaclArr(arr: Uint8Array): bigint.BigInteger { - let rev = new Uint8Array(arr); - rev = rev.reverse(); - return bigint.fromArray(Array.from(rev), 256, false); -} - -export namespace Edx25519 { - const revL = [ - 0xed, 0xd3, 0xf5, 0x5c, 0x1a, 0x63, 0x12, 0x58, 0xd6, 0x9c, 0xf7, 0xa2, - 0xde, 0xf9, 0xde, 0x14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x10, - ]; - - const L = bigint.fromArray(revL.reverse(), 256, false); - - export async function keyCreateFromSeed( - seed: OpaqueData, - ): Promise { - return nacl.crypto_edx25519_private_key_create_from_seed(seed); - } - - export async function keyCreate(): Promise { - return nacl.crypto_edx25519_private_key_create(); - } - - export async function getPublic( - priv: Edx25519PrivateKey, - ): Promise { - return nacl.crypto_edx25519_get_public(priv); - } - - export function sign( - msg: OpaqueData, - key: Edx25519PrivateKey, - ): Promise { - throw Error("not implemented"); - } - - async function deriveFactor( - pub: Edx25519PublicKey, - seed: OpaqueData, - ): Promise { - const res = kdfKw({ - outputLength: 64, - salt: seed, - ikm: pub, - info: stringToBytes("edx25519-derivation"), - }); - - return res; - } - - export async function privateKeyDerive( - priv: Edx25519PrivateKey, - seed: OpaqueData, - ): Promise { - const pub = await getPublic(priv); - const privDec = priv; - const a = bigintFromNaclArr(privDec.subarray(0, 32)); - const factorEnc = await deriveFactor(pub, seed); - const factorModL = bigintFromNaclArr(factorEnc).mod(L); - - const aPrime = a.divide(8).multiply(factorModL).mod(L).multiply(8).mod(L); - const bPrime = nacl - .hash(typedArrayConcat([privDec.subarray(32, 64), factorEnc])) - .subarray(0, 32); - - const newPriv = typedArrayConcat([bigintToNaclArr(aPrime, 32), bPrime]); - - return newPriv; - } - - export async function publicKeyDerive( - pub: Edx25519PublicKey, - seed: OpaqueData, - ): Promise { - const factorEnc = await deriveFactor(pub, seed); - const factorReduced = nacl.crypto_core_ed25519_scalar_reduce(factorEnc); - const res = nacl.crypto_scalarmult_ed25519_noclamp(factorReduced, pub); - return res; - } -} - -export interface AgeCommitment { - mask: number; - - /** - * Public keys, one for each age group specified in the age mask. - */ - publicKeys: Edx25519PublicKeyEnc[]; -} - -export interface AgeProof { - /** - * Private keys. Typically smaller than the number of public keys, - * because we drop private keys from age groups that are restricted. - */ - privateKeys: Edx25519PrivateKeyEnc[]; -} - -export interface AgeCommitmentProof { - commitment: AgeCommitment; - proof: AgeProof; -} - -function invariant(cond: boolean): asserts cond { - if (!cond) { - throw Error("invariant failed"); - } -} - -export namespace AgeRestriction { - /** - * Smallest age value that the protocol considers "unrestricted". - */ - export const AGE_UNRESTRICTED = 32; - - export function hashCommitment(ac: AgeCommitment): HashCodeString { - const hc = new nacl.HashState(); - for (const pub of ac.publicKeys) { - hc.update(decodeCrock(pub)); - } - return encodeCrock(hc.finish().subarray(0, 32)); - } - - export function countAgeGroups(mask: number): number { - let count = 0; - let m = mask; - while (m > 0) { - count += m & 1; - m = m >> 1; - } - return count; - } - - export function getAgeGroupIndex(mask: number, age: number): number { - invariant((mask & 1) === 1); - let i = 0; - let m = mask; - let a = age; - while (m > 0) { - if (a <= 0) { - break; - } - m = m >> 1; - i += m & 1; - a--; - } - return i; - } - - export function ageGroupSpecToMask(ageGroupSpec: string): number { - throw Error("not implemented"); - } - - export async function restrictionCommit( - ageMask: number, - age: number, - ): Promise { - invariant((ageMask & 1) === 1); - const numPubs = countAgeGroups(ageMask) - 1; - const numPrivs = getAgeGroupIndex(ageMask, age); - - const pubs: Edx25519PublicKey[] = []; - const privs: Edx25519PrivateKey[] = []; - - for (let i = 0; i < numPubs; i++) { - const priv = await Edx25519.keyCreate(); - const pub = await Edx25519.getPublic(priv); - pubs.push(pub); - if (i < numPrivs) { - privs.push(priv); - } - } - - return { - commitment: { - mask: ageMask, - publicKeys: pubs.map((x) => encodeCrock(x)), - }, - proof: { - privateKeys: privs.map((x) => encodeCrock(x)), - }, - }; - } - - export async function restrictionCommitSeeded( - ageMask: number, - age: number, - seed: Uint8Array, - ): Promise { - invariant((ageMask & 1) === 1); - const numPubs = countAgeGroups(ageMask) - 1; - const numPrivs = getAgeGroupIndex(ageMask, age); - - const pubs: Edx25519PublicKey[] = []; - const privs: Edx25519PrivateKey[] = []; - - for (let i = 0; i < numPubs; i++) { - const privSeed = await kdfKw({ - outputLength: 32, - ikm: seed, - info: stringToBytes("age-restriction-commit"), - salt: bufferForUint32(i), - }); - const priv = await Edx25519.keyCreateFromSeed(privSeed); - const pub = await Edx25519.getPublic(priv); - pubs.push(pub); - if (i < numPrivs) { - privs.push(priv); - } - } - - return { - commitment: { - mask: ageMask, - publicKeys: pubs.map((x) => encodeCrock(x)), - }, - proof: { - privateKeys: privs.map((x) => encodeCrock(x)), - }, - }; - } - - /** - * Check that c1 = c2*salt - */ - export async function commitCompare( - c1: AgeCommitment, - c2: AgeCommitment, - salt: OpaqueData, - ): Promise { - if (c1.publicKeys.length != c2.publicKeys.length) { - return false; - } - for (let i = 0; i < c1.publicKeys.length; i++) { - const k1 = decodeCrock(c1.publicKeys[i]); - const k2 = await Edx25519.publicKeyDerive( - decodeCrock(c2.publicKeys[i]), - salt, - ); - if (k1 != k2) { - return false; - } - } - return true; - } - - export async function commitmentDerive( - commitmentProof: AgeCommitmentProof, - salt: OpaqueData, - ): Promise { - const newPrivs: Edx25519PrivateKey[] = []; - const newPubs: Edx25519PublicKey[] = []; - - for (const oldPub of commitmentProof.commitment.publicKeys) { - newPubs.push(await Edx25519.publicKeyDerive(decodeCrock(oldPub), salt)); - } - - for (const oldPriv of commitmentProof.proof.privateKeys) { - newPrivs.push( - await Edx25519.privateKeyDerive(decodeCrock(oldPriv), salt), - ); - } - - return { - commitment: { - mask: commitmentProof.commitment.mask, - publicKeys: newPubs.map((x) => encodeCrock(x)), - }, - proof: { - privateKeys: newPrivs.map((x) => encodeCrock(x)), - }, - }; - } - - export function commitmentAttest( - commitmentProof: AgeCommitmentProof, - age: number, - ): Edx25519Signature { - const d = buildSigPS(TalerSignaturePurpose.WALLET_AGE_ATTESTATION) - .put(bufferForUint32(commitmentProof.commitment.mask)) - .put(bufferForUint32(age)) - .build(); - const group = getAgeGroupIndex(commitmentProof.commitment.mask, age); - if (group === 0) { - // No attestation required. - return new Uint8Array(64); - } - const priv = commitmentProof.proof.privateKeys[group - 1]; - const pub = commitmentProof.commitment.publicKeys[group - 1]; - const sig = nacl.crypto_edx25519_sign_detached( - d, - decodeCrock(priv), - decodeCrock(pub), - ); - return sig; - } - - export function commitmentVerify( - commitment: AgeCommitment, - sig: string, - age: number, - ): boolean { - const d = buildSigPS(TalerSignaturePurpose.WALLET_AGE_ATTESTATION) - .put(bufferForUint32(commitment.mask)) - .put(bufferForUint32(age)) - .build(); - const group = getAgeGroupIndex(commitment.mask, age); - if (group === 0) { - // No attestation required. - return true; - } - const pub = commitment.publicKeys[group - 1]; - return nacl.crypto_edx25519_sign_detached_verify( - d, - decodeCrock(sig), - decodeCrock(pub), - ); - } -} - -// FIXME: make it a branded type! -type EncryptionNonce = FlavorP; - -async function deriveKey( - keySeed: OpaqueData, - nonce: EncryptionNonce, - salt: string, -): Promise { - return kdfKw({ - outputLength: 32, - salt: nonce, - ikm: keySeed, - info: stringToBytes(salt), - }); -} - -async function encryptWithDerivedKey( - nonce: EncryptionNonce, - keySeed: OpaqueData, - plaintext: OpaqueData, - salt: string, -): Promise { - const key = await deriveKey(keySeed, nonce, salt); - const cipherText = secretbox(plaintext, nonce, key); - return typedArrayConcat([nonce, cipherText]); -} - -const nonceSize = 24; - -async function decryptWithDerivedKey( - ciphertext: OpaqueData, - keySeed: OpaqueData, - salt: string, -): Promise { - const ctBuf = ciphertext; - const nonceBuf = ctBuf.slice(0, nonceSize); - const enc = ctBuf.slice(nonceSize); - const key = await deriveKey(keySeed, nonceBuf, salt); - const clearText = nacl.secretbox_open(enc, nonceBuf, key); - if (!clearText) { - throw Error("could not decrypt"); - } - return clearText; -} - -enum ContractFormatTag { - PaymentOffer = 0, - PaymentRequest = 1, -} - -type MaterialEddsaPub = { - _materialType?: "eddsa-pub"; - _size?: 32; -}; - -type MaterialEddsaPriv = { - _materialType?: "ecdhe-priv"; - _size?: 32; -}; - -type MaterialEcdhePub = { - _materialType?: "ecdhe-pub"; - _size?: 32; -}; - -type MaterialEcdhePriv = { - _materialType?: "ecdhe-priv"; - _size?: 32; -}; - -type PursePublicKey = FlavorP & - MaterialEddsaPub; - -type ContractPrivateKey = FlavorP & - MaterialEcdhePriv; - -type MergePrivateKey = FlavorP & - MaterialEddsaPriv; - -const mergeSalt = "p2p-merge-contract"; -const depositSalt = "p2p-deposit-contract"; - -export function encryptContractForMerge( - pursePub: PursePublicKey, - contractPriv: ContractPrivateKey, - mergePriv: MergePrivateKey, - contractTerms: any, -): Promise { - const contractTermsCanon = canonicalJson(contractTerms) + "\0"; - const contractTermsBytes = stringToBytes(contractTermsCanon); - const contractTermsCompressed = fflate.zlibSync(contractTermsBytes); - const data = typedArrayConcat([ - bufferForUint32(ContractFormatTag.PaymentOffer), - bufferForUint32(contractTermsBytes.length), - mergePriv, - contractTermsCompressed, - ]); - const key = keyExchangeEcdheEddsa(contractPriv, pursePub); - return encryptWithDerivedKey(getRandomBytesF(24), key, data, mergeSalt); -} - -export function encryptContractForDeposit( - pursePub: PursePublicKey, - contractPriv: ContractPrivateKey, - contractTerms: any, -): Promise { - const contractTermsCanon = canonicalJson(contractTerms) + "\0"; - const contractTermsBytes = stringToBytes(contractTermsCanon); - const contractTermsCompressed = fflate.zlibSync(contractTermsBytes); - const data = typedArrayConcat([ - bufferForUint32(ContractFormatTag.PaymentRequest), - bufferForUint32(contractTermsBytes.length), - contractTermsCompressed, - ]); - const key = keyExchangeEcdheEddsa(contractPriv, pursePub); - return encryptWithDerivedKey(getRandomBytesF(24), key, data, depositSalt); -} - -export interface DecryptForMergeResult { - contractTerms: any; - mergePriv: Uint8Array; -} - -export interface DecryptForDepositResult { - contractTerms: any; -} - -export async function decryptContractForMerge( - enc: OpaqueData, - pursePub: PursePublicKey, - contractPriv: ContractPrivateKey, -): Promise { - const key = keyExchangeEcdheEddsa(contractPriv, pursePub); - const dec = await decryptWithDerivedKey(enc, key, mergeSalt); - const mergePriv = dec.slice(8, 8 + 32); - const contractTermsCompressed = dec.slice(8 + 32); - const contractTermsBuf = fflate.unzlibSync(contractTermsCompressed); - // Slice of the '\0' at the end and decode to a string - const contractTermsString = bytesToString( - contractTermsBuf.slice(0, contractTermsBuf.length - 1), - ); - return { - mergePriv: mergePriv, - contractTerms: JSON.parse(contractTermsString), - }; -} - -export async function decryptContractForDeposit( - enc: OpaqueData, - pursePub: PursePublicKey, - contractPriv: ContractPrivateKey, -): Promise { - const key = keyExchangeEcdheEddsa(contractPriv, pursePub); - const dec = await decryptWithDerivedKey(enc, key, depositSalt); - const contractTermsCompressed = dec.slice(8); - const contractTermsBuf = fflate.unzlibSync(contractTermsCompressed); - // Slice of the '\0' at the end and decode to a string - const contractTermsString = bytesToString( - contractTermsBuf.slice(0, contractTermsBuf.length - 1), - ); - return { - contractTerms: JSON.parse(contractTermsString), - }; -} diff --git a/packages/taler-util/src/talerTypes.ts b/packages/taler-util/src/talerTypes.ts deleted file mode 100644 index 1cb4e2bde..000000000 --- a/packages/taler-util/src/talerTypes.ts +++ /dev/null @@ -1,2028 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2019 GNUnet e.V. - - GNU Taler is free software; you can redistribute it and/or modify it under the - terms of the GNU General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - GNU Taler; see the file COPYING. If not, see - */ - -/** - * Type and schema definitions and helpers for the core GNU Taler protocol. - * - * Even though the rest of the wallet uses camelCase for fields, use snake_case - * here, since that's the convention for the Taler JSON+HTTP API. - */ - -/** - * Imports. - */ - -import { codecForAmountString } from "./amounts.js"; -import { - buildCodecForObject, - buildCodecForUnion, - Codec, - codecForAny, - codecForBoolean, - codecForConstNumber, - codecForConstString, - codecForList, - codecForMap, - codecForNumber, - codecForString, - codecOptional, -} from "./codec.js"; -import { strcmp } from "./helpers.js"; -import { AgeCommitmentProof, Edx25519PublicKeyEnc } from "./talerCrypto.js"; -import { - codecForAbsoluteTime, - codecForDuration, - codecForTimestamp, - TalerProtocolDuration, - TalerProtocolTimestamp, -} from "./time.js"; - -/** - * Denomination as found in the /keys response from the exchange. - */ -export class ExchangeDenomination { - /** - * Value of one coin of the denomination. - */ - value: string; - - /** - * Public signing key of the denomination. - */ - denom_pub: DenominationPubKey; - - /** - * Fee for withdrawing. - */ - fee_withdraw: string; - - /** - * Fee for depositing. - */ - fee_deposit: string; - - /** - * Fee for refreshing. - */ - fee_refresh: string; - - /** - * Fee for refunding. - */ - fee_refund: string; - - /** - * Start date from which withdraw is allowed. - */ - stamp_start: TalerProtocolTimestamp; - - /** - * End date for withdrawing. - */ - stamp_expire_withdraw: TalerProtocolTimestamp; - - /** - * Expiration date after which the exchange can forget about - * the currency. - */ - stamp_expire_legal: TalerProtocolTimestamp; - - /** - * Date after which the coins of this denomination can't be - * deposited anymore. - */ - stamp_expire_deposit: TalerProtocolTimestamp; - - /** - * Signature over the denomination information by the exchange's master - * signing key. - */ - master_sig: string; -} - -/** - * Signature by the auditor that a particular denomination key is audited. - */ -export class AuditorDenomSig { - /** - * Denomination public key's hash. - */ - denom_pub_h: string; - - /** - * The signature. - */ - auditor_sig: string; -} - -/** - * Auditor information as given by the exchange in /keys. - */ -export class ExchangeAuditor { - /** - * Auditor's public key. - */ - auditor_pub: string; - - /** - * Base URL of the auditor. - */ - auditor_url: string; - - /** - * List of signatures for denominations by the auditor. - */ - denomination_keys: AuditorDenomSig[]; -} - -export type ExchangeWithdrawValue = - | ExchangeRsaWithdrawValue - | ExchangeCsWithdrawValue; - -export interface ExchangeRsaWithdrawValue { - cipher: "RSA"; -} - -export interface ExchangeCsWithdrawValue { - cipher: "CS"; - - /** - * CSR R0 value - */ - r_pub_0: string; - - /** - * CSR R1 value - */ - r_pub_1: string; -} - -export interface RecoupRequest { - /** - * Hashed denomination public key of the coin we want to get - * paid back. - */ - denom_pub_hash: string; - - /** - * Signature over the coin public key by the denomination. - * - * The string variant is for the legacy exchange protocol. - */ - denom_sig: UnblindedSignature; - - /** - * Blinding key that was used during withdraw, - * used to prove that we were actually withdrawing the coin. - */ - coin_blind_key_secret: string; - - /** - * Signature of TALER_RecoupRequestPS created with the coin's private key. - */ - coin_sig: string; - - ewv: ExchangeWithdrawValue; -} - -export interface RecoupRefreshRequest { - /** - * Hashed enomination public key of the coin we want to get - * paid back. - */ - denom_pub_hash: string; - - /** - * Signature over the coin public key by the denomination. - * - * The string variant is for the legacy exchange protocol. - */ - denom_sig: UnblindedSignature; - - /** - * Coin's blinding factor. - */ - coin_blind_key_secret: string; - - /** - * Signature of TALER_RecoupRefreshRequestPS created with - * the coin's private key. - */ - coin_sig: string; - - ewv: ExchangeWithdrawValue; -} - -/** - * Response that we get from the exchange for a payback request. - */ -export interface RecoupConfirmation { - /** - * Public key of the reserve that will receive the payback. - */ - reserve_pub?: string; - - /** - * Public key of the old coin that will receive the recoup, - * provided if refreshed was true. - */ - old_coin_pub?: string; -} - -export type UnblindedSignature = RsaUnblindedSignature; - -export interface RsaUnblindedSignature { - cipher: DenomKeyType.Rsa; - rsa_signature: string; -} - -/** - * Deposit permission for a single coin. - */ -export interface CoinDepositPermission { - /** - * Signature by the coin. - */ - coin_sig: string; - - /** - * Public key of the coin being spend. - */ - coin_pub: string; - - /** - * Signature made by the denomination public key. - * - * The string variant is for legacy protocol support. - */ - - ub_sig: UnblindedSignature; - - /** - * The denomination public key associated with this coin. - */ - h_denom: string; - - /** - * The amount that is subtracted from this coin with this payment. - */ - contribution: string; - - /** - * URL of the exchange this coin was withdrawn from. - */ - exchange_url: string; - - minimum_age_sig?: EddsaSignatureString; - - age_commitment?: Edx25519PublicKeyEnc[]; -} - -/** - * Information about an exchange as stored inside a - * merchant's contract terms. - */ -export interface ExchangeHandle { - /** - * Master public signing key of the exchange. - */ - master_pub: string; - - /** - * Base URL of the exchange. - */ - url: string; -} - -export interface AuditorHandle { - /** - * Official name of the auditor. - */ - name: string; - - /** - * Master public signing key of the auditor. - */ - auditor_pub: string; - - /** - * Base URL of the auditor. - */ - url: string; -} - -// Delivery location, loosely modeled as a subset of -// ISO20022's PostalAddress25. -export interface Location { - // Nation with its own government. - country?: string; - - // Identifies a subdivision of a country such as state, region, county. - country_subdivision?: string; - - // Identifies a subdivision within a country sub-division. - district?: string; - - // Name of a built-up area, with defined boundaries, and a local government. - town?: string; - - // Specific location name within the town. - town_location?: string; - - // Identifier consisting of a group of letters and/or numbers that - // is added to a postal address to assist the sorting of mail. - post_code?: string; - - // Name of a street or thoroughfare. - street?: string; - - // Name of the building or house. - building_name?: string; - - // Number that identifies the position of a building on a street. - building_number?: string; - - // Free-form address lines, should not exceed 7 elements. - address_lines?: string[]; -} - -export interface MerchantInfo { - name: string; - jurisdiction?: Location; - address?: Location; - logo?: string; - website?: string; - email?: string; -} - -export interface Tax { - // the name of the tax - name: string; - - // amount paid in tax - tax: AmountString; -} - -export interface Product { - // merchant-internal identifier for the product. - product_id?: string; - - // Human-readable product description. - description: string; - - // Map from IETF BCP 47 language tags to localized descriptions - description_i18n?: { [lang_tag: string]: string }; - - // The number of units of the product to deliver to the customer. - quantity?: number; - - // The unit in which the product is measured (liters, kilograms, packages, etc.) - unit?: string; - - // The price of the product; this is the total price for quantity times unit of this product. - price?: AmountString; - - // An optional base64-encoded product image - image?: string; - - // a list of taxes paid by the merchant for this product. Can be empty. - taxes?: Tax[]; - - // time indicating when this product should be delivered - delivery_date?: TalerProtocolTimestamp; -} - -export interface InternationalizedString { - [lang_tag: string]: string; -} - -/** - * Contract terms from a merchant. - */ -export interface ContractTerms { - /** - * Hash of the merchant's wire details. - */ - h_wire: string; - - /** - * Hash of the merchant's wire details. - */ - auto_refund?: TalerProtocolDuration; - - /** - * Wire method the merchant wants to use. - */ - wire_method: string; - - /** - * Human-readable short summary of the contract. - */ - summary: string; - - summary_i18n?: InternationalizedString; - - /** - * Nonce used to ensure freshness. - */ - nonce: string; - - /** - * Total amount payable. - */ - amount: string; - - /** - * Auditors accepted by the merchant. - */ - auditors: AuditorHandle[]; - - /** - * Deadline to pay for the contract. - */ - pay_deadline: TalerProtocolTimestamp; - - /** - * Maximum deposit fee covered by the merchant. - */ - max_fee: string; - - /** - * Information about the merchant. - */ - merchant: MerchantInfo; - - /** - * Public key of the merchant. - */ - merchant_pub: string; - - /** - * Time indicating when the order should be delivered. - * May be overwritten by individual products. - */ - delivery_date?: TalerProtocolTimestamp; - - /** - * Delivery location for (all!) products. - */ - delivery_location?: Location; - - /** - * List of accepted exchanges. - */ - exchanges: ExchangeHandle[]; - - /** - * Products that are sold in this contract. - */ - products?: Product[]; - - /** - * Deadline for refunds. - */ - refund_deadline: TalerProtocolTimestamp; - - /** - * Deadline for the wire transfer. - */ - wire_transfer_deadline: TalerProtocolTimestamp; - - /** - * Time when the contract was generated by the merchant. - */ - timestamp: TalerProtocolTimestamp; - - /** - * Order id to uniquely identify the purchase within - * one merchant instance. - */ - order_id: string; - - /** - * Base URL of the merchant's backend. - */ - merchant_base_url: string; - - /** - * Fulfillment URL to view the product or - * delivery status. - */ - fulfillment_url?: string; - - /** - * URL meant to share the shopping cart. - */ - public_reorder_url?: string; - - /** - * Plain text fulfillment message in the merchant's default language. - */ - fulfillment_message?: string; - - /** - * Internationalized fulfillment messages. - */ - fulfillment_message_i18n?: InternationalizedString; - - /** - * Share of the wire fee that must be settled with one payment. - */ - wire_fee_amortization?: number; - - /** - * Maximum wire fee that the merchant agrees to pay for. - */ - max_wire_fee?: string; - - minimum_age?: number; - - /** - * Extra data, interpreted by the mechant only. - */ - extra?: any; -} - -/** - * Refund permission in the format that the merchant gives it to us. - */ -export interface MerchantAbortPayRefundDetails { - /** - * Amount to be refunded. - */ - refund_amount: string; - - /** - * Fee for the refund. - */ - refund_fee: string; - - /** - * Public key of the coin being refunded. - */ - coin_pub: string; - - /** - * Refund transaction ID between merchant and exchange. - */ - rtransaction_id: number; - - /** - * Exchange's key used for the signature. - */ - exchange_pub?: string; - - /** - * Exchange's signature to confirm the refund. - */ - exchange_sig?: string; - - /** - * Error replay from the exchange (if any). - */ - exchange_reply?: any; - - /** - * Error code from the exchange (if any). - */ - exchange_code?: number; - - /** - * HTTP status code of the exchange's response - * to the merchant's refund request. - */ - exchange_http_status: number; -} - -/** - * Response for a refund pickup or a /pay in abort mode. - */ -export interface MerchantRefundResponse { - /** - * Public key of the merchant - */ - merchant_pub: string; - - /** - * Contract terms hash of the contract that - * is being refunded. - */ - h_contract_terms: string; - - /** - * The signed refund permissions, to be sent to the exchange. - */ - refunds: MerchantAbortPayRefundDetails[]; -} - -/** - * Planchet detail sent to the merchant. - */ -export interface TipPlanchetDetail { - /** - * Hashed denomination public key. - */ - denom_pub_hash: string; - - /** - * Coin's blinded public key. - */ - coin_ev: CoinEnvelope; -} - -/** - * Request sent to the merchant to pick up a tip. - */ -export interface TipPickupRequest { - /** - * Identifier of the tip. - */ - tip_id: string; - - /** - * List of planchets the wallet wants to use for the tip. - */ - planchets: TipPlanchetDetail[]; -} - -/** - * Reserve signature, defined as separate class to facilitate - * schema validation. - */ -export interface MerchantBlindSigWrapperV1 { - /** - * Reserve signature. - */ - blind_sig: string; -} - -/** - * Response of the merchant - * to the TipPickupRequest. - */ -export interface MerchantTipResponseV1 { - /** - * The order of the signatures matches the planchets list. - */ - blind_sigs: MerchantBlindSigWrapperV1[]; -} - -export interface MerchantBlindSigWrapperV2 { - blind_sig: BlindedDenominationSignature; -} - -/** - * Response of the merchant - * to the TipPickupRequest. - */ -export interface MerchantTipResponseV2 { - /** - * The order of the signatures matches the planchets list. - */ - blind_sigs: MerchantBlindSigWrapperV2[]; -} - -/** - * Element of the payback list that the - * exchange gives us in /keys. - */ -export class Recoup { - /** - * The hash of the denomination public key for which the payback is offered. - */ - h_denom_pub: string; -} - -/** - * Structure of one exchange signing key in the /keys response. - */ -export class ExchangeSignKeyJson { - stamp_start: TalerProtocolTimestamp; - stamp_expire: TalerProtocolTimestamp; - stamp_end: TalerProtocolTimestamp; - key: EddsaPublicKeyString; - master_sig: EddsaSignatureString; -} - -/** - * Structure that the exchange gives us in /keys. - */ -export class ExchangeKeysJson { - /** - * List of offered denominations. - */ - denoms: ExchangeDenomination[]; - - /** - * The exchange's master public key. - */ - master_public_key: string; - - /** - * The list of auditors (partially) auditing the exchange. - */ - auditors: ExchangeAuditor[]; - - /** - * Timestamp when this response was issued. - */ - list_issue_date: TalerProtocolTimestamp; - - /** - * List of revoked denominations. - */ - recoup?: Recoup[]; - - /** - * Short-lived signing keys used to sign online - * responses. - */ - signkeys: ExchangeSignKeyJson[]; - - /** - * Protocol version. - */ - version: string; - - reserve_closing_delay: TalerProtocolDuration; - - global_fees: GlobalFees[]; -} - -export interface GlobalFees { - // What date (inclusive) does these fees go into effect? - start_date: TalerProtocolTimestamp; - - // What date (exclusive) does this fees stop going into effect? - end_date: TalerProtocolTimestamp; - - // KYC fee, charged when a user wants to create an account. - // The first year of the account_annual_fee after the KYC is - // always included. - kyc_fee: AmountString; - - // Account history fee, charged when a user wants to - // obtain a reserve/account history. - history_fee: AmountString; - - // Annual fee charged for having an open account at the - // exchange. Charged to the account. If the account - // balance is insufficient to cover this fee, the account - // is automatically deleted/closed. (Note that the exchange - // will keep the account history around for longer for - // regulatory reasons.) - account_fee: AmountString; - - // Purse fee, charged only if a purse is abandoned - // and was not covered by the account limit. - purse_fee: AmountString; - - // How long will the exchange preserve the account history? - // After an account was deleted/closed, the exchange will - // retain the account history for legal reasons until this time. - history_expiration: TalerProtocolDuration; - - // How long does the exchange promise to keep funds - // an account for which the KYC has never happened - // after a purse was merged into an account? Basically, - // after this time funds in an account without KYC are - // forfeit. - account_kyc_timeout: TalerProtocolDuration; - - // Non-negative number of concurrent purses that any - // account holder is allowed to create without having - // to pay the purse_fee. - purse_account_limit: number; - - // How long does an exchange keep a purse around after a purse - // has expired (or been successfully merged)? A 'GET' request - // for a purse will succeed until the purse expiration time - // plus this value. - purse_timeout: TalerProtocolDuration; - - // Signature of TALER_GlobalFeesPS. - master_sig: string; -} -/** - * Wire fees as announced by the exchange. - */ -export class WireFeesJson { - /** - * Cost of a wire transfer. - */ - wire_fee: string; - - wad_fee: string; - - /** - * Cost of clising a reserve. - */ - closing_fee: string; - - /** - * Signature made with the exchange's master key. - */ - sig: string; - - /** - * Date from which the fee applies. - */ - start_date: TalerProtocolTimestamp; - - /** - * Data after which the fee doesn't apply anymore. - */ - end_date: TalerProtocolTimestamp; -} - -export interface AccountInfo { - payto_uri: string; - master_sig: string; -} - -export interface ExchangeWireJson { - accounts: AccountInfo[]; - fees: { [methodName: string]: WireFeesJson[] }; -} - -/** - * Proposal returned from the contract URL. - */ -export class Proposal { - /** - * Contract terms for the propoal. - * Raw, un-decoded JSON object. - */ - contract_terms: any; - - /** - * Signature over contract, made by the merchant. The public key used for signing - * must be contract_terms.merchant_pub. - */ - sig: string; -} - -/** - * Response from the internal merchant API. - */ -export class CheckPaymentResponse { - order_status: string; - refunded: boolean | undefined; - refunded_amount: string | undefined; - contract_terms: any | undefined; - taler_pay_uri: string | undefined; - contract_url: string | undefined; -} - -/** - * Response from the bank. - */ -export class WithdrawOperationStatusResponse { - selection_done: boolean; - - transfer_done: boolean; - - aborted: boolean; - - amount: string; - - sender_wire?: string; - - suggested_exchange?: string; - - confirm_transfer_url?: string; - - wire_types: string[]; -} - -/** - * Response from the merchant. - */ -export class TipPickupGetResponse { - tip_amount: string; - - exchange_url: string; - - expiration: TalerProtocolTimestamp; -} - -export enum DenomKeyType { - Rsa = "RSA", - ClauseSchnorr = "CS", -} - -export namespace DenomKeyType { - export function toIntTag(t: DenomKeyType): number { - switch (t) { - case DenomKeyType.Rsa: - return 1; - case DenomKeyType.ClauseSchnorr: - return 2; - } - } -} - -export interface RsaBlindedDenominationSignature { - cipher: DenomKeyType.Rsa; - blinded_rsa_signature: string; -} - -export interface CSBlindedDenominationSignature { - cipher: DenomKeyType.ClauseSchnorr; -} - -export type BlindedDenominationSignature = - | RsaBlindedDenominationSignature - | CSBlindedDenominationSignature; - -export const codecForRsaBlindedDenominationSignature = () => - buildCodecForObject() - .property("cipher", codecForConstString(DenomKeyType.Rsa)) - .property("blinded_rsa_signature", codecForString()) - .build("RsaBlindedDenominationSignature"); - -export const codecForBlindedDenominationSignature = () => - buildCodecForUnion() - .discriminateOn("cipher") - .alternative(DenomKeyType.Rsa, codecForRsaBlindedDenominationSignature()) - .build("BlindedDenominationSignature"); - -export class WithdrawResponse { - ev_sig: BlindedDenominationSignature; -} - -export class WithdrawBatchResponse { - ev_sigs: WithdrawResponse[]; -} - -/** - * Easy to process format for the public data of coins - * managed by the wallet. - */ -export interface CoinDumpJson { - coins: Array<{ - /** - * The coin's denomination's public key. - */ - denom_pub: DenominationPubKey; - /** - * Hash of denom_pub. - */ - denom_pub_hash: string; - /** - * Value of the denomination (without any fees). - */ - denom_value: string; - /** - * Public key of the coin. - */ - coin_pub: string; - /** - * Base URL of the exchange for the coin. - */ - exchange_base_url: string; - /** - * Remaining value on the coin, to the knowledge of - * the wallet. - */ - remaining_value: string; - /** - * Public key of the parent coin. - * Only present if this coin was obtained via refreshing. - */ - refresh_parent_coin_pub: string | undefined; - /** - * Public key of the reserve for this coin. - * Only present if this coin was obtained via refreshing. - */ - withdrawal_reserve_pub: string | undefined; - /** - * Is the coin suspended? - * Suspended coins are not considered for payments. - */ - coin_suspended: boolean; - - /** - * Information about the age restriction - */ - ageCommitmentProof: AgeCommitmentProof | undefined; - }>; -} - -export interface MerchantPayResponse { - sig: string; -} - -export interface ExchangeMeltRequest { - coin_pub: CoinPublicKeyString; - confirm_sig: EddsaSignatureString; - denom_pub_hash: HashCodeString; - denom_sig: UnblindedSignature; - rc: string; - value_with_fee: AmountString; - age_commitment_hash?: HashCodeString; -} - -export interface ExchangeMeltResponse { - /** - * Which of the kappa indices does the client not have to reveal. - */ - noreveal_index: number; - - /** - * Signature of TALER_RefreshMeltConfirmationPS whereby the exchange - * affirms the successful melt and confirming the noreveal_index - */ - exchange_sig: EddsaSignatureString; - - /* - * public EdDSA key of the exchange that was used to generate the signature. - * Should match one of the exchange's signing keys from /keys. Again given - * explicitly as the client might otherwise be confused by clock skew as to - * which signing key was used. - */ - exchange_pub: EddsaPublicKeyString; - - /* - * Base URL to use for operations on the refresh context - * (so the reveal operation). If not given, - * the base URL is the same as the one used for this request. - * Can be used if the base URL for /refreshes/ differs from that - * for /coins/, i.e. for load balancing. Clients SHOULD - * respect the refresh_base_url if provided. Any HTTP server - * belonging to an exchange MUST generate a 307 or 308 redirection - * to the correct base URL should a client uses the wrong base - * URL, or if the base URL has changed since the melt. - * - * When melting the same coin twice (technically allowed - * as the response might have been lost on the network), - * the exchange may return different values for the refresh_base_url. - */ - refresh_base_url?: string; -} - -export interface ExchangeRevealItem { - ev_sig: BlindedDenominationSignature; -} - -export interface ExchangeRevealResponse { - // List of the exchange's blinded RSA signatures on the new coins. - ev_sigs: ExchangeRevealItem[]; -} - -interface MerchantOrderStatusPaid { - // Was the payment refunded (even partially, via refund or abort)? - refunded: boolean; - - // Is any amount of the refund still waiting to be picked up (even partially)? - refund_pending: boolean; - - // Amount that was refunded in total. - refund_amount: AmountString; - - // Amount that already taken by the wallet. - refund_taken: AmountString; -} - -interface MerchantOrderRefundResponse { - /** - * Amount that was refunded in total. - */ - refund_amount: AmountString; - - /** - * Successful refunds for this payment, empty array for none. - */ - refunds: MerchantCoinRefundStatus[]; - - /** - * Public key of the merchant. - */ - merchant_pub: EddsaPublicKeyString; -} - -export type MerchantCoinRefundStatus = - | MerchantCoinRefundSuccessStatus - | MerchantCoinRefundFailureStatus; - -export interface MerchantCoinRefundSuccessStatus { - type: "success"; - - // HTTP status of the exchange request, 200 (integer) required for refund confirmations. - exchange_status: 200; - - // the EdDSA :ref:signature (binary-only) with purpose - // TALER_SIGNATURE_EXCHANGE_CONFIRM_REFUND using a current signing key of the - // exchange affirming the successful refund - exchange_sig: EddsaSignatureString; - - // public EdDSA key of the exchange that was used to generate the signature. - // Should match one of the exchange's signing keys from /keys. It is given - // explicitly as the client might otherwise be confused by clock skew as to - // which signing key was used. - exchange_pub: EddsaPublicKeyString; - - // Refund transaction ID. - rtransaction_id: number; - - // public key of a coin that was refunded - coin_pub: EddsaPublicKeyString; - - // Amount that was refunded, including refund fee charged by the exchange - // to the customer. - refund_amount: AmountString; - - execution_time: TalerProtocolTimestamp; -} - -export interface MerchantCoinRefundFailureStatus { - type: "failure"; - - // HTTP status of the exchange request, must NOT be 200. - exchange_status: number; - - // Taler error code from the exchange reply, if available. - exchange_code?: number; - - // If available, HTTP reply from the exchange. - exchange_reply?: any; - - // Refund transaction ID. - rtransaction_id: number; - - // public key of a coin that was refunded - coin_pub: EddsaPublicKeyString; - - // Amount that was refunded, including refund fee charged by the exchange - // to the customer. - refund_amount: AmountString; - - execution_time: TalerProtocolTimestamp; -} - -export interface MerchantOrderStatusUnpaid { - /** - * URI that the wallet must process to complete the payment. - */ - taler_pay_uri: string; - - /** - * Alternative order ID which was paid for already in the same session. - * - * Only given if the same product was purchased before in the same session. - */ - already_paid_order_id?: string; -} - -/** - * Response body for the following endpoint: - * - * POST {talerBankIntegrationApi}/withdrawal-operation/{wopid} - */ -export interface BankWithdrawalOperationPostResponse { - transfer_done: boolean; -} - -export type DenominationPubKey = RsaDenominationPubKey | CsDenominationPubKey; - -export interface RsaDenominationPubKey { - readonly cipher: DenomKeyType.Rsa; - readonly rsa_public_key: string; - readonly age_mask: number; -} - -export interface CsDenominationPubKey { - readonly cipher: DenomKeyType.ClauseSchnorr; - readonly age_mask: number; - readonly cs_public_key: string; -} - -export namespace DenominationPubKey { - export function cmp( - p1: DenominationPubKey, - p2: DenominationPubKey, - ): -1 | 0 | 1 { - if (p1.cipher < p2.cipher) { - return -1; - } else if (p1.cipher > p2.cipher) { - return +1; - } else if ( - p1.cipher === DenomKeyType.Rsa && - p2.cipher === DenomKeyType.Rsa - ) { - if ((p1.age_mask ?? 0) < (p2.age_mask ?? 0)) { - return -1; - } else if ((p1.age_mask ?? 0) > (p2.age_mask ?? 0)) { - return 1; - } - return strcmp(p1.rsa_public_key, p2.rsa_public_key); - } else if ( - p1.cipher === DenomKeyType.ClauseSchnorr && - p2.cipher === DenomKeyType.ClauseSchnorr - ) { - if ((p1.age_mask ?? 0) < (p2.age_mask ?? 0)) { - return -1; - } else if ((p1.age_mask ?? 0) > (p2.age_mask ?? 0)) { - return 1; - } - return strcmp(p1.cs_public_key, p2.cs_public_key); - } else { - throw Error("unsupported cipher"); - } - } -} - -export const codecForRsaDenominationPubKey = () => - buildCodecForObject() - .property("cipher", codecForConstString(DenomKeyType.Rsa)) - .property("rsa_public_key", codecForString()) - .property("age_mask", codecForNumber()) - .build("DenominationPubKey"); - -export const codecForCsDenominationPubKey = () => - buildCodecForObject() - .property("cipher", codecForConstString(DenomKeyType.ClauseSchnorr)) - .property("cs_public_key", codecForString()) - .property("age_mask", codecForNumber()) - .build("CsDenominationPubKey"); - -export const codecForDenominationPubKey = () => - buildCodecForUnion() - .discriminateOn("cipher") - .alternative(DenomKeyType.Rsa, codecForRsaDenominationPubKey()) - .alternative(DenomKeyType.ClauseSchnorr, codecForCsDenominationPubKey()) - .build("DenominationPubKey"); - -export const codecForBankWithdrawalOperationPostResponse = - (): Codec => - buildCodecForObject() - .property("transfer_done", codecForBoolean()) - .build("BankWithdrawalOperationPostResponse"); - -export type AmountString = string; -export type Base32String = string; -export type EddsaSignatureString = string; -export type EddsaPublicKeyString = string; -export type CoinPublicKeyString = string; - -export const codecForDenomination = (): Codec => - buildCodecForObject() - .property("value", codecForString()) - .property("denom_pub", codecForDenominationPubKey()) - .property("fee_withdraw", codecForString()) - .property("fee_deposit", codecForString()) - .property("fee_refresh", codecForString()) - .property("fee_refund", codecForString()) - .property("stamp_start", codecForTimestamp) - .property("stamp_expire_withdraw", codecForTimestamp) - .property("stamp_expire_legal", codecForTimestamp) - .property("stamp_expire_deposit", codecForTimestamp) - .property("master_sig", codecForString()) - .build("Denomination"); - -export const codecForAuditorDenomSig = (): Codec => - buildCodecForObject() - .property("denom_pub_h", codecForString()) - .property("auditor_sig", codecForString()) - .build("AuditorDenomSig"); - -export const codecForAuditor = (): Codec => - buildCodecForObject() - .property("auditor_pub", codecForString()) - .property("auditor_url", codecForString()) - .property("denomination_keys", codecForList(codecForAuditorDenomSig())) - .build("Auditor"); - -export const codecForExchangeHandle = (): Codec => - buildCodecForObject() - .property("master_pub", codecForString()) - .property("url", codecForString()) - .build("ExchangeHandle"); - -export const codecForAuditorHandle = (): Codec => - buildCodecForObject() - .property("name", codecForString()) - .property("auditor_pub", codecForString()) - .property("url", codecForString()) - .build("AuditorHandle"); - -export const codecForLocation = (): Codec => - buildCodecForObject() - .property("country", codecOptional(codecForString())) - .property("country_subdivision", codecOptional(codecForString())) - .property("building_name", codecOptional(codecForString())) - .property("building_number", codecOptional(codecForString())) - .property("district", codecOptional(codecForString())) - .property("street", codecOptional(codecForString())) - .property("post_code", codecOptional(codecForString())) - .property("town", codecOptional(codecForString())) - .property("town_location", codecOptional(codecForString())) - .property("address_lines", codecOptional(codecForList(codecForString()))) - .build("Location"); - -export const codecForMerchantInfo = (): Codec => - buildCodecForObject() - .property("name", codecForString()) - .property("address", codecOptional(codecForLocation())) - .property("jurisdiction", codecOptional(codecForLocation())) - .build("MerchantInfo"); - -export const codecForTax = (): Codec => - buildCodecForObject() - .property("name", codecForString()) - .property("tax", codecForString()) - .build("Tax"); - -export const codecForInternationalizedString = - (): Codec => codecForMap(codecForString()); - -export const codecForProduct = (): Codec => - buildCodecForObject() - .property("product_id", codecOptional(codecForString())) - .property("description", codecForString()) - .property( - "description_i18n", - codecOptional(codecForInternationalizedString()), - ) - .property("quantity", codecOptional(codecForNumber())) - .property("unit", codecOptional(codecForString())) - .property("price", codecOptional(codecForString())) - .build("Tax"); - -export const codecForContractTerms = (): Codec => - buildCodecForObject() - .property("order_id", codecForString()) - .property("fulfillment_url", codecOptional(codecForString())) - .property("fulfillment_message", codecOptional(codecForString())) - .property( - "fulfillment_message_i18n", - codecOptional(codecForInternationalizedString()), - ) - .property("merchant_base_url", codecForString()) - .property("h_wire", codecForString()) - .property("auto_refund", codecOptional(codecForDuration)) - .property("wire_method", codecForString()) - .property("summary", codecForString()) - .property("summary_i18n", codecOptional(codecForInternationalizedString())) - .property("nonce", codecForString()) - .property("amount", codecForString()) - .property("auditors", codecForList(codecForAuditorHandle())) - .property("pay_deadline", codecForTimestamp) - .property("refund_deadline", codecForTimestamp) - .property("wire_transfer_deadline", codecForTimestamp) - .property("timestamp", codecForTimestamp) - .property("delivery_location", codecOptional(codecForLocation())) - .property("delivery_date", codecOptional(codecForTimestamp)) - .property("max_fee", codecForString()) - .property("max_wire_fee", codecOptional(codecForString())) - .property("merchant", codecForMerchantInfo()) - .property("merchant_pub", codecForString()) - .property("exchanges", codecForList(codecForExchangeHandle())) - .property("products", codecOptional(codecForList(codecForProduct()))) - .property("extra", codecForAny()) - .property("minimum_age", codecOptional(codecForNumber())) - .build("ContractTerms"); - -export const codecForMerchantRefundPermission = - (): Codec => - buildCodecForObject() - .property("refund_amount", codecForAmountString()) - .property("refund_fee", codecForAmountString()) - .property("coin_pub", codecForString()) - .property("rtransaction_id", codecForNumber()) - .property("exchange_http_status", codecForNumber()) - .property("exchange_code", codecOptional(codecForNumber())) - .property("exchange_reply", codecOptional(codecForAny())) - .property("exchange_sig", codecOptional(codecForString())) - .property("exchange_pub", codecOptional(codecForString())) - .build("MerchantRefundPermission"); - -export const codecForMerchantRefundResponse = - (): Codec => - buildCodecForObject() - .property("merchant_pub", codecForString()) - .property("h_contract_terms", codecForString()) - .property("refunds", codecForList(codecForMerchantRefundPermission())) - .build("MerchantRefundResponse"); - -export const codecForBlindSigWrapperV2 = (): Codec => - buildCodecForObject() - .property("blind_sig", codecForBlindedDenominationSignature()) - .build("MerchantBlindSigWrapperV2"); - -export const codecForMerchantTipResponseV2 = (): Codec => - buildCodecForObject() - .property("blind_sigs", codecForList(codecForBlindSigWrapperV2())) - .build("MerchantTipResponseV2"); - -export const codecForRecoup = (): Codec => - buildCodecForObject() - .property("h_denom_pub", codecForString()) - .build("Recoup"); - -export const codecForExchangeSigningKey = (): Codec => - buildCodecForObject() - .property("key", codecForString()) - .property("master_sig", codecForString()) - .property("stamp_end", codecForTimestamp) - .property("stamp_start", codecForTimestamp) - .property("stamp_expire", codecForTimestamp) - .build("ExchangeSignKeyJson"); - -export const codecForGlobalFees = (): Codec => - buildCodecForObject() - .property("start_date", codecForTimestamp) - .property("end_date", codecForTimestamp) - .property("kyc_fee", codecForAmountString()) - .property("history_fee", codecForAmountString()) - .property("account_fee", codecForAmountString()) - .property("purse_fee", codecForAmountString()) - .property("history_expiration", codecForDuration) - .property("account_kyc_timeout", codecForDuration) - .property("purse_account_limit", codecForNumber()) - .property("purse_timeout", codecForDuration) - .property("master_sig", codecForString()) - .build("GlobalFees"); - -export const codecForExchangeKeysJson = (): Codec => - buildCodecForObject() - .property("denoms", codecForList(codecForDenomination())) - .property("master_public_key", codecForString()) - .property("auditors", codecForList(codecForAuditor())) - .property("list_issue_date", codecForTimestamp) - .property("recoup", codecOptional(codecForList(codecForRecoup()))) - .property("signkeys", codecForList(codecForExchangeSigningKey())) - .property("version", codecForString()) - .property("reserve_closing_delay", codecForDuration) - .property("global_fees", codecForList(codecForGlobalFees())) - .build("ExchangeKeysJson"); - -export const codecForWireFeesJson = (): Codec => - buildCodecForObject() - .property("wire_fee", codecForString()) - .property("closing_fee", codecForString()) - .property("wad_fee", codecForString()) - .property("sig", codecForString()) - .property("start_date", codecForTimestamp) - .property("end_date", codecForTimestamp) - .build("WireFeesJson"); - -export const codecForAccountInfo = (): Codec => - buildCodecForObject() - .property("payto_uri", codecForString()) - .property("master_sig", codecForString()) - .build("AccountInfo"); - -export const codecForExchangeWireJson = (): Codec => - buildCodecForObject() - .property("accounts", codecForList(codecForAccountInfo())) - .property("fees", codecForMap(codecForList(codecForWireFeesJson()))) - .build("ExchangeWireJson"); - -export const codecForProposal = (): Codec => - buildCodecForObject() - .property("contract_terms", codecForAny()) - .property("sig", codecForString()) - .build("Proposal"); - -export const codecForCheckPaymentResponse = (): Codec => - buildCodecForObject() - .property("order_status", codecForString()) - .property("refunded", codecOptional(codecForBoolean())) - .property("refunded_amount", codecOptional(codecForString())) - .property("contract_terms", codecOptional(codecForAny())) - .property("taler_pay_uri", codecOptional(codecForString())) - .property("contract_url", codecOptional(codecForString())) - .build("CheckPaymentResponse"); - -export const codecForWithdrawOperationStatusResponse = - (): Codec => - buildCodecForObject() - .property("selection_done", codecForBoolean()) - .property("transfer_done", codecForBoolean()) - .property("aborted", codecForBoolean()) - .property("amount", codecForString()) - .property("sender_wire", codecOptional(codecForString())) - .property("suggested_exchange", codecOptional(codecForString())) - .property("confirm_transfer_url", codecOptional(codecForString())) - .property("wire_types", codecForList(codecForString())) - .build("WithdrawOperationStatusResponse"); - -export const codecForTipPickupGetResponse = (): Codec => - buildCodecForObject() - .property("tip_amount", codecForString()) - .property("exchange_url", codecForString()) - .property("expiration", codecForTimestamp) - .build("TipPickupGetResponse"); - -export const codecForRecoupConfirmation = (): Codec => - buildCodecForObject() - .property("reserve_pub", codecOptional(codecForString())) - .property("old_coin_pub", codecOptional(codecForString())) - .build("RecoupConfirmation"); - -export const codecForWithdrawResponse = (): Codec => - buildCodecForObject() - .property("ev_sig", codecForBlindedDenominationSignature()) - .build("WithdrawResponse"); - -export const codecForWithdrawBatchResponse = (): Codec => - buildCodecForObject() - .property("ev_sigs", codecForList(codecForWithdrawResponse())) - .build("WithdrawBatchResponse"); - -export const codecForMerchantPayResponse = (): Codec => - buildCodecForObject() - .property("sig", codecForString()) - .build("MerchantPayResponse"); - -export const codecForExchangeMeltResponse = (): Codec => - buildCodecForObject() - .property("exchange_pub", codecForString()) - .property("exchange_sig", codecForString()) - .property("noreveal_index", codecForNumber()) - .property("refresh_base_url", codecOptional(codecForString())) - .build("ExchangeMeltResponse"); - -export const codecForExchangeRevealItem = (): Codec => - buildCodecForObject() - .property("ev_sig", codecForBlindedDenominationSignature()) - .build("ExchangeRevealItem"); - -export const codecForExchangeRevealResponse = - (): Codec => - buildCodecForObject() - .property("ev_sigs", codecForList(codecForExchangeRevealItem())) - .build("ExchangeRevealResponse"); - -export const codecForMerchantCoinRefundSuccessStatus = - (): Codec => - buildCodecForObject() - .property("type", codecForConstString("success")) - .property("coin_pub", codecForString()) - .property("exchange_status", codecForConstNumber(200)) - .property("exchange_sig", codecForString()) - .property("rtransaction_id", codecForNumber()) - .property("refund_amount", codecForString()) - .property("exchange_pub", codecForString()) - .property("execution_time", codecForTimestamp) - .build("MerchantCoinRefundSuccessStatus"); - -export const codecForMerchantCoinRefundFailureStatus = - (): Codec => - buildCodecForObject() - .property("type", codecForConstString("failure")) - .property("coin_pub", codecForString()) - .property("exchange_status", codecForNumber()) - .property("rtransaction_id", codecForNumber()) - .property("refund_amount", codecForString()) - .property("exchange_code", codecOptional(codecForNumber())) - .property("exchange_reply", codecOptional(codecForAny())) - .property("execution_time", codecForTimestamp) - .build("MerchantCoinRefundFailureStatus"); - -export const codecForMerchantCoinRefundStatus = - (): Codec => - buildCodecForUnion() - .discriminateOn("type") - .alternative("success", codecForMerchantCoinRefundSuccessStatus()) - .alternative("failure", codecForMerchantCoinRefundFailureStatus()) - .build("MerchantCoinRefundStatus"); - -export const codecForMerchantOrderStatusPaid = - (): Codec => - buildCodecForObject() - .property("refund_amount", codecForString()) - .property("refund_taken", codecForString()) - .property("refund_pending", codecForBoolean()) - .property("refunded", codecForBoolean()) - .build("MerchantOrderStatusPaid"); - -export const codecForMerchantOrderRefundPickupResponse = - (): Codec => - buildCodecForObject() - .property("merchant_pub", codecForString()) - .property("refund_amount", codecForString()) - .property("refunds", codecForList(codecForMerchantCoinRefundStatus())) - .build("MerchantOrderRefundPickupResponse"); - -export const codecForMerchantOrderStatusUnpaid = - (): Codec => - buildCodecForObject() - .property("taler_pay_uri", codecForString()) - .property("already_paid_order_id", codecOptional(codecForString())) - .build("MerchantOrderStatusUnpaid"); - -export interface AbortRequest { - // hash of the order's contract terms (this is used to authenticate the - // wallet/customer in case $ORDER_ID is guessable). - h_contract: string; - - // List of coins the wallet would like to see refunds for. - // (Should be limited to the coins for which the original - // payment succeeded, as far as the wallet knows.) - coins: AbortingCoin[]; -} - -export interface AbortingCoin { - // Public key of a coin for which the wallet is requesting an abort-related refund. - coin_pub: EddsaPublicKeyString; - - // The amount to be refunded (matches the original contribution) - contribution: AmountString; - - // URL of the exchange this coin was withdrawn from. - exchange_url: string; -} - -export interface AbortResponse { - // List of refund responses about the coins that the wallet - // requested an abort for. In the same order as the 'coins' - // from the original request. - // The rtransaction_id is implied to be 0. - refunds: MerchantAbortPayRefundStatus[]; -} - -export const codecForMerchantAbortPayRefundSuccessStatus = - (): Codec => - buildCodecForObject() - .property("exchange_pub", codecForString()) - .property("exchange_sig", codecForString()) - .property("exchange_status", codecForConstNumber(200)) - .property("type", codecForConstString("success")) - .build("MerchantAbortPayRefundSuccessStatus"); - -export const codecForMerchantAbortPayRefundFailureStatus = - (): Codec => - buildCodecForObject() - .property("exchange_code", codecForNumber()) - .property("exchange_reply", codecForAny()) - .property("exchange_status", codecForNumber()) - .property("type", codecForConstString("failure")) - .build("MerchantAbortPayRefundFailureStatus"); - -export const codecForMerchantAbortPayRefundStatus = - (): Codec => - buildCodecForUnion() - .discriminateOn("type") - .alternative("success", codecForMerchantAbortPayRefundSuccessStatus()) - .alternative("failure", codecForMerchantAbortPayRefundFailureStatus()) - .build("MerchantAbortPayRefundStatus"); - -export const codecForAbortResponse = (): Codec => - buildCodecForObject() - .property("refunds", codecForList(codecForMerchantAbortPayRefundStatus())) - .build("AbortResponse"); - -export type MerchantAbortPayRefundStatus = - | MerchantAbortPayRefundSuccessStatus - | MerchantAbortPayRefundFailureStatus; - -// Details about why a refund failed. -export interface MerchantAbortPayRefundFailureStatus { - // Used as tag for the sum type RefundStatus sum type. - type: "failure"; - - // HTTP status of the exchange request, must NOT be 200. - exchange_status: number; - - // Taler error code from the exchange reply, if available. - exchange_code?: number; - - // If available, HTTP reply from the exchange. - exchange_reply?: unknown; -} - -// Additional details needed to verify the refund confirmation signature -// (h_contract_terms and merchant_pub) are already known -// to the wallet and thus not included. -export interface MerchantAbortPayRefundSuccessStatus { - // Used as tag for the sum type MerchantCoinRefundStatus sum type. - type: "success"; - - // HTTP status of the exchange request, 200 (integer) required for refund confirmations. - exchange_status: 200; - - // the EdDSA :ref:signature (binary-only) with purpose - // TALER_SIGNATURE_EXCHANGE_CONFIRM_REFUND using a current signing key of the - // exchange affirming the successful refund - exchange_sig: string; - - // public EdDSA key of the exchange that was used to generate the signature. - // Should match one of the exchange's signing keys from /keys. It is given - // explicitly as the client might otherwise be confused by clock skew as to - // which signing key was used. - exchange_pub: string; -} - -export interface TalerConfigResponse { - name: string; - version: string; - currency?: string; -} - -export const codecForTalerConfigResponse = (): Codec => - buildCodecForObject() - .property("name", codecForString()) - .property("version", codecForString()) - .property("currency", codecOptional(codecForString())) - .build("TalerConfigResponse"); - -export interface FutureKeysResponse { - future_denoms: any[]; - - future_signkeys: any[]; - - master_pub: string; - - denom_secmod_public_key: string; - - // Public key of the signkey security module. - signkey_secmod_public_key: string; -} - -export const codecForKeysManagementResponse = (): Codec => - buildCodecForObject() - .property("master_pub", codecForString()) - .property("future_signkeys", codecForList(codecForAny())) - .property("future_denoms", codecForList(codecForAny())) - .property("denom_secmod_public_key", codecForAny()) - .property("signkey_secmod_public_key", codecForAny()) - .build("FutureKeysResponse"); - -export interface MerchantConfigResponse { - currency: string; - name: string; - version: string; -} - -export const codecForMerchantConfigResponse = - (): Codec => - buildCodecForObject() - .property("currency", codecForString()) - .property("name", codecForString()) - .property("version", codecForString()) - .build("MerchantConfigResponse"); - -export enum ExchangeProtocolVersion { - /** - * Current version supported by the wallet. - */ - V12 = 12, -} - -export enum MerchantProtocolVersion { - /** - * Current version supported by the wallet. - */ - V3 = 3, -} - -export type CoinEnvelope = CoinEnvelopeRsa | CoinEnvelopeCs; - -export interface CoinEnvelopeRsa { - cipher: DenomKeyType.Rsa; - rsa_blinded_planchet: string; -} - -export interface CoinEnvelopeCs { - cipher: DenomKeyType.ClauseSchnorr; - // FIXME: add remaining fields -} - -export type HashCodeString = string; - -export interface ExchangeWithdrawRequest { - denom_pub_hash: HashCodeString; - reserve_sig: EddsaSignatureString; - coin_ev: CoinEnvelope; -} - -export interface ExchangeRefreshRevealRequest { - new_denoms_h: HashCodeString[]; - coin_evs: CoinEnvelope[]; - /** - * kappa - 1 transfer private keys (ephemeral ECDHE keys). - */ - transfer_privs: string[]; - - transfer_pub: EddsaPublicKeyString; - - link_sigs: EddsaSignatureString[]; - - /** - * Iff the corresponding denomination has support for age restriction, - * the client MUST provide the original age commitment, i.e. the vector - * of public keys. - */ - old_age_commitment?: Edx25519PublicKeyEnc[]; -} - -export interface DepositSuccess { - // Optional base URL of the exchange for looking up wire transfers - // associated with this transaction. If not given, - // the base URL is the same as the one used for this request. - // Can be used if the base URL for /transactions/ differs from that - // for /coins/, i.e. for load balancing. Clients SHOULD - // respect the transaction_base_url if provided. Any HTTP server - // belonging to an exchange MUST generate a 307 or 308 redirection - // to the correct base URL should a client uses the wrong base - // URL, or if the base URL has changed since the deposit. - transaction_base_url?: string; - - // timestamp when the deposit was received by the exchange. - exchange_timestamp: TalerProtocolTimestamp; - - // the EdDSA signature of TALER_DepositConfirmationPS using a current - // signing key of the exchange affirming the successful - // deposit and that the exchange will transfer the funds after the refund - // deadline, or as soon as possible if the refund deadline is zero. - exchange_sig: string; - - // public EdDSA key of the exchange that was used to - // generate the signature. - // Should match one of the exchange's signing keys from /keys. It is given - // explicitly as the client might otherwise be confused by clock skew as to - // which signing key was used. - exchange_pub: string; -} - -export const codecForDepositSuccess = (): Codec => - buildCodecForObject() - .property("exchange_pub", codecForString()) - .property("exchange_sig", codecForString()) - .property("exchange_timestamp", codecForTimestamp) - .property("transaction_base_url", codecOptional(codecForString())) - .build("DepositSuccess"); - -export interface PurseDeposit { - /** - * Amount to be deposited, can be a fraction of the - * coin's total value. - */ - amount: AmountString; - - /** - * Hash of denomination RSA key with which the coin is signed. - */ - denom_pub_hash: HashCodeString; - - /** - * Exchange's unblinded RSA signature of the coin. - */ - ub_sig: UnblindedSignature; - - /** - * Age commitment for the coin, if the denomination is age-restricted. - */ - age_commitment?: string[]; - - /** - * Attestation for the minimum age, if the denomination is age-restricted. - */ - attest?: string; - - /** - * Signature over TALER_PurseDepositSignaturePS - * of purpose TALER_SIGNATURE_WALLET_PURSE_DEPOSIT - * made by the customer with the - * coin's private key. - */ - coin_sig: EddsaSignatureString; - - /** - * Public key of the coin being deposited into the purse. - */ - coin_pub: EddsaPublicKeyString; -} - -export interface ExchangePurseMergeRequest { - // payto://-URI of the account the purse is to be merged into. - // Must be of the form: 'payto://taler/$EXCHANGE_URL/$RESERVE_PUB'. - payto_uri: string; - - // EdDSA signature of the account/reserve affirming the merge - // over a TALER_AccountMergeSignaturePS. - // Must be of purpose TALER_SIGNATURE_ACCOUNT_MERGE - reserve_sig: EddsaSignatureString; - - // EdDSA signature of the purse private key affirming the merge - // over a TALER_PurseMergeSignaturePS. - // Must be of purpose TALER_SIGNATURE_PURSE_MERGE. - merge_sig: EddsaSignatureString; - - // Client-side timestamp of when the merge request was made. - merge_timestamp: TalerProtocolTimestamp; -} - -export interface ExchangeGetContractResponse { - purse_pub: string; - econtract_sig: string; - econtract: string; -} - -export const codecForExchangeGetContractResponse = - (): Codec => - buildCodecForObject() - .property("purse_pub", codecForString()) - .property("econtract_sig", codecForString()) - .property("econtract", codecForString()) - .build("ExchangeGetContractResponse"); - -/** - * Contract terms between two wallets (as opposed to a merchant and wallet). - */ -export interface PeerContractTerms { - amount: AmountString; - summary: string; - purse_expiration: TalerProtocolTimestamp; -} - -export interface EncryptedContract { - // Encrypted contract. - econtract: string; - - // Signature over the (encrypted) contract. - econtract_sig: string; - - // Ephemeral public key for the DH operation to decrypt the encrypted contract. - contract_pub: string; -} - -/** - * Payload for /reserves/{reserve_pub}/purse - * endpoint of the exchange. - */ -export interface ExchangeReservePurseRequest { - /** - * Minimum amount that must be credited to the reserve, that is - * the total value of the purse minus the deposit fees. - * If the deposit fees are lower, the contribution to the - * reserve can be higher! - */ - purse_value: AmountString; - - // Minimum age required for all coins deposited into the purse. - min_age: number; - - // Purse fee the reserve owner is willing to pay - // for the purse creation. Optional, if not present - // the purse is to be created from the purse quota - // of the reserve. - purse_fee: AmountString; - - // Optional encrypted contract, in case the buyer is - // proposing the contract and thus establishing the - // purse with the payment. - econtract?: EncryptedContract; - - // EdDSA public key used to approve merges of this purse. - merge_pub: EddsaPublicKeyString; - - // EdDSA signature of the purse private key affirming the merge - // over a TALER_PurseMergeSignaturePS. - // Must be of purpose TALER_SIGNATURE_PURSE_MERGE. - merge_sig: EddsaSignatureString; - - // EdDSA signature of the account/reserve affirming the merge. - // Must be of purpose TALER_SIGNATURE_WALLET_ACCOUNT_MERGE - reserve_sig: EddsaSignatureString; - - // Purse public key. - purse_pub: EddsaPublicKeyString; - - // EdDSA signature of the purse over - // TALER_PurseRequestSignaturePS of - // purpose TALER_SIGNATURE_PURSE_REQUEST - // confirming that the - // above details hold for this purse. - purse_sig: EddsaSignatureString; - - // SHA-512 hash of the contact of the purse. - h_contract_terms: HashCodeString; - - // Client-side timestamp of when the merge request was made. - merge_timestamp: TalerProtocolTimestamp; - - // Indicative time by which the purse should expire - // if it has not been paid. - purse_expiration: TalerProtocolTimestamp; -} - -export interface ExchangePurseDeposits { - // Array of coins to deposit into the purse. - deposits: PurseDeposit[]; -} diff --git a/packages/taler-util/src/transactions-types.ts b/packages/taler-util/src/transactions-types.ts new file mode 100644 index 000000000..c1870e2e0 --- /dev/null +++ b/packages/taler-util/src/transactions-types.ts @@ -0,0 +1,568 @@ +/* + This file is part of GNU Taler + (C) 2019 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see + */ + +/** + * Type and schema definitions for the wallet's transaction list. + * + * @author Florian Dold + * @author Torsten Grote + */ + +/** + * Imports. + */ +import { TalerProtocolTimestamp } from "./time.js"; +import { + AmountString, + Product, + InternationalizedString, + MerchantInfo, + codecForInternationalizedString, + codecForMerchantInfo, + codecForProduct, + Location, +} from "./taler-types.js"; +import { + Codec, + buildCodecForObject, + codecOptional, + codecForString, + codecForList, + codecForAny, +} from "./codec.js"; +import { + RefreshReason, + TalerErrorDetail, + TransactionIdStr, +} from "./wallet-types.js"; + +export interface TransactionsRequest { + /** + * return only transactions in the given currency + */ + currency?: string; + + /** + * if present, results will be limited to transactions related to the given search string + */ + search?: string; +} + +export interface TransactionsResponse { + // a list of past and pending transactions sorted by pending, timestamp and transactionId. + // In case two events are both pending and have the same timestamp, + // they are sorted by the transactionId + // (lexically ascending and locale-independent comparison). + transactions: Transaction[]; +} + +export interface TransactionCommon { + // opaque unique ID for the transaction, used as a starting point for paginating queries + // and for invoking actions on the transaction (e.g. deleting/hiding it from the history) + transactionId: TransactionIdStr; + + // the type of the transaction; different types might provide additional information + type: TransactionType; + + // main timestamp of the transaction + timestamp: TalerProtocolTimestamp; + + // true if the transaction is still pending, false otherwise + // If a transaction is not longer pending, its timestamp will be updated, + // but its transactionId will remain unchanged + pending: boolean; + + /** + * True if the transaction encountered a problem that might be + * permanent. A frozen transaction won't be automatically retried. + */ + frozen: boolean; + + /** + * Raw amount of the transaction (exclusive of fees or other extra costs). + */ + amountRaw: AmountString; + + /** + * Amount added or removed from the wallet's balance (including all fees and other costs). + */ + amountEffective: AmountString; + + error?: TalerErrorDetail; +} + +export type Transaction = + | TransactionWithdrawal + | TransactionPayment + | TransactionRefund + | TransactionTip + | TransactionRefresh + | TransactionDeposit + | TransactionPeerPullCredit + | TransactionPeerPullDebit + | TransactionPeerPushCredit + | TransactionPeerPushDebit; + +export enum TransactionType { + Withdrawal = "withdrawal", + Payment = "payment", + Refund = "refund", + Refresh = "refresh", + Tip = "tip", + Deposit = "deposit", + PeerPushDebit = "peer-push-debit", + PeerPushCredit = "peer-push-credit", + PeerPullDebit = "peer-pull-debit", + PeerPullCredit = "peer-pull-credit", +} + +export enum WithdrawalType { + TalerBankIntegrationApi = "taler-bank-integration-api", + ManualTransfer = "manual-transfer", +} + +export type WithdrawalDetails = + | WithdrawalDetailsForManualTransfer + | WithdrawalDetailsForTalerBankIntegrationApi; + +interface WithdrawalDetailsForManualTransfer { + type: WithdrawalType.ManualTransfer; + + /** + * Payto URIs that the exchange supports. + * + * Already contains the amount and message. + */ + exchangePaytoUris: string[]; + + // Public key of the reserve + reservePub: string; +} + +interface WithdrawalDetailsForTalerBankIntegrationApi { + type: WithdrawalType.TalerBankIntegrationApi; + + /** + * Set to true if the bank has confirmed the withdrawal, false if not. + * An unconfirmed withdrawal usually requires user-input and should be highlighted in the UI. + * See also bankConfirmationUrl below. + */ + confirmed: boolean; + + /** + * If the withdrawal is unconfirmed, this can include a URL for user + * initiated confirmation. + */ + bankConfirmationUrl?: string; + + // Public key of the reserve + reservePub: string; +} + +// This should only be used for actual withdrawals +// and not for tips that have their own transactions type. +export interface TransactionWithdrawal extends TransactionCommon { + type: TransactionType.Withdrawal; + + /** + * Exchange of the withdrawal. + */ + exchangeBaseUrl: string; + + /** + * Amount that got subtracted from the reserve balance. + */ + amountRaw: AmountString; + + /** + * Amount that actually was (or will be) added to the wallet's balance. + */ + amountEffective: AmountString; + + withdrawalDetails: WithdrawalDetails; +} + +export interface PeerInfoShort { + expiration: TalerProtocolTimestamp | undefined; + summary: string | undefined; +} + +/** + * Credit because we were paid for a P2P invoice we created. + */ +export interface TransactionPeerPullCredit extends TransactionCommon { + type: TransactionType.PeerPullCredit; + + info: PeerInfoShort; + /** + * Exchange used. + */ + exchangeBaseUrl: string; + + /** + * Amount that got subtracted from the reserve balance. + */ + amountRaw: AmountString; + + /** + * Amount that actually was (or will be) added to the wallet's balance. + */ + amountEffective: AmountString; + + /** + * URI to send to the other party. + */ + talerUri: string; +} + +/** + * Debit because we paid someone's invoice. + */ +export interface TransactionPeerPullDebit extends TransactionCommon { + type: TransactionType.PeerPullDebit; + + info: PeerInfoShort; + /** + * Exchange used. + */ + exchangeBaseUrl: string; + + amountRaw: AmountString; + + amountEffective: AmountString; +} + +/** + * We sent money via a P2P payment. + */ +export interface TransactionPeerPushDebit extends TransactionCommon { + type: TransactionType.PeerPushDebit; + + info: PeerInfoShort; + /** + * Exchange used. + */ + exchangeBaseUrl: string; + + /** + * Amount that got subtracted from the reserve balance. + */ + amountRaw: AmountString; + + /** + * Amount that actually was (or will be) added to the wallet's balance. + */ + amountEffective: AmountString; + + /** + * URI to accept the payment. + */ + talerUri: string; +} + +/** + * We received money via a P2P payment. + */ +export interface TransactionPeerPushCredit extends TransactionCommon { + type: TransactionType.PeerPushCredit; + + info: PeerInfoShort; + /** + * Exchange used. + */ + exchangeBaseUrl: string; + + /** + * Amount that got subtracted from the reserve balance. + */ + amountRaw: AmountString; + + /** + * Amount that actually was (or will be) added to the wallet's balance. + */ + amountEffective: AmountString; +} + +export enum PaymentStatus { + /** + * Explicitly aborted after timeout / failure + */ + Aborted = "aborted", + + /** + * Payment failed, wallet will auto-retry. + * User should be given the option to retry now / abort. + */ + Failed = "failed", + + /** + * Paid successfully + */ + Paid = "paid", + + /** + * User accepted, payment is processing. + */ + Accepted = "accepted", +} + +export interface TransactionPayment extends TransactionCommon { + type: TransactionType.Payment; + + /** + * Additional information about the payment. + */ + info: OrderShortInfo; + + /** + * Wallet-internal end-to-end identifier for the payment. + */ + proposalId: string; + + /** + * How far did the wallet get with processing the payment? + */ + status: PaymentStatus; + + /** + * Amount that must be paid for the contract + */ + amountRaw: AmountString; + + /** + * Amount that was paid, including deposit, wire and refresh fees. + */ + amountEffective: AmountString; + + /** + * Amount that has been refunded by the merchant + */ + totalRefundRaw: AmountString; + + /** + * Amount will be added to the wallet's balance after fees and refreshing + */ + totalRefundEffective: AmountString; + + /** + * Amount pending to be picked up + */ + refundPending: AmountString | undefined; + + /** + * Reference to applied refunds + */ + refunds: RefundInfoShort[]; +} + +export interface OrderShortInfo { + /** + * Order ID, uniquely identifies the order within a merchant instance + */ + orderId: string; + + /** + * Hash of the contract terms. + */ + contractTermsHash: string; + + /** + * More information about the merchant + */ + merchant: MerchantInfo; + + /** + * Summary of the order, given by the merchant + */ + summary: string; + + /** + * Map from IETF BCP 47 language tags to localized summaries + */ + summary_i18n?: InternationalizedString; + + /** + * List of products that are part of the order + */ + products: Product[] | undefined; + + /** + * Time indicating when the order should be delivered. + * May be overwritten by individual products. + */ + delivery_date?: TalerProtocolTimestamp; + + /** + * Delivery location for (all!) products. + */ + delivery_location?: Location; + + /** + * URL of the fulfillment, given by the merchant + */ + fulfillmentUrl?: string; + + /** + * Plain text message that should be shown to the user + * when the payment is complete. + */ + fulfillmentMessage?: string; + + /** + * Translations of fulfillmentMessage. + */ + fulfillmentMessage_i18n?: InternationalizedString; +} + +export interface RefundInfoShort { + transactionId: string; + timestamp: TalerProtocolTimestamp; + amountEffective: AmountString; + amountRaw: AmountString; +} + +export interface TransactionRefund extends TransactionCommon { + type: TransactionType.Refund; + + // ID for the transaction that is refunded + refundedTransactionId: string; + + // Additional information about the refunded payment + info: OrderShortInfo; + + /** + * Amount pending to be picked up + */ + refundPending: AmountString | undefined; + + // Amount that has been refunded by the merchant + amountRaw: AmountString; + + // Amount will be added to the wallet's balance after fees and refreshing + amountEffective: AmountString; +} + +export interface TransactionTip extends TransactionCommon { + type: TransactionType.Tip; + + // Raw amount of the tip, without extra fees that apply + amountRaw: AmountString; + + /** + * More information about the merchant + */ + // merchant: MerchantInfo; + + // Amount will be (or was) added to the wallet's balance after fees and refreshing + amountEffective: AmountString; + + merchantBaseUrl: string; +} + +/** + * A transaction shown for refreshes. + * Only shown for (1) refreshes not associated with other transactions + * and (2) refreshes in an error state. + */ +export interface TransactionRefresh extends TransactionCommon { + type: TransactionType.Refresh; + + /** + * Exchange that the coins are refreshed with + */ + exchangeBaseUrl: string; + + refreshReason: RefreshReason; + + /** + * Transaction ID that caused this refresh. + */ + originatingTransactionId?: string; + + /** + * Always zero for refreshes + */ + amountRaw: AmountString; + + /** + * Fees, i.e. the effective, negative effect of the refresh + * on the balance. + */ + amountEffective: AmountString; +} + +/** + * Deposit transaction, which effectively sends + * money from this wallet somewhere else. + */ +export interface TransactionDeposit extends TransactionCommon { + type: TransactionType.Deposit; + + depositGroupId: string; + + /** + * Target for the deposit. + */ + targetPaytoUri: string; + + /** + * Raw amount that is being deposited + */ + amountRaw: AmountString; + + /** + * Effective amount that is being deposited + */ + amountEffective: AmountString; +} + +export interface TransactionByIdRequest { + transactionId: string; +} + +export const codecForTransactionByIdRequest = + (): Codec => + buildCodecForObject() + .property("transactionId", codecForString()) + .build("TransactionByIdRequest"); + +export const codecForTransactionsRequest = (): Codec => + buildCodecForObject() + .property("currency", codecOptional(codecForString())) + .property("search", codecOptional(codecForString())) + .build("TransactionsRequest"); + +// FIXME: do full validation here! +export const codecForTransactionsResponse = (): Codec => + buildCodecForObject() + .property("transactions", codecForList(codecForAny())) + .build("TransactionsResponse"); + +export const codecForOrderShortInfo = (): Codec => + buildCodecForObject() + .property("contractTermsHash", codecForString()) + .property("fulfillmentMessage", codecOptional(codecForString())) + .property( + "fulfillmentMessage_i18n", + codecOptional(codecForInternationalizedString()), + ) + .property("fulfillmentUrl", codecOptional(codecForString())) + .property("merchant", codecForMerchantInfo()) + .property("orderId", codecForString()) + .property("products", codecOptional(codecForList(codecForProduct()))) + .property("summary", codecForString()) + .property("summary_i18n", codecOptional(codecForInternationalizedString())) + .build("OrderShortInfo"); diff --git a/packages/taler-util/src/transactionsTypes.ts b/packages/taler-util/src/transactionsTypes.ts deleted file mode 100644 index 5fd01448c..000000000 --- a/packages/taler-util/src/transactionsTypes.ts +++ /dev/null @@ -1,564 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2019 Taler Systems S.A. - - GNU Taler is free software; you can redistribute it and/or modify it under the - terms of the GNU General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - GNU Taler; see the file COPYING. If not, see - */ - -/** - * Type and schema definitions for the wallet's transaction list. - * - * @author Florian Dold - * @author Torsten Grote - */ - -/** - * Imports. - */ -import { TalerProtocolTimestamp } from "./time.js"; -import { - AmountString, - Product, - InternationalizedString, - MerchantInfo, - codecForInternationalizedString, - codecForMerchantInfo, - codecForProduct, - Location, -} from "./talerTypes.js"; -import { - Codec, - buildCodecForObject, - codecOptional, - codecForString, - codecForList, - codecForAny, -} from "./codec.js"; -import { RefreshReason, TalerErrorDetail } from "./walletTypes.js"; - -export interface TransactionsRequest { - /** - * return only transactions in the given currency - */ - currency?: string; - - /** - * if present, results will be limited to transactions related to the given search string - */ - search?: string; -} - -export interface TransactionsResponse { - // a list of past and pending transactions sorted by pending, timestamp and transactionId. - // In case two events are both pending and have the same timestamp, - // they are sorted by the transactionId - // (lexically ascending and locale-independent comparison). - transactions: Transaction[]; -} - -export interface TransactionCommon { - // opaque unique ID for the transaction, used as a starting point for paginating queries - // and for invoking actions on the transaction (e.g. deleting/hiding it from the history) - transactionId: string; - - // the type of the transaction; different types might provide additional information - type: TransactionType; - - // main timestamp of the transaction - timestamp: TalerProtocolTimestamp; - - // true if the transaction is still pending, false otherwise - // If a transaction is not longer pending, its timestamp will be updated, - // but its transactionId will remain unchanged - pending: boolean; - - /** - * True if the transaction encountered a problem that might be - * permanent. A frozen transaction won't be automatically retried. - */ - frozen: boolean; - - /** - * Raw amount of the transaction (exclusive of fees or other extra costs). - */ - amountRaw: AmountString; - - /** - * Amount added or removed from the wallet's balance (including all fees and other costs). - */ - amountEffective: AmountString; - - error?: TalerErrorDetail; -} - -export type Transaction = - | TransactionWithdrawal - | TransactionPayment - | TransactionRefund - | TransactionTip - | TransactionRefresh - | TransactionDeposit - | TransactionPeerPullCredit - | TransactionPeerPullDebit - | TransactionPeerPushCredit - | TransactionPeerPushDebit; - -export enum TransactionType { - Withdrawal = "withdrawal", - Payment = "payment", - Refund = "refund", - Refresh = "refresh", - Tip = "tip", - Deposit = "deposit", - PeerPushDebit = "peer-push-debit", - PeerPushCredit = "peer-push-credit", - PeerPullDebit = "peer-pull-debit", - PeerPullCredit = "peer-pull-credit", -} - -export enum WithdrawalType { - TalerBankIntegrationApi = "taler-bank-integration-api", - ManualTransfer = "manual-transfer", -} - -export type WithdrawalDetails = - | WithdrawalDetailsForManualTransfer - | WithdrawalDetailsForTalerBankIntegrationApi; - -interface WithdrawalDetailsForManualTransfer { - type: WithdrawalType.ManualTransfer; - - /** - * Payto URIs that the exchange supports. - * - * Already contains the amount and message. - */ - exchangePaytoUris: string[]; - - // Public key of the reserve - reservePub: string; -} - -interface WithdrawalDetailsForTalerBankIntegrationApi { - type: WithdrawalType.TalerBankIntegrationApi; - - /** - * Set to true if the bank has confirmed the withdrawal, false if not. - * An unconfirmed withdrawal usually requires user-input and should be highlighted in the UI. - * See also bankConfirmationUrl below. - */ - confirmed: boolean; - - /** - * If the withdrawal is unconfirmed, this can include a URL for user - * initiated confirmation. - */ - bankConfirmationUrl?: string; - - // Public key of the reserve - reservePub: string; -} - -// This should only be used for actual withdrawals -// and not for tips that have their own transactions type. -export interface TransactionWithdrawal extends TransactionCommon { - type: TransactionType.Withdrawal; - - /** - * Exchange of the withdrawal. - */ - exchangeBaseUrl: string; - - /** - * Amount that got subtracted from the reserve balance. - */ - amountRaw: AmountString; - - /** - * Amount that actually was (or will be) added to the wallet's balance. - */ - amountEffective: AmountString; - - withdrawalDetails: WithdrawalDetails; -} - -export interface PeerInfoShort { - expiration: TalerProtocolTimestamp | undefined; - summary: string | undefined; -} - -/** - * Credit because we were paid for a P2P invoice we created. - */ -export interface TransactionPeerPullCredit extends TransactionCommon { - type: TransactionType.PeerPullCredit; - - info: PeerInfoShort; - /** - * Exchange used. - */ - exchangeBaseUrl: string; - - /** - * Amount that got subtracted from the reserve balance. - */ - amountRaw: AmountString; - - /** - * Amount that actually was (or will be) added to the wallet's balance. - */ - amountEffective: AmountString; - - /** - * URI to send to the other party. - */ - talerUri: string; -} - -/** - * Debit because we paid someone's invoice. - */ -export interface TransactionPeerPullDebit extends TransactionCommon { - type: TransactionType.PeerPullDebit; - - info: PeerInfoShort; - /** - * Exchange used. - */ - exchangeBaseUrl: string; - - amountRaw: AmountString; - - amountEffective: AmountString; -} - -/** - * We sent money via a P2P payment. - */ -export interface TransactionPeerPushDebit extends TransactionCommon { - type: TransactionType.PeerPushDebit; - - info: PeerInfoShort; - /** - * Exchange used. - */ - exchangeBaseUrl: string; - - /** - * Amount that got subtracted from the reserve balance. - */ - amountRaw: AmountString; - - /** - * Amount that actually was (or will be) added to the wallet's balance. - */ - amountEffective: AmountString; - - /** - * URI to accept the payment. - */ - talerUri: string; -} - -/** - * We received money via a P2P payment. - */ -export interface TransactionPeerPushCredit extends TransactionCommon { - type: TransactionType.PeerPushCredit; - - info: PeerInfoShort; - /** - * Exchange used. - */ - exchangeBaseUrl: string; - - /** - * Amount that got subtracted from the reserve balance. - */ - amountRaw: AmountString; - - /** - * Amount that actually was (or will be) added to the wallet's balance. - */ - amountEffective: AmountString; -} - -export enum PaymentStatus { - /** - * Explicitly aborted after timeout / failure - */ - Aborted = "aborted", - - /** - * Payment failed, wallet will auto-retry. - * User should be given the option to retry now / abort. - */ - Failed = "failed", - - /** - * Paid successfully - */ - Paid = "paid", - - /** - * User accepted, payment is processing. - */ - Accepted = "accepted", -} - -export interface TransactionPayment extends TransactionCommon { - type: TransactionType.Payment; - - /** - * Additional information about the payment. - */ - info: OrderShortInfo; - - /** - * Wallet-internal end-to-end identifier for the payment. - */ - proposalId: string; - - /** - * How far did the wallet get with processing the payment? - */ - status: PaymentStatus; - - /** - * Amount that must be paid for the contract - */ - amountRaw: AmountString; - - /** - * Amount that was paid, including deposit, wire and refresh fees. - */ - amountEffective: AmountString; - - /** - * Amount that has been refunded by the merchant - */ - totalRefundRaw: AmountString; - - /** - * Amount will be added to the wallet's balance after fees and refreshing - */ - totalRefundEffective: AmountString; - - /** - * Amount pending to be picked up - */ - refundPending: AmountString | undefined; - - /** - * Reference to applied refunds - */ - refunds: RefundInfoShort[]; -} - -export interface OrderShortInfo { - /** - * Order ID, uniquely identifies the order within a merchant instance - */ - orderId: string; - - /** - * Hash of the contract terms. - */ - contractTermsHash: string; - - /** - * More information about the merchant - */ - merchant: MerchantInfo; - - /** - * Summary of the order, given by the merchant - */ - summary: string; - - /** - * Map from IETF BCP 47 language tags to localized summaries - */ - summary_i18n?: InternationalizedString; - - /** - * List of products that are part of the order - */ - products: Product[] | undefined; - - /** - * Time indicating when the order should be delivered. - * May be overwritten by individual products. - */ - delivery_date?: TalerProtocolTimestamp; - - /** - * Delivery location for (all!) products. - */ - delivery_location?: Location; - - /** - * URL of the fulfillment, given by the merchant - */ - fulfillmentUrl?: string; - - /** - * Plain text message that should be shown to the user - * when the payment is complete. - */ - fulfillmentMessage?: string; - - /** - * Translations of fulfillmentMessage. - */ - fulfillmentMessage_i18n?: InternationalizedString; -} - -export interface RefundInfoShort { - transactionId: string; - timestamp: TalerProtocolTimestamp; - amountEffective: AmountString; - amountRaw: AmountString; -} - -export interface TransactionRefund extends TransactionCommon { - type: TransactionType.Refund; - - // ID for the transaction that is refunded - refundedTransactionId: string; - - // Additional information about the refunded payment - info: OrderShortInfo; - - /** - * Amount pending to be picked up - */ - refundPending: AmountString | undefined; - - // Amount that has been refunded by the merchant - amountRaw: AmountString; - - // Amount will be added to the wallet's balance after fees and refreshing - amountEffective: AmountString; -} - -export interface TransactionTip extends TransactionCommon { - type: TransactionType.Tip; - - // Raw amount of the tip, without extra fees that apply - amountRaw: AmountString; - - /** - * More information about the merchant - */ - // merchant: MerchantInfo; - - // Amount will be (or was) added to the wallet's balance after fees and refreshing - amountEffective: AmountString; - - merchantBaseUrl: string; -} - -/** - * A transaction shown for refreshes. - * Only shown for (1) refreshes not associated with other transactions - * and (2) refreshes in an error state. - */ -export interface TransactionRefresh extends TransactionCommon { - type: TransactionType.Refresh; - - /** - * Exchange that the coins are refreshed with - */ - exchangeBaseUrl: string; - - refreshReason: RefreshReason; - - /** - * Transaction ID that caused this refresh. - */ - originatingTransactionId?: string; - - /** - * Always zero for refreshes - */ - amountRaw: AmountString; - - /** - * Fees, i.e. the effective, negative effect of the refresh - * on the balance. - */ - amountEffective: AmountString; -} - -/** - * Deposit transaction, which effectively sends - * money from this wallet somewhere else. - */ -export interface TransactionDeposit extends TransactionCommon { - type: TransactionType.Deposit; - - depositGroupId: string; - - /** - * Target for the deposit. - */ - targetPaytoUri: string; - - /** - * Raw amount that is being deposited - */ - amountRaw: AmountString; - - /** - * Effective amount that is being deposited - */ - amountEffective: AmountString; -} - -export interface TransactionByIdRequest { - transactionId: string; -} - -export const codecForTransactionByIdRequest = - (): Codec => - buildCodecForObject() - .property("transactionId", codecForString()) - .build("TransactionByIdRequest"); - -export const codecForTransactionsRequest = (): Codec => - buildCodecForObject() - .property("currency", codecOptional(codecForString())) - .property("search", codecOptional(codecForString())) - .build("TransactionsRequest"); - -// FIXME: do full validation here! -export const codecForTransactionsResponse = (): Codec => - buildCodecForObject() - .property("transactions", codecForList(codecForAny())) - .build("TransactionsResponse"); - -export const codecForOrderShortInfo = (): Codec => - buildCodecForObject() - .property("contractTermsHash", codecForString()) - .property("fulfillmentMessage", codecOptional(codecForString())) - .property( - "fulfillmentMessage_i18n", - codecOptional(codecForInternationalizedString()), - ) - .property("fulfillmentUrl", codecOptional(codecForString())) - .property("merchant", codecForMerchantInfo()) - .property("orderId", codecForString()) - .property("products", codecOptional(codecForList(codecForProduct()))) - .property("summary", codecForString()) - .property("summary_i18n", codecOptional(codecForInternationalizedString())) - .build("OrderShortInfo"); diff --git a/packages/taler-util/src/types-test.ts b/packages/taler-util/src/types-test.ts index e8af13119..2915106c2 100644 --- a/packages/taler-util/src/types-test.ts +++ b/packages/taler-util/src/types-test.ts @@ -15,7 +15,7 @@ */ import test from "ava"; -import { codecForContractTerms } from "./talerTypes.js"; +import { codecForContractTerms } from "./taler-types.js"; test("contract terms validation", (t) => { const c = { diff --git a/packages/taler-util/src/wallet-types.ts b/packages/taler-util/src/wallet-types.ts new file mode 100644 index 000000000..cd6c2202e --- /dev/null +++ b/packages/taler-util/src/wallet-types.ts @@ -0,0 +1,1836 @@ +/* + This file is part of GNU Taler + (C) 2015-2020 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + TALER is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + TALER; see the file COPYING. If not, see + */ + +/** + * 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 + */ + +/** + * Imports. + */ +import { + AmountJson, + codecForAmountJson, + codecForAmountString, +} from "./amounts.js"; +import { + AbsoluteTime, + codecForAbsoluteTime, + codecForTimestamp, + TalerProtocolDuration, + TalerProtocolTimestamp, +} from "./time.js"; +import { + buildCodecForObject, + codecForString, + codecOptional, + Codec, + codecForList, + codecForBoolean, + codecForConstString, + codecForAny, + buildCodecForUnion, + codecForNumber, + codecForMap, +} from "./codec.js"; +import { + AmountString, + AuditorDenomSig, + codecForContractTerms, + CoinEnvelope, + ContractTerms, + DenominationPubKey, + DenomKeyType, + ExchangeAuditor, + UnblindedSignature, +} from "./taler-types.js"; +import { OrderShortInfo, codecForOrderShortInfo } from "./transactions-types.js"; +import { BackupRecovery } from "./backup-types.js"; +import { PaytoUri } from "./payto.js"; +import { TalerErrorCode } from "./taler-error-codes.js"; +import { AgeCommitmentProof } from "./taler-crypto.js"; +import { VersionMatchResult } from "./libtool-version.js"; + +/** + * Identifier for a transaction in the wallet. + */ +export type TransactionIdStr = `tx:${string}:${string}`; + +/** + * Identifier for a pending task in the wallet. + */ +export type PendingIdStr = `pd:${string}:string`; + +/** + * Response for the create reserve request to the wallet. + */ +export class CreateReserveResponse { + /** + * Exchange URL where the bank should create the reserve. + * The URL is canonicalized in the response. + */ + exchange: string; + + /** + * Reserve public key of the newly created reserve. + */ + reservePub: string; +} + +export interface Balance { + available: AmountString; + pendingIncoming: AmountString; + pendingOutgoing: AmountString; + + // Does the balance for this currency have a pending + // transaction? + hasPendingTransactions: boolean; + + // Is there a pending transaction that would affect the balance + // and requires user input? + requiresUserInput: boolean; +} + +export interface BalancesResponse { + balances: Balance[]; +} + +export const codecForBalance = (): Codec => + buildCodecForObject() + .property("available", codecForString()) + .property("hasPendingTransactions", codecForBoolean()) + .property("pendingIncoming", codecForString()) + .property("pendingOutgoing", codecForString()) + .property("requiresUserInput", codecForBoolean()) + .build("Balance"); + +export const codecForBalancesResponse = (): Codec => + buildCodecForObject() + .property("balances", codecForList(codecForBalance())) + .build("BalancesResponse"); + +/** + * For terseness. + */ +export function mkAmount( + value: number, + fraction: number, + currency: string, +): AmountJson { + return { value, fraction, currency }; +} + +export enum ConfirmPayResultType { + Done = "done", + Pending = "pending", +} + +/** + * Result for confirmPay + */ +export interface ConfirmPayResultDone { + type: ConfirmPayResultType.Done; + contractTerms: ContractTerms; + transactionId: string; +} + +export interface ConfirmPayResultPending { + type: ConfirmPayResultType.Pending; + transactionId: string; + lastError: TalerErrorDetail | undefined; +} + +export type ConfirmPayResult = ConfirmPayResultDone | ConfirmPayResultPending; + +export const codecForConfirmPayResultPending = + (): Codec => + buildCodecForObject() + .property("lastError", codecForAny()) + .property("transactionId", codecForString()) + .property("type", codecForConstString(ConfirmPayResultType.Pending)) + .build("ConfirmPayResultPending"); + +export const codecForConfirmPayResultDone = (): Codec => + buildCodecForObject() + .property("type", codecForConstString(ConfirmPayResultType.Done)) + .property("transactionId", codecForString()) + .property("contractTerms", codecForContractTerms()) + .build("ConfirmPayResultDone"); + +export const codecForConfirmPayResult = (): Codec => + buildCodecForUnion() + .discriminateOn("type") + .alternative( + ConfirmPayResultType.Pending, + codecForConfirmPayResultPending(), + ) + .alternative(ConfirmPayResultType.Done, codecForConfirmPayResultDone()) + .build("ConfirmPayResult"); + +/** + * Information about all sender wire details known to the wallet, + * as well as exchanges that accept these wire types. + */ +export interface SenderWireInfos { + /** + * Mapping from exchange base url to list of accepted + * wire types. + */ + exchangeWireTypes: { [exchangeBaseUrl: string]: string[] }; + + /** + * Sender wire information stored in the wallet. + */ + senderWires: string[]; +} + +/** + * Request to create a reserve. + */ +export interface CreateReserveRequest { + /** + * The initial amount for the reserve. + */ + amount: AmountJson; + + /** + * Exchange URL where the bank should create the reserve. + */ + exchange: string; + + /** + * Payto URI that identifies the exchange's account that the funds + * for this reserve go into. + */ + exchangePaytoUri?: string; + + /** + * Wire details (as a payto URI) for the bank account that sent the funds to + * the exchange. + */ + senderWire?: string; + + /** + * URL to fetch the withdraw status from the bank. + */ + bankWithdrawStatusUrl?: string; + + /** + * Forced denomination selection for the first withdrawal + * from this reserve, only used for testing. + */ + forcedDenomSel?: ForcedDenomSel; + + restrictAge?: number; +} + +export const codecForCreateReserveRequest = (): Codec => + buildCodecForObject() + .property("amount", codecForAmountJson()) + .property("exchange", codecForString()) + .property("exchangePaytoUri", codecForString()) + .property("senderWire", codecOptional(codecForString())) + .property("bankWithdrawStatusUrl", codecOptional(codecForString())) + .property("forcedDenomSel", codecForAny()) + .build("CreateReserveRequest"); + +/** + * Request to mark a reserve as confirmed. + */ +export interface ConfirmReserveRequest { + /** + * Public key of then reserve that should be marked + * as confirmed. + */ + reservePub: string; +} + +export const codecForConfirmReserveRequest = (): Codec => + buildCodecForObject() + .property("reservePub", codecForString()) + .build("ConfirmReserveRequest"); + +/** + * Wire coins to the user's own bank account. + */ +export class ReturnCoinsRequest { + /** + * The amount to wire. + */ + amount: AmountJson; + + /** + * The exchange to take the coins from. + */ + exchange: string; + + /** + * Wire details for the bank account of the customer that will + * receive the funds. + */ + senderWire?: string; + + /** + * Verify that a value matches the schema of this class and convert it into a + * member. + */ + static checked: (obj: any) => ReturnCoinsRequest; +} + +export interface PrepareRefundResult { + proposalId: string; + + effectivePaid: AmountString; + gone: AmountString; + granted: AmountString; + pending: boolean; + awaiting: AmountString; + + info: OrderShortInfo; +} + +export interface PrepareTipResult { + /** + * Unique ID for the tip assigned by the wallet. + * Typically different from the merchant-generated tip ID. + */ + walletTipId: string; + + /** + * Has the tip already been accepted? + */ + accepted: boolean; + + /** + * Amount that the merchant gave. + */ + tipAmountRaw: AmountString; + + /** + * Amount that arrived at the wallet. + * Might be lower than the raw amount due to fees. + */ + tipAmountEffective: AmountString; + + /** + * Base URL of the merchant backend giving then tip. + */ + merchantBaseUrl: string; + + /** + * Base URL of the exchange that is used to withdraw the tip. + * Determined by the merchant, the wallet/user has no choice here. + */ + exchangeBaseUrl: string; + + /** + * Time when the tip will expire. After it expired, it can't be picked + * up anymore. + */ + expirationTimestamp: TalerProtocolTimestamp; +} + +export interface AcceptTipResponse { + transactionId: string; +} + +export const codecForPrepareTipResult = (): Codec => + buildCodecForObject() + .property("accepted", codecForBoolean()) + .property("tipAmountRaw", codecForAmountString()) + .property("tipAmountEffective", codecForAmountString()) + .property("exchangeBaseUrl", codecForString()) + .property("merchantBaseUrl", codecForString()) + .property("expirationTimestamp", codecForTimestamp) + .property("walletTipId", codecForString()) + .build("PrepareTipResult"); + +export interface BenchmarkResult { + time: { [s: string]: number }; + repetitions: number; +} + +export enum PreparePayResultType { + PaymentPossible = "payment-possible", + InsufficientBalance = "insufficient-balance", + AlreadyConfirmed = "already-confirmed", +} + +export const codecForPreparePayResultPaymentPossible = + (): Codec => + buildCodecForObject() + .property("amountEffective", codecForAmountString()) + .property("amountRaw", codecForAmountString()) + .property("contractTerms", codecForContractTerms()) + .property("proposalId", codecForString()) + .property("contractTermsHash", codecForString()) + .property("noncePriv", codecForString()) + .property( + "status", + codecForConstString(PreparePayResultType.PaymentPossible), + ) + .build("PreparePayResultPaymentPossible"); + +export const codecForPreparePayResultInsufficientBalance = + (): Codec => + buildCodecForObject() + .property("amountRaw", codecForAmountString()) + .property("contractTerms", codecForAny()) + .property("proposalId", codecForString()) + .property("noncePriv", codecForString()) + .property( + "status", + codecForConstString(PreparePayResultType.InsufficientBalance), + ) + .build("PreparePayResultInsufficientBalance"); + +export const codecForPreparePayResultAlreadyConfirmed = + (): Codec => + buildCodecForObject() + .property( + "status", + codecForConstString(PreparePayResultType.AlreadyConfirmed), + ) + .property("amountEffective", codecForAmountString()) + .property("amountRaw", codecForAmountString()) + .property("paid", codecForBoolean()) + .property("contractTerms", codecForAny()) + .property("contractTermsHash", codecForString()) + .property("proposalId", codecForString()) + .build("PreparePayResultAlreadyConfirmed"); + +export const codecForPreparePayResult = (): Codec => + buildCodecForUnion() + .discriminateOn("status") + .alternative( + PreparePayResultType.AlreadyConfirmed, + codecForPreparePayResultAlreadyConfirmed(), + ) + .alternative( + PreparePayResultType.InsufficientBalance, + codecForPreparePayResultInsufficientBalance(), + ) + .alternative( + PreparePayResultType.PaymentPossible, + codecForPreparePayResultPaymentPossible(), + ) + .build("PreparePayResult"); + +/** + * Result of a prepare pay operation. + */ +export type PreparePayResult = + | PreparePayResultInsufficientBalance + | PreparePayResultAlreadyConfirmed + | PreparePayResultPaymentPossible; + +/** + * Payment is possible. + */ +export interface PreparePayResultPaymentPossible { + status: PreparePayResultType.PaymentPossible; + proposalId: string; + contractTerms: ContractTerms; + contractTermsHash: string; + amountRaw: string; + amountEffective: string; + noncePriv: string; +} + +export interface PreparePayResultInsufficientBalance { + status: PreparePayResultType.InsufficientBalance; + proposalId: string; + contractTerms: ContractTerms; + amountRaw: string; + noncePriv: string; +} + +export interface PreparePayResultAlreadyConfirmed { + status: PreparePayResultType.AlreadyConfirmed; + contractTerms: ContractTerms; + paid: boolean; + amountRaw: string; + amountEffective: string; + contractTermsHash: string; + proposalId: string; +} + +export interface BankWithdrawDetails { + selectionDone: boolean; + transferDone: boolean; + amount: AmountJson; + senderWire?: string; + suggestedExchange?: string; + confirmTransferUrl?: string; + wireTypes: string[]; +} + +export interface AcceptWithdrawalResponse { + reservePub: string; + confirmTransferUrl?: string; + transactionId: string; +} + +/** + * Details about a purchase, including refund status. + */ +export interface PurchaseDetails { + contractTerms: Record; + hasRefund: boolean; + totalRefundAmount: AmountJson; + totalRefundAndRefreshFees: AmountJson; +} + +export interface WalletDiagnostics { + walletManifestVersion: string; + walletManifestDisplayVersion: string; + errors: string[]; + firefoxIdbProblem: boolean; + dbOutdated: boolean; +} + +export interface TalerErrorDetail { + code: TalerErrorCode; + hint?: string; + [x: string]: unknown; +} + +/** + * Minimal information needed about a planchet for unblinding a signature. + * + * Can be a withdrawal/tipping/refresh planchet. + */ +export interface PlanchetUnblindInfo { + denomPub: DenominationPubKey; + blindingKey: string; +} + +export interface WithdrawalPlanchet { + coinPub: string; + coinPriv: string; + reservePub: string; + denomPubHash: string; + denomPub: DenominationPubKey; + blindingKey: string; + withdrawSig: string; + coinEv: CoinEnvelope; + coinValue: AmountJson; + coinEvHash: string; + ageCommitmentProof?: AgeCommitmentProof; +} + +export interface PlanchetCreationRequest { + secretSeed: string; + coinIndex: number; + value: AmountJson; + feeWithdraw: AmountJson; + denomPub: DenominationPubKey; + reservePub: string; + reservePriv: string; + restrictAge?: number; +} + +/** + * Reasons for why a coin is being refreshed. + */ +export enum RefreshReason { + Manual = "manual", + PayMerchant = "pay-merchant", + PayDeposit = "pay-deposit", + PayPeerPush = "pay-peer-push", + PayPeerPull = "pay-peer-pull", + Refund = "refund", + AbortPay = "abort-pay", + Recoup = "recoup", + BackupRestored = "backup-restored", + Scheduled = "scheduled", +} + +/** + * Wrapper for coin public keys. + */ +export interface CoinPublicKey { + readonly coinPub: string; +} + +/** + * Wrapper for refresh group IDs. + */ +export interface RefreshGroupId { + readonly refreshGroupId: string; +} + +/** + * Private data required to make a deposit permission. + */ +export interface DepositInfo { + exchangeBaseUrl: string; + contractTermsHash: string; + coinPub: string; + coinPriv: string; + spendAmount: AmountJson; + timestamp: TalerProtocolTimestamp; + refundDeadline: TalerProtocolTimestamp; + merchantPub: string; + feeDeposit: AmountJson; + wireInfoHash: string; + denomKeyType: DenomKeyType; + denomPubHash: string; + denomSig: UnblindedSignature; + + requiredMinimumAge?: number; + + ageCommitmentProof?: AgeCommitmentProof; +} + +export interface ExchangesListResponse { + exchanges: ExchangeListItem[]; +} + +export interface ExchangeDetailedResponse { + exchange: ExchangeFullDetails; +} + +export interface WalletCoreVersion { + hash: string | undefined; + version: string; + exchange: string; + merchant: string; + bank: string; + devMode?: boolean; +} + +export interface KnownBankAccountsInfo { + uri: PaytoUri; + kyc_completed: boolean; + currency: string; + alias: string; +} + +export interface KnownBankAccounts { + accounts: KnownBankAccountsInfo[]; +} + +export interface ExchangeTosStatusDetails { + acceptedVersion?: string; + currentVersion?: string; + contentType?: string; + content?: string; +} + +/** + * Wire fee for one wire method + */ +export interface WireFee { + /** + * Fee for wire transfers. + */ + wireFee: AmountJson; + + /** + * Fees to close and refund a reserve. + */ + closingFee: AmountJson; + + /** + * Fees for inter-exchange transfers from P2P payments. + */ + wadFee: AmountJson; + + /** + * Start date of the fee. + */ + startStamp: TalerProtocolTimestamp; + + /** + * End date of the fee. + */ + endStamp: TalerProtocolTimestamp; + + /** + * Signature made by the exchange master key. + */ + sig: string; +} + +/** + * Information about one of the exchange's bank accounts. + */ +export interface ExchangeAccount { + payto_uri: string; + master_sig: string; +} + +export type WireFeeMap = { [wireMethod: string]: WireFee[] }; + +export interface WireInfo { + feesForType: WireFeeMap; + accounts: ExchangeAccount[]; +} + +export interface ExchangeGlobalFees { + startDate: TalerProtocolTimestamp; + endDate: TalerProtocolTimestamp; + + kycFee: AmountJson; + historyFee: AmountJson; + accountFee: AmountJson; + purseFee: AmountJson; + + historyTimeout: TalerProtocolDuration; + kycTimeout: TalerProtocolDuration; + purseTimeout: TalerProtocolDuration; + + purseLimit: number; + + signature: string; +} + +const codecForExchangeAccount = (): Codec => + buildCodecForObject() + .property("payto_uri", codecForString()) + .property("master_sig", codecForString()) + .build("codecForExchangeAccount"); + +const codecForWireFee = (): Codec => + buildCodecForObject() + .property("sig", codecForString()) + .property("wireFee", codecForAmountJson()) + .property("wadFee", codecForAmountJson()) + .property("closingFee", codecForAmountJson()) + .property("startStamp", codecForTimestamp) + .property("endStamp", codecForTimestamp) + .build("codecForWireFee"); + +const codecForWireInfo = (): Codec => + buildCodecForObject() + .property("feesForType", codecForMap(codecForList(codecForWireFee()))) + .property("accounts", codecForList(codecForExchangeAccount())) + .build("codecForWireInfo"); + +export interface DenominationInfo { + /** + * Value of one coin of the denomination. + */ + value: AmountJson; + + /** + * Hash of the denomination public key. + * Stored in the database for faster lookups. + */ + denomPubHash: string; + + denomPub: DenominationPubKey; + + /** + * Fee for withdrawing. + */ + feeWithdraw: AmountJson; + + /** + * Fee for depositing. + */ + feeDeposit: AmountJson; + + /** + * Fee for refreshing. + */ + feeRefresh: AmountJson; + + /** + * Fee for refunding. + */ + feeRefund: AmountJson; + + /** + * Validity start date of the denomination. + */ + stampStart: TalerProtocolTimestamp; + + /** + * Date after which the currency can't be withdrawn anymore. + */ + stampExpireWithdraw: TalerProtocolTimestamp; + + /** + * Date after the denomination officially doesn't exist anymore. + */ + stampExpireLegal: TalerProtocolTimestamp; + + /** + * Data after which coins of this denomination can't be deposited anymore. + */ + stampExpireDeposit: TalerProtocolTimestamp; + + exchangeBaseUrl: string; +} + +export type DenomOperation = "deposit" | "withdraw" | "refresh" | "refund"; +export type DenomOperationMap = { [op in DenomOperation]: T }; + +export interface FeeDescription { + group: string; + from: AbsoluteTime; + until: AbsoluteTime; + fee?: AmountJson; +} + +export interface FeeDescriptionPair { + group: string; + from: AbsoluteTime; + until: AbsoluteTime; + left?: AmountJson; + right?: AmountJson; +} + +export interface TimePoint { + id: string; + group: string; + fee: AmountJson; + type: "start" | "end"; + moment: AbsoluteTime; + denom: T; +} + +export interface ExchangeFullDetails { + exchangeBaseUrl: string; + currency: string; + paytoUris: string[]; + tos: ExchangeTosStatusDetails; + auditors: ExchangeAuditor[]; + wireInfo: WireInfo; + denomFees: DenomOperationMap; + transferFees: Record; + globalFees: FeeDescription[]; +} + +export interface ExchangeListItem { + exchangeBaseUrl: string; + currency: string; + paytoUris: string[]; + tos: ExchangeTosStatusDetails; +} + +const codecForAuditorDenomSig = (): Codec => + buildCodecForObject() + .property("denom_pub_h", codecForString()) + .property("auditor_sig", codecForString()) + .build("AuditorDenomSig"); + +const codecForExchangeAuditor = (): Codec => + buildCodecForObject() + .property("auditor_pub", codecForString()) + .property("auditor_url", codecForString()) + .property("denomination_keys", codecForList(codecForAuditorDenomSig())) + .build("codecForExchangeAuditor"); + +const codecForExchangeTos = (): Codec => + buildCodecForObject() + .property("acceptedVersion", codecOptional(codecForString())) + .property("currentVersion", codecOptional(codecForString())) + .property("contentType", codecOptional(codecForString())) + .property("content", codecOptional(codecForString())) + .build("ExchangeTos"); + +export const codecForFeeDescriptionPair = (): Codec => + buildCodecForObject() + .property("group", codecForString()) + .property("from", codecForAbsoluteTime) + .property("until", codecForAbsoluteTime) + .property("left", codecOptional(codecForAmountJson())) + .property("right", codecOptional(codecForAmountJson())) + .build("FeeDescriptionPair"); + +export const codecForFeeDescription = (): Codec => + buildCodecForObject() + .property("group", codecForString()) + .property("from", codecForAbsoluteTime) + .property("until", codecForAbsoluteTime) + .property("fee", codecOptional(codecForAmountJson())) + .build("FeeDescription"); + +export const codecForFeesByOperations = (): Codec< + DenomOperationMap +> => + buildCodecForObject>() + .property("deposit", codecForList(codecForFeeDescription())) + .property("withdraw", codecForList(codecForFeeDescription())) + .property("refresh", codecForList(codecForFeeDescription())) + .property("refund", codecForList(codecForFeeDescription())) + .build("DenomOperationMap"); + +export const codecForExchangeFullDetails = (): Codec => + buildCodecForObject() + .property("currency", codecForString()) + .property("exchangeBaseUrl", codecForString()) + .property("paytoUris", codecForList(codecForString())) + .property("tos", codecForExchangeTos()) + .property("auditors", codecForList(codecForExchangeAuditor())) + .property("wireInfo", codecForWireInfo()) + .property("denomFees", codecForFeesByOperations()) + .property( + "transferFees", + codecForMap(codecForList(codecForFeeDescription())), + ) + .property("globalFees", codecForList(codecForFeeDescription())) + .build("ExchangeFullDetails"); + +export const codecForExchangeListItem = (): Codec => + buildCodecForObject() + .property("currency", codecForString()) + .property("exchangeBaseUrl", codecForString()) + .property("paytoUris", codecForList(codecForString())) + .property("tos", codecForExchangeTos()) + .build("ExchangeListItem"); + +export const codecForExchangesListResponse = (): Codec => + buildCodecForObject() + .property("exchanges", codecForList(codecForExchangeListItem())) + .build("ExchangesListResponse"); + +export interface AcceptManualWithdrawalResult { + /** + * Payto URIs that can be used to fund the withdrawal. + */ + exchangePaytoUris: string[]; + + /** + * Public key of the newly created reserve. + */ + reservePub: string; + + transactionId: string; +} + +export interface ManualWithdrawalDetails { + /** + * Did the user accept the current version of the exchange's + * terms of service? + */ + tosAccepted: boolean; + + /** + * Amount that the user will transfer to the exchange. + */ + amountRaw: AmountString; + + /** + * Amount that will be added to the user's wallet balance. + */ + amountEffective: AmountString; + + /** + * Ways to pay the exchange. + */ + paytoUris: string[]; + + /** + * If the exchange supports age-restricted coins it will return + * the array of ages. + */ + ageRestrictionOptions?: number[]; +} + +/** + * Selected denominations withn some extra info. + */ +export interface DenomSelectionState { + totalCoinValue: AmountJson; + totalWithdrawCost: AmountJson; + selectedDenoms: { + denomPubHash: string; + count: number; + }[]; +} + +/** + * Information about what will happen doing a withdrawal. + * + * Sent to the wallet frontend to be rendered and shown to the user. + */ +export interface ExchangeWithdrawalDetails { + exchangePaytoUris: string[]; + + /** + * Filtered wire info to send to the bank. + */ + exchangeWireAccounts: string[]; + + /** + * Selected denominations for withdraw. + */ + selectedDenoms: DenomSelectionState; + + /** + * Does the wallet know about an auditor for + * the exchange that the reserve. + */ + isAudited: boolean; + + /** + * Did the user already accept the current terms of service for the exchange? + */ + termsOfServiceAccepted: boolean; + + /** + * The exchange is trusted directly. + */ + isTrusted: boolean; + + /** + * The earliest deposit expiration of the selected coins. + */ + earliestDepositExpiration: TalerProtocolTimestamp; + + /** + * Number of currently offered denominations. + */ + numOfferedDenoms: number; + + /** + * Public keys of trusted auditors for the currency we're withdrawing. + */ + trustedAuditorPubs: string[]; + + /** + * Result of checking the wallet's version + * against the exchange's version. + * + * Older exchanges don't return version information. + */ + versionMatch: VersionMatchResult | undefined; + + /** + * Libtool-style version string for the exchange or "unknown" + * for older exchanges. + */ + exchangeVersion: string; + + /** + * Libtool-style version string for the wallet. + */ + walletVersion: string; + + /** + * Amount that will be subtracted from the reserve's balance. + */ + withdrawalAmountRaw: AmountString; + + /** + * Amount that will actually be added to the wallet's balance. + */ + withdrawalAmountEffective: AmountString; + + /** + * If the exchange supports age-restricted coins it will return + * the array of ages. + * + */ + ageRestrictionOptions?: number[]; +} + +export interface GetExchangeTosResult { + /** + * Markdown version of the current ToS. + */ + content: string; + + /** + * Version tag of the current ToS. + */ + currentEtag: string; + + /** + * Version tag of the last ToS that the user has accepted, + * if any. + */ + acceptedEtag: string | undefined; + + /** + * Accepted content type + */ + contentType: string; +} + +export interface TestPayArgs { + merchantBaseUrl: string; + merchantAuthToken?: string; + amount: string; + summary: string; + forcedCoinSel?: ForcedCoinSel; +} + +export const codecForTestPayArgs = (): Codec => + buildCodecForObject() + .property("merchantBaseUrl", codecForString()) + .property("merchantAuthToken", codecOptional(codecForString())) + .property("amount", codecForString()) + .property("summary", codecForString()) + .property("forcedCoinSel", codecForAny()) + .build("TestPayArgs"); + +export interface IntegrationTestArgs { + exchangeBaseUrl: string; + bankBaseUrl: string; + bankAccessApiBaseUrl?: string; + merchantBaseUrl: string; + merchantAuthToken?: string; + amountToWithdraw: string; + amountToSpend: string; +} + +export const codecForIntegrationTestArgs = (): Codec => + buildCodecForObject() + .property("exchangeBaseUrl", codecForString()) + .property("bankBaseUrl", codecForString()) + .property("merchantBaseUrl", codecForString()) + .property("merchantAuthToken", codecOptional(codecForString())) + .property("amountToSpend", codecForAmountString()) + .property("amountToWithdraw", codecForAmountString()) + .property("bankAccessApiBaseUrl", codecOptional(codecForAmountString())) + .build("IntegrationTestArgs"); + +export interface AddExchangeRequest { + exchangeBaseUrl: string; + forceUpdate?: boolean; +} + +export const codecForAddExchangeRequest = (): Codec => + buildCodecForObject() + .property("exchangeBaseUrl", codecForString()) + .property("forceUpdate", codecOptional(codecForBoolean())) + .build("AddExchangeRequest"); + +export interface ForceExchangeUpdateRequest { + exchangeBaseUrl: string; +} + +export const codecForForceExchangeUpdateRequest = + (): Codec => + buildCodecForObject() + .property("exchangeBaseUrl", codecForString()) + .build("AddExchangeRequest"); + +export interface GetExchangeTosRequest { + exchangeBaseUrl: string; + acceptedFormat?: string[]; +} + +export const codecForGetExchangeTosRequest = (): Codec => + buildCodecForObject() + .property("exchangeBaseUrl", codecForString()) + .property("acceptedFormat", codecOptional(codecForList(codecForString()))) + .build("GetExchangeTosRequest"); + +export interface AcceptManualWithdrawalRequest { + exchangeBaseUrl: string; + amount: string; + restrictAge?: number; +} + +export const codecForAcceptManualWithdrawalRequet = + (): Codec => + buildCodecForObject() + .property("exchangeBaseUrl", codecForString()) + .property("amount", codecForString()) + .property("restrictAge", codecOptional(codecForNumber())) + .build("AcceptManualWithdrawalRequest"); + +export interface GetWithdrawalDetailsForAmountRequest { + exchangeBaseUrl: string; + amount: string; + restrictAge?: number; +} + +export interface AcceptBankIntegratedWithdrawalRequest { + talerWithdrawUri: string; + exchangeBaseUrl: string; + forcedDenomSel?: ForcedDenomSel; + restrictAge?: number; +} + +export const codecForAcceptBankIntegratedWithdrawalRequest = + (): Codec => + buildCodecForObject() + .property("exchangeBaseUrl", codecForString()) + .property("talerWithdrawUri", codecForString()) + .property("forcedDenomSel", codecForAny()) + .property("restrictAge", codecOptional(codecForNumber())) + .build("AcceptBankIntegratedWithdrawalRequest"); + +export const codecForGetWithdrawalDetailsForAmountRequest = + (): Codec => + buildCodecForObject() + .property("exchangeBaseUrl", codecForString()) + .property("amount", codecForString()) + .property("restrictAge", codecOptional(codecForNumber())) + .build("GetWithdrawalDetailsForAmountRequest"); + +export interface AcceptExchangeTosRequest { + exchangeBaseUrl: string; + etag: string | undefined; +} + +export const codecForAcceptExchangeTosRequest = + (): Codec => + buildCodecForObject() + .property("exchangeBaseUrl", codecForString()) + .property("etag", codecOptional(codecForString())) + .build("AcceptExchangeTosRequest"); + +export interface ApplyRefundRequest { + talerRefundUri: string; +} + +export const codecForApplyRefundRequest = (): Codec => + buildCodecForObject() + .property("talerRefundUri", codecForString()) + .build("ApplyRefundRequest"); + +export interface ApplyRefundFromPurchaseIdRequest { + purchaseId: string; +} + +export const codecForApplyRefundFromPurchaseIdRequest = + (): Codec => + buildCodecForObject() + .property("purchaseId", codecForString()) + .build("ApplyRefundFromPurchaseIdRequest"); + +export interface GetWithdrawalDetailsForUriRequest { + talerWithdrawUri: string; + restrictAge?: number; +} +export const codecForGetWithdrawalDetailsForUri = + (): Codec => + buildCodecForObject() + .property("talerWithdrawUri", codecForString()) + .property("restrictAge", codecOptional(codecForNumber())) + .build("GetWithdrawalDetailsForUriRequest"); + +export interface ListKnownBankAccountsRequest { + currency?: string; +} +export const codecForListKnownBankAccounts = + (): Codec => + buildCodecForObject() + .property("currency", codecOptional(codecForString())) + .build("ListKnownBankAccountsRequest"); + +export interface AddKnownBankAccountsRequest { + payto: string; + alias: string; + currency: string; +} +export const codecForAddKnownBankAccounts = + (): Codec => + buildCodecForObject() + .property("payto", codecForString()) + .property("alias", codecForString()) + .property("currency", codecForString()) + .build("AddKnownBankAccountsRequest"); + +export interface ForgetKnownBankAccountsRequest { + payto: string; +} + +export const codecForForgetKnownBankAccounts = + (): Codec => + buildCodecForObject() + .property("payto", codecForString()) + .build("ForgetKnownBankAccountsRequest"); + +export interface AbortProposalRequest { + proposalId: string; +} + +export const codecForAbortProposalRequest = (): Codec => + buildCodecForObject() + .property("proposalId", codecForString()) + .build("AbortProposalRequest"); + +interface GetContractTermsDetailsRequest { + proposalId: string; +} + +export const codecForGetContractTermsDetails = + (): Codec => + buildCodecForObject() + .property("proposalId", codecForString()) + .build("GetContractTermsDetails"); + +export interface PreparePayRequest { + talerPayUri: string; +} + +export const codecForPreparePayRequest = (): Codec => + buildCodecForObject() + .property("talerPayUri", codecForString()) + .build("PreparePay"); + +export interface ConfirmPayRequest { + proposalId: string; + sessionId?: string; + forcedCoinSel?: ForcedCoinSel; +} + +export const codecForConfirmPayRequest = (): Codec => + buildCodecForObject() + .property("proposalId", codecForString()) + .property("sessionId", codecOptional(codecForString())) + .property("forcedCoinSel", codecForAny()) + .build("ConfirmPay"); + +export type CoreApiResponse = CoreApiResponseSuccess | CoreApiResponseError; + +export type CoreApiEnvelope = CoreApiResponse | CoreApiNotification; + +export interface CoreApiNotification { + type: "notification"; + payload: unknown; +} + +export interface CoreApiResponseSuccess { + // To distinguish the message from notifications + type: "response"; + operation: string; + id: string; + result: unknown; +} + +export interface CoreApiResponseError { + // To distinguish the message from notifications + type: "error"; + operation: string; + id: string; + error: TalerErrorDetail; +} + +export interface WithdrawTestBalanceRequest { + amount: string; + bankBaseUrl: string; + /** + * Bank access API base URL. Defaults to the bankBaseUrl. + */ + bankAccessApiBaseUrl?: string; + exchangeBaseUrl: string; + forcedDenomSel?: ForcedDenomSel; +} + +export const withdrawTestBalanceDefaults = { + amount: "TESTKUDOS:10", + bankBaseUrl: "https://bank.test.taler.net/", + exchangeBaseUrl: "https://exchange.test.taler.net/", +}; + +/** + * Request to the crypto worker to make a sync signature. + */ +export interface MakeSyncSignatureRequest { + accountPriv: string; + oldHash: string | undefined; + newHash: string; +} + +/** + * Planchet for a coin during refresh. + */ +export interface RefreshPlanchetInfo { + /** + * Public key for the coin. + */ + coinPub: string; + + /** + * Private key for the coin. + */ + coinPriv: string; + + /** + * Blinded public key. + */ + coinEv: CoinEnvelope; + + coinEvHash: string; + + /** + * Blinding key used. + */ + blindingKey: string; + + maxAge: number; + ageCommitmentProof?: AgeCommitmentProof; +} + +/** + * Strategy for loading recovery information. + */ +export enum RecoveryMergeStrategy { + /** + * Keep the local wallet root key, import and take over providers. + */ + Ours = "ours", + + /** + * Migrate to the wallet root key from the recovery information. + */ + Theirs = "theirs", +} + +/** + * Load recovery information into the wallet. + */ +export interface RecoveryLoadRequest { + recovery: BackupRecovery; + strategy?: RecoveryMergeStrategy; +} + +export const codecForWithdrawTestBalance = + (): Codec => + buildCodecForObject() + .property("amount", codecForString()) + .property("bankBaseUrl", codecForString()) + .property("exchangeBaseUrl", codecForString()) + .property("forcedDenomSel", codecForAny()) + .property("bankAccessApiBaseUrl", codecOptional(codecForString())) + .build("WithdrawTestBalanceRequest"); + +export interface ApplyRefundResponse { + contractTermsHash: string; + + transactionId: string; + + proposalId: string; + + amountEffectivePaid: AmountString; + + amountRefundGranted: AmountString; + + amountRefundGone: AmountString; + + pendingAtExchange: boolean; + + info: OrderShortInfo; +} + +export const codecForApplyRefundResponse = (): Codec => + buildCodecForObject() + .property("amountEffectivePaid", codecForAmountString()) + .property("amountRefundGone", codecForAmountString()) + .property("amountRefundGranted", codecForAmountString()) + .property("contractTermsHash", codecForString()) + .property("pendingAtExchange", codecForBoolean()) + .property("proposalId", codecForString()) + .property("transactionId", codecForString()) + .property("info", codecForOrderShortInfo()) + .build("ApplyRefundResponse"); + +export interface SetCoinSuspendedRequest { + coinPub: string; + suspended: boolean; +} + +export const codecForSetCoinSuspendedRequest = + (): Codec => + buildCodecForObject() + .property("coinPub", codecForString()) + .property("suspended", codecForBoolean()) + .build("SetCoinSuspendedRequest"); + +export interface ForceRefreshRequest { + coinPubList: string[]; +} + +export const codecForForceRefreshRequest = (): Codec => + buildCodecForObject() + .property("coinPubList", codecForList(codecForString())) + .build("ForceRefreshRequest"); + +export interface PrepareRefundRequest { + talerRefundUri: string; +} + +export const codecForPrepareRefundRequest = (): Codec => + buildCodecForObject() + .property("talerRefundUri", codecForString()) + .build("PrepareRefundRequest"); + +export interface PrepareTipRequest { + talerTipUri: string; +} + +export const codecForPrepareTipRequest = (): Codec => + buildCodecForObject() + .property("talerTipUri", codecForString()) + .build("PrepareTipRequest"); + +export interface AcceptTipRequest { + walletTipId: string; +} + +export const codecForAcceptTipRequest = (): Codec => + buildCodecForObject() + .property("walletTipId", codecForString()) + .build("AcceptTipRequest"); + +export interface AbortPayWithRefundRequest { + proposalId: string; +} + +export const codecForAbortPayWithRefundRequest = + (): Codec => + buildCodecForObject() + .property("proposalId", codecForString()) + .build("AbortPayWithRefundRequest"); + +export interface GetFeeForDepositRequest { + depositPaytoUri: string; + amount: AmountString; +} + +export interface DepositGroupFees { + coin: AmountJson; + wire: AmountJson; + refresh: AmountJson; +} + +export interface CreateDepositGroupRequest { + depositPaytoUri: string; + amount: AmountString; +} + +export const codecForGetFeeForDeposit = (): Codec => + buildCodecForObject() + .property("amount", codecForAmountString()) + .property("depositPaytoUri", codecForString()) + .build("GetFeeForDepositRequest"); + +export interface PrepareDepositRequest { + depositPaytoUri: string; + amount: AmountString; +} +export const codecForPrepareDepositRequest = (): Codec => + buildCodecForObject() + .property("amount", codecForAmountString()) + .property("depositPaytoUri", codecForString()) + .build("PrepareDepositRequest"); + +export interface PrepareDepositResponse { + totalDepositCost: AmountJson; + effectiveDepositAmount: AmountJson; +} + +export const codecForCreateDepositGroupRequest = + (): Codec => + buildCodecForObject() + .property("amount", codecForAmountString()) + .property("depositPaytoUri", codecForString()) + .build("CreateDepositGroupRequest"); + +export interface CreateDepositGroupResponse { + depositGroupId: string; + transactionId: string; +} + +export interface TrackDepositGroupRequest { + depositGroupId: string; +} + +export interface TrackDepositGroupResponse { + responses: { + status: number; + body: any; + }[]; +} + +export const codecForTrackDepositGroupRequest = + (): Codec => + buildCodecForObject() + .property("depositGroupId", codecForAmountString()) + .build("TrackDepositGroupRequest"); + +export interface WithdrawUriInfoResponse { + amount: AmountString; + defaultExchangeBaseUrl?: string; + possibleExchanges: ExchangeListItem[]; +} + +export const codecForWithdrawUriInfoResponse = + (): Codec => + buildCodecForObject() + .property("amount", codecForAmountString()) + .property("defaultExchangeBaseUrl", codecOptional(codecForString())) + .property("possibleExchanges", codecForList(codecForExchangeListItem())) + .build("WithdrawUriInfoResponse"); + +export interface WalletCurrencyInfo { + trustedAuditors: { + currency: string; + auditorPub: string; + auditorBaseUrl: string; + }[]; + trustedExchanges: { + currency: string; + exchangeMasterPub: string; + exchangeBaseUrl: string; + }[]; +} + +export interface DeleteTransactionRequest { + transactionId: string; +} + +export interface RetryTransactionRequest { + transactionId: string; +} + +export const codecForDeleteTransactionRequest = + (): Codec => + buildCodecForObject() + .property("transactionId", codecForString()) + .build("DeleteTransactionRequest"); + +export const codecForRetryTransactionRequest = + (): Codec => + buildCodecForObject() + .property("transactionId", codecForString()) + .build("RetryTransactionRequest"); + +export interface SetWalletDeviceIdRequest { + /** + * New wallet device ID to set. + */ + walletDeviceId: string; +} + +export const codecForSetWalletDeviceIdRequest = + (): Codec => + buildCodecForObject() + .property("walletDeviceId", codecForString()) + .build("SetWalletDeviceIdRequest"); + +export interface WithdrawFakebankRequest { + amount: AmountString; + exchange: string; + bank: string; +} + +export const codecForWithdrawFakebankRequest = + (): Codec => + buildCodecForObject() + .property("amount", codecForAmountString()) + .property("bank", codecForString()) + .property("exchange", codecForString()) + .build("WithdrawFakebankRequest"); + +export interface ImportDb { + dump: any; +} + +export const codecForImportDbRequest = (): Codec => + buildCodecForObject() + .property("dump", codecForAny()) + .build("ImportDbRequest"); + +export interface ForcedDenomSel { + denoms: { + value: AmountString; + count: number; + }[]; +} + +/** + * Forced coin selection for deposits/payments. + */ +export interface ForcedCoinSel { + coins: { + value: AmountString; + contribution: AmountString; + }[]; +} + +export interface TestPayResult { + payCoinSelection: PayCoinSelection; +} + +/** + * Result of selecting coins, contains the exchange, and selected + * coins with their denomination. + */ +export interface PayCoinSelection { + /** + * Amount requested by the merchant. + */ + paymentAmount: AmountJson; + + /** + * Public keys of the coins that were selected. + */ + coinPubs: string[]; + + /** + * Amount that each coin contributes. + */ + coinContributions: AmountJson[]; + + /** + * How much of the wire fees is the customer paying? + */ + customerWireFees: AmountJson; + + /** + * How much of the deposit fees is the customer paying? + */ + customerDepositFees: AmountJson; +} + +export interface InitiatePeerPushPaymentRequest { + amount: AmountString; + partialContractTerms: any; +} + +export interface InitiatePeerPushPaymentResponse { + exchangeBaseUrl: string; + pursePub: string; + mergePriv: string; + contractPriv: string; + talerUri: string; + transactionId: string; +} + +export const codecForInitiatePeerPushPaymentRequest = + (): Codec => + buildCodecForObject() + .property("amount", codecForAmountString()) + .property("partialContractTerms", codecForAny()) + .build("InitiatePeerPushPaymentRequest"); + +export interface CheckPeerPushPaymentRequest { + talerUri: string; +} + +export interface CheckPeerPullPaymentRequest { + talerUri: string; +} + +export interface CheckPeerPushPaymentResponse { + contractTerms: any; + amount: AmountString; + peerPushPaymentIncomingId: string; +} + +export interface CheckPeerPullPaymentResponse { + contractTerms: any; + amount: AmountString; + peerPullPaymentIncomingId: string; +} + +export const codecForCheckPeerPushPaymentRequest = + (): Codec => + buildCodecForObject() + .property("talerUri", codecForString()) + .build("CheckPeerPushPaymentRequest"); + +export const codecForCheckPeerPullPaymentRequest = + (): Codec => + buildCodecForObject() + .property("talerUri", codecForString()) + .build("CheckPeerPullPaymentRequest"); + +export interface AcceptPeerPushPaymentRequest { + /** + * Transparent identifier of the incoming peer push payment. + */ + peerPushPaymentIncomingId: string; +} +export interface AcceptPeerPushPaymentResponse { + transactionId: string; +} + +export interface AcceptPeerPullPaymentResponse { + transactionId: string; +} + +export const codecForAcceptPeerPushPaymentRequest = + (): Codec => + buildCodecForObject() + .property("peerPushPaymentIncomingId", codecForString()) + .build("AcceptPeerPushPaymentRequest"); + +export interface AcceptPeerPullPaymentRequest { + /** + * Transparent identifier of the incoming peer pull payment. + */ + peerPullPaymentIncomingId: string; +} + +export interface SetDevModeRequest { + devModeEnabled: boolean; +} + +export const codecForSetDevModeRequest = (): Codec => + buildCodecForObject() + .property("devModeEnabled", codecForBoolean()) + .build("SetDevModeRequest"); + +export interface ApplyDevExperimentRequest { + devExperimentUri: string; +} + +export const codecForApplyDevExperiment = + (): Codec => + buildCodecForObject() + .property("devExperimentUri", codecForString()) + .build("ApplyDevExperimentRequest"); + +export const codecForAcceptPeerPullPaymentRequest = + (): Codec => + buildCodecForObject() + .property("peerPullPaymentIncomingId", codecForString()) + .build("AcceptPeerPllPaymentRequest"); + +export interface InitiatePeerPullPaymentRequest { + /** + * FIXME: Make this optional? + */ + exchangeBaseUrl: string; + amount: AmountString; + partialContractTerms: any; +} + +export const codecForInitiatePeerPullPaymentRequest = + (): Codec => + buildCodecForObject() + .property("partialContractTerms", codecForAny()) + .property("amount", codecForAmountString()) + .property("exchangeBaseUrl", codecForAmountString()) + .build("InitiatePeerPullPaymentRequest"); + +export interface InitiatePeerPullPaymentResponse { + /** + * Taler URI for the other party to make the payment + * that was requested. + */ + talerUri: string; + + transactionId: string; +} diff --git a/packages/taler-util/src/walletTypes.ts b/packages/taler-util/src/walletTypes.ts deleted file mode 100644 index 0b2ef1d5f..000000000 --- a/packages/taler-util/src/walletTypes.ts +++ /dev/null @@ -1,1826 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2015-2020 Taler Systems SA - - TALER is free software; you can redistribute it and/or modify it under the - terms of the GNU General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - TALER is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - TALER; see the file COPYING. If not, see - */ - -/** - * 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 - */ - -/** - * Imports. - */ -import { - AmountJson, - codecForAmountJson, - codecForAmountString, -} from "./amounts.js"; -import { - AbsoluteTime, - codecForAbsoluteTime, - codecForTimestamp, - TalerProtocolDuration, - TalerProtocolTimestamp, -} from "./time.js"; -import { - buildCodecForObject, - codecForString, - codecOptional, - Codec, - codecForList, - codecForBoolean, - codecForConstString, - codecForAny, - buildCodecForUnion, - codecForNumber, - codecForMap, -} from "./codec.js"; -import { - AmountString, - AuditorDenomSig, - codecForContractTerms, - CoinEnvelope, - ContractTerms, - DenominationPubKey, - DenomKeyType, - ExchangeAuditor, - UnblindedSignature, -} from "./talerTypes.js"; -import { OrderShortInfo, codecForOrderShortInfo } from "./transactionsTypes.js"; -import { BackupRecovery } from "./backupTypes.js"; -import { PaytoUri } from "./payto.js"; -import { TalerErrorCode } from "./taler-error-codes.js"; -import { AgeCommitmentProof } from "./talerCrypto.js"; -import { VersionMatchResult } from "./libtool-version.js"; - -/** - * Response for the create reserve request to the wallet. - */ -export class CreateReserveResponse { - /** - * Exchange URL where the bank should create the reserve. - * The URL is canonicalized in the response. - */ - exchange: string; - - /** - * Reserve public key of the newly created reserve. - */ - reservePub: string; -} - -export interface Balance { - available: AmountString; - pendingIncoming: AmountString; - pendingOutgoing: AmountString; - - // Does the balance for this currency have a pending - // transaction? - hasPendingTransactions: boolean; - - // Is there a pending transaction that would affect the balance - // and requires user input? - requiresUserInput: boolean; -} - -export interface BalancesResponse { - balances: Balance[]; -} - -export const codecForBalance = (): Codec => - buildCodecForObject() - .property("available", codecForString()) - .property("hasPendingTransactions", codecForBoolean()) - .property("pendingIncoming", codecForString()) - .property("pendingOutgoing", codecForString()) - .property("requiresUserInput", codecForBoolean()) - .build("Balance"); - -export const codecForBalancesResponse = (): Codec => - buildCodecForObject() - .property("balances", codecForList(codecForBalance())) - .build("BalancesResponse"); - -/** - * For terseness. - */ -export function mkAmount( - value: number, - fraction: number, - currency: string, -): AmountJson { - return { value, fraction, currency }; -} - -export enum ConfirmPayResultType { - Done = "done", - Pending = "pending", -} - -/** - * Result for confirmPay - */ -export interface ConfirmPayResultDone { - type: ConfirmPayResultType.Done; - contractTerms: ContractTerms; - transactionId: string; -} - -export interface ConfirmPayResultPending { - type: ConfirmPayResultType.Pending; - transactionId: string; - lastError: TalerErrorDetail | undefined; -} - -export type ConfirmPayResult = ConfirmPayResultDone | ConfirmPayResultPending; - -export const codecForConfirmPayResultPending = - (): Codec => - buildCodecForObject() - .property("lastError", codecForAny()) - .property("transactionId", codecForString()) - .property("type", codecForConstString(ConfirmPayResultType.Pending)) - .build("ConfirmPayResultPending"); - -export const codecForConfirmPayResultDone = (): Codec => - buildCodecForObject() - .property("type", codecForConstString(ConfirmPayResultType.Done)) - .property("transactionId", codecForString()) - .property("contractTerms", codecForContractTerms()) - .build("ConfirmPayResultDone"); - -export const codecForConfirmPayResult = (): Codec => - buildCodecForUnion() - .discriminateOn("type") - .alternative( - ConfirmPayResultType.Pending, - codecForConfirmPayResultPending(), - ) - .alternative(ConfirmPayResultType.Done, codecForConfirmPayResultDone()) - .build("ConfirmPayResult"); - -/** - * Information about all sender wire details known to the wallet, - * as well as exchanges that accept these wire types. - */ -export interface SenderWireInfos { - /** - * Mapping from exchange base url to list of accepted - * wire types. - */ - exchangeWireTypes: { [exchangeBaseUrl: string]: string[] }; - - /** - * Sender wire information stored in the wallet. - */ - senderWires: string[]; -} - -/** - * Request to create a reserve. - */ -export interface CreateReserveRequest { - /** - * The initial amount for the reserve. - */ - amount: AmountJson; - - /** - * Exchange URL where the bank should create the reserve. - */ - exchange: string; - - /** - * Payto URI that identifies the exchange's account that the funds - * for this reserve go into. - */ - exchangePaytoUri?: string; - - /** - * Wire details (as a payto URI) for the bank account that sent the funds to - * the exchange. - */ - senderWire?: string; - - /** - * URL to fetch the withdraw status from the bank. - */ - bankWithdrawStatusUrl?: string; - - /** - * Forced denomination selection for the first withdrawal - * from this reserve, only used for testing. - */ - forcedDenomSel?: ForcedDenomSel; - - restrictAge?: number; -} - -export const codecForCreateReserveRequest = (): Codec => - buildCodecForObject() - .property("amount", codecForAmountJson()) - .property("exchange", codecForString()) - .property("exchangePaytoUri", codecForString()) - .property("senderWire", codecOptional(codecForString())) - .property("bankWithdrawStatusUrl", codecOptional(codecForString())) - .property("forcedDenomSel", codecForAny()) - .build("CreateReserveRequest"); - -/** - * Request to mark a reserve as confirmed. - */ -export interface ConfirmReserveRequest { - /** - * Public key of then reserve that should be marked - * as confirmed. - */ - reservePub: string; -} - -export const codecForConfirmReserveRequest = (): Codec => - buildCodecForObject() - .property("reservePub", codecForString()) - .build("ConfirmReserveRequest"); - -/** - * Wire coins to the user's own bank account. - */ -export class ReturnCoinsRequest { - /** - * The amount to wire. - */ - amount: AmountJson; - - /** - * The exchange to take the coins from. - */ - exchange: string; - - /** - * Wire details for the bank account of the customer that will - * receive the funds. - */ - senderWire?: string; - - /** - * Verify that a value matches the schema of this class and convert it into a - * member. - */ - static checked: (obj: any) => ReturnCoinsRequest; -} - -export interface PrepareRefundResult { - proposalId: string; - - effectivePaid: AmountString; - gone: AmountString; - granted: AmountString; - pending: boolean; - awaiting: AmountString; - - info: OrderShortInfo; -} - -export interface PrepareTipResult { - /** - * Unique ID for the tip assigned by the wallet. - * Typically different from the merchant-generated tip ID. - */ - walletTipId: string; - - /** - * Has the tip already been accepted? - */ - accepted: boolean; - - /** - * Amount that the merchant gave. - */ - tipAmountRaw: AmountString; - - /** - * Amount that arrived at the wallet. - * Might be lower than the raw amount due to fees. - */ - tipAmountEffective: AmountString; - - /** - * Base URL of the merchant backend giving then tip. - */ - merchantBaseUrl: string; - - /** - * Base URL of the exchange that is used to withdraw the tip. - * Determined by the merchant, the wallet/user has no choice here. - */ - exchangeBaseUrl: string; - - /** - * Time when the tip will expire. After it expired, it can't be picked - * up anymore. - */ - expirationTimestamp: TalerProtocolTimestamp; -} - -export interface AcceptTipResponse { - transactionId: string; -} - -export const codecForPrepareTipResult = (): Codec => - buildCodecForObject() - .property("accepted", codecForBoolean()) - .property("tipAmountRaw", codecForAmountString()) - .property("tipAmountEffective", codecForAmountString()) - .property("exchangeBaseUrl", codecForString()) - .property("merchantBaseUrl", codecForString()) - .property("expirationTimestamp", codecForTimestamp) - .property("walletTipId", codecForString()) - .build("PrepareTipResult"); - -export interface BenchmarkResult { - time: { [s: string]: number }; - repetitions: number; -} - -export enum PreparePayResultType { - PaymentPossible = "payment-possible", - InsufficientBalance = "insufficient-balance", - AlreadyConfirmed = "already-confirmed", -} - -export const codecForPreparePayResultPaymentPossible = - (): Codec => - buildCodecForObject() - .property("amountEffective", codecForAmountString()) - .property("amountRaw", codecForAmountString()) - .property("contractTerms", codecForContractTerms()) - .property("proposalId", codecForString()) - .property("contractTermsHash", codecForString()) - .property("noncePriv", codecForString()) - .property( - "status", - codecForConstString(PreparePayResultType.PaymentPossible), - ) - .build("PreparePayResultPaymentPossible"); - -export const codecForPreparePayResultInsufficientBalance = - (): Codec => - buildCodecForObject() - .property("amountRaw", codecForAmountString()) - .property("contractTerms", codecForAny()) - .property("proposalId", codecForString()) - .property("noncePriv", codecForString()) - .property( - "status", - codecForConstString(PreparePayResultType.InsufficientBalance), - ) - .build("PreparePayResultInsufficientBalance"); - -export const codecForPreparePayResultAlreadyConfirmed = - (): Codec => - buildCodecForObject() - .property( - "status", - codecForConstString(PreparePayResultType.AlreadyConfirmed), - ) - .property("amountEffective", codecForAmountString()) - .property("amountRaw", codecForAmountString()) - .property("paid", codecForBoolean()) - .property("contractTerms", codecForAny()) - .property("contractTermsHash", codecForString()) - .property("proposalId", codecForString()) - .build("PreparePayResultAlreadyConfirmed"); - -export const codecForPreparePayResult = (): Codec => - buildCodecForUnion() - .discriminateOn("status") - .alternative( - PreparePayResultType.AlreadyConfirmed, - codecForPreparePayResultAlreadyConfirmed(), - ) - .alternative( - PreparePayResultType.InsufficientBalance, - codecForPreparePayResultInsufficientBalance(), - ) - .alternative( - PreparePayResultType.PaymentPossible, - codecForPreparePayResultPaymentPossible(), - ) - .build("PreparePayResult"); - -/** - * Result of a prepare pay operation. - */ -export type PreparePayResult = - | PreparePayResultInsufficientBalance - | PreparePayResultAlreadyConfirmed - | PreparePayResultPaymentPossible; - -/** - * Payment is possible. - */ -export interface PreparePayResultPaymentPossible { - status: PreparePayResultType.PaymentPossible; - proposalId: string; - contractTerms: ContractTerms; - contractTermsHash: string; - amountRaw: string; - amountEffective: string; - noncePriv: string; -} - -export interface PreparePayResultInsufficientBalance { - status: PreparePayResultType.InsufficientBalance; - proposalId: string; - contractTerms: ContractTerms; - amountRaw: string; - noncePriv: string; -} - -export interface PreparePayResultAlreadyConfirmed { - status: PreparePayResultType.AlreadyConfirmed; - contractTerms: ContractTerms; - paid: boolean; - amountRaw: string; - amountEffective: string; - contractTermsHash: string; - proposalId: string; -} - -export interface BankWithdrawDetails { - selectionDone: boolean; - transferDone: boolean; - amount: AmountJson; - senderWire?: string; - suggestedExchange?: string; - confirmTransferUrl?: string; - wireTypes: string[]; -} - -export interface AcceptWithdrawalResponse { - reservePub: string; - confirmTransferUrl?: string; - transactionId: string; -} - -/** - * Details about a purchase, including refund status. - */ -export interface PurchaseDetails { - contractTerms: Record; - hasRefund: boolean; - totalRefundAmount: AmountJson; - totalRefundAndRefreshFees: AmountJson; -} - -export interface WalletDiagnostics { - walletManifestVersion: string; - walletManifestDisplayVersion: string; - errors: string[]; - firefoxIdbProblem: boolean; - dbOutdated: boolean; -} - -export interface TalerErrorDetail { - code: TalerErrorCode; - hint?: string; - [x: string]: unknown; -} - -/** - * Minimal information needed about a planchet for unblinding a signature. - * - * Can be a withdrawal/tipping/refresh planchet. - */ -export interface PlanchetUnblindInfo { - denomPub: DenominationPubKey; - blindingKey: string; -} - -export interface WithdrawalPlanchet { - coinPub: string; - coinPriv: string; - reservePub: string; - denomPubHash: string; - denomPub: DenominationPubKey; - blindingKey: string; - withdrawSig: string; - coinEv: CoinEnvelope; - coinValue: AmountJson; - coinEvHash: string; - ageCommitmentProof?: AgeCommitmentProof; -} - -export interface PlanchetCreationRequest { - secretSeed: string; - coinIndex: number; - value: AmountJson; - feeWithdraw: AmountJson; - denomPub: DenominationPubKey; - reservePub: string; - reservePriv: string; - restrictAge?: number; -} - -/** - * Reasons for why a coin is being refreshed. - */ -export enum RefreshReason { - Manual = "manual", - PayMerchant = "pay-merchant", - PayDeposit = "pay-deposit", - PayPeerPush = "pay-peer-push", - PayPeerPull = "pay-peer-pull", - Refund = "refund", - AbortPay = "abort-pay", - Recoup = "recoup", - BackupRestored = "backup-restored", - Scheduled = "scheduled", -} - -/** - * Wrapper for coin public keys. - */ -export interface CoinPublicKey { - readonly coinPub: string; -} - -/** - * Wrapper for refresh group IDs. - */ -export interface RefreshGroupId { - readonly refreshGroupId: string; -} - -/** - * Private data required to make a deposit permission. - */ -export interface DepositInfo { - exchangeBaseUrl: string; - contractTermsHash: string; - coinPub: string; - coinPriv: string; - spendAmount: AmountJson; - timestamp: TalerProtocolTimestamp; - refundDeadline: TalerProtocolTimestamp; - merchantPub: string; - feeDeposit: AmountJson; - wireInfoHash: string; - denomKeyType: DenomKeyType; - denomPubHash: string; - denomSig: UnblindedSignature; - - requiredMinimumAge?: number; - - ageCommitmentProof?: AgeCommitmentProof; -} - -export interface ExchangesListResponse { - exchanges: ExchangeListItem[]; -} - -export interface ExchangeDetailedResponse { - exchange: ExchangeFullDetails; -} - -export interface WalletCoreVersion { - hash: string | undefined; - version: string; - exchange: string; - merchant: string; - bank: string; - devMode?: boolean; -} - -export interface KnownBankAccountsInfo { - uri: PaytoUri; - kyc_completed: boolean; - currency: string; - alias: string; -} - -export interface KnownBankAccounts { - accounts: KnownBankAccountsInfo[]; -} - -export interface ExchangeTosStatusDetails { - acceptedVersion?: string; - currentVersion?: string; - contentType?: string; - content?: string; -} - -/** - * Wire fee for one wire method - */ -export interface WireFee { - /** - * Fee for wire transfers. - */ - wireFee: AmountJson; - - /** - * Fees to close and refund a reserve. - */ - closingFee: AmountJson; - - /** - * Fees for inter-exchange transfers from P2P payments. - */ - wadFee: AmountJson; - - /** - * Start date of the fee. - */ - startStamp: TalerProtocolTimestamp; - - /** - * End date of the fee. - */ - endStamp: TalerProtocolTimestamp; - - /** - * Signature made by the exchange master key. - */ - sig: string; -} - -/** - * Information about one of the exchange's bank accounts. - */ -export interface ExchangeAccount { - payto_uri: string; - master_sig: string; -} - -export type WireFeeMap = { [wireMethod: string]: WireFee[] }; - -export interface WireInfo { - feesForType: WireFeeMap; - accounts: ExchangeAccount[]; -} - -export interface ExchangeGlobalFees { - startDate: TalerProtocolTimestamp; - endDate: TalerProtocolTimestamp; - - kycFee: AmountJson; - historyFee: AmountJson; - accountFee: AmountJson; - purseFee: AmountJson; - - historyTimeout: TalerProtocolDuration; - kycTimeout: TalerProtocolDuration; - purseTimeout: TalerProtocolDuration; - - purseLimit: number; - - signature: string; -} - -const codecForExchangeAccount = (): Codec => - buildCodecForObject() - .property("payto_uri", codecForString()) - .property("master_sig", codecForString()) - .build("codecForExchangeAccount"); - -const codecForWireFee = (): Codec => - buildCodecForObject() - .property("sig", codecForString()) - .property("wireFee", codecForAmountJson()) - .property("wadFee", codecForAmountJson()) - .property("closingFee", codecForAmountJson()) - .property("startStamp", codecForTimestamp) - .property("endStamp", codecForTimestamp) - .build("codecForWireFee"); - -const codecForWireInfo = (): Codec => - buildCodecForObject() - .property("feesForType", codecForMap(codecForList(codecForWireFee()))) - .property("accounts", codecForList(codecForExchangeAccount())) - .build("codecForWireInfo"); - -export interface DenominationInfo { - /** - * Value of one coin of the denomination. - */ - value: AmountJson; - - /** - * Hash of the denomination public key. - * Stored in the database for faster lookups. - */ - denomPubHash: string; - - denomPub: DenominationPubKey; - - /** - * Fee for withdrawing. - */ - feeWithdraw: AmountJson; - - /** - * Fee for depositing. - */ - feeDeposit: AmountJson; - - /** - * Fee for refreshing. - */ - feeRefresh: AmountJson; - - /** - * Fee for refunding. - */ - feeRefund: AmountJson; - - /** - * Validity start date of the denomination. - */ - stampStart: TalerProtocolTimestamp; - - /** - * Date after which the currency can't be withdrawn anymore. - */ - stampExpireWithdraw: TalerProtocolTimestamp; - - /** - * Date after the denomination officially doesn't exist anymore. - */ - stampExpireLegal: TalerProtocolTimestamp; - - /** - * Data after which coins of this denomination can't be deposited anymore. - */ - stampExpireDeposit: TalerProtocolTimestamp; - - exchangeBaseUrl: string; -} - -export type DenomOperation = "deposit" | "withdraw" | "refresh" | "refund"; -export type DenomOperationMap = { [op in DenomOperation]: T }; - -export interface FeeDescription { - group: string; - from: AbsoluteTime; - until: AbsoluteTime; - fee?: AmountJson; -} - -export interface FeeDescriptionPair { - group: string; - from: AbsoluteTime; - until: AbsoluteTime; - left?: AmountJson; - right?: AmountJson; -} - -export interface TimePoint { - id: string; - group: string; - fee: AmountJson; - type: "start" | "end"; - moment: AbsoluteTime; - denom: T; -} - -export interface ExchangeFullDetails { - exchangeBaseUrl: string; - currency: string; - paytoUris: string[]; - tos: ExchangeTosStatusDetails; - auditors: ExchangeAuditor[]; - wireInfo: WireInfo; - denomFees: DenomOperationMap; - transferFees: Record; - globalFees: FeeDescription[]; -} - -export interface ExchangeListItem { - exchangeBaseUrl: string; - currency: string; - paytoUris: string[]; - tos: ExchangeTosStatusDetails; -} - -const codecForAuditorDenomSig = (): Codec => - buildCodecForObject() - .property("denom_pub_h", codecForString()) - .property("auditor_sig", codecForString()) - .build("AuditorDenomSig"); - -const codecForExchangeAuditor = (): Codec => - buildCodecForObject() - .property("auditor_pub", codecForString()) - .property("auditor_url", codecForString()) - .property("denomination_keys", codecForList(codecForAuditorDenomSig())) - .build("codecForExchangeAuditor"); - -const codecForExchangeTos = (): Codec => - buildCodecForObject() - .property("acceptedVersion", codecOptional(codecForString())) - .property("currentVersion", codecOptional(codecForString())) - .property("contentType", codecOptional(codecForString())) - .property("content", codecOptional(codecForString())) - .build("ExchangeTos"); - -export const codecForFeeDescriptionPair = (): Codec => - buildCodecForObject() - .property("group", codecForString()) - .property("from", codecForAbsoluteTime) - .property("until", codecForAbsoluteTime) - .property("left", codecOptional(codecForAmountJson())) - .property("right", codecOptional(codecForAmountJson())) - .build("FeeDescriptionPair"); - -export const codecForFeeDescription = (): Codec => - buildCodecForObject() - .property("group", codecForString()) - .property("from", codecForAbsoluteTime) - .property("until", codecForAbsoluteTime) - .property("fee", codecOptional(codecForAmountJson())) - .build("FeeDescription"); - -export const codecForFeesByOperations = (): Codec< - DenomOperationMap -> => - buildCodecForObject>() - .property("deposit", codecForList(codecForFeeDescription())) - .property("withdraw", codecForList(codecForFeeDescription())) - .property("refresh", codecForList(codecForFeeDescription())) - .property("refund", codecForList(codecForFeeDescription())) - .build("DenomOperationMap"); - -export const codecForExchangeFullDetails = (): Codec => - buildCodecForObject() - .property("currency", codecForString()) - .property("exchangeBaseUrl", codecForString()) - .property("paytoUris", codecForList(codecForString())) - .property("tos", codecForExchangeTos()) - .property("auditors", codecForList(codecForExchangeAuditor())) - .property("wireInfo", codecForWireInfo()) - .property("denomFees", codecForFeesByOperations()) - .property( - "transferFees", - codecForMap(codecForList(codecForFeeDescription())), - ) - .property("globalFees", codecForList(codecForFeeDescription())) - .build("ExchangeFullDetails"); - -export const codecForExchangeListItem = (): Codec => - buildCodecForObject() - .property("currency", codecForString()) - .property("exchangeBaseUrl", codecForString()) - .property("paytoUris", codecForList(codecForString())) - .property("tos", codecForExchangeTos()) - .build("ExchangeListItem"); - -export const codecForExchangesListResponse = (): Codec => - buildCodecForObject() - .property("exchanges", codecForList(codecForExchangeListItem())) - .build("ExchangesListResponse"); - -export interface AcceptManualWithdrawalResult { - /** - * Payto URIs that can be used to fund the withdrawal. - */ - exchangePaytoUris: string[]; - - /** - * Public key of the newly created reserve. - */ - reservePub: string; - - transactionId: string; -} - -export interface ManualWithdrawalDetails { - /** - * Did the user accept the current version of the exchange's - * terms of service? - */ - tosAccepted: boolean; - - /** - * Amount that the user will transfer to the exchange. - */ - amountRaw: AmountString; - - /** - * Amount that will be added to the user's wallet balance. - */ - amountEffective: AmountString; - - /** - * Ways to pay the exchange. - */ - paytoUris: string[]; - - /** - * If the exchange supports age-restricted coins it will return - * the array of ages. - */ - ageRestrictionOptions?: number[]; -} - -/** - * Selected denominations withn some extra info. - */ -export interface DenomSelectionState { - totalCoinValue: AmountJson; - totalWithdrawCost: AmountJson; - selectedDenoms: { - denomPubHash: string; - count: number; - }[]; -} - -/** - * Information about what will happen doing a withdrawal. - * - * Sent to the wallet frontend to be rendered and shown to the user. - */ -export interface ExchangeWithdrawalDetails { - exchangePaytoUris: string[]; - - /** - * Filtered wire info to send to the bank. - */ - exchangeWireAccounts: string[]; - - /** - * Selected denominations for withdraw. - */ - selectedDenoms: DenomSelectionState; - - /** - * Does the wallet know about an auditor for - * the exchange that the reserve. - */ - isAudited: boolean; - - /** - * Did the user already accept the current terms of service for the exchange? - */ - termsOfServiceAccepted: boolean; - - /** - * The exchange is trusted directly. - */ - isTrusted: boolean; - - /** - * The earliest deposit expiration of the selected coins. - */ - earliestDepositExpiration: TalerProtocolTimestamp; - - /** - * Number of currently offered denominations. - */ - numOfferedDenoms: number; - - /** - * Public keys of trusted auditors for the currency we're withdrawing. - */ - trustedAuditorPubs: string[]; - - /** - * Result of checking the wallet's version - * against the exchange's version. - * - * Older exchanges don't return version information. - */ - versionMatch: VersionMatchResult | undefined; - - /** - * Libtool-style version string for the exchange or "unknown" - * for older exchanges. - */ - exchangeVersion: string; - - /** - * Libtool-style version string for the wallet. - */ - walletVersion: string; - - /** - * Amount that will be subtracted from the reserve's balance. - */ - withdrawalAmountRaw: AmountString; - - /** - * Amount that will actually be added to the wallet's balance. - */ - withdrawalAmountEffective: AmountString; - - /** - * If the exchange supports age-restricted coins it will return - * the array of ages. - * - */ - ageRestrictionOptions?: number[]; -} - -export interface GetExchangeTosResult { - /** - * Markdown version of the current ToS. - */ - content: string; - - /** - * Version tag of the current ToS. - */ - currentEtag: string; - - /** - * Version tag of the last ToS that the user has accepted, - * if any. - */ - acceptedEtag: string | undefined; - - /** - * Accepted content type - */ - contentType: string; -} - -export interface TestPayArgs { - merchantBaseUrl: string; - merchantAuthToken?: string; - amount: string; - summary: string; - forcedCoinSel?: ForcedCoinSel; -} - -export const codecForTestPayArgs = (): Codec => - buildCodecForObject() - .property("merchantBaseUrl", codecForString()) - .property("merchantAuthToken", codecOptional(codecForString())) - .property("amount", codecForString()) - .property("summary", codecForString()) - .property("forcedCoinSel", codecForAny()) - .build("TestPayArgs"); - -export interface IntegrationTestArgs { - exchangeBaseUrl: string; - bankBaseUrl: string; - bankAccessApiBaseUrl?: string; - merchantBaseUrl: string; - merchantAuthToken?: string; - amountToWithdraw: string; - amountToSpend: string; -} - -export const codecForIntegrationTestArgs = (): Codec => - buildCodecForObject() - .property("exchangeBaseUrl", codecForString()) - .property("bankBaseUrl", codecForString()) - .property("merchantBaseUrl", codecForString()) - .property("merchantAuthToken", codecOptional(codecForString())) - .property("amountToSpend", codecForAmountString()) - .property("amountToWithdraw", codecForAmountString()) - .property("bankAccessApiBaseUrl", codecOptional(codecForAmountString())) - .build("IntegrationTestArgs"); - -export interface AddExchangeRequest { - exchangeBaseUrl: string; - forceUpdate?: boolean; -} - -export const codecForAddExchangeRequest = (): Codec => - buildCodecForObject() - .property("exchangeBaseUrl", codecForString()) - .property("forceUpdate", codecOptional(codecForBoolean())) - .build("AddExchangeRequest"); - -export interface ForceExchangeUpdateRequest { - exchangeBaseUrl: string; -} - -export const codecForForceExchangeUpdateRequest = - (): Codec => - buildCodecForObject() - .property("exchangeBaseUrl", codecForString()) - .build("AddExchangeRequest"); - -export interface GetExchangeTosRequest { - exchangeBaseUrl: string; - acceptedFormat?: string[]; -} - -export const codecForGetExchangeTosRequest = (): Codec => - buildCodecForObject() - .property("exchangeBaseUrl", codecForString()) - .property("acceptedFormat", codecOptional(codecForList(codecForString()))) - .build("GetExchangeTosRequest"); - -export interface AcceptManualWithdrawalRequest { - exchangeBaseUrl: string; - amount: string; - restrictAge?: number; -} - -export const codecForAcceptManualWithdrawalRequet = - (): Codec => - buildCodecForObject() - .property("exchangeBaseUrl", codecForString()) - .property("amount", codecForString()) - .property("restrictAge", codecOptional(codecForNumber())) - .build("AcceptManualWithdrawalRequest"); - -export interface GetWithdrawalDetailsForAmountRequest { - exchangeBaseUrl: string; - amount: string; - restrictAge?: number; -} - -export interface AcceptBankIntegratedWithdrawalRequest { - talerWithdrawUri: string; - exchangeBaseUrl: string; - forcedDenomSel?: ForcedDenomSel; - restrictAge?: number; -} - -export const codecForAcceptBankIntegratedWithdrawalRequest = - (): Codec => - buildCodecForObject() - .property("exchangeBaseUrl", codecForString()) - .property("talerWithdrawUri", codecForString()) - .property("forcedDenomSel", codecForAny()) - .property("restrictAge", codecOptional(codecForNumber())) - .build("AcceptBankIntegratedWithdrawalRequest"); - -export const codecForGetWithdrawalDetailsForAmountRequest = - (): Codec => - buildCodecForObject() - .property("exchangeBaseUrl", codecForString()) - .property("amount", codecForString()) - .property("restrictAge", codecOptional(codecForNumber())) - .build("GetWithdrawalDetailsForAmountRequest"); - -export interface AcceptExchangeTosRequest { - exchangeBaseUrl: string; - etag: string | undefined; -} - -export const codecForAcceptExchangeTosRequest = - (): Codec => - buildCodecForObject() - .property("exchangeBaseUrl", codecForString()) - .property("etag", codecOptional(codecForString())) - .build("AcceptExchangeTosRequest"); - -export interface ApplyRefundRequest { - talerRefundUri: string; -} - -export const codecForApplyRefundRequest = (): Codec => - buildCodecForObject() - .property("talerRefundUri", codecForString()) - .build("ApplyRefundRequest"); - -export interface ApplyRefundFromPurchaseIdRequest { - purchaseId: string; -} - -export const codecForApplyRefundFromPurchaseIdRequest = - (): Codec => - buildCodecForObject() - .property("purchaseId", codecForString()) - .build("ApplyRefundFromPurchaseIdRequest"); - -export interface GetWithdrawalDetailsForUriRequest { - talerWithdrawUri: string; - restrictAge?: number; -} -export const codecForGetWithdrawalDetailsForUri = - (): Codec => - buildCodecForObject() - .property("talerWithdrawUri", codecForString()) - .property("restrictAge", codecOptional(codecForNumber())) - .build("GetWithdrawalDetailsForUriRequest"); - -export interface ListKnownBankAccountsRequest { - currency?: string; -} -export const codecForListKnownBankAccounts = - (): Codec => - buildCodecForObject() - .property("currency", codecOptional(codecForString())) - .build("ListKnownBankAccountsRequest"); - -export interface AddKnownBankAccountsRequest { - payto: string; - alias: string; - currency: string; -} -export const codecForAddKnownBankAccounts = - (): Codec => - buildCodecForObject() - .property("payto", codecForString()) - .property("alias", codecForString()) - .property("currency", codecForString()) - .build("AddKnownBankAccountsRequest"); - -export interface ForgetKnownBankAccountsRequest { - payto: string; -} - -export const codecForForgetKnownBankAccounts = - (): Codec => - buildCodecForObject() - .property("payto", codecForString()) - .build("ForgetKnownBankAccountsRequest"); - -export interface AbortProposalRequest { - proposalId: string; -} - -export const codecForAbortProposalRequest = (): Codec => - buildCodecForObject() - .property("proposalId", codecForString()) - .build("AbortProposalRequest"); - -interface GetContractTermsDetailsRequest { - proposalId: string; -} - -export const codecForGetContractTermsDetails = - (): Codec => - buildCodecForObject() - .property("proposalId", codecForString()) - .build("GetContractTermsDetails"); - -export interface PreparePayRequest { - talerPayUri: string; -} - -export const codecForPreparePayRequest = (): Codec => - buildCodecForObject() - .property("talerPayUri", codecForString()) - .build("PreparePay"); - -export interface ConfirmPayRequest { - proposalId: string; - sessionId?: string; - forcedCoinSel?: ForcedCoinSel; -} - -export const codecForConfirmPayRequest = (): Codec => - buildCodecForObject() - .property("proposalId", codecForString()) - .property("sessionId", codecOptional(codecForString())) - .property("forcedCoinSel", codecForAny()) - .build("ConfirmPay"); - -export type CoreApiResponse = CoreApiResponseSuccess | CoreApiResponseError; - -export type CoreApiEnvelope = CoreApiResponse | CoreApiNotification; - -export interface CoreApiNotification { - type: "notification"; - payload: unknown; -} - -export interface CoreApiResponseSuccess { - // To distinguish the message from notifications - type: "response"; - operation: string; - id: string; - result: unknown; -} - -export interface CoreApiResponseError { - // To distinguish the message from notifications - type: "error"; - operation: string; - id: string; - error: TalerErrorDetail; -} - -export interface WithdrawTestBalanceRequest { - amount: string; - bankBaseUrl: string; - /** - * Bank access API base URL. Defaults to the bankBaseUrl. - */ - bankAccessApiBaseUrl?: string; - exchangeBaseUrl: string; - forcedDenomSel?: ForcedDenomSel; -} - -export const withdrawTestBalanceDefaults = { - amount: "TESTKUDOS:10", - bankBaseUrl: "https://bank.test.taler.net/", - exchangeBaseUrl: "https://exchange.test.taler.net/", -}; - -/** - * Request to the crypto worker to make a sync signature. - */ -export interface MakeSyncSignatureRequest { - accountPriv: string; - oldHash: string | undefined; - newHash: string; -} - -/** - * Planchet for a coin during refresh. - */ -export interface RefreshPlanchetInfo { - /** - * Public key for the coin. - */ - coinPub: string; - - /** - * Private key for the coin. - */ - coinPriv: string; - - /** - * Blinded public key. - */ - coinEv: CoinEnvelope; - - coinEvHash: string; - - /** - * Blinding key used. - */ - blindingKey: string; - - maxAge: number; - ageCommitmentProof?: AgeCommitmentProof; -} - -/** - * Strategy for loading recovery information. - */ -export enum RecoveryMergeStrategy { - /** - * Keep the local wallet root key, import and take over providers. - */ - Ours = "ours", - - /** - * Migrate to the wallet root key from the recovery information. - */ - Theirs = "theirs", -} - -/** - * Load recovery information into the wallet. - */ -export interface RecoveryLoadRequest { - recovery: BackupRecovery; - strategy?: RecoveryMergeStrategy; -} - -export const codecForWithdrawTestBalance = - (): Codec => - buildCodecForObject() - .property("amount", codecForString()) - .property("bankBaseUrl", codecForString()) - .property("exchangeBaseUrl", codecForString()) - .property("forcedDenomSel", codecForAny()) - .property("bankAccessApiBaseUrl", codecOptional(codecForString())) - .build("WithdrawTestBalanceRequest"); - -export interface ApplyRefundResponse { - contractTermsHash: string; - - transactionId: string; - - proposalId: string; - - amountEffectivePaid: AmountString; - - amountRefundGranted: AmountString; - - amountRefundGone: AmountString; - - pendingAtExchange: boolean; - - info: OrderShortInfo; -} - -export const codecForApplyRefundResponse = (): Codec => - buildCodecForObject() - .property("amountEffectivePaid", codecForAmountString()) - .property("amountRefundGone", codecForAmountString()) - .property("amountRefundGranted", codecForAmountString()) - .property("contractTermsHash", codecForString()) - .property("pendingAtExchange", codecForBoolean()) - .property("proposalId", codecForString()) - .property("transactionId", codecForString()) - .property("info", codecForOrderShortInfo()) - .build("ApplyRefundResponse"); - -export interface SetCoinSuspendedRequest { - coinPub: string; - suspended: boolean; -} - -export const codecForSetCoinSuspendedRequest = - (): Codec => - buildCodecForObject() - .property("coinPub", codecForString()) - .property("suspended", codecForBoolean()) - .build("SetCoinSuspendedRequest"); - -export interface ForceRefreshRequest { - coinPubList: string[]; -} - -export const codecForForceRefreshRequest = (): Codec => - buildCodecForObject() - .property("coinPubList", codecForList(codecForString())) - .build("ForceRefreshRequest"); - -export interface PrepareRefundRequest { - talerRefundUri: string; -} - -export const codecForPrepareRefundRequest = (): Codec => - buildCodecForObject() - .property("talerRefundUri", codecForString()) - .build("PrepareRefundRequest"); - -export interface PrepareTipRequest { - talerTipUri: string; -} - -export const codecForPrepareTipRequest = (): Codec => - buildCodecForObject() - .property("talerTipUri", codecForString()) - .build("PrepareTipRequest"); - -export interface AcceptTipRequest { - walletTipId: string; -} - -export const codecForAcceptTipRequest = (): Codec => - buildCodecForObject() - .property("walletTipId", codecForString()) - .build("AcceptTipRequest"); - -export interface AbortPayWithRefundRequest { - proposalId: string; -} - -export const codecForAbortPayWithRefundRequest = - (): Codec => - buildCodecForObject() - .property("proposalId", codecForString()) - .build("AbortPayWithRefundRequest"); - -export interface GetFeeForDepositRequest { - depositPaytoUri: string; - amount: AmountString; -} - -export interface DepositGroupFees { - coin: AmountJson; - wire: AmountJson; - refresh: AmountJson; -} - -export interface CreateDepositGroupRequest { - depositPaytoUri: string; - amount: AmountString; -} - -export const codecForGetFeeForDeposit = (): Codec => - buildCodecForObject() - .property("amount", codecForAmountString()) - .property("depositPaytoUri", codecForString()) - .build("GetFeeForDepositRequest"); - -export interface PrepareDepositRequest { - depositPaytoUri: string; - amount: AmountString; -} -export const codecForPrepareDepositRequest = (): Codec => - buildCodecForObject() - .property("amount", codecForAmountString()) - .property("depositPaytoUri", codecForString()) - .build("PrepareDepositRequest"); - -export interface PrepareDepositResponse { - totalDepositCost: AmountJson; - effectiveDepositAmount: AmountJson; -} - -export const codecForCreateDepositGroupRequest = - (): Codec => - buildCodecForObject() - .property("amount", codecForAmountString()) - .property("depositPaytoUri", codecForString()) - .build("CreateDepositGroupRequest"); - -export interface CreateDepositGroupResponse { - depositGroupId: string; - transactionId: string; -} - -export interface TrackDepositGroupRequest { - depositGroupId: string; -} - -export interface TrackDepositGroupResponse { - responses: { - status: number; - body: any; - }[]; -} - -export const codecForTrackDepositGroupRequest = - (): Codec => - buildCodecForObject() - .property("depositGroupId", codecForAmountString()) - .build("TrackDepositGroupRequest"); - -export interface WithdrawUriInfoResponse { - amount: AmountString; - defaultExchangeBaseUrl?: string; - possibleExchanges: ExchangeListItem[]; -} - -export const codecForWithdrawUriInfoResponse = - (): Codec => - buildCodecForObject() - .property("amount", codecForAmountString()) - .property("defaultExchangeBaseUrl", codecOptional(codecForString())) - .property("possibleExchanges", codecForList(codecForExchangeListItem())) - .build("WithdrawUriInfoResponse"); - -export interface WalletCurrencyInfo { - trustedAuditors: { - currency: string; - auditorPub: string; - auditorBaseUrl: string; - }[]; - trustedExchanges: { - currency: string; - exchangeMasterPub: string; - exchangeBaseUrl: string; - }[]; -} - -export interface DeleteTransactionRequest { - transactionId: string; -} - -export interface RetryTransactionRequest { - transactionId: string; -} - -export const codecForDeleteTransactionRequest = - (): Codec => - buildCodecForObject() - .property("transactionId", codecForString()) - .build("DeleteTransactionRequest"); - -export const codecForRetryTransactionRequest = - (): Codec => - buildCodecForObject() - .property("transactionId", codecForString()) - .build("RetryTransactionRequest"); - -export interface SetWalletDeviceIdRequest { - /** - * New wallet device ID to set. - */ - walletDeviceId: string; -} - -export const codecForSetWalletDeviceIdRequest = - (): Codec => - buildCodecForObject() - .property("walletDeviceId", codecForString()) - .build("SetWalletDeviceIdRequest"); - -export interface WithdrawFakebankRequest { - amount: AmountString; - exchange: string; - bank: string; -} - -export const codecForWithdrawFakebankRequest = - (): Codec => - buildCodecForObject() - .property("amount", codecForAmountString()) - .property("bank", codecForString()) - .property("exchange", codecForString()) - .build("WithdrawFakebankRequest"); - -export interface ImportDb { - dump: any; -} - -export const codecForImportDbRequest = (): Codec => - buildCodecForObject() - .property("dump", codecForAny()) - .build("ImportDbRequest"); - -export interface ForcedDenomSel { - denoms: { - value: AmountString; - count: number; - }[]; -} - -/** - * Forced coin selection for deposits/payments. - */ -export interface ForcedCoinSel { - coins: { - value: AmountString; - contribution: AmountString; - }[]; -} - -export interface TestPayResult { - payCoinSelection: PayCoinSelection; -} - -/** - * Result of selecting coins, contains the exchange, and selected - * coins with their denomination. - */ -export interface PayCoinSelection { - /** - * Amount requested by the merchant. - */ - paymentAmount: AmountJson; - - /** - * Public keys of the coins that were selected. - */ - coinPubs: string[]; - - /** - * Amount that each coin contributes. - */ - coinContributions: AmountJson[]; - - /** - * How much of the wire fees is the customer paying? - */ - customerWireFees: AmountJson; - - /** - * How much of the deposit fees is the customer paying? - */ - customerDepositFees: AmountJson; -} - -export interface InitiatePeerPushPaymentRequest { - amount: AmountString; - partialContractTerms: any; -} - -export interface InitiatePeerPushPaymentResponse { - exchangeBaseUrl: string; - pursePub: string; - mergePriv: string; - contractPriv: string; - talerUri: string; - transactionId: string; -} - -export const codecForInitiatePeerPushPaymentRequest = - (): Codec => - buildCodecForObject() - .property("amount", codecForAmountString()) - .property("partialContractTerms", codecForAny()) - .build("InitiatePeerPushPaymentRequest"); - -export interface CheckPeerPushPaymentRequest { - talerUri: string; -} - -export interface CheckPeerPullPaymentRequest { - talerUri: string; -} - -export interface CheckPeerPushPaymentResponse { - contractTerms: any; - amount: AmountString; - peerPushPaymentIncomingId: string; -} - -export interface CheckPeerPullPaymentResponse { - contractTerms: any; - amount: AmountString; - peerPullPaymentIncomingId: string; -} - -export const codecForCheckPeerPushPaymentRequest = - (): Codec => - buildCodecForObject() - .property("talerUri", codecForString()) - .build("CheckPeerPushPaymentRequest"); - -export const codecForCheckPeerPullPaymentRequest = - (): Codec => - buildCodecForObject() - .property("talerUri", codecForString()) - .build("CheckPeerPullPaymentRequest"); - -export interface AcceptPeerPushPaymentRequest { - /** - * Transparent identifier of the incoming peer push payment. - */ - peerPushPaymentIncomingId: string; -} -export interface AcceptPeerPushPaymentResponse { - transactionId: string; -} - -export interface AcceptPeerPullPaymentResponse { - transactionId: string; -} - -export const codecForAcceptPeerPushPaymentRequest = - (): Codec => - buildCodecForObject() - .property("peerPushPaymentIncomingId", codecForString()) - .build("AcceptPeerPushPaymentRequest"); - -export interface AcceptPeerPullPaymentRequest { - /** - * Transparent identifier of the incoming peer pull payment. - */ - peerPullPaymentIncomingId: string; -} - -export interface SetDevModeRequest { - devModeEnabled: boolean; -} - -export const codecForSetDevModeRequest = (): Codec => - buildCodecForObject() - .property("devModeEnabled", codecForBoolean()) - .build("SetDevModeRequest"); - -export interface ApplyDevExperimentRequest { - devExperimentUri: string; -} - -export const codecForApplyDevExperiment = - (): Codec => - buildCodecForObject() - .property("devExperimentUri", codecForString()) - .build("ApplyDevExperimentRequest"); - -export const codecForAcceptPeerPullPaymentRequest = - (): Codec => - buildCodecForObject() - .property("peerPullPaymentIncomingId", codecForString()) - .build("AcceptPeerPllPaymentRequest"); - -export interface InitiatePeerPullPaymentRequest { - /** - * FIXME: Make this optional? - */ - exchangeBaseUrl: string; - amount: AmountString; - partialContractTerms: any; -} - -export const codecForInitiatePeerPullPaymentRequest = - (): Codec => - buildCodecForObject() - .property("partialContractTerms", codecForAny()) - .property("amount", codecForAmountString()) - .property("exchangeBaseUrl", codecForAmountString()) - .build("InitiatePeerPullPaymentRequest"); - -export interface InitiatePeerPullPaymentResponse { - /** - * Taler URI for the other party to make the payment - * that was requested. - */ - talerUri: string; - - transactionId: string; -} -- cgit v1.2.3