gnunet-svn
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

[taler-wallet-core] branch master updated (0828e65f -> 1ad064a2)


From: gnunet
Subject: [taler-wallet-core] branch master updated (0828e65f -> 1ad064a2)
Date: Wed, 02 Dec 2020 21:56:22 +0100

This is an automated email from the git hooks/post-receive script.

dold pushed a change to branch master
in repository wallet-core.

    from 0828e65f fix static types
     new 89f1a281 backup WIP
     new 92885fa1 run until done at end of test
     new 1ad064a2 regression tests

The 3 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 packages/taler-integrationtests/src/harness.ts     |  13 +-
 .../taler-integrationtests/src/test-payment.ts     |   2 +
 packages/taler-wallet-android/src/index.ts         |   7 +
 packages/taler-wallet-core/package.json            |   1 +
 .../src/crypto/talerCrypto-test.ts                 |  91 +++++
 .../src/crypto/workers/cryptoApi.ts                |   5 +
 .../src/crypto/workers/cryptoImplementation.ts     |  18 +
 packages/taler-wallet-core/src/db.ts               |  21 +-
 .../taler-wallet-core/src/headless/NodeHttpLib.ts  |  56 +--
 .../taler-wallet-core/src/operations/backup.ts     | 402 +++++++++++++++++++++
 .../taler-wallet-core/src/types/backupTypes.ts     | 215 +++++++++++
 packages/taler-wallet-core/src/types/dbTypes.ts    |  48 ++-
 packages/taler-wallet-core/src/types/schemacore.ts |  58 ---
 .../taler-wallet-core/src/types/walletTypes.ts     |   9 +
 packages/taler-wallet-core/src/util/http.ts        |  20 +
 packages/taler-wallet-core/src/util/query.ts       |   4 +-
 packages/taler-wallet-core/src/wallet.ts           |  13 +
 .../src/browserHttpLib.ts                          |  25 +-
 pnpm-lock.yaml                                     |   6 +
 19 files changed, 906 insertions(+), 108 deletions(-)
 create mode 100644 packages/taler-wallet-core/src/operations/backup.ts
 create mode 100644 packages/taler-wallet-core/src/types/backupTypes.ts
 delete mode 100644 packages/taler-wallet-core/src/types/schemacore.ts

diff --git a/packages/taler-integrationtests/src/harness.ts 
b/packages/taler-integrationtests/src/harness.ts
index a08a8a02..2ac4c3d0 100644
--- a/packages/taler-integrationtests/src/harness.ts
+++ b/packages/taler-integrationtests/src/harness.ts
@@ -1515,12 +1515,17 @@ export class WalletCli {
   }
 
   async runPending(): Promise<void> {
-    await sh(
+    await runCommand(
       this.globalTestState,
       `wallet-${this.name}`,
-      `taler-wallet-cli ${this.timetravelArg ?? ""} --no-throttle --wallet-db 
${
-        this.dbfile
-      } run-pending`,
+      "taler-wallet-cli",
+      [
+        "--no-throttle",
+        ...this.timetravelArgArr,
+        "--wallet-db",
+        this.dbfile,
+        "run-pending",
+      ],
     );
   }
 
diff --git a/packages/taler-integrationtests/src/test-payment.ts 
b/packages/taler-integrationtests/src/test-payment.ts
index c82770d7..8a1240da 100644
--- a/packages/taler-integrationtests/src/test-payment.ts
+++ b/packages/taler-integrationtests/src/test-payment.ts
@@ -48,4 +48,6 @@ runTest(async (t: GlobalTestState) => {
   };
 
   await makeTestPayment(t, { wallet, merchant, order });
+
+  await wallet.runUntilDone();
 });
diff --git a/packages/taler-wallet-android/src/index.ts 
b/packages/taler-wallet-android/src/index.ts
index 07d15d58..bfda8ab7 100644
--- a/packages/taler-wallet-android/src/index.ts
+++ b/packages/taler-wallet-android/src/index.ts
@@ -38,6 +38,8 @@ import {
   WalletNotification,
   WALLET_EXCHANGE_PROTOCOL_VERSION,
   WALLET_MERCHANT_PROTOCOL_VERSION,
+  bytesToString,
+  stringToBytes,
 } from "taler-wallet-core";
 
 import fs from "fs";
