finish first complete end-to-end backup/sync test

This commit is contained in:
Florian Dold 2021-03-10 17:11:59 +01:00
parent ac89c3d277
commit 1392dc47c6
No known key found for this signature in database
GPG Key ID: D2E4F00F29D02A4B
9 changed files with 429 additions and 215 deletions

View File

@ -82,6 +82,7 @@ import {
CreateDepositGroupResponse, CreateDepositGroupResponse,
TrackDepositGroupRequest, TrackDepositGroupRequest,
TrackDepositGroupResponse, TrackDepositGroupResponse,
RecoveryLoadRequest,
} from "@gnu-taler/taler-wallet-core"; } from "@gnu-taler/taler-wallet-core";
import { URL } from "url"; import { URL } from "url";
import axios, { AxiosError } from "axios"; import axios, { AxiosError } from "axios";
@ -102,6 +103,7 @@ import { CoinConfig } from "./denomStructures";
import { import {
AddBackupProviderRequest, AddBackupProviderRequest,
BackupInfo, BackupInfo,
BackupRecovery,
} from "@gnu-taler/taler-wallet-core/src/operations/backup"; } from "@gnu-taler/taler-wallet-core/src/operations/backup";
const exec = util.promisify(require("child_process").exec); const exec = util.promisify(require("child_process").exec);
@ -1887,6 +1889,22 @@ export class WalletCli {
throw new OperationFailedError(resp.error); throw new OperationFailedError(resp.error);
} }
async exportBackupRecovery(): Promise<BackupRecovery> {
const resp = await this.apiRequest("exportBackupRecovery", {});
if (resp.type === "response") {
return resp.result as BackupRecovery;
}
throw new OperationFailedError(resp.error);
}
async importBackupRecovery(req: RecoveryLoadRequest): Promise<void> {
const resp = await this.apiRequest("importBackupRecovery", req);
if (resp.type === "response") {
return;
}
throw new OperationFailedError(resp.error);
}
async runBackupCycle(): Promise<void> { async runBackupCycle(): Promise<void> {
const resp = await this.apiRequest("runBackupCycle", {}); const resp = await this.apiRequest("runBackupCycle", {});
if (resp.type === "response") { if (resp.type === "response") {

View File

@ -19,7 +19,6 @@
*/ */
import axios from "axios"; import axios from "axios";
import { Configuration, URL } from "@gnu-taler/taler-wallet-core"; import { Configuration, URL } from "@gnu-taler/taler-wallet-core";
import { getRandomIban, getRandomString } from "./helpers";
import * as fs from "fs"; import * as fs from "fs";
import * as util from "util"; import * as util from "util";
import { import {
@ -87,6 +86,8 @@ export class SyncService {
config.setString("sync", "port", `${sc.httpPort}`); config.setString("sync", "port", `${sc.httpPort}`);
config.setString("sync", "db", "postgres"); config.setString("sync", "db", "postgres");
config.setString("syncdb-postgres", "config", sc.database); config.setString("syncdb-postgres", "config", sc.database);
config.setString("sync", "payment_backend_url", sc.paymentBackendUrl);
config.setString("sync", "upload_limit_mb", `${sc.uploadLimitMb}`);
config.write(cfgFilename); config.write(cfgFilename);
return new SyncService(gc, sc, cfgFilename); return new SyncService(gc, sc, cfgFilename);

View File

@ -17,9 +17,12 @@
/** /**
* Imports. * Imports.
*/ */
import { GlobalTestState, BankApi, BankAccessApi } from "./harness"; import { GlobalTestState, BankApi, BankAccessApi, WalletCli } from "./harness";
import { createSimpleTestkudosEnvironment } from "./helpers"; import {
import { codecForBalancesResponse } from "@gnu-taler/taler-wallet-core"; createSimpleTestkudosEnvironment,
makeTestPayment,
withdrawViaBank,
} from "./helpers";
import { SyncService } from "./sync"; import { SyncService } from "./sync";
/** /**
@ -28,7 +31,13 @@ import { SyncService } from "./sync";
export async function runWalletBackupBasicTest(t: GlobalTestState) { export async function runWalletBackupBasicTest(t: GlobalTestState) {
// Set up test environment // Set up test environment
const { commonDb, merchant, wallet, bank, exchange } = await createSimpleTestkudosEnvironment(t); const {
commonDb,
merchant,
wallet,
bank,
exchange,
} = await createSimpleTestkudosEnvironment(t);
const sync = await SyncService.create(t, { const sync = await SyncService.create(t, {
currency: "TESTKUDOS", currency: "TESTKUDOS",
@ -69,5 +78,48 @@ export async function runWalletBackupBasicTest(t: GlobalTestState) {
{ {
const bi = await wallet.getBackupInfo(); const bi = await wallet.getBackupInfo();
console.log(bi); console.log(bi);
t.assertDeepEqual(
bi.providers[0].paymentStatus.type,
"insufficient-balance",
);
}
await withdrawViaBank(t, { wallet, bank, exchange, amount: "TESTKUDOS:10" });
await wallet.runBackupCycle();
{
const bi = await wallet.getBackupInfo();
console.log(bi);
}
await withdrawViaBank(t, { wallet, bank, exchange, amount: "TESTKUDOS:5" });
await wallet.runBackupCycle();
{
const bi = await wallet.getBackupInfo();
console.log(bi);
}
const backupRecovery = await wallet.exportBackupRecovery();
const wallet2 = new WalletCli(t, "wallet2");
// Check that the second wallet is a fresh wallet.
{
const bal = await wallet2.getBalances();
t.assertTrue(bal.balances.length === 0);
}
await wallet2.importBackupRecovery({ recovery: backupRecovery });
await wallet2.runBackupCycle();
// Check that now the old balance is available!
{
const bal = await wallet2.getBalances();
t.assertTrue(bal.balances.length === 1);
console.log(bal);
} }
} }

View File

@ -15,68 +15,47 @@
*/ */
import { import {
Stores,
Amounts,
CoinSourceType,
CoinStatus,
RefundState,
AbortStatus, AbortStatus,
ProposalStatus,
getTimestampNow,
encodeCrock,
stringToBytes,
getRandomBytes,
AmountJson, AmountJson,
Amounts,
codecForContractTerms, codecForContractTerms,
CoinSource, CoinSource,
CoinSourceType,
CoinStatus,
DenominationStatus, DenominationStatus,
DenomSelectionState, DenomSelectionState,
ExchangeUpdateStatus, ExchangeUpdateStatus,
ExchangeWireInfo, ExchangeWireInfo,
getTimestampNow,
PayCoinSelection, PayCoinSelection,
ProposalDownload, ProposalDownload,
ProposalStatus,
RefreshReason, RefreshReason,
RefreshSessionRecord, RefreshSessionRecord,
RefundState,
ReserveBankInfo, ReserveBankInfo,
ReserveRecordStatus, ReserveRecordStatus,
Stores,
TransactionHandle, TransactionHandle,
WalletContractData, WalletContractData,
WalletRefundItem, WalletRefundItem,
} from "../.."; } from "../..";
import { hash } from "../../crypto/primitives/nacl-fast";
import { import {
WalletBackupContentV1,
BackupExchange,
BackupCoin,
BackupDenomination,
BackupReserve,
BackupPurchase,
BackupProposal,
BackupRefreshGroup,
BackupBackupProvider,
BackupTip,
BackupRecoupGroup,
BackupWithdrawalGroup,
BackupBackupProviderTerms,
BackupCoinSource,
BackupCoinSourceType, BackupCoinSourceType,
BackupExchangeWireFee,
BackupRefundItem,
BackupRefundState,
BackupProposalStatus,
BackupRefreshOldCoin,
BackupRefreshSession,
BackupDenomSel, BackupDenomSel,
BackupProposalStatus,
BackupPurchase,
BackupRefreshReason, BackupRefreshReason,
BackupRefundState,
WalletBackupContentV1,
} from "../../types/backupTypes"; } from "../../types/backupTypes";
import { canonicalizeBaseUrl, canonicalJson, j2s } from "../../util/helpers"; import { j2s } from "../../util/helpers";
import { checkDbInvariant, checkLogicInvariant } from "../../util/invariants"; import { checkDbInvariant, checkLogicInvariant } from "../../util/invariants";
import { Logger } from "../../util/logging"; import { Logger } from "../../util/logging";
import { initRetryInfo } from "../../util/retries"; import { initRetryInfo } from "../../util/retries";
import { InternalWalletState } from "../state"; import { InternalWalletState } from "../state";
import { provideBackupState } from "./state"; import { provideBackupState } from "./state";
const logger = new Logger("operations/backup/import.ts"); const logger = new Logger("operations/backup/import.ts");
function checkBackupInvariant(b: boolean, m?: string): asserts b { function checkBackupInvariant(b: boolean, m?: string): asserts b {
@ -230,6 +209,9 @@ export async function importBackup(
cryptoComp: BackupCryptoPrecomputedData, cryptoComp: BackupCryptoPrecomputedData,
): Promise<void> { ): Promise<void> {
await provideBackupState(ws); await provideBackupState(ws);
logger.info(`importing backup ${j2s(backupBlobArg)}`);
return ws.db.runWithWriteTransaction( return ws.db.runWithWriteTransaction(
[ [
Stores.config, Stores.config,

View File

@ -27,7 +27,11 @@
import { InternalWalletState } from "../state"; import { InternalWalletState } from "../state";
import { WalletBackupContentV1 } from "../../types/backupTypes"; import { WalletBackupContentV1 } from "../../types/backupTypes";
import { TransactionHandle } from "../../util/query"; import { TransactionHandle } from "../../util/query";
import { ConfigRecord, Stores } from "../../types/dbTypes"; import {
BackupProviderRecord,
ConfigRecord,
Stores,
} from "../../types/dbTypes";
import { checkDbInvariant, checkLogicInvariant } from "../../util/invariants"; import { checkDbInvariant, checkLogicInvariant } from "../../util/invariants";
import { codecForAmountString } from "../../util/amounts"; import { codecForAmountString } from "../../util/amounts";
import { import {
@ -41,7 +45,13 @@ import {
stringToBytes, stringToBytes,
} from "../../crypto/talerCrypto"; } from "../../crypto/talerCrypto";
import { canonicalizeBaseUrl, canonicalJson, j2s } from "../../util/helpers"; import { canonicalizeBaseUrl, canonicalJson, j2s } from "../../util/helpers";
import { getTimestampNow, Timestamp } from "../../util/time"; import {
durationAdd,
durationFromSpec,
getTimestampNow,
Timestamp,
timestampAddDuration,
} from "../../util/time";
import { URL } from "../../util/url"; import { URL } from "../../util/url";
import { AmountString } from "../../types/talerTypes"; import { AmountString } from "../../types/talerTypes";
import { import {
@ -70,7 +80,7 @@ import {
} from "../../types/walletTypes"; } from "../../types/walletTypes";
import { CryptoApi } from "../../crypto/workers/cryptoApi"; import { CryptoApi } from "../../crypto/workers/cryptoApi";
import { secretbox, secretbox_open } from "../../crypto/primitives/nacl-fast"; import { secretbox, secretbox_open } from "../../crypto/primitives/nacl-fast";
import { confirmPay, preparePayForUri } from "../pay"; import { checkPaymentByProposalId, confirmPay, preparePayForUri } from "../pay";
import { exportBackup } from "./export"; import { exportBackup } from "./export";
import { BackupCryptoPrecomputedData, importBackup } from "./import"; import { BackupCryptoPrecomputedData, importBackup } from "./import";
import { import {
@ -79,6 +89,7 @@ import {
getWalletBackupState, getWalletBackupState,
WalletBackupConfState, WalletBackupConfState,
} from "./state"; } from "./state";
import { PaymentStatus } from "../../types/transactionsTypes";
const logger = new Logger("operations/backup.ts"); const logger = new Logger("operations/backup.ts");
@ -216,23 +227,31 @@ function deriveBlobSecret(bc: WalletBackupConfState): Uint8Array {
); );
} }
interface BackupForProviderArgs {
backupConfig: WalletBackupConfState;
provider: BackupProviderRecord;
currentBackupHash: ArrayBuffer;
encBackup: ArrayBuffer;
backupJson: WalletBackupContentV1;
/** /**
* Do one backup cycle that consists of: * Should we attempt one more upload after trying
* 1. Exporting a backup and try to upload it. * to pay?
* Stop if this step succeeds.
* 2. Download, verify and import backups from connected sync accounts.
* 3. Upload the updated backup blob.
*/ */
export async function runBackupCycle(ws: InternalWalletState): Promise<void> { retryAfterPayment: boolean;
const providers = await ws.db.iter(Stores.backupProviders).toArray(); }
logger.trace("got backup providers", providers);
const backupJson = await exportBackup(ws);
const backupConfig = await provideBackupState(ws);
const encBackup = await encryptBackup(backupConfig, backupJson);
const currentBackupHash = hash(encBackup); async function runBackupCycleForProvider(
ws: InternalWalletState,
for (const provider of providers) { args: BackupForProviderArgs,
): Promise<void> {
const {
backupConfig,
provider,
currentBackupHash,
encBackup,
backupJson,
} = args;
const accountKeyPair = deriveAccountKeyPair(backupConfig, provider.baseUrl); const accountKeyPair = deriveAccountKeyPair(backupConfig, provider.baseUrl);
logger.trace(`trying to upload backup to ${provider.baseUrl}`); logger.trace(`trying to upload backup to ${provider.baseUrl}`);
@ -274,35 +293,37 @@ export async function runBackupCycle(ws: InternalWalletState): Promise<void> {
throw Error("no taler URI available to pay provider"); throw Error("no taler URI available to pay provider");
} }
const res = await preparePayForUri(ws, talerUri); const res = await preparePayForUri(ws, talerUri);
let proposalId: string | undefined; let proposalId = res.proposalId;
let doPay: boolean = false;
switch (res.status) { switch (res.status) {
case PreparePayResultType.InsufficientBalance: case PreparePayResultType.InsufficientBalance:
// FIXME: record in provider state! // FIXME: record in provider state!
logger.warn("insufficient balance to pay for backup provider"); logger.warn("insufficient balance to pay for backup provider");
break;
case PreparePayResultType.PaymentPossible:
case PreparePayResultType.AlreadyConfirmed:
proposalId = res.proposalId; proposalId = res.proposalId;
break; break;
case PreparePayResultType.PaymentPossible:
doPay = true;
break;
case PreparePayResultType.AlreadyConfirmed:
break;
} }
if (!proposalId) {
continue; // FIXME: check if the provider is overcharging us!
}
const p = proposalId;
await ws.db.runWithWriteTransaction( await ws.db.runWithWriteTransaction(
[Stores.backupProviders], [Stores.backupProviders],
async (tx) => { async (tx) => {
const provRec = await tx.get( const provRec = await tx.get(Stores.backupProviders, provider.baseUrl);
Stores.backupProviders,
provider.baseUrl,
);
checkDbInvariant(!!provRec); checkDbInvariant(!!provRec);
const ids = new Set(provRec.paymentProposalIds); const ids = new Set(provRec.paymentProposalIds);
ids.add(p); ids.add(proposalId);
provRec.paymentProposalIds = Array.from(ids); provRec.paymentProposalIds = Array.from(ids).sort();
provRec.currentPaymentProposalId = proposalId;
await tx.put(Stores.backupProviders, provRec); await tx.put(Stores.backupProviders, provRec);
}, },
); );
if (doPay) {
const confirmRes = await confirmPay(ws, proposalId); const confirmRes = await confirmPay(ws, proposalId);
switch (confirmRes.type) { switch (confirmRes.type) {
case ConfirmPayResultType.Pending: case ConfirmPayResultType.Pending:
@ -310,6 +331,16 @@ export async function runBackupCycle(ws: InternalWalletState): Promise<void> {
break; break;
} }
} }
if (args.retryAfterPayment) {
await runBackupCycleForProvider(ws, {
...args,
retryAfterPayment: false,
});
}
return;
}
if (resp.status === HttpResponseStatus.NoContent) { if (resp.status === HttpResponseStatus.NoContent) {
await ws.db.runWithWriteTransaction( await ws.db.runWithWriteTransaction(
[Stores.backupProviders], [Stores.backupProviders],
@ -320,14 +351,14 @@ export async function runBackupCycle(ws: InternalWalletState): Promise<void> {
} }
prov.lastBackupHash = encodeCrock(currentBackupHash); prov.lastBackupHash = encodeCrock(currentBackupHash);
prov.lastBackupTimestamp = getTimestampNow(); prov.lastBackupTimestamp = getTimestampNow();
prov.lastBackupClock = prov.lastBackupClock = backupJson.clocks[backupJson.current_device_id];
backupJson.clocks[backupJson.current_device_id];
prov.lastError = undefined; prov.lastError = undefined;
await tx.put(Stores.backupProviders, prov); await tx.put(Stores.backupProviders, prov);
}, },
); );
continue; return;
} }
if (resp.status === HttpResponseStatus.Conflict) { if (resp.status === HttpResponseStatus.Conflict) {
logger.info("conflicting backup found"); logger.info("conflicting backup found");
const backupEnc = new Uint8Array(await resp.bytes()); const backupEnc = new Uint8Array(await resp.bytes());
@ -350,7 +381,7 @@ export async function runBackupCycle(ws: InternalWalletState): Promise<void> {
}, },
); );
logger.info("processed existing backup"); logger.info("processed existing backup");
continue; return;
} }
// Some other response that we did not expect! // Some other response that we did not expect!
@ -359,16 +390,41 @@ export async function runBackupCycle(ws: InternalWalletState): Promise<void> {
const err = await readTalerErrorResponse(resp); const err = await readTalerErrorResponse(resp);
logger.error(`got error response from backup provider: ${j2s(err)}`); logger.error(`got error response from backup provider: ${j2s(err)}`);
await ws.db.runWithWriteTransaction( await ws.db.runWithWriteTransaction([Stores.backupProviders], async (tx) => {
[Stores.backupProviders],
async (tx) => {
const prov = await tx.get(Stores.backupProviders, provider.baseUrl); const prov = await tx.get(Stores.backupProviders, provider.baseUrl);
if (!prov) { if (!prov) {
return; return;
} }
prov.lastError = err; prov.lastError = err;
}, await tx.put(Stores.backupProviders, prov);
); });
}
/**
* Do one backup cycle that consists of:
* 1. Exporting a backup and try to upload it.
* Stop if this step succeeds.
* 2. Download, verify and import backups from connected sync accounts.
* 3. Upload the updated backup blob.
*/
export async function runBackupCycle(ws: InternalWalletState): Promise<void> {
const providers = await ws.db.iter(Stores.backupProviders).toArray();
logger.trace("got backup providers", providers);
const backupJson = await exportBackup(ws);
const backupConfig = await provideBackupState(ws);
const encBackup = await encryptBackup(backupConfig, backupJson);
const currentBackupHash = hash(encBackup);
for (const provider of providers) {
await runBackupCycleForProvider(ws, {
provider,
backupJson,
backupConfig,
encBackup,
currentBackupHash,
retryAfterPayment: true,
});
} }
} }
@ -462,8 +518,15 @@ export interface ProviderInfo {
lastRemoteClock?: number; lastRemoteClock?: number;
lastBackupTimestamp?: Timestamp; lastBackupTimestamp?: Timestamp;
paymentProposalIds: string[]; paymentProposalIds: string[];
paymentStatus: ProviderPaymentStatus;
} }
export type ProviderPaymentStatus =
| ProviderPaymentPaid
| ProviderPaymentInsufficientBalance
| ProviderPaymentUnpaid
| ProviderPaymentPending;
export interface BackupInfo { export interface BackupInfo {
walletRootPub: string; walletRootPub: string;
deviceId: string; deviceId: string;
@ -483,6 +546,71 @@ export async function importBackupPlain(
await importBackup(ws, blob, cryptoData); await importBackup(ws, blob, cryptoData);
} }
export enum ProviderPaymentType {
Unpaid = "unpaid",
Pending = "pending",
InsufficientBalance = "insufficient-balance",
Paid = "paid",
}
export interface ProviderPaymentUnpaid {
type: ProviderPaymentType.Unpaid;
}
export interface ProviderPaymentInsufficientBalance {
type: ProviderPaymentType.InsufficientBalance;
}
export interface ProviderPaymentPending {
type: ProviderPaymentType.Pending;
}
export interface ProviderPaymentPaid {
type: ProviderPaymentType.Paid;
paidUntil: Timestamp;
}
async function getProviderPaymentInfo(
ws: InternalWalletState,
provider: BackupProviderRecord,
): Promise<ProviderPaymentStatus> {
if (!provider.currentPaymentProposalId) {
return {
type: ProviderPaymentType.Unpaid,
};
}
const status = await checkPaymentByProposalId(
ws,
provider.currentPaymentProposalId,
);
if (status.status === PreparePayResultType.InsufficientBalance) {
return {
type: ProviderPaymentType.InsufficientBalance,
};
}
if (status.status === PreparePayResultType.PaymentPossible) {
return {
type: ProviderPaymentType.Pending,
};
}
if (status.status === PreparePayResultType.AlreadyConfirmed) {
if (status.paid) {
return {
type: ProviderPaymentType.Paid,
paidUntil: timestampAddDuration(
status.contractTerms.timestamp,
durationFromSpec({ years: 1 }),
),
};
} else {
return {
type: ProviderPaymentType.Pending,
};
}
}
throw Error("not reached");
}
/** /**
* Get information about the current state of wallet backups. * Get information about the current state of wallet backups.
*/ */
@ -490,19 +618,24 @@ export async function getBackupInfo(
ws: InternalWalletState, ws: InternalWalletState,
): Promise<BackupInfo> { ): Promise<BackupInfo> {
const backupConfig = await provideBackupState(ws); const backupConfig = await provideBackupState(ws);
const providers = await ws.db.iter(Stores.backupProviders).toArray(); const providerRecords = await ws.db.iter(Stores.backupProviders).toArray();
return { const providers: ProviderInfo[] = [];
deviceId: backupConfig.deviceId, for (const x of providerRecords) {
lastLocalClock: backupConfig.clocks[backupConfig.deviceId], providers.push({
walletRootPub: backupConfig.walletRootPub,
providers: providers.map((x) => ({
active: x.active, active: x.active,
lastRemoteClock: x.lastBackupClock, lastRemoteClock: x.lastBackupClock,
syncProviderBaseUrl: x.baseUrl, syncProviderBaseUrl: x.baseUrl,
lastBackupTimestamp: x.lastBackupTimestamp, lastBackupTimestamp: x.lastBackupTimestamp,
paymentProposalIds: x.paymentProposalIds, paymentProposalIds: x.paymentProposalIds,
lastError: x.lastError, lastError: x.lastError,
})), paymentStatus: await getProviderPaymentInfo(ws, x),
});
}
return {
deviceId: backupConfig.deviceId,
lastLocalClock: backupConfig.clocks[backupConfig.deviceId],
walletRootPub: backupConfig.walletRootPub,
providers,
}; };
} }

View File

@ -1150,36 +1150,11 @@ async function submitPay(
}; };
} }
/** export async function checkPaymentByProposalId(
* Check if a payment for the given taler://pay/ URI is possible.
*
* If the payment is possible, the signature are already generated but not
* yet send to the merchant.
*/
export async function preparePayForUri(
ws: InternalWalletState, ws: InternalWalletState,
talerPayUri: string, proposalId: string,
sessionId?: string,
): Promise<PreparePayResult> { ): Promise<PreparePayResult> {
const uriResult = parsePayUri(talerPayUri);
if (!uriResult) {
throw OperationFailedError.fromCode(
TalerErrorCode.WALLET_INVALID_TALER_PAY_URI,
`invalid taler://pay URI (${talerPayUri})`,
{
talerPayUri,
},
);
}
let proposalId = await startDownloadProposal(
ws,
uriResult.merchantBaseUrl,
uriResult.orderId,
uriResult.sessionId,
uriResult.claimToken,
);
let proposal = await ws.db.get(Stores.proposals, proposalId); let proposal = await ws.db.get(Stores.proposals, proposalId);
if (!proposal) { if (!proposal) {
throw Error(`could not get proposal ${proposalId}`); throw Error(`could not get proposal ${proposalId}`);
@ -1238,7 +1213,7 @@ export async function preparePayForUri(
}; };
} }
if (purchase.lastSessionId !== uriResult.sessionId) { if (purchase.lastSessionId !== sessionId) {
logger.trace( logger.trace(
"automatically re-submitting payment with different session ID", "automatically re-submitting payment with different session ID",
); );
@ -1247,7 +1222,7 @@ export async function preparePayForUri(
if (!p) { if (!p) {
return; return;
} }
p.lastSessionId = uriResult.sessionId; p.lastSessionId = sessionId;
await tx.put(Stores.purchases, p); await tx.put(Stores.purchases, p);
}); });
const r = await guardOperationException( const r = await guardOperationException(
@ -1292,6 +1267,39 @@ export async function preparePayForUri(
} }
} }
/**
* Check if a payment for the given taler://pay/ URI is possible.
*
* If the payment is possible, the signature are already generated but not
* yet send to the merchant.
*/
export async function preparePayForUri(
ws: InternalWalletState,
talerPayUri: string,
): Promise<PreparePayResult> {
const uriResult = parsePayUri(talerPayUri);
if (!uriResult) {
throw OperationFailedError.fromCode(
TalerErrorCode.WALLET_INVALID_TALER_PAY_URI,
`invalid taler://pay URI (${talerPayUri})`,
{
talerPayUri,
},
);
}
let proposalId = await startDownloadProposal(
ws,
uriResult.merchantBaseUrl,
uriResult.orderId,
uriResult.sessionId,
uriResult.claimToken,
);
return checkPaymentByProposalId(ws, proposalId, uriResult.sessionId);
}
/** /**
* Generate deposit permissions for a purchase. * Generate deposit permissions for a purchase.
* *

View File

@ -1462,8 +1462,19 @@ export interface BackupProviderRecord {
lastBackupTimestamp?: Timestamp; lastBackupTimestamp?: Timestamp;
/**
* Proposal that we're currently trying to pay for.
*
* (Also included in paymentProposalIds.)
*/
currentPaymentProposalId?: string; currentPaymentProposalId?: string;
/**
* Proposals that were used to pay (or attempt to pay) the provider.
*
* Stored to display a history of payments to the provider, and
* to make sure that the wallet isn't overpaying.
*/
paymentProposalIds: string[]; paymentProposalIds: string[];
/** /**

View File

@ -59,7 +59,7 @@ export function canonicalizeBaseUrl(url: string): string {
*/ */
export function canonicalJson(obj: any): string { export function canonicalJson(obj: any): string {
// Check for cycles, etc. // Check for cycles, etc.
JSON.stringify(obj); obj = JSON.parse(JSON.stringify(obj));
if (typeof obj === "string" || typeof obj === "number" || obj === null) { if (typeof obj === "string" || typeof obj === "number" || obj === null) {
return JSON.stringify(obj); return JSON.stringify(obj);
} }

View File

@ -22,7 +22,7 @@
/** /**
* Imports. * Imports.
*/ */
import { TalerErrorCode } from "."; import { codecForAny, TalerErrorCode } from ".";
import { CryptoWorkerFactory } from "./crypto/workers/cryptoApi"; import { CryptoWorkerFactory } from "./crypto/workers/cryptoApi";
import { import {
addBackupProvider, addBackupProvider,
@ -1159,6 +1159,15 @@ export class Wallet {
await runBackupCycle(this.ws); await runBackupCycle(this.ws);
return {}; return {};
} }
case "exportBackupRecovery": {
const resp = await getBackupRecovery(this.ws);
return resp;
}
case "importBackupRecovery": {
const req = codecForAny().decode(payload);
await loadBackupRecovery(this.ws, req);
return {};
}
case "getBackupInfo": { case "getBackupInfo": {
const resp = await getBackupInfo(this.ws); const resp = await getBackupInfo(this.ws);
return resp; return resp;