/*
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.
*
* Current limitations:
* 1. Deletion tomb stones aren't supported yet
* 2. Exchange/auditor trust isn't exported yet
* (see https://bugs.gnunet.org/view.php?id=6448)
*
* 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).
*
* @author Florian Dold
*/
/**
* Imports.
*/
import { Timestamp } from "../util/time";
import {
ReserveClosingTransaction,
ReserveCreditTransaction,
ReserveRecoupTransaction,
ReserveWithdrawTransaction,
} from "./ReserveTransaction";
/**
* Type alias for strings that are to be treated like amounts.
*/
type BackupAmountString = 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: "gnu-taler-wallet-backup-content";
/**
* Version of the schema.
*/
schema_version: 1;
/**
* Monotonically increasing clock of the wallet,
* used to determine causality when merging backups.
*/
clock: 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;
/**
* Per-exchange data sorted by exchange master public key.
*
* Sorted by the exchange public key.
*/
exchanges: BackupExchange[];
/**
* Grouped refresh sessions.
*
* Sorted by the refresh group ID.
*/
refresh_groups: BackupRefreshGroup[];
/**
* Tips.
*
* Sorted by the wallet tip ID.
*/
tips: BackupTip[];
/**
* Proposals from merchants. The proposal may
* be deleted as soon as it has been accepted (and thus
* turned into a purchase).
*
* Sorted by the proposal ID.
*/
proposals: BackupProposal[];
/**
* Accepted purchases.
*
* Sorted by the proposal ID.
*/
purchases: BackupPurchase[];
/**
* All backup providers.
*
* Sorted by the provider base URL.
*/
backup_providers: BackupBackupProvider[];
/**
* Recoup groups.
*/
recoup_groups: BackupRecoupGroup[];
}
/**
* Backup information about one backup storage provider.
*/
export class BackupBackupProvider {
/**
* Canonicalized base URL of the provider.
*/
base_url: string;
/**
* 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;
/**
* Last proposal ID to pay for the backup provider.
*/
pay_proposal_id?: string;
}
/**
* 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_started: Timestamp;
/**
* Timestamp when the recoup finished.
*/
timestamp_finished: Timestamp | undefined;
/**
* Information about each coin being recouped.
*/
coins: {
coin_pub: string;
finished: boolean;
old_amount: BackupAmountString;
}[];
/**
* Public keys of coins that should be scheduled for refreshing
* after all individual recoups are done.
*/
recoup_refresh_coins: string[];
}
/**
* 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)
*/
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;
/**
* Key used by the exchange used to sign the coin.
*/
denom_pub: string;
/**
* Unblinded signature by the exchange.
*/
denom_sig: string;
/**
* 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?
*
* FIXME: If we always refresh when importing a backup, do
* we even need this flag?
*/
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;
/**
* Has the user accepted the tip? Only after the tip has been accepted coins
* withdrawn from the tip may be used.
*/
timestamp_accepted: Timestamp | undefined;
/**
* When was the tip first scanned by the wallet?
*/
timestamp_created: Timestamp;
/**
* Timestamp for when the wallet finished picking up the tip
* from the merchant.
*/
timestam_picked_up: Timestamp | undefined;
/**
* The tipped amount.
*/
tip_amount_raw: BackupAmountString;
/**
* Timestamp, the tip can't be picked up anymore after this deadline.
*/
timestamp_expiration: Timestamp;
/**
* 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;
/**
* Planchets, the members included in TipPlanchetDetail will be sent to the
* merchant.
*/
planchets?: {
blinding_key: string;
coin_priv: string;
}[];
/**
* Selected denominations. Determines the effective tip amount.
*/
selected_denoms: {
denom_pub_hash: string;
count: number;
}[];
}
/**
* 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",
}
/**
* Planchet for a coin during refresh.
*/
export interface BackupRefreshPlanchet {
/**
* Private key for the coin.
*/
private_key: string;
/**
* Blinding key used.
*/
blinding_key: string;
}
/**
* 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 {
/**
* Signature made by the old coin to confirm the melting.
*/
confirm_sig: string;
/**
* Hased denominations of the newly requested coins.
*/
new_denom_hashes: string[];
/**
* Planchets for each cut-and-choose instance.
*/
planchets_for_gammas: BackupRefreshPlanchet[][];
/**
* Private keys for the transfer public keys.
*/
transfer_privs: string[];
/**
* The no-reveal-index after we've done the melting.
*/
noreveal_index?: number;
/**
* Hash of the session.
*/
hash: string;
/**
* Timestamp when the refresh session finished.
*/
timestamp_finished: Timestamp | undefined;
/**
* When has this refresh session been created?
*/
timestamp_created: Timestamp;
}
/**
* 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: {
/**
* 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;
/**
* Coin is skipped (finished without a refresh session) because
* there is not enough value left on it.
*/
skipped: boolean;
/**
* Refresh session (if created) or undefined it not created yet.
*/
refresh_session: BackupRefreshSession | undefined;
}[];
/**
* Timestamp when the refresh group finished.
*/
timestamp_finished: Timestamp | undefined;
}
/**
* Backup information for a withdrawal group.
*
* Always part of a BackupReserve.
*/
export interface BackupWithdrawalGroup {
withdrawal_group_id: string;
/**
* When was the withdrawal operation started started?
* Timestamp in milliseconds.
*/
timestamp_start: Timestamp;
/**
* When was the withdrawal operation completed?
*/
timestamp_finish?: Timestamp;
/**
* 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;
/**
* Multiset of denominations selected for withdrawal.
*/
selected_denoms: {
denom_pub_hash: string;
count: number;
}[];
/**
* One planchet/coin for each selected denomination.
*/
planchets: {
blinding_key: string;
coin_priv: string;
}[];
}
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: Timestamp;
/**
* Time when the wallet became aware of the refund.
*/
obtained_time: Timestamp;
/**
* 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;
export interface BackupPurchase {
/**
* Proposal ID for this purchase. Uniquely identifies the
* purchase and the proposal.
*/
proposal_id: string;
/**
* Contract terms we got from the merchant.
*/
contract_terms_raw: string;
pay_coins: {
/**
* Public keys of the coins that were selected.
*/
coin_pubs: string[];
/**
* Deposit permission signature of each coin.
*/
coin_sigs: string[];
/**
* Amount that each coin contributes.
*/
contribution: BackupAmountString;
};
/**
* Timestamp of the first time that sending a payment to the merchant
* for this purchase was successful.
*/
timestamp_first_successful_pay: Timestamp | undefined;
/**
* Signature by the merchant confirming the payment.
*/
merchant_pay_sig: string | undefined;
/**
* When was the purchase made?
* Refers to the time that the user accepted.
*/
timestamp_accept: Timestamp;
/**
* Pending refunds for the purchase. A refund is pending
* when the merchant reports a transient error from the exchange.
*/
refunds: BackupRefundItem[];
/**
* When was the last refund made?
* Set to 0 if no refund was made on the purchase.
*/
timestamp_last_refund_status: Timestamp | undefined;
/**
* Abort status of the payment.
*/
abort_status?: "abort-refund" | "abort-finished";
/**
* Continue querying the refund status until this deadline has expired.
*/
auto_refund_deadline: Timestamp | 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: string;
/**
* 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: Timestamp;
/**
* Date after which the currency can't be withdrawn anymore.
*/
stamp_expire_withdraw: Timestamp;
/**
* Date after the denomination officially doesn't exist anymore.
*/
stamp_expire_legal: Timestamp;
/**
* Data after which coins of this denomination can't be deposited anymore.
*/
stamp_expire_deposit: Timestamp;
/**
* 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;
}
export interface BackupReserve {
/**
* The reserve private key.
*/
reserve_priv: string;
/**
* Time when the reserve was created.
*/
timestamp_created: Timestamp;
/**
* Wire information (as payto URI) for the bank account that
* transfered funds for this reserve.
*/
sender_wire?: string;
/**
* Amount that was sent by the user to fund the reserve.
*/
instructed_amount: BackupAmountString;
/**
* Extra state for when this is a withdrawal involving
* a Taler-integrated bank.
*/
bank_info?: {
/**
* Status URL that the wallet will use to query the status
* of the Taler withdrawal operation on the bank's side.
*/
status_url: string;
/**
* URL that the user should be instructed to navigate to
* in order to confirm the transfer (or show instructions/help
* on how to do that at a PoS terminal).
*/
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.
*/
timestamp_reserve_info_posted: Timestamp | undefined;
/**
* Time when the reserve was confirmed by the bank.
*
* Set to undefined if not confirmed yet.
*/
timestamp_bank_confirmed: Timestamp | undefined;
};
/**
* Pre-allocated withdrawal group ID that will be
* used for the first withdrawal.
*
* (Already created so it can be referenced in the transactions list
* before it really exists, as there'll be an entry for the withdrawal
* even before the withdrawal group really has been created).
*/
initial_withdrawal_group_id: string;
/**
* Denominations selected for the initial withdrawal.
* Stored here to show costs before withdrawal has begun.
*/
initial_selected_denoms: {
denom_pub_hash: string;
count: number;
}[];
/**
* Groups of withdrawal operations for this reserve. Typically just one.
*/
withdrawal_groups: BackupWithdrawalGroup[];
}
/**
* Wire fee for one wire method 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;
/**
* Fees to close and refund a reserve.
*/
closing_fee: string;
/**
* Start date of the fee.
*/
start_stamp: Timestamp;
/**
* End date of the fee.
*/
end_stamp: Timestamp;
/**
* Signature made by the exchange master key.
*/
sig: string;
}
/**
* Structure of one exchange signing key in the /keys response.
*/
export class BackupExchangeSignKey {
stamp_start: Timestamp;
stamp_expire: Timestamp;
stamp_end: Timestamp;
key: string;
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 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: AuditorDenomSig[];
}
/**
* Backup information about an exchange.
*/
export interface BackupExchange {
/**
* 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[];
/**
* Reserves at the exchange.
*/
reserves: BackupReserve[];
coins: BackupCoin[];
/**
* Last observed protocol version.
*/
protocol_version: string;
/**
* 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[];
/**
* Bank accounts offered by the exchange;
*/
accounts: {
payto_uri: string;
master_sig: string;
}[];
/**
* ETag for last terms of service download.
*/
tos_etag_last: string | undefined;
/**
* ETag for last terms of service download.
*/
tos_etag_accepted: string | undefined;
}
export enum WalletReserveHistoryItemType {
Credit = "credit",
Withdraw = "withdraw",
Closing = "closing",
Recoup = "recoup",
}
export interface BackupReserveHistoryCreditItem {
type: WalletReserveHistoryItemType.Credit;
/**
* Amount we expect to see credited.
*/
expected_amount?: BackupAmountString;
/**
* Item from the reserve transaction history that this
* wallet reserve history item matches up with.
*/
matched_exchange_transaction?: ReserveCreditTransaction;
}
export interface BackupReserveHistoryWithdrawItem {
type: WalletReserveHistoryItemType.Withdraw;
expected_amount?: BackupAmountString;
/**
* Hash of the blinded coin.
*
* When this value is set, it indicates that a withdrawal is active
* in the wallet for the
*/
expected_coin_ev_hash?: string;
/**
* Item from the reserve transaction history that this
* wallet reserve history item matches up with.
*/
matched_exchange_transaction?: ReserveWithdrawTransaction;
}
export interface BackupReserveHistoryClosingItem {
type: WalletReserveHistoryItemType.Closing;
/**
* Item from the reserve transaction history that this
* wallet reserve history item matches up with.
*/
matched_exchange_transaction?: ReserveClosingTransaction;
}
export interface BackupReserveHistoryRecoupItem {
type: WalletReserveHistoryItemType.Recoup;
/**
* Amount we expect to see recouped.
*/
expected_amount?: BackupAmountString;
/**
* Item from the reserve transaction history that this
* wallet reserve history item matches up with.
*/
matched_exchange_transaction?: ReserveRecoupTransaction;
}
export type BackupReserveHistoryItem =
| BackupReserveHistoryCreditItem
| BackupReserveHistoryWithdrawItem
| BackupReserveHistoryRecoupItem
| BackupReserveHistoryClosingItem;
export enum ProposalStatus {
/**
* Not downloaded yet.
*/
Downloading = "downloading",
/**
* Proposal downloaded, but the user needs to accept/reject it.
*/
Proposed = "proposed",
/**
* The user has accepted the proposal.
*/
Accepted = "accepted",
/**
* The user has rejected the proposal.
*/
Refused = "refused",
/**
* Downloading or processing the proposal has failed permanently.
*/
PermanentlyFailed = "permanently-failed",
/**
* Downloaded proposal was detected as a re-purchase.
*/
Repurchase = "repurchase",
}
/**
* Proposal by a merchant.
*/
export interface BackupProposal {
/**
* Downloaded data from the merchant.
*/
contract_terms_raw?: string;
/**
* Unique ID when the order is stored in the wallet DB.
*/
proposal_id: string;
/**
* Timestamp of when the record
* was created.
*/
timestamp: Timestamp;
/**
* Private key for the nonce.
*/
nonce_priv: string;
/**
* Public key for the nonce.
*/
nonce_pub: string;
/**
* Claim token initially given by the merchant.
*/
claim_token: string | undefined;
/**
* Status of the proposal.
*/
proposal_status: ProposalStatus;
/**
* 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;
}