@@ -57,6 +59,10 @@ export class AndroidHttpLib implements HttpRequestLibrary {
 
   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> {
     if (this.useNfcTunnel) {
       const myId = this.requestId++;
@@ -120,6 +126,7 @@ export class AndroidHttpLib implements HttpRequestLibrary {
         requestMethod: "FIXME",
         json: async () => JSON.parse(msg.responseText),
         text: async () => msg.responseText,
+        bytes: async () => { throw Error("bytes() not supported for tunnel 
response") },
       };
       p.resolve(resp);
     } else {
diff --git a/packages/taler-wallet-core/package.json 
b/packages/taler-wallet-core/package.json
index 72f9f379..62e4c898 100644
--- a/packages/taler-wallet-core/package.json
+++ b/packages/taler-wallet-core/package.json
@@ -58,6 +58,7 @@
     "@types/node": "^14.14.7",
     "axios": "^0.21.0",
     "big-integer": "^1.6.48",
+    "fflate": "^0.3.10",
     "idb-bridge": "workspace:*",
     "source-map-support": "^0.5.19",
     "tslib": "^2.0.3"
diff --git a/packages/taler-wallet-core/src/crypto/talerCrypto-test.ts 
b/packages/taler-wallet-core/src/crypto/talerCrypto-test.ts
index b273b018..a8db6c44 100644
--- a/packages/taler-wallet-core/src/crypto/talerCrypto-test.ts
+++ b/packages/taler-wallet-core/src/crypto/talerCrypto-test.ts
@@ -195,3 +195,94 @@ test("incremental hashing #2", (t) => {
   t.deepEqual(encodeCrock(h1), encodeCrock(h3));
   t.deepEqual(encodeCrock(h1), encodeCrock(h2));
 });
+
+test("taler-exchange-tvg eddsa_ecdh #2", (t) => {
+  const priv_ecdhe = "W5FH9CFS3YPGSCV200GE8TH6MAACPKKGEG2A5JTFSD1HZ5RYT7Q0";
+  const pub_ecdhe = "FER9CRS2T8783TAANPZ134R704773XT0ZT1XPFXZJ9D4QX67ZN00";
+  const priv_eddsa = "MSZ1TBKC6YQ19ZFP3NTJVKWNVGFP35BBRW8FTAQJ9Z2B96VC9P4G";
+  const pub_eddsa = "Y7MKG85PBT8ZEGHF08JBVZXEV70TS0PY5Y2CMEN1WXEDN63KP1A0";
+  const key_material =
+    
"G6RA58N61K7MT3WA13Q7VRTE1FQS6H43RX9HK8Z5TGAB61601GEGX51JRHHQMNKNM2R9AVC1STSGQDRHGKWVYP584YGBCTVMMJYQF30";
+
+  const myEcdhePub = ecdheGetPublic(decodeCrock(priv_ecdhe));
+  t.deepEqual(encodeCrock(myEcdhePub), pub_ecdhe);
+
+  const myEddsaPub = eddsaGetPublic(decodeCrock(priv_eddsa));
+  t.deepEqual(encodeCrock(myEddsaPub), pub_eddsa);
+
+  const myKm1 = keyExchangeEddsaEcdhe(
+    decodeCrock(priv_eddsa),
+    decodeCrock(pub_ecdhe),
+  );
+  t.deepEqual(encodeCrock(myKm1), key_material);
+
+  const myKm2 = keyExchangeEcdheEddsa(
+    decodeCrock(priv_ecdhe),
+    decodeCrock(pub_eddsa),
+  );
+  t.deepEqual(encodeCrock(myKm2), key_material);
+});
+
+test("eddsa_ecdh regression #1 (wallet)", (t) => {
+  const myKm2 = keyExchangeEcdheEddsa(
+    decodeCrock("95P1ZRBAFZNXJ1DVZ3EANBM5KZ99AB5DNNY28CCVNWM902E9B7H0"),
+    decodeCrock("MWE4WM4D3FXRQ66X785CHX44XCN0ZDT2X3ND9CTFSVFW2GPKK2VG"),
+  );
+  t.deepEqual(
+    encodeCrock(myKm2),
+    
"5R9JBB5HBHC05XHXXFQVVN9ZKSXY955QZ2TP191793PCJ2SBAES3ZS3S4ZP2711XH81YN07WHCADX8B65V2DWEP1YS48SSXXRE03JKR",
+  );
+});
+
+test("blinding regression (exchange log)", (t) => {
+  const messageHash =
+    
"7T0M81R8NVB13SYYBDPY0XTP1ZMG091WM6YWFZE8E12DCDD41J242K75EH0KYQRMQJE278BCH8AYE7B89QZYGG9WBN8RHY8TVVDX9MR";
+  const rsaPublicKey =
+    
"020000XXM4C51WSNR83AYQG2D0HH5Y8D94R7WRZ7T6JG1TSMHF449TR0W4E2V68TZDWHWKW05T6Z132DEQ8K38EPHVSD77PJF8C9GSQ482JK2CAQ2PRYX1BQTY4Z1EYZ0Y6VXBY6D27RGCREM3G2X7FXMZJ1ADMVPPFYCSNJYWW2WP4NSVRJ5787K06AGD9F90RT7MKMPPQK0Q69PQHG2001";
+  const bks = "VAXMY73TB736XCAM3PJR51E52KH2H3D20RGBWR59X25RRNBKV0N0";
+  const bm =
+    
"1BQH71QPF60K9TPE5TNNHDSEQ0N6AK9SEGXXXTW4HHWMR58PF8CEYN8C6NHZ8H19A3A7GRTJCGHHYJJPKX3RQ1SG2MDHWZFP98CDC8TTGZ3TN4FZG7J5MYEJ8239TV1BPQZSFEBYXE7ZJQEBEGW0SWJ6QHX2WM5JFS2ZDW1NXZ720JJVKD0MHE2Y0M3BGQE0SHVV5YMPZ6EME";
+
+  const myBm = rsaBlind(
+    decodeCrock(messageHash),
+    decodeCrock(bks),
+    decodeCrock(rsaPublicKey),
+  );
+  t.deepEqual(encodeCrock(myBm), bm);
+});
+
+
+test("taler-exchange-tvg blind signing (post-regression)", (t) => {
+  const messageHash =
+    
"H9RYMXXSRMF2PH8ANYJ0FPW0NYV51120QNJ8HBVTFQ0EN6MVMYMZ5M3KQFDAVTN1S3YFZT42JJ4YS77DAWXHSEC72Q763E7QY0VT8TR";
+  const rsaPublicKey =
+    
"040000YFQZJJMK4EJKC9KHQWNMX4E9HHJQKXNFRGFTTW9AN72EJGY55EKCQJ34D8X1Y65DEE5N0D50E1NKFG51V2JJH3DKBZTRG4MJQP1647QDR5FRAM2848013FAW813CNT40EEN9JJM205XSMY525E2NZ9P9RQS1N7D1T05ZNEPXCKG2YPNF0SCNFSRJVJG0WS36BJJN9WH62H8MPBVHJQXXH5HN1FKDZ11GN0KCY9ZX1BRM3HTN66529MMB1MVCDFQKTBH18NZM430XHZFRHR3RQ5A82HNG8649VH686YHA1FNJJAHEZ1P1RCFY93GCJ31W2BJ56N2NYZJJ4CFNAP503D3VYE5HFWFB2XVMW7Q3XXM9KX0PGZMZBN2BRDKNCEYPXPQTYFSPFB1N36KJ7EHA40R8YQ04002";
+  const bks = "D76VVVQ678V75M07K07MGN11Z3DPNFRTEEGPSSN3XMEJKQ6WM0F0";
+  const bm =
+    
"G9HCGGFNAPAQDCA41FA80VEQME90HDSQ4FZ7BV3A3RNYJS3C9BTM843E07FMH1V5M4Z8DHJQ8Q0BFH8RNBE08QQX1MB67D02ZRNVTJKGSKYMTPSQ4P5G4JJ36EVCQGSJ0GHG20XC93XJQC4JHJ1S0HN5X14VRMMTFXPHM5ZMNZZB2T21QY21CKHVBV907NH9P5XDYJXZQTV2XK8XNMQXT1Q3NCGEBVPQB2WF85SFSWZ0PS1ENHY5CGTKXE8K5NCAEAADQKGZCWJH2E2M7HW901CP863RV4PRA7HPHC29FWZ5EE8M3SKNY9V79K8CGK83WY6A97EAK98597FS10PTHFJD7NSCDM6EF9WAV70611GAQGDEDVAGZ7V48KWZ8FNC0WWRACBKZT0WPA3GPYMRZ9X8K8";
+  const bs =
+    
"PMTZ3MWWS4XKFV308HX4PR3NKTJF7TTGDXMR7V4VB42JZV58GT18SB1A1WXKJ7VTK9E92F4FWCEMMA9NTR9EQPJX7G91CBS3N7J9746BF301G8XSQ0M459RGKD5ZX95S5CAG56E61Y29X0HJWB9NC3MT19946NGVZ3027H0MMS5YPZ5HBBD6BJV7BW273YEBD6Q0BSY0WSE8YKG6B4F9KG295ZNTV0GV6TSDQPT4W32F4A1Z1KDVT83XHT68M0HWGSJ9J3MWR2205H2WKRRCAKX87HSWK1HSEZ1EQB5NE998P16MRX8KJWGZFM2ZHNEV20YREP1W4BKETD84JPT050QDS6VJRMNMK4MBF5BCCPCYC12H8F9WD4Z05F0WMMTH7P4Y5FG8QW3N0K732G9K2MS00R";
+  const sig =
+    
"SH9V61328M82B0VSSS760922R2Z79WYD6AZRVX0QYS6XYMBB9T8A228G41SK3VZCT826SC489TKMX7G4J6XP3P9VKYDPERP0QYZ0ZH1W4PGS9FARF3A8QHHHRAEQPJYWZM6NW1T7HXAWNK2R256YRJVF703XB0VJT0CJT8PT3ZBM73E584BH8PXNBJ14DZ8K4JGGNQN7WQC2HANX9G452GPVEC898KY6K1Y8D2P6KCHTBHFHVT35QM9NKNERR068QMSMT0FF546SGVFKBCA3SKHWYP94Y845RSR08KFV62DHXXM7EBD664Z7S7RCSD97DB7MHKZ3EXPYHHSF2FZW6NG14GJ25B1ZH51KAB9DKHWYR3S1XTCMXWGJT572EFCE9T90F5R54Z6RDXPJS7FDBWQ6TR";
+
+  const myBm = rsaBlind(
+    decodeCrock(messageHash),
+    decodeCrock(bks),
+    decodeCrock(rsaPublicKey),
+  );
+  t.deepEqual(encodeCrock(myBm), bm);
+
+  const mySig = rsaUnblind(
+    decodeCrock(bs),
+    decodeCrock(rsaPublicKey),
+    decodeCrock(bks),
+  );
+  t.deepEqual(encodeCrock(mySig), sig);
+
+  const v = rsaVerify(
+    decodeCrock(messageHash),
+    decodeCrock(sig),
+    decodeCrock(rsaPublicKey),
+  );
+  t.true(v);
+});
\ No newline at end of file
diff --git a/packages/taler-wallet-core/src/crypto/workers/cryptoApi.ts 
b/packages/taler-wallet-core/src/crypto/workers/cryptoApi.ts
index 286de5a1..29f3b02b 100644
--- a/packages/taler-wallet-core/src/crypto/workers/cryptoApi.ts
+++ b/packages/taler-wallet-core/src/crypto/workers/cryptoApi.ts
@@ -42,6 +42,7 @@ import {
   PlanchetCreationResult,
   PlanchetCreationRequest,
   DepositInfo,
+  MakeSyncSignatureRequest,
 } from "../../types/walletTypes";
 
 import * as timer from "../../util/timer";
@@ -455,4 +456,8 @@ export class CryptoApi {
   benchmark(repetitions: number): Promise<BenchmarkResult> {
     return this.doRpc<BenchmarkResult>("benchmark", 1, repetitions);
   }
+
+  makeSyncSignature(req: MakeSyncSignatureRequest): Promise<string> {
+    return this.doRpc<string>("makeSyncSignature", 3, req);
+  }
 }
diff --git 
a/packages/taler-wallet-core/src/crypto/workers/cryptoImplementation.ts 
b/packages/taler-wallet-core/src/crypto/workers/cryptoImplementation.ts
index 46ac7c8a..41836fdf 100644
--- a/packages/taler-wallet-core/src/crypto/workers/cryptoImplementation.ts
+++ b/packages/taler-wallet-core/src/crypto/workers/cryptoImplementation.ts
@@ -43,6 +43,7 @@ import {
   PlanchetCreationResult,
   PlanchetCreationRequest,
   DepositInfo,
+  MakeSyncSignatureRequest,
 } from "../../types/walletTypes";
 import { AmountJson, Amounts } from "../../util/amounts";
 import * as timer from "../../util/timer";
@@ -85,6 +86,7 @@ enum SignaturePurpose {
   WALLET_COIN_LINK = 1204,
   EXCHANGE_CONFIRM_RECOUP = 1039,
   EXCHANGE_CONFIRM_RECOUP_REFRESH = 1041,
+  SYNC_BACKUP_UPLOAD = 1450,
 }
 
 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);
+  }
 }
diff --git a/packages/taler-wallet-core/src/db.ts 
b/packages/taler-wallet-core/src/db.ts
index ecc5509d..6f5b6b45 100644
--- a/packages/taler-wallet-core/src/db.ts
+++ b/packages/taler-wallet-core/src/db.ts
@@ -1,7 +1,12 @@
 import { Stores } from "./types/dbTypes";
 import { openDatabase, Database, Store, Index } from "./util/query";
-import { IDBFactory, IDBDatabase, IDBObjectStore, IDBTransaction } from 
"idb-bridge";
-import { Logger } from './util/logging';
+import {
+  IDBFactory,
+  IDBDatabase,
+  IDBObjectStore,
+  IDBTransaction,
+} from "idb-bridge";
+import { Logger } from "./util/logging";
 
 /**
  * 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
  * are added.
  */
-export const WALLET_DB_MINOR_VERSION = 2;
+export const WALLET_DB_MINOR_VERSION = 3;
 
 const logger = new Logger("db.ts");
 
@@ -43,7 +48,9 @@ export function openTalerDatabase(
           const s = db.createObjectStore(si.name, si.storeParams);
           for (const indexName in si as any) {
             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);
             }
           }
@@ -59,7 +66,8 @@ export function openTalerDatabase(
       if ((Stores as any)[n] instanceof Store) {
         const si: Store<string, any> = (Stores as any)[n];
         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);
         } else {
           s = upgradeTransaction.objectStore(si.name);
@@ -67,7 +75,8 @@ export function openTalerDatabase(
         for (const indexName in si as any) {
           if ((si as any)[indexName] instanceof Index) {
             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);
             }
           }
diff --git a/packages/taler-wallet-core/src/headless/NodeHttpLib.ts 
b/packages/taler-wallet-core/src/headless/NodeHttpLib.ts
index ed4e0e1e..5eefb24f 100644
--- a/packages/taler-wallet-core/src/headless/NodeHttpLib.ts
+++ b/packages/taler-wallet-core/src/headless/NodeHttpLib.ts
@@ -31,6 +31,7 @@ import { OperationFailedError, makeErrorDetails } from 
"../operations/errors";
 import { TalerErrorCode } from "../TalerErrorCode";
 import { URL } from "../util/url";
 import { Logger } from "../util/logging";
+import { bytesToString } from '../crypto/talerCrypto';
 
 const logger = new Logger("NodeHttpLib.ts");
 
@@ -48,12 +49,10 @@ export class NodeHttpLib implements HttpRequestLibrary {
     this.throttlingEnabled = enabled;
   }
 
-  private async req(
-    method: "POST" | "GET",
-    url: string,
-    body: any,
-    opt?: HttpRequestOptions,
-  ): Promise<HttpResponse> {
+  async fetch(url: string, opt?: HttpRequestOptions): Promise<HttpResponse> {
+    const method = opt?.method ?? "GET";
+    let body = opt?.body;
+
     const parsedUrl = new URL(url);
     if (this.throttlingEnabled && this.throttle.applyThrottle(url)) {
       throw OperationFailedError.fromCode(
@@ -75,7 +74,7 @@ export class NodeHttpLib implements HttpRequestLibrary {
       resp = await Axios({
         method,
         url: url,
-        responseType: "text",
+        responseType: "arraybuffer",
         headers: opt?.headers,
         validateStatus: () => true,
         transformResponse: (x) => x,
@@ -93,26 +92,18 @@ export class NodeHttpLib implements HttpRequestLibrary {
       );
     }
 
-    const respText = resp.data;
-    if (typeof respText !== "string") {
-      throw new OperationFailedError(
-        makeErrorDetails(
-          TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE,
-          "unexpected response type",
-          {
-            httpStatusCode: resp.status,
-            requestUrl: url,
-            requestMethod: method,
-          },
-        ),
-      );
+    const makeText = async(): Promise<string> => {
+      const respText = new Uint8Array(resp.data);
+      return bytesToString(respText);
     }
+
     const makeJson = async (): Promise<any> => {
       let responseJson;
+      const respText = await makeText();
       try {
         responseJson = JSON.parse(respText);
       } catch (e) {
-        logger.trace(`invalid json: '${respText}'`);
+        logger.trace(`invalid json: '${resp.data}'`);
         throw new OperationFailedError(
           makeErrorDetails(
             TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE,
@@ -141,6 +132,13 @@ export class NodeHttpLib implements HttpRequestLibrary {
       }
       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();
     for (const hn of Object.keys(resp.headers)) {
       headers.set(hn, resp.headers[hn]);
@@ -150,13 +148,17 @@ export class NodeHttpLib implements HttpRequestLibrary {
       requestMethod: method,
       headers,
       status: resp.status,
-      text: async () => resp.data,
+      text: makeText,
       json: makeJson,
+      bytes: makeBytes,
     };
-  }
 
+  }
   async get(url: string, opt?: HttpRequestOptions): Promise<HttpResponse> {
-    return this.req("GET", url, undefined, opt);
+    return this.fetch(url, {
+      method: "GET",
+      ...opt,
+    });
   }
 
   async postJson(
@@ -164,6 +166,10 @@ export class NodeHttpLib implements HttpRequestLibrary {
     body: any,
     opt?: HttpRequestOptions,
   ): Promise<HttpResponse> {
-    return this.req("POST", url, body, opt);
+    return this.fetch(url, {
+      method: "POST",
+      body,
+      ...opt,
+    });
   }
 }
diff --git a/packages/taler-wallet-core/src/operations/backup.ts 
b/packages/taler-wallet-core/src/operations/backup.ts
new file mode 100644
index 00000000..dbcb3337
--- /dev/null
+++ b/packages/taler-wallet-core/src/operations/backup.ts
@@ -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");
+}
diff --git a/packages/taler-wallet-core/src/types/backupTypes.ts 
b/packages/taler-wallet-core/src/types/backupTypes.ts
new file mode 100644
index 00000000..72d0486b
--- /dev/null
+++ b/packages/taler-wallet-core/src/types/backupTypes.ts
@@ -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;
+}
diff --git a/packages/taler-wallet-core/src/types/dbTypes.ts 
b/packages/taler-wallet-core/src/types/dbTypes.ts
index 349713eb..26400dd3 100644
--- a/packages/taler-wallet-core/src/types/dbTypes.ts
+++ b/packages/taler-wallet-core/src/types/dbTypes.ts
@@ -31,6 +31,7 @@ import {
   MerchantInfo,
   Product,
   InternationalizedString,
+  AmountString,
 } from "./talerTypes";
 
 import { Index, Store } from "../util/query";
@@ -706,6 +707,10 @@ export enum CoinSourceType {
 
 export interface WithdrawCoinSource {
   type: CoinSourceType.Withdraw;
+
+  /**
+   * Can be the empty string for orphaned coins.
+   */
   withdrawalGroupId: string;
 
   /**
@@ -1395,9 +1400,9 @@ export interface PurchaseRecord {
  * Configuration key/value entries to configure
  * the wallet.
  */
-export interface ConfigRecord {
+export interface ConfigRecord<T> {
   key: string;
-  value: any;
+  value: T;
 }
 
 export interface DenominationSelectionInfo {
@@ -1531,6 +1536,30 @@ export enum ImportPayloadType {
   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> {
   constructor() {
     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() {
     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.
  */
@@ -1716,4 +1757,5 @@ export const Stores = {
   withdrawalGroups: new WithdrawalGroupsStore(),
   planchets: new PlanchetsStore(),
   bankWithdrawUris: new BankWithdrawUrisStore(),
+  backupProviders: new BackupProvidersStore(),
 };
diff --git a/packages/taler-wallet-core/src/types/schemacore.ts 
b/packages/taler-wallet-core/src/types/schemacore.ts
deleted file mode 100644
index 820f68d1..00000000
--- a/packages/taler-wallet-core/src/types/schemacore.ts
+++ /dev/null
@@ -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;
-}
diff --git a/packages/taler-wallet-core/src/types/walletTypes.ts 
b/packages/taler-wallet-core/src/types/walletTypes.ts
index 7940497a..ab7d3b4d 100644
--- a/packages/taler-wallet-core/src/types/walletTypes.ts
+++ b/packages/taler-wallet-core/src/types/walletTypes.ts
@@ -885,6 +885,15 @@ export const withdrawTestBalanceDefaults = {
   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<
   WithdrawTestBalanceRequest
 > =>
diff --git a/packages/taler-wallet-core/src/util/http.ts 
b/packages/taler-wallet-core/src/util/http.ts
index 1a2459f7..1ec9c2f5 100644
--- a/packages/taler-wallet-core/src/util/http.ts
+++ b/packages/taler-wallet-core/src/util/http.ts
@@ -17,6 +17,8 @@
 /**
  * Helpers for doing XMLHttpRequest-s that are based on ES6 promises.
  * 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;
   json(): Promise<any>;
   text(): Promise<string>;
+  bytes(): Promise<ArrayBuffer>;
 }
 
 export interface HttpRequestOptions {
+  method?: "POST" | "PUT" | "GET";
   headers?: { [name: string]: string };
   timeout?: Duration;
+  body?: string | ArrayBuffer | ArrayBufferView;
 }
 
 export enum HttpResponseStatus {
   Ok = 200,
   Gone = 210,
+  PaymentRequired = 402,
 }
 
 /**
@@ -82,6 +88,12 @@ export class Headers {
       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,
     opt?: HttpRequestOptions,
   ): Promise<HttpResponse>;
+
+  /**
+   * Make an HTTP POST request with a JSON body.
+   */
+  fetch(
+    url: string,
+    opt?: HttpRequestOptions,
+  ): Promise<HttpResponse>;
 }
 
 type TalerErrorResponse = {
diff --git a/packages/taler-wallet-core/src/util/query.ts 
b/packages/taler-wallet-core/src/util/query.ts
index 08572fbd..beb14cad 100644
--- a/packages/taler-wallet-core/src/util/query.ts
+++ b/packages/taler-wallet-core/src/util/query.ts
@@ -595,8 +595,8 @@ export class Database {
   }
 
   async put<St extends Store<string, any>>(
-    store: St extends Store<infer N, infer R> ? Store<N, R> : never,
-    value: St extends Store<any, infer R> ? R : never,
+    store: St,
+    value: StoreContent<St>,
     key?: any,
   ): Promise<any> {
     const tx = this.db.transaction([store.name], "readwrite");
diff --git a/packages/taler-wallet-core/src/wallet.ts 
b/packages/taler-wallet-core/src/wallet.ts
index 1140a13c..4491a167 100644
--- a/packages/taler-wallet-core/src/wallet.ts
+++ b/packages/taler-wallet-core/src/wallet.ts
@@ -152,6 +152,7 @@ import {
   testPay,
 } from "./operations/testing";
 import { TalerErrorCode } from ".";
+import { addBackupProvider, codecForAddBackupProviderRequest, runBackupCycle, 
exportBackup } from './operations/backup';
 
 const builtinCurrencies: CurrencyRecord[] = [
   {
@@ -1074,6 +1075,18 @@ export class Wallet {
         await this.acceptTip(req.walletTipId);
         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(
       TalerErrorCode.WALLET_CORE_API_OPERATION_UNKNOWN,
diff --git a/packages/taler-wallet-webextension/src/browserHttpLib.ts 
b/packages/taler-wallet-webextension/src/browserHttpLib.ts
index 96484bc9..bfc85563 100644
--- a/packages/taler-wallet-webextension/src/browserHttpLib.ts
+++ b/packages/taler-wallet-webextension/src/browserHttpLib.ts
@@ -34,12 +34,9 @@ const logger = new Logger("browserHttpLib");
  * browser's XMLHttpRequest.
  */
 export class BrowserHttpLib implements HttpRequestLibrary {
-  private req(
-    method: string,
-    url: string,
-    requestBody?: any,
-    options?: HttpRequestOptions,
-  ): Promise<HttpResponse> {
+  fetch(url: string, options?: HttpRequestOptions): Promise<HttpResponse> {
+    const method = options?.method ?? "GET";
+    let requestBody = options?.body;
     return new Promise<HttpResponse>((resolve, reject) => {
       const myRequest = new XMLHttpRequest();
       myRequest.open(method, url);
@@ -48,7 +45,7 @@ export class BrowserHttpLib implements HttpRequestLibrary {
           myRequest.setRequestHeader(headerName, options.headers[headerName]);
         }
       }
-      myRequest.setRequestHeader;
+      myRequest.responseType = "arraybuffer";
       if (requestBody) {
         myRequest.send(requestBody);
       } else {
@@ -130,6 +127,7 @@ export class BrowserHttpLib implements HttpRequestLibrary {
             requestMethod: method,
             json: makeJson,
             text: async () => myRequest.responseText,
+            bytes: async () => myRequest.response,
           };
           resolve(resp);
         }
@@ -138,15 +136,22 @@ export class BrowserHttpLib implements HttpRequestLibrary 
{
   }
 
   get(url: string, opt?: HttpRequestOptions): Promise<HttpResponse> {
-    return this.req("GET", url, undefined, opt);
+    return this.fetch(url, {
+      method: "GET",
+      ...opt,
+    });
   }
 
   postJson(
     url: string,
-    body: unknown,
+    body: any,
     opt?: HttpRequestOptions,
   ): Promise<HttpResponse> {
-    return this.req("POST", url, JSON.stringify(body), opt);
+    return this.fetch(url, {
+      method: "POST",
+      body,
+      ...opt,
+    });
   }
 
   stop(): void {
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index e233d539..a81089d8 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -124,6 +124,7 @@ importers:
       '@types/node': 14.14.7
       axios: 0.21.0
       big-integer: 1.6.48
+      fflate: 0.3.10
       idb-bridge: 'link:../idb-bridge'
       source-map-support: 0.5.19
       tslib: 2.0.3
@@ -167,6 +168,7 @@ importers:
       eslint-plugin-react: ^7.21.5
       eslint-plugin-react-hooks: ^4.2.0
       esm: ^3.2.25
+      fflate: ^0.3.10
       idb-bridge: 'workspace:*'
       jed: ^1.1.1
       nyc: ^15.1.0
@@ -2338,6 +2340,10 @@ packages:
     dev: true
     resolution:
       integrity: 
sha512-i7FVWL8HhVY+CTkwFxkN2mk3h+787ixS5S63eb78diVRc1MCssarHq3W5cj0av7YDSwmaV928RNag+U1etRQ7w==
+  /fflate/0.3.10:
+    dev: false
+    resolution:
+      integrity: 
sha512-s5j69APkUPPbzdI20Ix4pPtQP+1Qi58YcFRpE7aO/P1kEywUYjbl2RjZRVEMdnySO9pr4MB0BHPbxkiahrtD/Q==
   /figures/3.2.0:
     dependencies:
       escape-string-regexp: 1.0.5

-- 
To stop receiving notification emails like this one, please contact
gnunet@gnunet.org.



reply via email to

[Prev in Thread] Current Thread [Next in Thread]