wallet-core: make basic backup work again
This commit is contained in:
parent
52ec740c82
commit
16a5bb4083
@ -47,6 +47,15 @@
|
|||||||
* 3. Derived information is never backed up (hashed values, public keys
|
* 3. Derived information is never backed up (hashed values, public keys
|
||||||
* when we know the private key).
|
* 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 <dold@taler.net>
|
* @author Florian Dold <dold@taler.net>
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@ -56,6 +65,23 @@
|
|||||||
import { DenominationPubKey, UnblindedSignature } from "./talerTypes.js";
|
import { DenominationPubKey, UnblindedSignature } from "./talerTypes.js";
|
||||||
import { TalerProtocolDuration, TalerProtocolTimestamp } from "./time.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 alias for strings that are to be treated like amounts.
|
||||||
*/
|
*/
|
||||||
@ -93,12 +119,14 @@ export interface WalletBackupContentV1 {
|
|||||||
/**
|
/**
|
||||||
* Magic constant to identify that this is a backup content JSON.
|
* Magic constant to identify that this is a backup content JSON.
|
||||||
*/
|
*/
|
||||||
schema_id: "gnu-taler-wallet-backup-content";
|
schema_id: typeof BACKUP_TAG;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Version of the schema.
|
* Version of the schema.
|
||||||
*/
|
*/
|
||||||
schema_version: 1;
|
schema_version: typeof BACKUP_VERSION_MAJOR;
|
||||||
|
|
||||||
|
minor_version: number;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Root public key of the wallet. This field is present as
|
* Root public key of the wallet. This field is present as
|
||||||
@ -131,6 +159,13 @@ export interface WalletBackupContentV1 {
|
|||||||
|
|
||||||
exchange_details: BackupExchangeDetails[];
|
exchange_details: BackupExchangeDetails[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Withdrawal groups.
|
||||||
|
*
|
||||||
|
* Sorted by the withdrawal group ID.
|
||||||
|
*/
|
||||||
|
withdrawal_groups: BackupWithdrawalGroup[];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Grouped refresh sessions.
|
* Grouped refresh sessions.
|
||||||
*
|
*
|
||||||
@ -208,6 +243,118 @@ export interface WalletBackupContentV1 {
|
|||||||
tombstones: Tombstone[];
|
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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Detailled 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 "<type>:<key>"
|
* Tombstone in the format "<type>:<key>"
|
||||||
*/
|
*/
|
||||||
@ -619,46 +766,6 @@ export interface BackupRefreshGroup {
|
|||||||
finish_is_failure?: boolean;
|
finish_is_failure?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Backup information for a withdrawal group.
|
|
||||||
*
|
|
||||||
* Always part of a BackupReserve.
|
|
||||||
*/
|
|
||||||
export interface BackupWithdrawalGroup {
|
|
||||||
withdrawal_group_id: string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Secret seed to derive the planchets.
|
|
||||||
*/
|
|
||||||
secret_seed: string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* When was the withdrawal operation started started?
|
|
||||||
* Timestamp in milliseconds.
|
|
||||||
*/
|
|
||||||
timestamp_created: TalerProtocolTimestamp;
|
|
||||||
|
|
||||||
timestamp_finish?: TalerProtocolTimestamp;
|
|
||||||
finish_is_failure?: boolean;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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: BackupDenomSel;
|
|
||||||
|
|
||||||
selected_denoms_id: OperationUid;
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum BackupRefundState {
|
export enum BackupRefundState {
|
||||||
Failed = "failed",
|
Failed = "failed",
|
||||||
Applied = "applied",
|
Applied = "applied",
|
||||||
@ -914,101 +1021,6 @@ export type BackupDenomSel = {
|
|||||||
count: number;
|
count: number;
|
||||||
}[];
|
}[];
|
||||||
|
|
||||||
export interface BackupReserve {
|
|
||||||
/**
|
|
||||||
* The reserve private key.
|
|
||||||
*/
|
|
||||||
reserve_priv: string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Time when the reserve was created.
|
|
||||||
*/
|
|
||||||
timestamp_created: TalerProtocolTimestamp;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Timestamp of the last observed activity.
|
|
||||||
*
|
|
||||||
* Used to compute when to give up querying the exchange.
|
|
||||||
*/
|
|
||||||
timestamp_last_activity: TalerProtocolTimestamp;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Timestamp of when the reserve closed.
|
|
||||||
*
|
|
||||||
* Note that the last activity can be after the closing time
|
|
||||||
* due to recouping.
|
|
||||||
*/
|
|
||||||
timestamp_closed?: TalerProtocolTimestamp;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Wire information (as payto URI) for the bank account that
|
|
||||||
* transferred 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: TalerProtocolTimestamp | undefined;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Time when the reserve was confirmed by the bank.
|
|
||||||
*
|
|
||||||
* Set to undefined if not confirmed yet.
|
|
||||||
*/
|
|
||||||
timestamp_bank_confirmed: TalerProtocolTimestamp | 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: BackupDenomSel;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Groups of withdrawal operations for this reserve. Typically just one.
|
|
||||||
*/
|
|
||||||
withdrawal_groups: BackupWithdrawalGroup[];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Wire fee for one wire payment target type as stored in the
|
* Wire fee for one wire payment target type as stored in the
|
||||||
* wallet's database.
|
* wallet's database.
|
||||||
@ -1148,11 +1160,6 @@ export interface BackupExchangeDetails {
|
|||||||
*/
|
*/
|
||||||
denominations: BackupDenomination[];
|
denominations: BackupDenomination[];
|
||||||
|
|
||||||
/**
|
|
||||||
* Reserves at the exchange.
|
|
||||||
*/
|
|
||||||
reserves: BackupReserve[];
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Last observed protocol version.
|
* Last observed protocol version.
|
||||||
*/
|
*/
|
||||||
|
@ -87,10 +87,14 @@ export interface TransactionCommon {
|
|||||||
*/
|
*/
|
||||||
frozen: boolean;
|
frozen: boolean;
|
||||||
|
|
||||||
// Raw amount of the transaction (exclusive of fees or other extra costs)
|
/**
|
||||||
|
* Raw amount of the transaction (exclusive of fees or other extra costs).
|
||||||
|
*/
|
||||||
amountRaw: AmountString;
|
amountRaw: AmountString;
|
||||||
|
|
||||||
// Amount added or removed from the wallet's balance (including all fees and other costs)
|
/**
|
||||||
|
* Amount added or removed from the wallet's balance (including all fees and other costs).
|
||||||
|
*/
|
||||||
amountEffective: AmountString;
|
amountEffective: AmountString;
|
||||||
|
|
||||||
error?: TalerErrorDetail;
|
error?: TalerErrorDetail;
|
||||||
@ -509,7 +513,8 @@ export interface TransactionByIdRequest {
|
|||||||
transactionId: string;
|
transactionId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const codecForTransactionByIdRequest = (): Codec<TransactionByIdRequest> =>
|
export const codecForTransactionByIdRequest =
|
||||||
|
(): Codec<TransactionByIdRequest> =>
|
||||||
buildCodecForObject<TransactionByIdRequest>()
|
buildCodecForObject<TransactionByIdRequest>()
|
||||||
.property("transactionId", codecForString())
|
.property("transactionId", codecForString())
|
||||||
.build("TransactionByIdRequest");
|
.build("TransactionByIdRequest");
|
||||||
|
@ -886,6 +886,26 @@ currenciesCli
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
advancedCli
|
||||||
|
.subcommand("clearDatabase", "clear-database", {
|
||||||
|
help: "Clear the database, irrevocable deleting all data in the wallet.",
|
||||||
|
})
|
||||||
|
.action(async (args) => {
|
||||||
|
await withWallet(args, async (wallet) => {
|
||||||
|
await wallet.client.call(WalletApiOperation.ClearDb, {});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
advancedCli
|
||||||
|
.subcommand("recycle", "recycle", {
|
||||||
|
help: "Export, clear and re-import the database via the backup mechamism.",
|
||||||
|
})
|
||||||
|
.action(async (args) => {
|
||||||
|
await withWallet(args, async (wallet) => {
|
||||||
|
await wallet.client.call(WalletApiOperation.Recycle, {});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
advancedCli
|
advancedCli
|
||||||
.subcommand("payPrepare", "pay-prepare", {
|
.subcommand("payPrepare", "pay-prepare", {
|
||||||
help: "Claim an order but don't pay yet.",
|
help: "Claim an order but don't pay yet.",
|
||||||
|
@ -17,9 +17,13 @@
|
|||||||
/**
|
/**
|
||||||
* Imports.
|
* Imports.
|
||||||
*/
|
*/
|
||||||
|
import { j2s } from "@gnu-taler/taler-util";
|
||||||
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
|
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
|
||||||
import { GlobalTestState, WalletCli } from "../harness/harness.js";
|
import { GlobalTestState, WalletCli } from "../harness/harness.js";
|
||||||
import { createSimpleTestkudosEnvironment, withdrawViaBank } from "../harness/helpers.js";
|
import {
|
||||||
|
createSimpleTestkudosEnvironment,
|
||||||
|
withdrawViaBank,
|
||||||
|
} from "../harness/helpers.js";
|
||||||
import { SyncService } from "../harness/sync";
|
import { SyncService } from "../harness/sync";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -28,13 +32,8 @@ import { SyncService } from "../harness/sync";
|
|||||||
export async function runWalletBackupBasicTest(t: GlobalTestState) {
|
export async function runWalletBackupBasicTest(t: GlobalTestState) {
|
||||||
// Set up test environment
|
// Set up test environment
|
||||||
|
|
||||||
const {
|
const { commonDb, merchant, wallet, bank, exchange } =
|
||||||
commonDb,
|
await createSimpleTestkudosEnvironment(t);
|
||||||
merchant,
|
|
||||||
wallet,
|
|
||||||
bank,
|
|
||||||
exchange,
|
|
||||||
} = await createSimpleTestkudosEnvironment(t);
|
|
||||||
|
|
||||||
const sync = await SyncService.create(t, {
|
const sync = await SyncService.create(t, {
|
||||||
currency: "TESTKUDOS",
|
currency: "TESTKUDOS",
|
||||||
@ -106,6 +105,9 @@ export async function runWalletBackupBasicTest(t: GlobalTestState) {
|
|||||||
{},
|
{},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const txs = await wallet.client.call(WalletApiOperation.GetTransactions, {});
|
||||||
|
console.log(`backed up transactions ${j2s(txs)}`);
|
||||||
|
|
||||||
const wallet2 = new WalletCli(t, "wallet2");
|
const wallet2 = new WalletCli(t, "wallet2");
|
||||||
|
|
||||||
// Check that the second wallet is a fresh wallet.
|
// Check that the second wallet is a fresh wallet.
|
||||||
@ -129,6 +131,11 @@ export async function runWalletBackupBasicTest(t: GlobalTestState) {
|
|||||||
|
|
||||||
// Now do some basic checks that the restored wallet is still functional
|
// Now do some basic checks that the restored wallet is still functional
|
||||||
{
|
{
|
||||||
|
const txs = await wallet2.client.call(
|
||||||
|
WalletApiOperation.GetTransactions,
|
||||||
|
{},
|
||||||
|
);
|
||||||
|
console.log(`restored transactions ${j2s(txs)}`);
|
||||||
const bal1 = await wallet2.client.call(WalletApiOperation.GetBalances, {});
|
const bal1 = await wallet2.client.call(WalletApiOperation.GetBalances, {});
|
||||||
|
|
||||||
t.assertAmountEquals(bal1.balances[0].available, "TESTKUDOS:14.1");
|
t.assertAmountEquals(bal1.balances[0].available, "TESTKUDOS:14.1");
|
||||||
@ -140,8 +147,16 @@ export async function runWalletBackupBasicTest(t: GlobalTestState) {
|
|||||||
amount: "TESTKUDOS:10",
|
amount: "TESTKUDOS:10",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await exchange.runWirewatchOnce();
|
||||||
|
|
||||||
await wallet2.runUntilDone();
|
await wallet2.runUntilDone();
|
||||||
|
|
||||||
|
const txs2 = await wallet2.client.call(
|
||||||
|
WalletApiOperation.GetTransactions,
|
||||||
|
{},
|
||||||
|
);
|
||||||
|
console.log(`tx after withdraw after restore ${j2s(txs2)}`);
|
||||||
|
|
||||||
const bal2 = await wallet2.client.call(WalletApiOperation.GetBalances, {});
|
const bal2 = await wallet2.client.call(WalletApiOperation.GetBalances, {});
|
||||||
|
|
||||||
t.assertAmountEquals(bal2.balances[0].available, "TESTKUDOS:23.82");
|
t.assertAmountEquals(bal2.balances[0].available, "TESTKUDOS:23.82");
|
||||||
|
@ -19,7 +19,11 @@
|
|||||||
*/
|
*/
|
||||||
import { PreparePayResultType } from "@gnu-taler/taler-util";
|
import { PreparePayResultType } from "@gnu-taler/taler-util";
|
||||||
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
|
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
|
||||||
import { GlobalTestState, WalletCli, MerchantPrivateApi } from "../harness/harness.js";
|
import {
|
||||||
|
GlobalTestState,
|
||||||
|
WalletCli,
|
||||||
|
MerchantPrivateApi,
|
||||||
|
} from "../harness/harness.js";
|
||||||
import {
|
import {
|
||||||
createSimpleTestkudosEnvironment,
|
createSimpleTestkudosEnvironment,
|
||||||
makeTestPayment,
|
makeTestPayment,
|
||||||
@ -33,13 +37,8 @@ import { SyncService } from "../harness/sync";
|
|||||||
export async function runWalletBackupDoublespendTest(t: GlobalTestState) {
|
export async function runWalletBackupDoublespendTest(t: GlobalTestState) {
|
||||||
// Set up test environment
|
// Set up test environment
|
||||||
|
|
||||||
const {
|
const { commonDb, merchant, wallet, bank, exchange } =
|
||||||
commonDb,
|
await createSimpleTestkudosEnvironment(t);
|
||||||
merchant,
|
|
||||||
wallet,
|
|
||||||
bank,
|
|
||||||
exchange,
|
|
||||||
} = await createSimpleTestkudosEnvironment(t);
|
|
||||||
|
|
||||||
const sync = await SyncService.create(t, {
|
const sync = await SyncService.create(t, {
|
||||||
currency: "TESTKUDOS",
|
currency: "TESTKUDOS",
|
||||||
@ -139,8 +138,9 @@ export async function runWalletBackupDoublespendTest(t: GlobalTestState) {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
t.assertTrue(
|
t.assertDeepEqual(
|
||||||
preparePayResult.status === PreparePayResultType.PaymentPossible,
|
preparePayResult.status,
|
||||||
|
PreparePayResultType.PaymentPossible,
|
||||||
);
|
);
|
||||||
|
|
||||||
const res = await wallet2.client.call(WalletApiOperation.ConfirmPay, {
|
const res = await wallet2.client.call(WalletApiOperation.ConfirmPay, {
|
||||||
|
@ -72,6 +72,33 @@ function upgradeFromStoreMap(
|
|||||||
throw Error("upgrade not supported");
|
throw Error("upgrade not supported");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function promiseFromTransaction(transaction: IDBTransaction): Promise<void> {
|
||||||
|
return new Promise<void>((resolve, reject) => {
|
||||||
|
transaction.oncomplete = () => {
|
||||||
|
resolve();
|
||||||
|
};
|
||||||
|
transaction.onerror = () => {
|
||||||
|
reject();
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Purge all data in the given database.
|
||||||
|
*/
|
||||||
|
export function clearDatabase(db: IDBDatabase): Promise<void> {
|
||||||
|
// db.objectStoreNames is a DOMStringList, so we need to convert
|
||||||
|
let stores: string[] = [];
|
||||||
|
for (let i = 0; i < db.objectStoreNames.length; i++) {
|
||||||
|
stores.push(db.objectStoreNames[i]);
|
||||||
|
}
|
||||||
|
const tx = db.transaction(stores, "readwrite");
|
||||||
|
for (const store of stores) {
|
||||||
|
tx.objectStore(store).clear();
|
||||||
|
}
|
||||||
|
return promiseFromTransaction(tx);
|
||||||
|
}
|
||||||
|
|
||||||
function onTalerDbUpgradeNeeded(
|
function onTalerDbUpgradeNeeded(
|
||||||
db: IDBDatabase,
|
db: IDBDatabase,
|
||||||
oldVersion: number,
|
oldVersion: number,
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
This file is part of GNU Taler
|
This file is part of GNU Taler
|
||||||
(C) 2021 Taler Systems S.A.
|
(C) 2021-2022 Taler Systems S.A.
|
||||||
|
|
||||||
GNU Taler is free software; you can redistribute it and/or modify it under the
|
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
|
terms of the GNU General Public License as published by the Free Software
|
||||||
@ -49,6 +49,24 @@ import {
|
|||||||
import { RetryInfo, RetryTags } from "./util/retries.js";
|
import { RetryInfo, RetryTags } from "./util/retries.js";
|
||||||
import { Event, IDBDatabase } from "@gnu-taler/idb-bridge";
|
import { Event, IDBDatabase } from "@gnu-taler/idb-bridge";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This file contains the database schema of the Taler wallet together
|
||||||
|
* with some helper functions.
|
||||||
|
*
|
||||||
|
* Some design considerations:
|
||||||
|
* - By convention, each object store must have a corresponding "<Name>Record"
|
||||||
|
* interface defined for it.
|
||||||
|
* - For records that represent operations, there should be exactly
|
||||||
|
* one top-level enum field that indicates the status of the operation.
|
||||||
|
* This field should be present even if redundant, because the field
|
||||||
|
* will have an index.
|
||||||
|
* - Amounts are stored as strings, except when they are needed for
|
||||||
|
* indexing.
|
||||||
|
* - Optional fields should be avoided, use "T | undefined" instead.
|
||||||
|
*
|
||||||
|
* @author Florian Dold <dold@taler.net>
|
||||||
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Name of the Taler database. This is effectively the major
|
* Name of the Taler database. This is effectively the major
|
||||||
* version of the DB schema. Whenever it changes, custom import logic
|
* version of the DB schema. Whenever it changes, custom import logic
|
||||||
@ -76,6 +94,9 @@ export const CURRENT_DB_CONFIG_KEY = "currentMainDbName";
|
|||||||
*/
|
*/
|
||||||
export const WALLET_DB_MINOR_VERSION = 1;
|
export const WALLET_DB_MINOR_VERSION = 1;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Status of a withdrawal.
|
||||||
|
*/
|
||||||
export enum ReserveRecordStatus {
|
export enum ReserveRecordStatus {
|
||||||
/**
|
/**
|
||||||
* Reserve must be registered with the bank.
|
* Reserve must be registered with the bank.
|
||||||
@ -520,6 +541,9 @@ export interface PlanchetRecord {
|
|||||||
*/
|
*/
|
||||||
coinIdx: number;
|
coinIdx: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* FIXME: make this an enum!
|
||||||
|
*/
|
||||||
withdrawalDone: boolean;
|
withdrawalDone: boolean;
|
||||||
|
|
||||||
lastError: TalerErrorDetail | undefined;
|
lastError: TalerErrorDetail | undefined;
|
||||||
@ -639,6 +663,9 @@ export interface CoinRecord {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Amount that's left on the coin.
|
* Amount that's left on the coin.
|
||||||
|
*
|
||||||
|
* FIXME: This is pretty redundant with "allocation" and "status".
|
||||||
|
* Do we really need this?
|
||||||
*/
|
*/
|
||||||
currentAmount: AmountJson;
|
currentAmount: AmountJson;
|
||||||
|
|
||||||
@ -716,6 +743,9 @@ export interface ProposalDownload {
|
|||||||
*/
|
*/
|
||||||
contractTermsRaw: any;
|
contractTermsRaw: any;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extracted / parsed data from the contract terms.
|
||||||
|
*/
|
||||||
contractData: WalletContractData;
|
contractData: WalletContractData;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -780,6 +810,9 @@ export interface TipRecord {
|
|||||||
*/
|
*/
|
||||||
tipAmountRaw: AmountJson;
|
tipAmountRaw: AmountJson;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Effect on the balance (including fees etc).
|
||||||
|
*/
|
||||||
tipAmountEffective: AmountJson;
|
tipAmountEffective: AmountJson;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -800,6 +833,9 @@ export interface TipRecord {
|
|||||||
/**
|
/**
|
||||||
* Denomination selection made by the wallet for picking up
|
* Denomination selection made by the wallet for picking up
|
||||||
* this tip.
|
* this tip.
|
||||||
|
*
|
||||||
|
* FIXME: Put this into some DenomSelectionCacheRecord instead of
|
||||||
|
* storing it here!
|
||||||
*/
|
*/
|
||||||
denomsSel: DenomSelectionState;
|
denomsSel: DenomSelectionState;
|
||||||
|
|
||||||
@ -889,6 +925,8 @@ export interface RefreshGroupRecord {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* No coins are pending, but at least one is frozen.
|
* No coins are pending, but at least one is frozen.
|
||||||
|
*
|
||||||
|
* FIXME: What does this mean?
|
||||||
*/
|
*/
|
||||||
frozen?: boolean;
|
frozen?: boolean;
|
||||||
}
|
}
|
||||||
@ -1319,11 +1357,15 @@ export interface WithdrawalGroupRecord {
|
|||||||
/**
|
/**
|
||||||
* Operation status of the withdrawal group.
|
* Operation status of the withdrawal group.
|
||||||
* Used for indexing in the database.
|
* Used for indexing in the database.
|
||||||
|
*
|
||||||
|
* FIXME: Redundant with reserveStatus
|
||||||
*/
|
*/
|
||||||
operationStatus: OperationStatus;
|
operationStatus: OperationStatus;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Current status of the reserve.
|
* Current status of the reserve.
|
||||||
|
*
|
||||||
|
* FIXME: Wrong name!
|
||||||
*/
|
*/
|
||||||
reserveStatus: ReserveRecordStatus;
|
reserveStatus: ReserveRecordStatus;
|
||||||
|
|
||||||
@ -1756,6 +1798,10 @@ export interface CoinAvailabilityRecord {
|
|||||||
freshCoinCount: number;
|
freshCoinCount: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Schema definition for the IndexedDB
|
||||||
|
* wallet database.
|
||||||
|
*/
|
||||||
export const WalletStoresV1 = {
|
export const WalletStoresV1 = {
|
||||||
coinAvailability: describeStore(
|
coinAvailability: describeStore(
|
||||||
"coinAvailability",
|
"coinAvailability",
|
||||||
|
@ -25,6 +25,7 @@
|
|||||||
* Imports.
|
* Imports.
|
||||||
*/
|
*/
|
||||||
import {
|
import {
|
||||||
|
AbsoluteTime,
|
||||||
Amounts,
|
Amounts,
|
||||||
BackupBackupProvider,
|
BackupBackupProvider,
|
||||||
BackupBackupProviderTerms,
|
BackupBackupProviderTerms,
|
||||||
@ -35,6 +36,7 @@ import {
|
|||||||
BackupExchange,
|
BackupExchange,
|
||||||
BackupExchangeDetails,
|
BackupExchangeDetails,
|
||||||
BackupExchangeWireFee,
|
BackupExchangeWireFee,
|
||||||
|
BackupOperationStatus,
|
||||||
BackupProposal,
|
BackupProposal,
|
||||||
BackupProposalStatus,
|
BackupProposalStatus,
|
||||||
BackupPurchase,
|
BackupPurchase,
|
||||||
@ -44,30 +46,35 @@ import {
|
|||||||
BackupRefreshSession,
|
BackupRefreshSession,
|
||||||
BackupRefundItem,
|
BackupRefundItem,
|
||||||
BackupRefundState,
|
BackupRefundState,
|
||||||
BackupReserve,
|
|
||||||
BackupTip,
|
BackupTip,
|
||||||
|
BackupWgInfo,
|
||||||
|
BackupWgType,
|
||||||
BackupWithdrawalGroup,
|
BackupWithdrawalGroup,
|
||||||
|
BACKUP_VERSION_MAJOR,
|
||||||
|
BACKUP_VERSION_MINOR,
|
||||||
canonicalizeBaseUrl,
|
canonicalizeBaseUrl,
|
||||||
canonicalJson,
|
canonicalJson,
|
||||||
Logger,
|
|
||||||
WalletBackupContentV1,
|
|
||||||
hash,
|
|
||||||
encodeCrock,
|
encodeCrock,
|
||||||
getRandomBytes,
|
getRandomBytes,
|
||||||
|
hash,
|
||||||
|
Logger,
|
||||||
stringToBytes,
|
stringToBytes,
|
||||||
AbsoluteTime,
|
WalletBackupContentV1,
|
||||||
} from "@gnu-taler/taler-util";
|
} from "@gnu-taler/taler-util";
|
||||||
import { InternalWalletState } from "../../internal-wallet-state.js";
|
|
||||||
import {
|
import {
|
||||||
AbortStatus,
|
AbortStatus,
|
||||||
CoinSourceType,
|
CoinSourceType,
|
||||||
CoinStatus,
|
CoinStatus,
|
||||||
DenominationRecord,
|
DenominationRecord,
|
||||||
|
OperationStatus,
|
||||||
ProposalStatus,
|
ProposalStatus,
|
||||||
RefreshCoinStatus,
|
RefreshCoinStatus,
|
||||||
RefundState,
|
RefundState,
|
||||||
WALLET_BACKUP_STATE_KEY,
|
WALLET_BACKUP_STATE_KEY,
|
||||||
|
WithdrawalRecordType,
|
||||||
} from "../../db.js";
|
} from "../../db.js";
|
||||||
|
import { InternalWalletState } from "../../internal-wallet-state.js";
|
||||||
|
import { assertUnreachable } from "../../util/assertUnreachable.js";
|
||||||
import { getWalletBackupState, provideBackupState } from "./state.js";
|
import { getWalletBackupState, provideBackupState } from "./state.js";
|
||||||
|
|
||||||
const logger = new Logger("backup/export.ts");
|
const logger = new Logger("backup/export.ts");
|
||||||
@ -100,31 +107,75 @@ export async function exportBackup(
|
|||||||
const backupDenominationsByExchange: {
|
const backupDenominationsByExchange: {
|
||||||
[url: string]: BackupDenomination[];
|
[url: string]: BackupDenomination[];
|
||||||
} = {};
|
} = {};
|
||||||
const backupReservesByExchange: { [url: string]: BackupReserve[] } = {};
|
|
||||||
const backupPurchases: BackupPurchase[] = [];
|
const backupPurchases: BackupPurchase[] = [];
|
||||||
const backupProposals: BackupProposal[] = [];
|
const backupProposals: BackupProposal[] = [];
|
||||||
const backupRefreshGroups: BackupRefreshGroup[] = [];
|
const backupRefreshGroups: BackupRefreshGroup[] = [];
|
||||||
const backupBackupProviders: BackupBackupProvider[] = [];
|
const backupBackupProviders: BackupBackupProvider[] = [];
|
||||||
const backupTips: BackupTip[] = [];
|
const backupTips: BackupTip[] = [];
|
||||||
const backupRecoupGroups: BackupRecoupGroup[] = [];
|
const backupRecoupGroups: BackupRecoupGroup[] = [];
|
||||||
const withdrawalGroupsByReserve: {
|
const backupWithdrawalGroups: BackupWithdrawalGroup[] = [];
|
||||||
[reservePub: string]: BackupWithdrawalGroup[];
|
|
||||||
} = {};
|
|
||||||
|
|
||||||
await tx.withdrawalGroups.iter().forEachAsync(async (wg) => {
|
await tx.withdrawalGroups.iter().forEachAsync(async (wg) => {
|
||||||
const withdrawalGroups = (withdrawalGroupsByReserve[wg.reservePub] ??=
|
let info: BackupWgInfo;
|
||||||
[]);
|
switch (wg.wgInfo.withdrawalType) {
|
||||||
withdrawalGroups.push({
|
case WithdrawalRecordType.BankIntegrated:
|
||||||
|
info = {
|
||||||
|
type: BackupWgType.BankIntegrated,
|
||||||
|
exchange_payto_uri: wg.wgInfo.bankInfo.exchangePaytoUri,
|
||||||
|
taler_withdraw_uri: wg.wgInfo.bankInfo.talerWithdrawUri,
|
||||||
|
confirm_url: wg.wgInfo.bankInfo.confirmUrl,
|
||||||
|
timestamp_bank_confirmed:
|
||||||
|
wg.wgInfo.bankInfo.timestampBankConfirmed,
|
||||||
|
timestamp_reserve_info_posted:
|
||||||
|
wg.wgInfo.bankInfo.timestampReserveInfoPosted,
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
case WithdrawalRecordType.BankManual:
|
||||||
|
info = {
|
||||||
|
type: BackupWgType.BankManual,
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
case WithdrawalRecordType.PeerPullCredit:
|
||||||
|
info = {
|
||||||
|
type: BackupWgType.PeerPullCredit,
|
||||||
|
contract_priv: wg.wgInfo.contractPriv,
|
||||||
|
contract_terms: wg.wgInfo.contractTerms,
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
case WithdrawalRecordType.PeerPushCredit:
|
||||||
|
info = {
|
||||||
|
type: BackupWgType.PeerPushCredit,
|
||||||
|
contract_terms: wg.wgInfo.contractTerms,
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
case WithdrawalRecordType.Recoup:
|
||||||
|
info = {
|
||||||
|
type: BackupWgType.Recoup,
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
assertUnreachable(wg.wgInfo);
|
||||||
|
}
|
||||||
|
backupWithdrawalGroups.push({
|
||||||
raw_withdrawal_amount: Amounts.stringify(wg.rawWithdrawalAmount),
|
raw_withdrawal_amount: Amounts.stringify(wg.rawWithdrawalAmount),
|
||||||
selected_denoms: wg.denomsSel.selectedDenoms.map((x) => ({
|
info,
|
||||||
count: x.count,
|
|
||||||
denom_pub_hash: x.denomPubHash,
|
|
||||||
})),
|
|
||||||
timestamp_created: wg.timestampStart,
|
timestamp_created: wg.timestampStart,
|
||||||
timestamp_finish: wg.timestampFinish,
|
timestamp_finish: wg.timestampFinish,
|
||||||
withdrawal_group_id: wg.withdrawalGroupId,
|
withdrawal_group_id: wg.withdrawalGroupId,
|
||||||
secret_seed: wg.secretSeed,
|
secret_seed: wg.secretSeed,
|
||||||
selected_denoms_id: wg.denomSelUid,
|
exchange_base_url: wg.exchangeBaseUrl,
|
||||||
|
instructed_amount: Amounts.stringify(wg.instructedAmount),
|
||||||
|
reserve_priv: wg.reservePriv,
|
||||||
|
restrict_age: wg.restrictAge,
|
||||||
|
operation_status:
|
||||||
|
wg.operationStatus == OperationStatus.Finished
|
||||||
|
? BackupOperationStatus.Finished
|
||||||
|
: BackupOperationStatus.Pending,
|
||||||
|
selected_denoms_uid: wg.denomSelUid,
|
||||||
|
selected_denoms: wg.denomsSel.selectedDenoms.map((x) => ({
|
||||||
|
count: x.count,
|
||||||
|
denom_pub_hash: x.denomPubHash,
|
||||||
|
})),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -299,7 +350,6 @@ export async function exportBackup(
|
|||||||
tos_accepted_timestamp: ex.termsOfServiceAcceptedTimestamp,
|
tos_accepted_timestamp: ex.termsOfServiceAcceptedTimestamp,
|
||||||
denominations:
|
denominations:
|
||||||
backupDenominationsByExchange[ex.exchangeBaseUrl] ?? [],
|
backupDenominationsByExchange[ex.exchangeBaseUrl] ?? [],
|
||||||
reserves: backupReservesByExchange[ex.exchangeBaseUrl] ?? [],
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -439,7 +489,8 @@ export async function exportBackup(
|
|||||||
|
|
||||||
const backupBlob: WalletBackupContentV1 = {
|
const backupBlob: WalletBackupContentV1 = {
|
||||||
schema_id: "gnu-taler-wallet-backup-content",
|
schema_id: "gnu-taler-wallet-backup-content",
|
||||||
schema_version: 1,
|
schema_version: BACKUP_VERSION_MAJOR,
|
||||||
|
minor_version: BACKUP_VERSION_MINOR,
|
||||||
exchanges: backupExchanges,
|
exchanges: backupExchanges,
|
||||||
exchange_details: backupExchangeDetails,
|
exchange_details: backupExchangeDetails,
|
||||||
wallet_root_pub: bs.walletRootPub,
|
wallet_root_pub: bs.walletRootPub,
|
||||||
@ -456,6 +507,8 @@ export async function exportBackup(
|
|||||||
intern_table: {},
|
intern_table: {},
|
||||||
error_reports: [],
|
error_reports: [],
|
||||||
tombstones: [],
|
tombstones: [],
|
||||||
|
// FIXME!
|
||||||
|
withdrawal_groups: backupWithdrawalGroups,
|
||||||
};
|
};
|
||||||
|
|
||||||
// If the backup changed, we change our nonce and timestamp.
|
// If the backup changed, we change our nonce and timestamp.
|
||||||
|
@ -24,6 +24,7 @@ import {
|
|||||||
BackupPurchase,
|
BackupPurchase,
|
||||||
BackupRefreshReason,
|
BackupRefreshReason,
|
||||||
BackupRefundState,
|
BackupRefundState,
|
||||||
|
BackupWgType,
|
||||||
codecForContractTerms,
|
codecForContractTerms,
|
||||||
DenomKeyType,
|
DenomKeyType,
|
||||||
j2s,
|
j2s,
|
||||||
@ -53,8 +54,11 @@ import {
|
|||||||
WalletContractData,
|
WalletContractData,
|
||||||
WalletRefundItem,
|
WalletRefundItem,
|
||||||
WalletStoresV1,
|
WalletStoresV1,
|
||||||
|
WgInfo,
|
||||||
|
WithdrawalRecordType,
|
||||||
} from "../../db.js";
|
} from "../../db.js";
|
||||||
import { InternalWalletState } from "../../internal-wallet-state.js";
|
import { InternalWalletState } from "../../internal-wallet-state.js";
|
||||||
|
import { assertUnreachable } from "../../util/assertUnreachable.js";
|
||||||
import {
|
import {
|
||||||
checkDbInvariant,
|
checkDbInvariant,
|
||||||
checkLogicInvariant,
|
checkLogicInvariant,
|
||||||
@ -444,6 +448,91 @@ export async function importBackup(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (const backupWg of backupBlob.withdrawal_groups) {
|
||||||
|
const reservePub = cryptoComp.reservePrivToPub[backupWg.reserve_priv];
|
||||||
|
checkLogicInvariant(!!reservePub);
|
||||||
|
const ts = makeEventId(TombstoneTag.DeleteReserve, reservePub);
|
||||||
|
if (tombstoneSet.has(ts)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const existingWg = await tx.withdrawalGroups.get(
|
||||||
|
backupWg.withdrawal_group_id,
|
||||||
|
);
|
||||||
|
if (existingWg) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let wgInfo: WgInfo;
|
||||||
|
switch (backupWg.info.type) {
|
||||||
|
case BackupWgType.BankIntegrated:
|
||||||
|
wgInfo = {
|
||||||
|
withdrawalType: WithdrawalRecordType.BankIntegrated,
|
||||||
|
bankInfo: {
|
||||||
|
exchangePaytoUri: backupWg.info.exchange_payto_uri,
|
||||||
|
talerWithdrawUri: backupWg.info.taler_withdraw_uri,
|
||||||
|
confirmUrl: backupWg.info.confirm_url,
|
||||||
|
timestampBankConfirmed:
|
||||||
|
backupWg.info.timestamp_bank_confirmed,
|
||||||
|
timestampReserveInfoPosted:
|
||||||
|
backupWg.info.timestamp_reserve_info_posted,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
case BackupWgType.BankManual:
|
||||||
|
wgInfo = {
|
||||||
|
withdrawalType: WithdrawalRecordType.BankManual,
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
case BackupWgType.PeerPullCredit:
|
||||||
|
wgInfo = {
|
||||||
|
withdrawalType: WithdrawalRecordType.PeerPullCredit,
|
||||||
|
contractTerms: backupWg.info.contract_terms,
|
||||||
|
contractPriv: backupWg.info.contract_priv,
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
case BackupWgType.PeerPushCredit:
|
||||||
|
wgInfo = {
|
||||||
|
withdrawalType: WithdrawalRecordType.PeerPushCredit,
|
||||||
|
contractTerms: backupWg.info.contract_terms,
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
case BackupWgType.Recoup:
|
||||||
|
wgInfo = {
|
||||||
|
withdrawalType: WithdrawalRecordType.Recoup,
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
assertUnreachable(backupWg.info);
|
||||||
|
}
|
||||||
|
await tx.withdrawalGroups.put({
|
||||||
|
withdrawalGroupId: backupWg.withdrawal_group_id,
|
||||||
|
exchangeBaseUrl: backupWg.exchange_base_url,
|
||||||
|
instructedAmount: Amounts.parseOrThrow(backupWg.instructed_amount),
|
||||||
|
secretSeed: backupWg.secret_seed,
|
||||||
|
operationStatus: backupWg.timestamp_finish
|
||||||
|
? OperationStatus.Finished
|
||||||
|
: OperationStatus.Pending,
|
||||||
|
denomsSel: await getDenomSelStateFromBackup(
|
||||||
|
tx,
|
||||||
|
backupWg.exchange_base_url,
|
||||||
|
backupWg.selected_denoms,
|
||||||
|
),
|
||||||
|
denomSelUid: backupWg.selected_denoms_uid,
|
||||||
|
rawWithdrawalAmount: Amounts.parseOrThrow(
|
||||||
|
backupWg.raw_withdrawal_amount,
|
||||||
|
),
|
||||||
|
reservePriv: backupWg.reserve_priv,
|
||||||
|
reservePub,
|
||||||
|
reserveStatus: backupWg.timestamp_finish
|
||||||
|
? ReserveRecordStatus.Dormant
|
||||||
|
: ReserveRecordStatus.QueryingStatus, // FIXME!
|
||||||
|
timestampStart: backupWg.timestamp_created,
|
||||||
|
wgInfo,
|
||||||
|
restrictAge: backupWg.restrict_age,
|
||||||
|
senderWire: undefined, // FIXME!
|
||||||
|
timestampFinish: backupWg.timestamp_finish,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// FIXME: import reserves with new schema
|
// FIXME: import reserves with new schema
|
||||||
|
|
||||||
// for (const backupReserve of backupExchangeDetails.reserves) {
|
// for (const backupReserve of backupExchangeDetails.reserves) {
|
||||||
|
@ -187,11 +187,11 @@ async function computeBackupCryptoData(
|
|||||||
cryptoData.rsaDenomPubToHash[backupDenom.denom_pub.rsa_public_key] =
|
cryptoData.rsaDenomPubToHash[backupDenom.denom_pub.rsa_public_key] =
|
||||||
encodeCrock(hashDenomPub(backupDenom.denom_pub));
|
encodeCrock(hashDenomPub(backupDenom.denom_pub));
|
||||||
}
|
}
|
||||||
for (const backupReserve of backupExchangeDetails.reserves) {
|
|
||||||
cryptoData.reservePrivToPub[backupReserve.reserve_priv] = encodeCrock(
|
|
||||||
eddsaGetPublic(decodeCrock(backupReserve.reserve_priv)),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
for (const backupWg of backupContent.withdrawal_groups) {
|
||||||
|
cryptoData.reservePrivToPub[backupWg.reserve_priv] = encodeCrock(
|
||||||
|
eddsaGetPublic(decodeCrock(backupWg.reserve_priv)),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
for (const prop of backupContent.proposals) {
|
for (const prop of backupContent.proposals) {
|
||||||
const { h: contractTermsHash } = await cryptoApi.hashString({
|
const { h: contractTermsHash } = await cryptoApi.hashString({
|
||||||
|
@ -96,12 +96,13 @@ import { DbAccess, GetReadOnlyAccess } from "../util/query.js";
|
|||||||
import {
|
import {
|
||||||
OperationAttemptResult,
|
OperationAttemptResult,
|
||||||
OperationAttemptResultType,
|
OperationAttemptResultType,
|
||||||
|
RetryTags,
|
||||||
} from "../util/retries.js";
|
} from "../util/retries.js";
|
||||||
import {
|
import {
|
||||||
WALLET_BANK_INTEGRATION_PROTOCOL_VERSION,
|
WALLET_BANK_INTEGRATION_PROTOCOL_VERSION,
|
||||||
WALLET_EXCHANGE_PROTOCOL_VERSION,
|
WALLET_EXCHANGE_PROTOCOL_VERSION,
|
||||||
} from "../versions.js";
|
} from "../versions.js";
|
||||||
import { makeCoinAvailable } from "../wallet.js";
|
import { makeCoinAvailable, storeOperationPending } from "../wallet.js";
|
||||||
import {
|
import {
|
||||||
getExchangeDetails,
|
getExchangeDetails,
|
||||||
getExchangePaytoUri,
|
getExchangePaytoUri,
|
||||||
@ -1099,6 +1100,7 @@ export async function processWithdrawalGroup(
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (withdrawalGroup.denomsSel.selectedDenoms.length === 0) {
|
if (withdrawalGroup.denomsSel.selectedDenoms.length === 0) {
|
||||||
|
logger.warn("Finishing empty withdrawal group (no denoms)");
|
||||||
await ws.db
|
await ws.db
|
||||||
.mktx((x) => [x.withdrawalGroups])
|
.mktx((x) => [x.withdrawalGroups])
|
||||||
.runReadWrite(async (tx) => {
|
.runReadWrite(async (tx) => {
|
||||||
@ -1107,6 +1109,7 @@ export async function processWithdrawalGroup(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
wg.operationStatus = OperationStatus.Finished;
|
wg.operationStatus = OperationStatus.Finished;
|
||||||
|
wg.timestampFinish = TalerProtocolTimestamp.now();
|
||||||
await tx.withdrawalGroups.put(wg);
|
await tx.withdrawalGroups.put(wg);
|
||||||
});
|
});
|
||||||
return {
|
return {
|
||||||
@ -1185,7 +1188,7 @@ export async function processWithdrawalGroup(
|
|||||||
errorsPerCoin[x.coinIdx] = x.lastError;
|
errorsPerCoin[x.coinIdx] = x.lastError;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
logger.trace(`now withdrawn ${numFinished} of ${numTotalCoins} coins`);
|
logger.info(`now withdrawn ${numFinished} of ${numTotalCoins} coins`);
|
||||||
if (wg.timestampFinish === undefined && numFinished === numTotalCoins) {
|
if (wg.timestampFinish === undefined && numFinished === numTotalCoins) {
|
||||||
finishedForFirstTime = true;
|
finishedForFirstTime = true;
|
||||||
wg.timestampFinish = TalerProtocolTimestamp.now();
|
wg.timestampFinish = TalerProtocolTimestamp.now();
|
||||||
|
@ -409,10 +409,12 @@ export type GetReadWriteAccess<BoundStores> = {
|
|||||||
|
|
||||||
type ReadOnlyTransactionFunction<BoundStores, T> = (
|
type ReadOnlyTransactionFunction<BoundStores, T> = (
|
||||||
t: GetReadOnlyAccess<BoundStores>,
|
t: GetReadOnlyAccess<BoundStores>,
|
||||||
|
rawTx: IDBTransaction,
|
||||||
) => Promise<T>;
|
) => Promise<T>;
|
||||||
|
|
||||||
type ReadWriteTransactionFunction<BoundStores, T> = (
|
type ReadWriteTransactionFunction<BoundStores, T> = (
|
||||||
t: GetReadWriteAccess<BoundStores>,
|
t: GetReadWriteAccess<BoundStores>,
|
||||||
|
rawTx: IDBTransaction,
|
||||||
) => Promise<T>;
|
) => Promise<T>;
|
||||||
|
|
||||||
export interface TransactionContext<BoundStores> {
|
export interface TransactionContext<BoundStores> {
|
||||||
@ -420,22 +422,10 @@ export interface TransactionContext<BoundStores> {
|
|||||||
runReadOnly<T>(f: ReadOnlyTransactionFunction<BoundStores, T>): Promise<T>;
|
runReadOnly<T>(f: ReadOnlyTransactionFunction<BoundStores, T>): Promise<T>;
|
||||||
}
|
}
|
||||||
|
|
||||||
type CheckDescriptor<T> = T extends StoreWithIndexes<
|
|
||||||
infer SN,
|
|
||||||
infer SD,
|
|
||||||
infer IM
|
|
||||||
>
|
|
||||||
? StoreWithIndexes<SN, SD, IM>
|
|
||||||
: unknown;
|
|
||||||
|
|
||||||
type GetPickerType<F, SM> = F extends (x: SM) => infer Out
|
|
||||||
? { [P in keyof Out]: CheckDescriptor<Out[P]> }
|
|
||||||
: unknown;
|
|
||||||
|
|
||||||
function runTx<Arg, Res>(
|
function runTx<Arg, Res>(
|
||||||
tx: IDBTransaction,
|
tx: IDBTransaction,
|
||||||
arg: Arg,
|
arg: Arg,
|
||||||
f: (t: Arg) => Promise<Res>,
|
f: (t: Arg, t2: IDBTransaction) => Promise<Res>,
|
||||||
): Promise<Res> {
|
): Promise<Res> {
|
||||||
const stack = Error("Failed transaction was started here.");
|
const stack = Error("Failed transaction was started here.");
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
@ -474,7 +464,7 @@ function runTx<Arg, Res>(
|
|||||||
logger.error(msg);
|
logger.error(msg);
|
||||||
reject(new TransactionAbortedError(msg));
|
reject(new TransactionAbortedError(msg));
|
||||||
};
|
};
|
||||||
const resP = Promise.resolve().then(() => f(arg));
|
const resP = Promise.resolve().then(() => f(arg, tx));
|
||||||
resP
|
resP
|
||||||
.then((result) => {
|
.then((result) => {
|
||||||
gotFunResult = true;
|
gotFunResult = true;
|
||||||
@ -624,6 +614,46 @@ export class DbAccess<StoreMap> {
|
|||||||
return this.db;
|
return this.db;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run a transaction with all object stores.
|
||||||
|
*/
|
||||||
|
mktxAll(): TransactionContext<StoreMap> {
|
||||||
|
const storeNames: string[] = [];
|
||||||
|
const accessibleStores: { [x: string]: StoreWithIndexes<any, any, any> } =
|
||||||
|
{};
|
||||||
|
|
||||||
|
for (let i = 0; i < this.db.objectStoreNames.length; i++) {
|
||||||
|
const sn = this.db.objectStoreNames[i];
|
||||||
|
const swi = (this.stores as any)[sn] as StoreWithIndexes<any, any, any>;
|
||||||
|
if (!swi) {
|
||||||
|
throw Error(`store metadata not available (${sn})`);
|
||||||
|
}
|
||||||
|
storeNames.push(sn);
|
||||||
|
accessibleStores[sn] = swi;
|
||||||
|
}
|
||||||
|
|
||||||
|
const runReadOnly = <T>(
|
||||||
|
txf: ReadOnlyTransactionFunction<StoreMap, T>,
|
||||||
|
): Promise<T> => {
|
||||||
|
const tx = this.db.transaction(storeNames, "readonly");
|
||||||
|
const readContext = makeReadContext(tx, accessibleStores);
|
||||||
|
return runTx(tx, readContext, txf);
|
||||||
|
};
|
||||||
|
|
||||||
|
const runReadWrite = <T>(
|
||||||
|
txf: ReadWriteTransactionFunction<StoreMap, T>,
|
||||||
|
): Promise<T> => {
|
||||||
|
const tx = this.db.transaction(storeNames, "readwrite");
|
||||||
|
const writeContext = makeWriteContext(tx, accessibleStores);
|
||||||
|
return runTx(tx, writeContext, txf);
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
runReadOnly,
|
||||||
|
runReadWrite,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Run a transaction with selected object stores.
|
* Run a transaction with selected object stores.
|
||||||
*
|
*
|
||||||
@ -638,13 +668,14 @@ export class DbAccess<StoreMap> {
|
|||||||
[X in StoreNamesOf<StoreList>]: StoreList[number] & { storeName: X };
|
[X in StoreNamesOf<StoreList>]: StoreList[number] & { storeName: X };
|
||||||
},
|
},
|
||||||
>(namePicker: (x: StoreMap) => StoreList): TransactionContext<BoundStores> {
|
>(namePicker: (x: StoreMap) => StoreList): TransactionContext<BoundStores> {
|
||||||
|
const storeNames: string[] = [];
|
||||||
|
const accessibleStores: { [x: string]: StoreWithIndexes<any, any, any> } =
|
||||||
|
{};
|
||||||
|
|
||||||
const storePick = namePicker(this.stores) as any;
|
const storePick = namePicker(this.stores) as any;
|
||||||
if (typeof storePick !== "object" || storePick === null) {
|
if (typeof storePick !== "object" || storePick === null) {
|
||||||
throw Error();
|
throw Error();
|
||||||
}
|
}
|
||||||
const storeNames: string[] = [];
|
|
||||||
const accessibleStores: { [x: string]: StoreWithIndexes<any, any, any> } =
|
|
||||||
{};
|
|
||||||
for (const swiPicked of storePick) {
|
for (const swiPicked of storePick) {
|
||||||
const swi = swiPicked as StoreWithIndexes<any, any, any>;
|
const swi = swiPicked as StoreWithIndexes<any, any, any>;
|
||||||
if (swi.mark !== storeWithIndexesSymbol) {
|
if (swi.mark !== storeWithIndexesSymbol) {
|
||||||
|
@ -134,6 +134,8 @@ export enum WalletApiOperation {
|
|||||||
InitiatePeerPullPayment = "initiatePeerPullPayment",
|
InitiatePeerPullPayment = "initiatePeerPullPayment",
|
||||||
CheckPeerPullPayment = "checkPeerPullPayment",
|
CheckPeerPullPayment = "checkPeerPullPayment",
|
||||||
AcceptPeerPullPayment = "acceptPeerPullPayment",
|
AcceptPeerPullPayment = "acceptPeerPullPayment",
|
||||||
|
ClearDb = "clearDb",
|
||||||
|
Recycle = "recycle",
|
||||||
}
|
}
|
||||||
|
|
||||||
export type WalletOperations = {
|
export type WalletOperations = {
|
||||||
@ -317,6 +319,14 @@ export type WalletOperations = {
|
|||||||
request: AcceptPeerPullPaymentRequest;
|
request: AcceptPeerPullPaymentRequest;
|
||||||
response: {};
|
response: {};
|
||||||
};
|
};
|
||||||
|
[WalletApiOperation.ClearDb]: {
|
||||||
|
request: {};
|
||||||
|
response: {};
|
||||||
|
};
|
||||||
|
[WalletApiOperation.Recycle]: {
|
||||||
|
request: {};
|
||||||
|
response: {};
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export type RequestType<
|
export type RequestType<
|
||||||
|
@ -99,6 +99,7 @@ import {
|
|||||||
CryptoDispatcher,
|
CryptoDispatcher,
|
||||||
CryptoWorkerFactory,
|
CryptoWorkerFactory,
|
||||||
} from "./crypto/workers/cryptoDispatcher.js";
|
} from "./crypto/workers/cryptoDispatcher.js";
|
||||||
|
import { clearDatabase } from "./db-utils.js";
|
||||||
import {
|
import {
|
||||||
AuditorTrustRecord,
|
AuditorTrustRecord,
|
||||||
CoinRecord,
|
CoinRecord,
|
||||||
@ -114,7 +115,6 @@ import {
|
|||||||
makeErrorDetail,
|
makeErrorDetail,
|
||||||
TalerError,
|
TalerError,
|
||||||
} from "./errors.js";
|
} from "./errors.js";
|
||||||
import { createDenominationTimeline } from "./index.browser.js";
|
|
||||||
import {
|
import {
|
||||||
ExchangeOperations,
|
ExchangeOperations,
|
||||||
InternalWalletState,
|
InternalWalletState,
|
||||||
@ -131,6 +131,7 @@ import {
|
|||||||
codecForRunBackupCycle,
|
codecForRunBackupCycle,
|
||||||
getBackupInfo,
|
getBackupInfo,
|
||||||
getBackupRecovery,
|
getBackupRecovery,
|
||||||
|
importBackupPlain,
|
||||||
loadBackupRecovery,
|
loadBackupRecovery,
|
||||||
processBackupForProvider,
|
processBackupForProvider,
|
||||||
removeBackupProvider,
|
removeBackupProvider,
|
||||||
@ -215,6 +216,7 @@ import {
|
|||||||
} from "./pending-types.js";
|
} from "./pending-types.js";
|
||||||
import { assertUnreachable } from "./util/assertUnreachable.js";
|
import { assertUnreachable } from "./util/assertUnreachable.js";
|
||||||
import { AsyncOpMemoMap, AsyncOpMemoSingle } from "./util/asyncMemo.js";
|
import { AsyncOpMemoMap, AsyncOpMemoSingle } from "./util/asyncMemo.js";
|
||||||
|
import { createDenominationTimeline } from "./util/denominations.js";
|
||||||
import {
|
import {
|
||||||
HttpRequestLibrary,
|
HttpRequestLibrary,
|
||||||
readSuccessResponseJsonOrThrow,
|
readSuccessResponseJsonOrThrow,
|
||||||
@ -1060,8 +1062,11 @@ async function dispatchRequestInternal(
|
|||||||
`wallet must be initialized before running operation ${operation}`,
|
`wallet must be initialized before running operation ${operation}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
// FIXME: Can we make this more type-safe by using the request/response type
|
||||||
|
// definitions we already have?
|
||||||
switch (operation) {
|
switch (operation) {
|
||||||
case "initWallet": {
|
case "initWallet": {
|
||||||
|
logger.info("initializing wallet");
|
||||||
ws.initCalled = true;
|
ws.initCalled = true;
|
||||||
if (typeof payload === "object" && (payload as any).skipDefaults) {
|
if (typeof payload === "object" && (payload as any).skipDefaults) {
|
||||||
logger.info("skipping defaults");
|
logger.info("skipping defaults");
|
||||||
@ -1371,6 +1376,15 @@ async function dispatchRequestInternal(
|
|||||||
logger.info(`started fakebank withdrawal: ${j2s(fbResp)}`);
|
logger.info(`started fakebank withdrawal: ${j2s(fbResp)}`);
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
case "clearDb":
|
||||||
|
await clearDatabase(ws.db.idbHandle());
|
||||||
|
return {};
|
||||||
|
case "recycle": {
|
||||||
|
const backup = await exportBackup(ws);
|
||||||
|
await clearDatabase(ws.db.idbHandle());
|
||||||
|
await importBackupPlain(ws, backup);
|
||||||
|
return {};
|
||||||
|
}
|
||||||
case "exportDb": {
|
case "exportDb": {
|
||||||
const dbDump = await exportDb(ws.db.idbHandle());
|
const dbDump = await exportDb(ws.db.idbHandle());
|
||||||
return dbDump;
|
return dbDump;
|
||||||
|
Loading…
Reference in New Issue
Block a user