backup WIP
This commit is contained in:
parent
0828e65f88
commit
89f1a281fe
@ -38,6 +38,8 @@ import {
|
|||||||
WalletNotification,
|
WalletNotification,
|
||||||
WALLET_EXCHANGE_PROTOCOL_VERSION,
|
WALLET_EXCHANGE_PROTOCOL_VERSION,
|
||||||
WALLET_MERCHANT_PROTOCOL_VERSION,
|
WALLET_MERCHANT_PROTOCOL_VERSION,
|
||||||
|
bytesToString,
|
||||||
|
stringToBytes,
|
||||||
} from "taler-wallet-core";
|
} from "taler-wallet-core";
|
||||||
|
|
||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
@ -57,6 +59,10 @@ export class AndroidHttpLib implements HttpRequestLibrary {
|
|||||||
|
|
||||||
constructor(private sendMessage: (m: string) => void) {}
|
constructor(private sendMessage: (m: string) => void) {}
|
||||||
|
|
||||||
|
fetch(url: string, opt?: HttpRequestOptions): Promise<HttpResponse> {
|
||||||
|
return this.nodeHttpLib.fetch(url, opt);
|
||||||
|
}
|
||||||
|
|
||||||
get(url: string, opt?: HttpRequestOptions): Promise<HttpResponse> {
|
get(url: string, opt?: HttpRequestOptions): Promise<HttpResponse> {
|
||||||
if (this.useNfcTunnel) {
|
if (this.useNfcTunnel) {
|
||||||
const myId = this.requestId++;
|
const myId = this.requestId++;
|
||||||
@ -120,6 +126,7 @@ export class AndroidHttpLib implements HttpRequestLibrary {
|
|||||||
requestMethod: "FIXME",
|
requestMethod: "FIXME",
|
||||||
json: async () => JSON.parse(msg.responseText),
|
json: async () => JSON.parse(msg.responseText),
|
||||||
text: async () => msg.responseText,
|
text: async () => msg.responseText,
|
||||||
|
bytes: async () => { throw Error("bytes() not supported for tunnel response") },
|
||||||
};
|
};
|
||||||
p.resolve(resp);
|
p.resolve(resp);
|
||||||
} else {
|
} else {
|
||||||
|
@ -58,6 +58,7 @@
|
|||||||
"@types/node": "^14.14.7",
|
"@types/node": "^14.14.7",
|
||||||
"axios": "^0.21.0",
|
"axios": "^0.21.0",
|
||||||
"big-integer": "^1.6.48",
|
"big-integer": "^1.6.48",
|
||||||
|
"fflate": "^0.3.10",
|
||||||
"idb-bridge": "workspace:*",
|
"idb-bridge": "workspace:*",
|
||||||
"source-map-support": "^0.5.19",
|
"source-map-support": "^0.5.19",
|
||||||
"tslib": "^2.0.3"
|
"tslib": "^2.0.3"
|
||||||
|
@ -42,6 +42,7 @@ import {
|
|||||||
PlanchetCreationResult,
|
PlanchetCreationResult,
|
||||||
PlanchetCreationRequest,
|
PlanchetCreationRequest,
|
||||||
DepositInfo,
|
DepositInfo,
|
||||||
|
MakeSyncSignatureRequest,
|
||||||
} from "../../types/walletTypes";
|
} from "../../types/walletTypes";
|
||||||
|
|
||||||
import * as timer from "../../util/timer";
|
import * as timer from "../../util/timer";
|
||||||
@ -455,4 +456,8 @@ export class CryptoApi {
|
|||||||
benchmark(repetitions: number): Promise<BenchmarkResult> {
|
benchmark(repetitions: number): Promise<BenchmarkResult> {
|
||||||
return this.doRpc<BenchmarkResult>("benchmark", 1, repetitions);
|
return this.doRpc<BenchmarkResult>("benchmark", 1, repetitions);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
makeSyncSignature(req: MakeSyncSignatureRequest): Promise<string> {
|
||||||
|
return this.doRpc<string>("makeSyncSignature", 3, req);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -43,6 +43,7 @@ import {
|
|||||||
PlanchetCreationResult,
|
PlanchetCreationResult,
|
||||||
PlanchetCreationRequest,
|
PlanchetCreationRequest,
|
||||||
DepositInfo,
|
DepositInfo,
|
||||||
|
MakeSyncSignatureRequest,
|
||||||
} from "../../types/walletTypes";
|
} from "../../types/walletTypes";
|
||||||
import { AmountJson, Amounts } from "../../util/amounts";
|
import { AmountJson, Amounts } from "../../util/amounts";
|
||||||
import * as timer from "../../util/timer";
|
import * as timer from "../../util/timer";
|
||||||
@ -85,6 +86,7 @@ enum SignaturePurpose {
|
|||||||
WALLET_COIN_LINK = 1204,
|
WALLET_COIN_LINK = 1204,
|
||||||
EXCHANGE_CONFIRM_RECOUP = 1039,
|
EXCHANGE_CONFIRM_RECOUP = 1039,
|
||||||
EXCHANGE_CONFIRM_RECOUP_REFRESH = 1041,
|
EXCHANGE_CONFIRM_RECOUP_REFRESH = 1041,
|
||||||
|
SYNC_BACKUP_UPLOAD = 1450,
|
||||||
}
|
}
|
||||||
|
|
||||||
function amountToBuffer(amount: AmountJson): Uint8Array {
|
function amountToBuffer(amount: AmountJson): Uint8Array {
|
||||||
@ -589,4 +591,20 @@ export class CryptoImplementation {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
makeSyncSignature(req: MakeSyncSignatureRequest): string {
|
||||||
|
const hNew = decodeCrock(req.newHash);
|
||||||
|
let hOld: Uint8Array;
|
||||||
|
if (req.oldHash) {
|
||||||
|
hOld = decodeCrock(req.oldHash);
|
||||||
|
} else {
|
||||||
|
hOld = new Uint8Array(64);
|
||||||
|
}
|
||||||
|
const sigBlob = new SignaturePurposeBuilder(SignaturePurpose.SYNC_BACKUP_UPLOAD)
|
||||||
|
.put(hOld)
|
||||||
|
.put(hNew)
|
||||||
|
.build();
|
||||||
|
const uploadSig = eddsaSign(sigBlob, decodeCrock(req.accountPriv));
|
||||||
|
return encodeCrock(uploadSig);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,12 @@
|
|||||||
import { Stores } from "./types/dbTypes";
|
import { Stores } from "./types/dbTypes";
|
||||||
import { openDatabase, Database, Store, Index } from "./util/query";
|
import { openDatabase, Database, Store, Index } from "./util/query";
|
||||||
import { IDBFactory, IDBDatabase, IDBObjectStore, IDBTransaction } from "idb-bridge";
|
import {
|
||||||
import { Logger } from './util/logging';
|
IDBFactory,
|
||||||
|
IDBDatabase,
|
||||||
|
IDBObjectStore,
|
||||||
|
IDBTransaction,
|
||||||
|
} from "idb-bridge";
|
||||||
|
import { Logger } from "./util/logging";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Name of the Taler database. This is effectively the major
|
* Name of the Taler database. This is effectively the major
|
||||||
@ -18,7 +23,7 @@ const TALER_DB_NAME = "taler-wallet-prod-v1";
|
|||||||
* backwards-compatible way or object stores and indices
|
* backwards-compatible way or object stores and indices
|
||||||
* are added.
|
* are added.
|
||||||
*/
|
*/
|
||||||
export const WALLET_DB_MINOR_VERSION = 2;
|
export const WALLET_DB_MINOR_VERSION = 3;
|
||||||
|
|
||||||
const logger = new Logger("db.ts");
|
const logger = new Logger("db.ts");
|
||||||
|
|
||||||
@ -43,7 +48,9 @@ export function openTalerDatabase(
|
|||||||
const s = db.createObjectStore(si.name, si.storeParams);
|
const s = db.createObjectStore(si.name, si.storeParams);
|
||||||
for (const indexName in si as any) {
|
for (const indexName in si as any) {
|
||||||
if ((si as any)[indexName] instanceof Index) {
|
if ((si as any)[indexName] instanceof Index) {
|
||||||
const ii: Index<string, string, any, any> = (si as any)[indexName];
|
const ii: Index<string, string, any, any> = (si as any)[
|
||||||
|
indexName
|
||||||
|
];
|
||||||
s.createIndex(ii.indexName, ii.keyPath, ii.options);
|
s.createIndex(ii.indexName, ii.keyPath, ii.options);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -59,7 +66,8 @@ export function openTalerDatabase(
|
|||||||
if ((Stores as any)[n] instanceof Store) {
|
if ((Stores as any)[n] instanceof Store) {
|
||||||
const si: Store<string, any> = (Stores as any)[n];
|
const si: Store<string, any> = (Stores as any)[n];
|
||||||
let s: IDBObjectStore;
|
let s: IDBObjectStore;
|
||||||
if ((si.storeParams?.versionAdded ?? 1) > oldVersion) {
|
const storeVersionAdded = si.storeParams?.versionAdded ?? 1;
|
||||||
|
if (storeVersionAdded > oldVersion) {
|
||||||
s = db.createObjectStore(si.name, si.storeParams);
|
s = db.createObjectStore(si.name, si.storeParams);
|
||||||
} else {
|
} else {
|
||||||
s = upgradeTransaction.objectStore(si.name);
|
s = upgradeTransaction.objectStore(si.name);
|
||||||
@ -67,7 +75,8 @@ export function openTalerDatabase(
|
|||||||
for (const indexName in si as any) {
|
for (const indexName in si as any) {
|
||||||
if ((si as any)[indexName] instanceof Index) {
|
if ((si as any)[indexName] instanceof Index) {
|
||||||
const ii: Index<string, string, any, any> = (si as any)[indexName];
|
const ii: Index<string, string, any, any> = (si as any)[indexName];
|
||||||
if ((ii.options?.versionAdded ?? 0) > oldVersion) {
|
const indexVersionAdded = ii.options?.versionAdded ?? 0;
|
||||||
|
if (indexVersionAdded > oldVersion || storeVersionAdded > oldVersion) {
|
||||||
s.createIndex(ii.indexName, ii.keyPath, ii.options);
|
s.createIndex(ii.indexName, ii.keyPath, ii.options);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -31,6 +31,7 @@ import { OperationFailedError, makeErrorDetails } from "../operations/errors";
|
|||||||
import { TalerErrorCode } from "../TalerErrorCode";
|
import { TalerErrorCode } from "../TalerErrorCode";
|
||||||
import { URL } from "../util/url";
|
import { URL } from "../util/url";
|
||||||
import { Logger } from "../util/logging";
|
import { Logger } from "../util/logging";
|
||||||
|
import { bytesToString } from '../crypto/talerCrypto';
|
||||||
|
|
||||||
const logger = new Logger("NodeHttpLib.ts");
|
const logger = new Logger("NodeHttpLib.ts");
|
||||||
|
|
||||||
@ -48,12 +49,10 @@ export class NodeHttpLib implements HttpRequestLibrary {
|
|||||||
this.throttlingEnabled = enabled;
|
this.throttlingEnabled = enabled;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async req(
|
async fetch(url: string, opt?: HttpRequestOptions): Promise<HttpResponse> {
|
||||||
method: "POST" | "GET",
|
const method = opt?.method ?? "GET";
|
||||||
url: string,
|
let body = opt?.body;
|
||||||
body: any,
|
|
||||||
opt?: HttpRequestOptions,
|
|
||||||
): Promise<HttpResponse> {
|
|
||||||
const parsedUrl = new URL(url);
|
const parsedUrl = new URL(url);
|
||||||
if (this.throttlingEnabled && this.throttle.applyThrottle(url)) {
|
if (this.throttlingEnabled && this.throttle.applyThrottle(url)) {
|
||||||
throw OperationFailedError.fromCode(
|
throw OperationFailedError.fromCode(
|
||||||
@ -75,7 +74,7 @@ export class NodeHttpLib implements HttpRequestLibrary {
|
|||||||
resp = await Axios({
|
resp = await Axios({
|
||||||
method,
|
method,
|
||||||
url: url,
|
url: url,
|
||||||
responseType: "text",
|
responseType: "arraybuffer",
|
||||||
headers: opt?.headers,
|
headers: opt?.headers,
|
||||||
validateStatus: () => true,
|
validateStatus: () => true,
|
||||||
transformResponse: (x) => x,
|
transformResponse: (x) => x,
|
||||||
@ -93,26 +92,18 @@ export class NodeHttpLib implements HttpRequestLibrary {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const respText = resp.data;
|
const makeText = async(): Promise<string> => {
|
||||||
if (typeof respText !== "string") {
|
const respText = new Uint8Array(resp.data);
|
||||||
throw new OperationFailedError(
|
return bytesToString(respText);
|
||||||
makeErrorDetails(
|
|
||||||
TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE,
|
|
||||||
"unexpected response type",
|
|
||||||
{
|
|
||||||
httpStatusCode: resp.status,
|
|
||||||
requestUrl: url,
|
|
||||||
requestMethod: method,
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const makeJson = async (): Promise<any> => {
|
const makeJson = async (): Promise<any> => {
|
||||||
let responseJson;
|
let responseJson;
|
||||||
|
const respText = await makeText();
|
||||||
try {
|
try {
|
||||||
responseJson = JSON.parse(respText);
|
responseJson = JSON.parse(respText);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.trace(`invalid json: '${respText}'`);
|
logger.trace(`invalid json: '${resp.data}'`);
|
||||||
throw new OperationFailedError(
|
throw new OperationFailedError(
|
||||||
makeErrorDetails(
|
makeErrorDetails(
|
||||||
TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE,
|
TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE,
|
||||||
@ -141,6 +132,13 @@ export class NodeHttpLib implements HttpRequestLibrary {
|
|||||||
}
|
}
|
||||||
return responseJson;
|
return responseJson;
|
||||||
};
|
};
|
||||||
|
const makeBytes = async () => {
|
||||||
|
if (!(resp.data instanceof ArrayBuffer)) {
|
||||||
|
throw Error("expected array buffer");
|
||||||
|
}
|
||||||
|
const buf = resp.data;
|
||||||
|
return buf;
|
||||||
|
};
|
||||||
const headers = new Headers();
|
const headers = new Headers();
|
||||||
for (const hn of Object.keys(resp.headers)) {
|
for (const hn of Object.keys(resp.headers)) {
|
||||||
headers.set(hn, resp.headers[hn]);
|
headers.set(hn, resp.headers[hn]);
|
||||||
@ -150,13 +148,17 @@ export class NodeHttpLib implements HttpRequestLibrary {
|
|||||||
requestMethod: method,
|
requestMethod: method,
|
||||||
headers,
|
headers,
|
||||||
status: resp.status,
|
status: resp.status,
|
||||||
text: async () => resp.data,
|
text: makeText,
|
||||||
json: makeJson,
|
json: makeJson,
|
||||||
|
bytes: makeBytes,
|
||||||
};
|
};
|
||||||
}
|
|
||||||
|
|
||||||
|
}
|
||||||
async get(url: string, opt?: HttpRequestOptions): Promise<HttpResponse> {
|
async get(url: string, opt?: HttpRequestOptions): Promise<HttpResponse> {
|
||||||
return this.req("GET", url, undefined, opt);
|
return this.fetch(url, {
|
||||||
|
method: "GET",
|
||||||
|
...opt,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async postJson(
|
async postJson(
|
||||||
@ -164,6 +166,10 @@ export class NodeHttpLib implements HttpRequestLibrary {
|
|||||||
body: any,
|
body: any,
|
||||||
opt?: HttpRequestOptions,
|
opt?: HttpRequestOptions,
|
||||||
): Promise<HttpResponse> {
|
): Promise<HttpResponse> {
|
||||||
return this.req("POST", url, body, opt);
|
return this.fetch(url, {
|
||||||
|
method: "POST",
|
||||||
|
body,
|
||||||
|
...opt,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
402
packages/taler-wallet-core/src/operations/backup.ts
Normal file
402
packages/taler-wallet-core/src/operations/backup.ts
Normal file
@ -0,0 +1,402 @@
|
|||||||
|
/*
|
||||||
|
This file is part of GNU Taler
|
||||||
|
(C) 2020 Taler Systems SA
|
||||||
|
|
||||||
|
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 <http://www.gnu.org/licenses/>
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implementation of wallet backups (export/import/upload) and sync
|
||||||
|
* server management.
|
||||||
|
*
|
||||||
|
* @author Florian Dold <dold@taler.net>
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Imports.
|
||||||
|
*/
|
||||||
|
import { InternalWalletState } from "./state";
|
||||||
|
import {
|
||||||
|
BackupCoin,
|
||||||
|
BackupCoinSource,
|
||||||
|
BackupCoinSourceType,
|
||||||
|
BackupExchangeData,
|
||||||
|
WalletBackupContentV1,
|
||||||
|
} from "../types/backupTypes";
|
||||||
|
import { TransactionHandle } from "../util/query";
|
||||||
|
import {
|
||||||
|
CoinSourceType,
|
||||||
|
CoinStatus,
|
||||||
|
ConfigRecord,
|
||||||
|
Stores,
|
||||||
|
} from "../types/dbTypes";
|
||||||
|
import { checkDbInvariant } from "../util/invariants";
|
||||||
|
import { Amounts, codecForAmountString } from "../util/amounts";
|
||||||
|
import {
|
||||||
|
decodeCrock,
|
||||||
|
eddsaGetPublic,
|
||||||
|
EddsaKeyPair,
|
||||||
|
encodeCrock,
|
||||||
|
getRandomBytes,
|
||||||
|
hash,
|
||||||
|
stringToBytes,
|
||||||
|
} from "../crypto/talerCrypto";
|
||||||
|
import { canonicalizeBaseUrl, canonicalJson, j2s } from "../util/helpers";
|
||||||
|
import { Timestamp } from "../util/time";
|
||||||
|
import { URL } from "../util/url";
|
||||||
|
import { AmountString } from "../types/talerTypes";
|
||||||
|
import {
|
||||||
|
buildCodecForObject,
|
||||||
|
Codec,
|
||||||
|
codecForNumber,
|
||||||
|
codecForString,
|
||||||
|
} from "../util/codec";
|
||||||
|
import {
|
||||||
|
HttpResponseStatus,
|
||||||
|
readSuccessResponseJsonOrThrow,
|
||||||
|
} from "../util/http";
|
||||||
|
import { Logger } from "../util/logging";
|
||||||
|
import { gzipSync } from "fflate";
|
||||||
|
import { sign_keyPair_fromSeed } from "../crypto/primitives/nacl-fast";
|
||||||
|
import { kdf } from "../crypto/primitives/kdf";
|
||||||
|
|
||||||
|
interface WalletBackupConfState {
|
||||||
|
walletRootPub: string;
|
||||||
|
walletRootPriv: string;
|
||||||
|
clock: number;
|
||||||
|
lastBackupHash?: string;
|
||||||
|
lastBackupNonce?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const WALLET_BACKUP_STATE_KEY = "walletBackupState";
|
||||||
|
|
||||||
|
const logger = new Logger("operations/backup.ts");
|
||||||
|
|
||||||
|
async function provideBackupState(
|
||||||
|
ws: InternalWalletState,
|
||||||
|
): Promise<WalletBackupConfState> {
|
||||||
|
const bs: ConfigRecord<WalletBackupConfState> | undefined = await ws.db.get(
|
||||||
|
Stores.config,
|
||||||
|
WALLET_BACKUP_STATE_KEY,
|
||||||
|
);
|
||||||
|
if (bs) {
|
||||||
|
return bs.value;
|
||||||
|
}
|
||||||
|
// We need to generate the key outside of the transaction
|
||||||
|
// due to how IndexedDB works.
|
||||||
|
const k = await ws.cryptoApi.createEddsaKeypair();
|
||||||
|
return await ws.db.runWithWriteTransaction([Stores.config], async (tx) => {
|
||||||
|
let backupStateEntry:
|
||||||
|
| ConfigRecord<WalletBackupConfState>
|
||||||
|
| undefined = await tx.get(Stores.config, WALLET_BACKUP_STATE_KEY);
|
||||||
|
if (!backupStateEntry) {
|
||||||
|
backupStateEntry = {
|
||||||
|
key: WALLET_BACKUP_STATE_KEY,
|
||||||
|
value: {
|
||||||
|
walletRootPub: k.pub,
|
||||||
|
walletRootPriv: k.priv,
|
||||||
|
clock: 0,
|
||||||
|
lastBackupHash: undefined,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
await tx.put(Stores.config, backupStateEntry);
|
||||||
|
}
|
||||||
|
return backupStateEntry.value;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getWalletBackupState(
|
||||||
|
ws: InternalWalletState,
|
||||||
|
tx: TransactionHandle<typeof Stores.config>,
|
||||||
|
): Promise<WalletBackupConfState> {
|
||||||
|
let bs = await tx.get(Stores.config, WALLET_BACKUP_STATE_KEY);
|
||||||
|
checkDbInvariant(!!bs, "wallet backup state should be in DB");
|
||||||
|
return bs.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function exportBackup(
|
||||||
|
ws: InternalWalletState,
|
||||||
|
): Promise<WalletBackupContentV1> {
|
||||||
|
await provideBackupState(ws);
|
||||||
|
return ws.db.runWithWriteTransaction(
|
||||||
|
[Stores.config, Stores.exchanges, Stores.coins],
|
||||||
|
async (tx) => {
|
||||||
|
const bs = await getWalletBackupState(ws, tx);
|
||||||
|
|
||||||
|
const exchanges: BackupExchangeData[] = [];
|
||||||
|
const coins: BackupCoin[] = [];
|
||||||
|
|
||||||
|
await tx.iter(Stores.exchanges).forEach((ex) => {
|
||||||
|
if (!ex.details) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
exchanges.push({
|
||||||
|
exchangeBaseUrl: ex.baseUrl,
|
||||||
|
exchangeMasterPub: ex.details?.masterPublicKey,
|
||||||
|
termsOfServiceAcceptedEtag: ex.termsOfServiceAcceptedEtag,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
await tx.iter(Stores.coins).forEach((coin) => {
|
||||||
|
let bcs: BackupCoinSource;
|
||||||
|
switch (coin.coinSource.type) {
|
||||||
|
case CoinSourceType.Refresh:
|
||||||
|
bcs = {
|
||||||
|
type: BackupCoinSourceType.Refresh,
|
||||||
|
oldCoinPub: coin.coinSource.oldCoinPub,
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
case CoinSourceType.Tip:
|
||||||
|
bcs = {
|
||||||
|
type: BackupCoinSourceType.Tip,
|
||||||
|
coinIndex: coin.coinSource.coinIndex,
|
||||||
|
walletTipId: coin.coinSource.walletTipId,
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
case CoinSourceType.Withdraw:
|
||||||
|
bcs = {
|
||||||
|
type: BackupCoinSourceType.Withdraw,
|
||||||
|
coinIndex: coin.coinSource.coinIndex,
|
||||||
|
reservePub: coin.coinSource.reservePub,
|
||||||
|
withdrawalGroupId: coin.coinSource.withdrawalGroupId,
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
coins.push({
|
||||||
|
exchangeBaseUrl: coin.exchangeBaseUrl,
|
||||||
|
blindingKey: coin.blindingKey,
|
||||||
|
coinPriv: coin.coinPriv,
|
||||||
|
coinPub: coin.coinPub,
|
||||||
|
coinSource: bcs,
|
||||||
|
currentAmount: Amounts.stringify(coin.currentAmount),
|
||||||
|
fresh: coin.status === CoinStatus.Fresh,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const backupBlob: WalletBackupContentV1 = {
|
||||||
|
schemaId: "gnu-taler-wallet-backup",
|
||||||
|
schemaVersion: 1,
|
||||||
|
clock: bs.clock,
|
||||||
|
coins: coins,
|
||||||
|
exchanges: exchanges,
|
||||||
|
planchets: [],
|
||||||
|
refreshSessions: [],
|
||||||
|
reserves: [],
|
||||||
|
walletRootPub: bs.walletRootPub,
|
||||||
|
};
|
||||||
|
|
||||||
|
// If the backup changed, we increment our clock.
|
||||||
|
|
||||||
|
let h = encodeCrock(hash(stringToBytes(canonicalJson(backupBlob))));
|
||||||
|
if (h != bs.lastBackupHash) {
|
||||||
|
backupBlob.clock = ++bs.clock;
|
||||||
|
bs.lastBackupHash = encodeCrock(
|
||||||
|
hash(stringToBytes(canonicalJson(backupBlob))),
|
||||||
|
);
|
||||||
|
bs.lastBackupNonce = encodeCrock(getRandomBytes(32));
|
||||||
|
await tx.put(Stores.config, {
|
||||||
|
key: WALLET_BACKUP_STATE_KEY,
|
||||||
|
value: bs,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return backupBlob;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BackupRequest {
|
||||||
|
backupBlob: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function encryptBackup(
|
||||||
|
config: WalletBackupConfState,
|
||||||
|
blob: WalletBackupContentV1,
|
||||||
|
): Promise<Uint8Array> {
|
||||||
|
throw Error("not implemented");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function importBackup(
|
||||||
|
ws: InternalWalletState,
|
||||||
|
backupRequest: BackupRequest,
|
||||||
|
): Promise<void> {
|
||||||
|
throw Error("not implemented");
|
||||||
|
}
|
||||||
|
|
||||||
|
function deriveAccountKeyPair(
|
||||||
|
bc: WalletBackupConfState,
|
||||||
|
providerUrl: string,
|
||||||
|
): EddsaKeyPair {
|
||||||
|
const privateKey = kdf(
|
||||||
|
32,
|
||||||
|
decodeCrock(bc.walletRootPriv),
|
||||||
|
stringToBytes("taler-sync-account-key-salt"),
|
||||||
|
stringToBytes(providerUrl),
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
eddsaPriv: privateKey,
|
||||||
|
eddsaPub: eddsaGetPublic(privateKey),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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();
|
||||||
|
const backupConfig = await provideBackupState(ws);
|
||||||
|
|
||||||
|
logger.trace("got backup providers", providers);
|
||||||
|
const backupJsonContent = canonicalJson(await exportBackup(ws));
|
||||||
|
logger.trace("backup JSON size", backupJsonContent.length);
|
||||||
|
const compressedContent = gzipSync(stringToBytes(backupJsonContent));
|
||||||
|
logger.trace("backup compressed JSON size", compressedContent.length);
|
||||||
|
|
||||||
|
const h = hash(compressedContent);
|
||||||
|
|
||||||
|
for (const provider of providers) {
|
||||||
|
const accountKeyPair = deriveAccountKeyPair(backupConfig, provider.baseUrl);
|
||||||
|
logger.trace(`trying to upload backup to ${provider.baseUrl}`);
|
||||||
|
|
||||||
|
const syncSig = await ws.cryptoApi.makeSyncSignature({
|
||||||
|
newHash: encodeCrock(h),
|
||||||
|
oldHash: provider.lastBackupHash,
|
||||||
|
accountPriv: encodeCrock(accountKeyPair.eddsaPriv),
|
||||||
|
});
|
||||||
|
|
||||||
|
logger.trace(`sync signature is ${syncSig}`);
|
||||||
|
|
||||||
|
const accountBackupUrl = new URL(
|
||||||
|
`/backups/${encodeCrock(accountKeyPair.eddsaPub)}`,
|
||||||
|
provider.baseUrl,
|
||||||
|
);
|
||||||
|
|
||||||
|
const resp = await ws.http.fetch(accountBackupUrl.href, {
|
||||||
|
method: "POST",
|
||||||
|
body: compressedContent,
|
||||||
|
headers: {
|
||||||
|
"content-type": "application/octet-stream",
|
||||||
|
"sync-signature": syncSig,
|
||||||
|
"if-none-match": encodeCrock(h),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
logger.trace(`response status: ${resp.status}`);
|
||||||
|
|
||||||
|
if (resp.status === HttpResponseStatus.PaymentRequired) {
|
||||||
|
logger.trace("payment required for backup");
|
||||||
|
logger.trace(`headers: ${j2s(resp.headers)}`)
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (resp.status === HttpResponseStatus.Ok) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.trace(`response body: ${j2s(await resp.json())}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SyncTermsOfServiceResponse {
|
||||||
|
// maximum backup size supported
|
||||||
|
storage_limit_in_megabytes: number;
|
||||||
|
|
||||||
|
// Fee for an account, per year.
|
||||||
|
annual_fee: AmountString;
|
||||||
|
|
||||||
|
// protocol version supported by the server,
|
||||||
|
// for now always "0.0".
|
||||||
|
version: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const codecForSyncTermsOfServiceResponse = (): Codec<
|
||||||
|
SyncTermsOfServiceResponse
|
||||||
|
> =>
|
||||||
|
buildCodecForObject<SyncTermsOfServiceResponse>()
|
||||||
|
.property("storage_limit_in_megabytes", codecForNumber())
|
||||||
|
.property("annual_fee", codecForAmountString())
|
||||||
|
.property("version", codecForString())
|
||||||
|
.build("SyncTermsOfServiceResponse");
|
||||||
|
|
||||||
|
export interface AddBackupProviderRequest {
|
||||||
|
backupProviderBaseUrl: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const codecForAddBackupProviderRequest = (): Codec<
|
||||||
|
AddBackupProviderRequest
|
||||||
|
> =>
|
||||||
|
buildCodecForObject<AddBackupProviderRequest>()
|
||||||
|
.property("backupProviderBaseUrl", codecForString())
|
||||||
|
.build("AddBackupProviderRequest");
|
||||||
|
|
||||||
|
export async function addBackupProvider(
|
||||||
|
ws: InternalWalletState,
|
||||||
|
req: AddBackupProviderRequest,
|
||||||
|
): Promise<void> {
|
||||||
|
await provideBackupState(ws);
|
||||||
|
const canonUrl = canonicalizeBaseUrl(req.backupProviderBaseUrl);
|
||||||
|
const oldProv = await ws.db.get(Stores.backupProviders, canonUrl);
|
||||||
|
if (oldProv) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const termsUrl = new URL("terms", canonUrl);
|
||||||
|
const resp = await ws.http.get(termsUrl.href);
|
||||||
|
const terms = await readSuccessResponseJsonOrThrow(
|
||||||
|
resp,
|
||||||
|
codecForSyncTermsOfServiceResponse(),
|
||||||
|
);
|
||||||
|
await ws.db.put(Stores.backupProviders, {
|
||||||
|
active: true,
|
||||||
|
annualFee: terms.annual_fee,
|
||||||
|
baseUrl: canonUrl,
|
||||||
|
storageLimitInMegabytes: terms.storage_limit_in_megabytes,
|
||||||
|
supportedProtocolVersion: terms.version,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function removeBackupProvider(
|
||||||
|
syncProviderBaseUrl: string,
|
||||||
|
): Promise<void> {}
|
||||||
|
|
||||||
|
export async function restoreFromRecoverySecret(): Promise<void> {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Information about one provider.
|
||||||
|
*
|
||||||
|
* We don't store the account key here,
|
||||||
|
* as that's derived from the wallet root key.
|
||||||
|
*/
|
||||||
|
export interface ProviderInfo {
|
||||||
|
syncProviderBaseUrl: string;
|
||||||
|
lastRemoteClock: number;
|
||||||
|
lastBackup?: Timestamp;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BackupInfo {
|
||||||
|
walletRootPub: string;
|
||||||
|
deviceId: string;
|
||||||
|
lastLocalClock: number;
|
||||||
|
providers: ProviderInfo[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get information about the current state of wallet backups.
|
||||||
|
*/
|
||||||
|
export function getBackupInfo(ws: InternalWalletState): Promise<BackupInfo> {
|
||||||
|
throw Error("not implemented");
|
||||||
|
}
|
215
packages/taler-wallet-core/src/types/backupTypes.ts
Normal file
215
packages/taler-wallet-core/src/types/backupTypes.ts
Normal file
@ -0,0 +1,215 @@
|
|||||||
|
/*
|
||||||
|
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 <http://www.gnu.org/licenses/>
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type declarations for backup.
|
||||||
|
*
|
||||||
|
* Contains some redundancy with the other type declarations,
|
||||||
|
* as the backup schema must be very stable.
|
||||||
|
*
|
||||||
|
* @author Florian Dold <dold@taler.net>
|
||||||
|
*/
|
||||||
|
|
||||||
|
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 {
|
||||||
|
schemaId: "gnu-taler-wallet-backup";
|
||||||
|
|
||||||
|
schemaVersion: 1;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Monotonically increasing clock of the wallet,
|
||||||
|
* used to determine causality when merging backups.
|
||||||
|
*/
|
||||||
|
clock: number;
|
||||||
|
|
||||||
|
walletRootPub: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Per-exchange data sorted by exchange master public key.
|
||||||
|
*/
|
||||||
|
exchanges: BackupExchangeData[];
|
||||||
|
|
||||||
|
reserves: ReserveBackupData[];
|
||||||
|
|
||||||
|
coins: BackupCoin[];
|
||||||
|
|
||||||
|
planchets: BackupWithdrawalPlanchet[];
|
||||||
|
|
||||||
|
refreshSessions: BackupRefreshSession[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BackupRefreshSession {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export interface BackupReserve {
|
||||||
|
reservePub: string;
|
||||||
|
reservePriv: string;
|
||||||
|
/**
|
||||||
|
* The exchange base URL.
|
||||||
|
*/
|
||||||
|
exchangeBaseUrl: string;
|
||||||
|
|
||||||
|
bankConfirmUrl?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wire information (as payto URI) for the bank account that
|
||||||
|
* transfered funds for this reserve.
|
||||||
|
*/
|
||||||
|
senderWire?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ReserveBackupData {
|
||||||
|
/**
|
||||||
|
* The reserve public key.
|
||||||
|
*/
|
||||||
|
reservePub: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The reserve private key.
|
||||||
|
*/
|
||||||
|
reservePriv: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The exchange base URL.
|
||||||
|
*/
|
||||||
|
exchangeBaseUrl: string;
|
||||||
|
|
||||||
|
instructedAmount: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wire information (as payto URI) for the bank account that
|
||||||
|
* transfered funds for this reserve.
|
||||||
|
*/
|
||||||
|
senderWire?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BackupExchangeData {
|
||||||
|
exchangeBaseUrl: string;
|
||||||
|
exchangeMasterPub: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ETag for last terms of service download.
|
||||||
|
*/
|
||||||
|
termsOfServiceAcceptedEtag: string | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export interface BackupWithdrawalPlanchet {
|
||||||
|
coinSource: BackupWithdrawCoinSource | BackupTipCoinSource;
|
||||||
|
blindingKey: string;
|
||||||
|
coinPriv: string;
|
||||||
|
coinPub: string;
|
||||||
|
denomPubHash: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base URL that identifies the exchange from which we are getting the
|
||||||
|
* coin.
|
||||||
|
*/
|
||||||
|
exchangeBaseUrl: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export enum BackupCoinSourceType {
|
||||||
|
Withdraw = "withdraw",
|
||||||
|
Refresh = "refresh",
|
||||||
|
Tip = "tip",
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BackupWithdrawCoinSource {
|
||||||
|
type: BackupCoinSourceType.Withdraw;
|
||||||
|
withdrawalGroupId: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Index of the coin in the withdrawal session.
|
||||||
|
*/
|
||||||
|
coinIndex: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reserve public key for the reserve we got this coin from.
|
||||||
|
*/
|
||||||
|
reservePub: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BackupRefreshCoinSource {
|
||||||
|
type: BackupCoinSourceType.Refresh;
|
||||||
|
oldCoinPub: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BackupTipCoinSource {
|
||||||
|
type: BackupCoinSourceType.Tip;
|
||||||
|
walletTipId: string;
|
||||||
|
coinIndex: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type BackupCoinSource =
|
||||||
|
| BackupWithdrawCoinSource
|
||||||
|
| BackupRefreshCoinSource
|
||||||
|
| BackupTipCoinSource;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Coin that has been withdrawn and might have been
|
||||||
|
* (partially) spent.
|
||||||
|
*/
|
||||||
|
export interface BackupCoin {
|
||||||
|
/**
|
||||||
|
* Public key of the coin.
|
||||||
|
*/
|
||||||
|
coinPub: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Private key of the coin.
|
||||||
|
*/
|
||||||
|
coinPriv: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Where did the coin come from (withdrawal/refresh/tip)?
|
||||||
|
* Used for recouping coins.
|
||||||
|
*/
|
||||||
|
coinSource: BackupCoinSource;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Is the coin still fresh
|
||||||
|
*/
|
||||||
|
fresh: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Blinding key used when withdrawing the coin.
|
||||||
|
* Potentionally used again during payback.
|
||||||
|
*/
|
||||||
|
blindingKey: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Amount that's left on the coin.
|
||||||
|
*/
|
||||||
|
currentAmount: BackupAmountString;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base URL that identifies the exchange from which we got the
|
||||||
|
* coin.
|
||||||
|
*/
|
||||||
|
exchangeBaseUrl: string;
|
||||||
|
}
|
@ -31,6 +31,7 @@ import {
|
|||||||
MerchantInfo,
|
MerchantInfo,
|
||||||
Product,
|
Product,
|
||||||
InternationalizedString,
|
InternationalizedString,
|
||||||
|
AmountString,
|
||||||
} from "./talerTypes";
|
} from "./talerTypes";
|
||||||
|
|
||||||
import { Index, Store } from "../util/query";
|
import { Index, Store } from "../util/query";
|
||||||
@ -706,6 +707,10 @@ export enum CoinSourceType {
|
|||||||
|
|
||||||
export interface WithdrawCoinSource {
|
export interface WithdrawCoinSource {
|
||||||
type: CoinSourceType.Withdraw;
|
type: CoinSourceType.Withdraw;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Can be the empty string for orphaned coins.
|
||||||
|
*/
|
||||||
withdrawalGroupId: string;
|
withdrawalGroupId: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -1395,9 +1400,9 @@ export interface PurchaseRecord {
|
|||||||
* Configuration key/value entries to configure
|
* Configuration key/value entries to configure
|
||||||
* the wallet.
|
* the wallet.
|
||||||
*/
|
*/
|
||||||
export interface ConfigRecord {
|
export interface ConfigRecord<T> {
|
||||||
key: string;
|
key: string;
|
||||||
value: any;
|
value: T;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DenominationSelectionInfo {
|
export interface DenominationSelectionInfo {
|
||||||
@ -1531,6 +1536,30 @@ export enum ImportPayloadType {
|
|||||||
CoreSchema = "core-schema",
|
CoreSchema = "core-schema",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface BackupProviderRecord {
|
||||||
|
baseUrl: string;
|
||||||
|
|
||||||
|
supportedProtocolVersion: string;
|
||||||
|
|
||||||
|
annualFee: AmountString;
|
||||||
|
|
||||||
|
storageLimitInMegabytes: number;
|
||||||
|
|
||||||
|
active: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hash of the last backup that we already
|
||||||
|
* merged.
|
||||||
|
*/
|
||||||
|
lastBackupHash?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clock of the last backup that we already
|
||||||
|
* merged.
|
||||||
|
*/
|
||||||
|
lastBackupClock?: number;
|
||||||
|
}
|
||||||
|
|
||||||
class ExchangesStore extends Store<"exchanges", ExchangeRecord> {
|
class ExchangesStore extends Store<"exchanges", ExchangeRecord> {
|
||||||
constructor() {
|
constructor() {
|
||||||
super("exchanges", { keyPath: "baseUrl" });
|
super("exchanges", { keyPath: "baseUrl" });
|
||||||
@ -1609,7 +1638,7 @@ class CurrenciesStore extends Store<"currencies", CurrencyRecord> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class ConfigStore extends Store<"config", ConfigRecord> {
|
class ConfigStore extends Store<"config", ConfigRecord<any>> {
|
||||||
constructor() {
|
constructor() {
|
||||||
super("config", { keyPath: "key" });
|
super("config", { keyPath: "key" });
|
||||||
}
|
}
|
||||||
@ -1690,6 +1719,18 @@ class BankWithdrawUrisStore extends Store<
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
*/
|
||||||
|
class BackupProvidersStore extends Store<
|
||||||
|
"backupProviders",
|
||||||
|
BackupProviderRecord
|
||||||
|
> {
|
||||||
|
constructor() {
|
||||||
|
super("backupProviders", { keyPath: "baseUrl", versionAdded: 3 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The stores and indices for the wallet database.
|
* The stores and indices for the wallet database.
|
||||||
*/
|
*/
|
||||||
@ -1716,4 +1757,5 @@ export const Stores = {
|
|||||||
withdrawalGroups: new WithdrawalGroupsStore(),
|
withdrawalGroups: new WithdrawalGroupsStore(),
|
||||||
planchets: new PlanchetsStore(),
|
planchets: new PlanchetsStore(),
|
||||||
bankWithdrawUris: new BankWithdrawUrisStore(),
|
bankWithdrawUris: new BankWithdrawUrisStore(),
|
||||||
|
backupProviders: new BackupProvidersStore(),
|
||||||
};
|
};
|
||||||
|
@ -1,58 +0,0 @@
|
|||||||
/*
|
|
||||||
This file is part of GNU Taler
|
|
||||||
(C) 2019 Taler Systems S.A.
|
|
||||||
|
|
||||||
GNU Taler is free software; you can redistribute it and/or modify it under the
|
|
||||||
terms of the GNU General Public License as published by the Free Software
|
|
||||||
Foundation; either version 3, or (at your option) any later version.
|
|
||||||
|
|
||||||
GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
|
|
||||||
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
|
||||||
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
|
||||||
|
|
||||||
You should have received a copy of the GNU General Public License along with
|
|
||||||
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Core of the wallet's schema, used for painless export, import
|
|
||||||
* and schema migration.
|
|
||||||
*
|
|
||||||
* If this schema is extended, it must be extended in a completely
|
|
||||||
* backwards-compatible way.
|
|
||||||
*/
|
|
||||||
|
|
||||||
interface CoreCoin {
|
|
||||||
exchangeBaseUrl: string;
|
|
||||||
coinPub: string;
|
|
||||||
coinPriv: string;
|
|
||||||
amountRemaining: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface CorePurchase {
|
|
||||||
noncePub: string;
|
|
||||||
noncePriv: string;
|
|
||||||
paySig: string;
|
|
||||||
contractTerms: any;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface CoreReserve {
|
|
||||||
reservePub: string;
|
|
||||||
reservePriv: string;
|
|
||||||
exchangeBaseUrl: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface SchemaCore {
|
|
||||||
coins: CoreCoin[];
|
|
||||||
purchases: CorePurchase[];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Schema version (of full schema) of wallet that exported the core schema.
|
|
||||||
*/
|
|
||||||
versionExporter: number;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Schema version of the database that has been exported to the core schema
|
|
||||||
*/
|
|
||||||
versionSourceDatabase: number;
|
|
||||||
}
|
|
@ -885,6 +885,15 @@ export const withdrawTestBalanceDefaults = {
|
|||||||
exchangeBaseUrl: "https://exchange.test.taler.net/",
|
exchangeBaseUrl: "https://exchange.test.taler.net/",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Request to the crypto worker to make a sync signature.
|
||||||
|
*/
|
||||||
|
export interface MakeSyncSignatureRequest {
|
||||||
|
accountPriv: string;
|
||||||
|
oldHash: string | undefined;
|
||||||
|
newHash: string;
|
||||||
|
}
|
||||||
|
|
||||||
export const codecForWithdrawTestBalance = (): Codec<
|
export const codecForWithdrawTestBalance = (): Codec<
|
||||||
WithdrawTestBalanceRequest
|
WithdrawTestBalanceRequest
|
||||||
> =>
|
> =>
|
||||||
|
@ -17,6 +17,8 @@
|
|||||||
/**
|
/**
|
||||||
* Helpers for doing XMLHttpRequest-s that are based on ES6 promises.
|
* Helpers for doing XMLHttpRequest-s that are based on ES6 promises.
|
||||||
* Allows for easy mocking for test cases.
|
* Allows for easy mocking for test cases.
|
||||||
|
*
|
||||||
|
* The API is inspired by the HTML5 fetch API.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -47,16 +49,20 @@ export interface HttpResponse {
|
|||||||
headers: Headers;
|
headers: Headers;
|
||||||
json(): Promise<any>;
|
json(): Promise<any>;
|
||||||
text(): Promise<string>;
|
text(): Promise<string>;
|
||||||
|
bytes(): Promise<ArrayBuffer>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface HttpRequestOptions {
|
export interface HttpRequestOptions {
|
||||||
|
method?: "POST" | "PUT" | "GET";
|
||||||
headers?: { [name: string]: string };
|
headers?: { [name: string]: string };
|
||||||
timeout?: Duration;
|
timeout?: Duration;
|
||||||
|
body?: string | ArrayBuffer | ArrayBufferView;
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum HttpResponseStatus {
|
export enum HttpResponseStatus {
|
||||||
Ok = 200,
|
Ok = 200,
|
||||||
Gone = 210,
|
Gone = 210,
|
||||||
|
PaymentRequired = 402,
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -82,6 +88,12 @@ export class Headers {
|
|||||||
this.headerMap.set(normalizedName, value);
|
this.headerMap.set(normalizedName, value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
toJSON(): any {
|
||||||
|
const m: Record<string, string> = {};
|
||||||
|
this.headerMap.forEach((v, k) => m[k] = v);
|
||||||
|
return m;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -104,6 +116,14 @@ export interface HttpRequestLibrary {
|
|||||||
body: any,
|
body: any,
|
||||||
opt?: HttpRequestOptions,
|
opt?: HttpRequestOptions,
|
||||||
): Promise<HttpResponse>;
|
): Promise<HttpResponse>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Make an HTTP POST request with a JSON body.
|
||||||
|
*/
|
||||||
|
fetch(
|
||||||
|
url: string,
|
||||||
|
opt?: HttpRequestOptions,
|
||||||
|
): Promise<HttpResponse>;
|
||||||
}
|
}
|
||||||
|
|
||||||
type TalerErrorResponse = {
|
type TalerErrorResponse = {
|
||||||
|
@ -595,8 +595,8 @@ export class Database {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async put<St extends Store<string, any>>(
|
async put<St extends Store<string, any>>(
|
||||||
store: St extends Store<infer N, infer R> ? Store<N, R> : never,
|
store: St,
|
||||||
value: St extends Store<any, infer R> ? R : never,
|
value: StoreContent<St>,
|
||||||
key?: any,
|
key?: any,
|
||||||
): Promise<any> {
|
): Promise<any> {
|
||||||
const tx = this.db.transaction([store.name], "readwrite");
|
const tx = this.db.transaction([store.name], "readwrite");
|
||||||
|
@ -152,6 +152,7 @@ import {
|
|||||||
testPay,
|
testPay,
|
||||||
} from "./operations/testing";
|
} from "./operations/testing";
|
||||||
import { TalerErrorCode } from ".";
|
import { TalerErrorCode } from ".";
|
||||||
|
import { addBackupProvider, codecForAddBackupProviderRequest, runBackupCycle, exportBackup } from './operations/backup';
|
||||||
|
|
||||||
const builtinCurrencies: CurrencyRecord[] = [
|
const builtinCurrencies: CurrencyRecord[] = [
|
||||||
{
|
{
|
||||||
@ -1074,6 +1075,18 @@ export class Wallet {
|
|||||||
await this.acceptTip(req.walletTipId);
|
await this.acceptTip(req.walletTipId);
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
case "exportBackup": {
|
||||||
|
return exportBackup(this.ws);
|
||||||
|
}
|
||||||
|
case "addBackupProvider": {
|
||||||
|
const req = codecForAddBackupProviderRequest().decode(payload);
|
||||||
|
await addBackupProvider(this.ws, req);
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
case "runBackupCycle": {
|
||||||
|
await runBackupCycle(this.ws);
|
||||||
|
return {};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
throw OperationFailedError.fromCode(
|
throw OperationFailedError.fromCode(
|
||||||
TalerErrorCode.WALLET_CORE_API_OPERATION_UNKNOWN,
|
TalerErrorCode.WALLET_CORE_API_OPERATION_UNKNOWN,
|
||||||
|
@ -34,12 +34,9 @@ const logger = new Logger("browserHttpLib");
|
|||||||
* browser's XMLHttpRequest.
|
* browser's XMLHttpRequest.
|
||||||
*/
|
*/
|
||||||
export class BrowserHttpLib implements HttpRequestLibrary {
|
export class BrowserHttpLib implements HttpRequestLibrary {
|
||||||
private req(
|
fetch(url: string, options?: HttpRequestOptions): Promise<HttpResponse> {
|
||||||
method: string,
|
const method = options?.method ?? "GET";
|
||||||
url: string,
|
let requestBody = options?.body;
|
||||||
requestBody?: any,
|
|
||||||
options?: HttpRequestOptions,
|
|
||||||
): Promise<HttpResponse> {
|
|
||||||
return new Promise<HttpResponse>((resolve, reject) => {
|
return new Promise<HttpResponse>((resolve, reject) => {
|
||||||
const myRequest = new XMLHttpRequest();
|
const myRequest = new XMLHttpRequest();
|
||||||
myRequest.open(method, url);
|
myRequest.open(method, url);
|
||||||
@ -48,7 +45,7 @@ export class BrowserHttpLib implements HttpRequestLibrary {
|
|||||||
myRequest.setRequestHeader(headerName, options.headers[headerName]);
|
myRequest.setRequestHeader(headerName, options.headers[headerName]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
myRequest.setRequestHeader;
|
myRequest.responseType = "arraybuffer";
|
||||||
if (requestBody) {
|
if (requestBody) {
|
||||||
myRequest.send(requestBody);
|
myRequest.send(requestBody);
|
||||||
} else {
|
} else {
|
||||||
@ -130,6 +127,7 @@ export class BrowserHttpLib implements HttpRequestLibrary {
|
|||||||
requestMethod: method,
|
requestMethod: method,
|
||||||
json: makeJson,
|
json: makeJson,
|
||||||
text: async () => myRequest.responseText,
|
text: async () => myRequest.responseText,
|
||||||
|
bytes: async () => myRequest.response,
|
||||||
};
|
};
|
||||||
resolve(resp);
|
resolve(resp);
|
||||||
}
|
}
|
||||||
@ -138,15 +136,22 @@ export class BrowserHttpLib implements HttpRequestLibrary {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get(url: string, opt?: HttpRequestOptions): Promise<HttpResponse> {
|
get(url: string, opt?: HttpRequestOptions): Promise<HttpResponse> {
|
||||||
return this.req("GET", url, undefined, opt);
|
return this.fetch(url, {
|
||||||
|
method: "GET",
|
||||||
|
...opt,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
postJson(
|
postJson(
|
||||||
url: string,
|
url: string,
|
||||||
body: unknown,
|
body: any,
|
||||||
opt?: HttpRequestOptions,
|
opt?: HttpRequestOptions,
|
||||||
): Promise<HttpResponse> {
|
): Promise<HttpResponse> {
|
||||||
return this.req("POST", url, JSON.stringify(body), opt);
|
return this.fetch(url, {
|
||||||
|
method: "POST",
|
||||||
|
body,
|
||||||
|
...opt,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
stop(): void {
|
stop(): void {
|
||||||
|
@ -124,6 +124,7 @@ importers:
|
|||||||
'@types/node': 14.14.7
|
'@types/node': 14.14.7
|
||||||
axios: 0.21.0
|
axios: 0.21.0
|
||||||
big-integer: 1.6.48
|
big-integer: 1.6.48
|
||||||
|
fflate: 0.3.10
|
||||||
idb-bridge: 'link:../idb-bridge'
|
idb-bridge: 'link:../idb-bridge'
|
||||||
source-map-support: 0.5.19
|
source-map-support: 0.5.19
|
||||||
tslib: 2.0.3
|
tslib: 2.0.3
|
||||||
@ -167,6 +168,7 @@ importers:
|
|||||||
eslint-plugin-react: ^7.21.5
|
eslint-plugin-react: ^7.21.5
|
||||||
eslint-plugin-react-hooks: ^4.2.0
|
eslint-plugin-react-hooks: ^4.2.0
|
||||||
esm: ^3.2.25
|
esm: ^3.2.25
|
||||||
|
fflate: ^0.3.10
|
||||||
idb-bridge: 'workspace:*'
|
idb-bridge: 'workspace:*'
|
||||||
jed: ^1.1.1
|
jed: ^1.1.1
|
||||||
nyc: ^15.1.0
|
nyc: ^15.1.0
|
||||||
@ -2338,6 +2340,10 @@ packages:
|
|||||||
dev: true
|
dev: true
|
||||||
resolution:
|
resolution:
|
||||||
integrity: sha512-i7FVWL8HhVY+CTkwFxkN2mk3h+787ixS5S63eb78diVRc1MCssarHq3W5cj0av7YDSwmaV928RNag+U1etRQ7w==
|
integrity: sha512-i7FVWL8HhVY+CTkwFxkN2mk3h+787ixS5S63eb78diVRc1MCssarHq3W5cj0av7YDSwmaV928RNag+U1etRQ7w==
|
||||||
|
/fflate/0.3.10:
|
||||||
|
dev: false
|
||||||
|
resolution:
|
||||||
|
integrity: sha512-s5j69APkUPPbzdI20Ix4pPtQP+1Qi58YcFRpE7aO/P1kEywUYjbl2RjZRVEMdnySO9pr4MB0BHPbxkiahrtD/Q==
|
||||||
/figures/3.2.0:
|
/figures/3.2.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
escape-string-regexp: 1.0.5
|
escape-string-regexp: 1.0.5
|
||||||
|
Loading…
Reference in New Issue
Block a user