wallet robustness WIP

This commit is contained in:
Florian Dold 2019-11-30 00:36:20 +01:00
parent 809fa18644
commit aaf7e1338d
No known key found for this signature in database
GPG Key ID: D2E4F00F29D02A4B
20 changed files with 1075 additions and 431 deletions

View File

@ -60,8 +60,6 @@ const paths = {
dist: [ dist: [
"dist/*-bundle.js", "dist/*-bundle.js",
"dist/*-bundle.js.map", "dist/*-bundle.js.map",
"emscripten/taler-emscripten-lib.js",
"emscripten/taler-emscripten-lib.wasm",
"img/icon.png", "img/icon.png",
"img/logo.png", "img/logo.png",
"src/webex/**/*.{js,css,html}", "src/webex/**/*.{js,css,html}",
@ -149,7 +147,7 @@ function dist_prod() {
} }
function compile_prod(callback) { function compile_prod(callback) {
let config = require("./webpack.config.js")({ prod: true }); let config = require("./webpack.config.js")({ mode: "production" });
webpack(config, function(err, stats) { webpack(config, function(err, stats) {
if (err) { if (err) {
throw new gutil.PluginError("webpack", err); throw new gutil.PluginError("webpack", err);

View File

@ -64,7 +64,7 @@
"@types/urijs": "^1.19.3", "@types/urijs": "^1.19.3",
"axios": "^0.19.0", "axios": "^0.19.0",
"big-integer": "^1.6.48", "big-integer": "^1.6.48",
"idb-bridge": "^0.0.14", "idb-bridge": "^0.0.15",
"qrcode-generator": "^1.4.3", "qrcode-generator": "^1.4.3",
"source-map-support": "^0.5.12", "source-map-support": "^0.5.12",
"urijs": "^1.18.10" "urijs": "^1.18.10"

View File

@ -27,7 +27,7 @@ import { AmountJson } from "../amounts";
import { import {
CoinRecord, CoinRecord,
DenominationRecord, DenominationRecord,
PreCoinRecord, PlanchetRecord,
RefreshSessionRecord, RefreshSessionRecord,
ReserveRecord, ReserveRecord,
TipPlanchet, TipPlanchet,
@ -38,7 +38,7 @@ import { CryptoWorker } from "./cryptoWorker";
import { ContractTerms, PaybackRequest } from "../talerTypes"; import { ContractTerms, PaybackRequest } from "../talerTypes";
import { BenchmarkResult, CoinWithDenom, PayCoinInfo } from "../walletTypes"; import { BenchmarkResult, CoinWithDenom, PayCoinInfo, PlanchetCreationResult } from "../walletTypes";
import * as timer from "../timer"; import * as timer from "../timer";
@ -173,6 +173,7 @@ export class CryptoApi {
*/ */
wake(ws: WorkerState, work: WorkItem): void { wake(ws: WorkerState, work: WorkItem): void {
if (this.stopped) { if (this.stopped) {
console.log("cryptoApi is stopped");
CryptoApi.enableTracing && console.log("not waking, as cryptoApi is stopped"); CryptoApi.enableTracing && console.log("not waking, as cryptoApi is stopped");
return; return;
} }
@ -299,7 +300,6 @@ export class CryptoApi {
priority: number, priority: number,
...args: any[] ...args: any[]
): Promise<T> { ): Promise<T> {
CryptoApi.enableTracing && console.log("cryptoApi: doRpc called");
const p: Promise<T> = new Promise<T>((resolve, reject) => { const p: Promise<T> = new Promise<T>((resolve, reject) => {
const rpcId = this.nextRpcId++; const rpcId = this.nextRpcId++;
const workItem: WorkItem = { const workItem: WorkItem = {
@ -332,16 +332,14 @@ export class CryptoApi {
throw Error("assertion failed"); throw Error("assertion failed");
}); });
return p.then((r: T) => { return p;
return r;
});
} }
createPreCoin( createPlanchet(
denom: DenominationRecord, denom: DenominationRecord,
reserve: ReserveRecord, reserve: ReserveRecord,
): Promise<PreCoinRecord> { ): Promise<PlanchetCreationResult> {
return this.doRpc<PreCoinRecord>("createPreCoin", 1, denom, reserve); return this.doRpc<PlanchetCreationResult>("createPlanchet", 1, denom, reserve);
} }
createTipPlanchet(denom: DenominationRecord): Promise<TipPlanchet> { createTipPlanchet(denom: DenominationRecord): Promise<TipPlanchet> {

View File

@ -28,8 +28,7 @@ import {
CoinRecord, CoinRecord,
CoinStatus, CoinStatus,
DenominationRecord, DenominationRecord,
PreCoinRecord, RefreshPlanchetRecord,
RefreshPreCoinRecord,
RefreshSessionRecord, RefreshSessionRecord,
ReserveRecord, ReserveRecord,
TipPlanchet, TipPlanchet,
@ -42,6 +41,7 @@ import {
CoinWithDenom, CoinWithDenom,
PayCoinInfo, PayCoinInfo,
Timestamp, Timestamp,
PlanchetCreationResult,
} from "../walletTypes"; } from "../walletTypes";
import { canonicalJson, getTalerStampSec } from "../helpers"; import { canonicalJson, getTalerStampSec } from "../helpers";
import { AmountJson } from "../amounts"; import { AmountJson } from "../amounts";
@ -154,10 +154,10 @@ export class CryptoImplementation {
* Create a pre-coin of the given denomination to be withdrawn from then given * Create a pre-coin of the given denomination to be withdrawn from then given
* reserve. * reserve.
*/ */
createPreCoin( createPlanchet(
denom: DenominationRecord, denom: DenominationRecord,
reserve: ReserveRecord, reserve: ReserveRecord,
): PreCoinRecord { ): PlanchetCreationResult {
const reservePub = decodeCrock(reserve.reservePub); const reservePub = decodeCrock(reserve.reservePub);
const reservePriv = decodeCrock(reserve.reservePriv); const reservePriv = decodeCrock(reserve.reservePriv);
const denomPub = decodeCrock(denom.denomPub); const denomPub = decodeCrock(denom.denomPub);
@ -179,7 +179,7 @@ export class CryptoImplementation {
const sig = eddsaSign(withdrawRequest, reservePriv); const sig = eddsaSign(withdrawRequest, reservePriv);
const preCoin: PreCoinRecord = { const planchet: PlanchetCreationResult = {
blindingKey: encodeCrock(blindingFactor), blindingKey: encodeCrock(blindingFactor),
coinEv: encodeCrock(ev), coinEv: encodeCrock(ev),
coinPriv: encodeCrock(coinKeyPair.eddsaPriv), coinPriv: encodeCrock(coinKeyPair.eddsaPriv),
@ -188,11 +188,10 @@ export class CryptoImplementation {
denomPub: encodeCrock(denomPub), denomPub: encodeCrock(denomPub),
denomPubHash: encodeCrock(denomPubHash), denomPubHash: encodeCrock(denomPubHash),
exchangeBaseUrl: reserve.exchangeBaseUrl, exchangeBaseUrl: reserve.exchangeBaseUrl,
isFromTip: false,
reservePub: encodeCrock(reservePub), reservePub: encodeCrock(reservePub),
withdrawSig: encodeCrock(sig), withdrawSig: encodeCrock(sig),
}; };
return preCoin; return planchet;
} }
/** /**
@ -424,7 +423,7 @@ export class CryptoImplementation {
const transferPubs: string[] = []; const transferPubs: string[] = [];
const transferPrivs: string[] = []; const transferPrivs: string[] = [];
const preCoinsForGammas: RefreshPreCoinRecord[][] = []; const planchetsForGammas: RefreshPlanchetRecord[][] = [];
for (let i = 0; i < kappa; i++) { for (let i = 0; i < kappa; i++) {
const transferKeyPair = createEcdheKeyPair(); const transferKeyPair = createEcdheKeyPair();
@ -442,7 +441,7 @@ export class CryptoImplementation {
sessionHc.update(amountToBuffer(valueWithFee)); sessionHc.update(amountToBuffer(valueWithFee));
for (let i = 0; i < kappa; i++) { for (let i = 0; i < kappa; i++) {
const preCoins: RefreshPreCoinRecord[] = []; const planchets: RefreshPlanchetRecord[] = [];
for (let j = 0; j < newCoinDenoms.length; j++) { for (let j = 0; j < newCoinDenoms.length; j++) {
const transferPriv = decodeCrock(transferPrivs[i]); const transferPriv = decodeCrock(transferPrivs[i]);
const oldCoinPub = decodeCrock(meltCoin.coinPub); const oldCoinPub = decodeCrock(meltCoin.coinPub);
@ -456,16 +455,16 @@ export class CryptoImplementation {
const pubHash = hash(coinPub); const pubHash = hash(coinPub);
const denomPub = decodeCrock(newCoinDenoms[j].denomPub); const denomPub = decodeCrock(newCoinDenoms[j].denomPub);
const ev = rsaBlind(pubHash, blindingFactor, denomPub); const ev = rsaBlind(pubHash, blindingFactor, denomPub);
const preCoin: RefreshPreCoinRecord = { const planchet: RefreshPlanchetRecord = {
blindingKey: encodeCrock(blindingFactor), blindingKey: encodeCrock(blindingFactor),
coinEv: encodeCrock(ev), coinEv: encodeCrock(ev),
privateKey: encodeCrock(coinPriv), privateKey: encodeCrock(coinPriv),
publicKey: encodeCrock(coinPub), publicKey: encodeCrock(coinPub),
}; };
preCoins.push(preCoin); planchets.push(planchet);
sessionHc.update(ev); sessionHc.update(ev);
} }
preCoinsForGammas.push(preCoins); planchetsForGammas.push(planchets);
} }
const sessionHash = sessionHc.finish(); const sessionHash = sessionHc.finish();
@ -496,7 +495,7 @@ export class CryptoImplementation {
newDenomHashes: newCoinDenoms.map(d => d.denomPubHash), newDenomHashes: newCoinDenoms.map(d => d.denomPubHash),
newDenoms: newCoinDenoms.map(d => d.denomPub), newDenoms: newCoinDenoms.map(d => d.denomPub),
norevealIndex: undefined, norevealIndex: undefined,
preCoinsForGammas, planchetsForGammas: planchetsForGammas,
transferPrivs, transferPrivs,
transferPubs, transferPubs,
valueOutput, valueOutput,

View File

@ -88,5 +88,5 @@ export function kdf(
output.set(chunk, i * 32); output.set(chunk, i * 32);
} }
return output; return output.slice(0, outputLength);
} }

View File

@ -237,6 +237,9 @@ function rsaFullDomainHash(hm: Uint8Array, rsaPub: RsaPub): bigint.BigInteger {
function rsaPubDecode(rsaPub: Uint8Array): RsaPub { function rsaPubDecode(rsaPub: Uint8Array): RsaPub {
const modulusLength = (rsaPub[0] << 8) | rsaPub[1]; const modulusLength = (rsaPub[0] << 8) | rsaPub[1];
const exponentLength = (rsaPub[2] << 8) | rsaPub[3]; const exponentLength = (rsaPub[2] << 8) | rsaPub[3];
if (4 + exponentLength + modulusLength != rsaPub.length) {
throw Error("invalid RSA public key (format wrong)");
}
const modulus = rsaPub.slice(4, 4 + modulusLength); const modulus = rsaPub.slice(4, 4 + modulusLength);
const exponent = rsaPub.slice( const exponent = rsaPub.slice(
4 + modulusLength, 4 + modulusLength,

View File

@ -57,6 +57,13 @@ export enum ReserveRecordStatus {
*/ */
REGISTERING_BANK = "registering-bank", REGISTERING_BANK = "registering-bank",
/**
* We've registered reserve's information with the bank
* and are now waiting for the user to confirm the withdraw
* with the bank (typically 2nd factor auth).
*/
WAIT_CONFIRM_BANK = "wait-confirm-bank",
/** /**
* Querying reserve status with the exchange. * Querying reserve status with the exchange.
*/ */
@ -117,22 +124,26 @@ export interface ReserveRecord {
timestampConfirmed: Timestamp | undefined; timestampConfirmed: Timestamp | undefined;
/** /**
* Current amount left in the reserve * Amount that's still available for withdrawing
* from this reserve.
*/ */
currentAmount: AmountJson | null; withdrawRemainingAmount: AmountJson;
/**
* Amount allocated for withdrawing.
* The corresponding withdraw operation may or may not
* have been completed yet.
*/
withdrawAllocatedAmount: AmountJson;
withdrawCompletedAmount: AmountJson;
/** /**
* Amount requested when the reserve was created. * Amount requested when the reserve was created.
* When a reserve is re-used (rare!) the current_amount can * When a reserve is re-used (rare!) the current_amount can
* be higher than the requested_amount * be higher than the requested_amount
*/ */
requestedAmount: AmountJson; initiallyRequestedAmount: AmountJson;
/**
* What's the current amount that sits
* in precoins?
*/
precoinAmount: AmountJson;
/** /**
* We got some payback to this reserve. We'll cease to automatically * We got some payback to this reserve. We'll cease to automatically
@ -154,8 +165,19 @@ export interface ReserveRecord {
bankWithdrawStatusUrl?: string; bankWithdrawStatusUrl?: string;
/**
* URL that the bank gave us to redirect the customer
* to in order to confirm a withdrawal.
*/
bankWithdrawConfirmUrl?: string;
reserveStatus: ReserveRecordStatus; reserveStatus: ReserveRecordStatus;
/**
* Time of the last successful status query.
*/
lastStatusQuery: Timestamp | undefined;
lastError?: OperationError; lastError?: OperationError;
} }
@ -421,7 +443,16 @@ export interface ExchangeRecord {
/** /**
* A coin that isn't yet signed by an exchange. * A coin that isn't yet signed by an exchange.
*/ */
export interface PreCoinRecord { export interface PlanchetRecord {
withdrawSessionId: string;
/**
* Index of the coin in the withdrawal session.
*/
coinIndex: number;
/**
* Public key of the coin.
*/
coinPub: string; coinPub: string;
coinPriv: string; coinPriv: string;
reservePub: string; reservePub: string;
@ -443,7 +474,7 @@ export interface PreCoinRecord {
/** /**
* Planchet for a coin during refrehs. * Planchet for a coin during refrehs.
*/ */
export interface RefreshPreCoinRecord { export interface RefreshPlanchetRecord {
/** /**
* Public key for the coin. * Public key for the coin.
*/ */
@ -485,6 +516,16 @@ export enum CoinStatus {
* of the wallet database. * of the wallet database.
*/ */
export interface CoinRecord { export interface CoinRecord {
/**
* Withdraw session ID, or "" (empty string) if withdrawn via refresh.
*/
withdrawSessionId: string;
/**
* Index of the coin in the withdrawal session.
*/
coinIndex: number;
/** /**
* Public key of the coin. * Public key of the coin.
*/ */
@ -546,11 +587,17 @@ export interface CoinRecord {
status: CoinStatus; status: CoinStatus;
} }
export enum ProposalStatus {
PROPOSED = "proposed",
ACCEPTED = "accepted",
REJECTED = "rejected",
}
/** /**
* Proposal record, stored in the wallet's database. * Record for a downloaded order, stored in the wallet's database.
*/ */
@Checkable.Class() @Checkable.Class()
export class ProposalDownloadRecord { export class ProposalRecord {
/** /**
* URL where the proposal was downloaded. * URL where the proposal was downloaded.
*/ */
@ -576,10 +623,10 @@ export class ProposalDownloadRecord {
contractTermsHash: string; contractTermsHash: string;
/** /**
* Serial ID when the offer is stored in the wallet DB. * Unique ID when the order is stored in the wallet DB.
*/ */
@Checkable.Optional(Checkable.Number()) @Checkable.String()
id?: number; proposalId: string;
/** /**
* Timestamp (in ms) of when the record * Timestamp (in ms) of when the record
@ -594,6 +641,9 @@ export class ProposalDownloadRecord {
@Checkable.String() @Checkable.String()
noncePriv: string; noncePriv: string;
@Checkable.String()
proposalStatus: ProposalStatus;
/** /**
* Session ID we got when downloading the contract. * Session ID we got when downloading the contract.
*/ */
@ -604,7 +654,7 @@ export class ProposalDownloadRecord {
* Verify that a value matches the schema of this class and convert it into a * Verify that a value matches the schema of this class and convert it into a
* member. * member.
*/ */
static checked: (obj: any) => ProposalDownloadRecord; static checked: (obj: any) => ProposalRecord;
} }
/** /**
@ -717,9 +767,9 @@ export interface RefreshSessionRecord {
newDenoms: string[]; newDenoms: string[];
/** /**
* Precoins for each cut-and-choose instance. * Planchets for each cut-and-choose instance.
*/ */
preCoinsForGammas: RefreshPreCoinRecord[][]; planchetsForGammas: RefreshPlanchetRecord[][];
/** /**
* The transfer keys, kappa of them. * The transfer keys, kappa of them.
@ -933,7 +983,9 @@ export interface CoinsReturnRecord {
wire: any; wire: any;
} }
export interface WithdrawalRecord { export interface WithdrawalSessionRecord {
withdrawSessionId: string;
/** /**
* Reserve that we're withdrawing from. * Reserve that we're withdrawing from.
*/ */
@ -956,9 +1008,29 @@ export interface WithdrawalRecord {
*/ */
withdrawalAmount: string; withdrawalAmount: string;
numCoinsTotal: number; denoms: string[];
numCoinsWithdrawn: number; /**
* Coins in this session that are withdrawn are set to true.
*/
withdrawn: boolean[];
/**
* Coins in this session already have a planchet are set to true.
*/
planchetCreated: boolean[];
}
export interface BankWithdrawUriRecord {
/**
* The withdraw URI we got from the bank.
*/
talerWithdrawUri: string;
/**
* Reserve that was created for the withdraw URI.
*/
reservePub: string;
} }
/* tslint:disable:completed-docs */ /* tslint:disable:completed-docs */
@ -967,7 +1039,7 @@ export interface WithdrawalRecord {
* The stores and indices for the wallet database. * The stores and indices for the wallet database.
*/ */
export namespace Stores { export namespace Stores {
class ExchangeStore extends Store<ExchangeRecord> { class ExchangesStore extends Store<ExchangeRecord> {
constructor() { constructor() {
super("exchanges", { keyPath: "baseUrl" }); super("exchanges", { keyPath: "baseUrl" });
} }
@ -988,16 +1060,18 @@ export namespace Stores {
"denomPubIndex", "denomPubIndex",
"denomPub", "denomPub",
); );
byWithdrawalWithIdx = new Index<any, CoinRecord>(
this,
"planchetsByWithdrawalWithIdxIndex",
["withdrawSessionId", "coinIndex"],
);
} }
class ProposalsStore extends Store<ProposalDownloadRecord> { class ProposalsStore extends Store<ProposalRecord> {
constructor() { constructor() {
super("proposals", { super("proposals", { keyPath: "proposalId" });
autoIncrement: true,
keyPath: "id",
});
} }
urlIndex = new Index<string, ProposalDownloadRecord>( urlIndex = new Index<string, ProposalRecord>(
this, this,
"urlIndex", "urlIndex",
"url", "url",
@ -1084,28 +1158,39 @@ export namespace Stores {
} }
} }
class WithdrawalsStore extends Store<WithdrawalRecord> { class WithdrawalSessionsStore extends Store<WithdrawalSessionRecord> {
constructor() { constructor() {
super("withdrawals", { keyPath: "id", autoIncrement: true }); super("withdrawals", { keyPath: "withdrawSessionId" });
} }
byReservePub = new Index<string, WithdrawalRecord>( byReservePub = new Index<string, WithdrawalSessionRecord>(
this, this,
"withdrawalsReservePubIndex", "withdrawalsReservePubIndex",
"reservePub", "reservePub",
); );
} }
class PreCoinsStore extends Store<PreCoinRecord> { class BankWithdrawUrisStore extends Store<BankWithdrawUriRecord> {
constructor() { constructor() {
super("precoins", { super("bankWithdrawUris", { keyPath: "talerWithdrawUri" });
}
}
class PlanchetsStore extends Store<PlanchetRecord> {
constructor() {
super("planchets", {
keyPath: "coinPub", keyPath: "coinPub",
}); });
} }
byReservePub = new Index<string, PreCoinRecord>( byReservePub = new Index<string, PlanchetRecord>(
this, this,
"precoinsReservePubIndex", "planchetsReservePubIndex",
"reservePub", "reservePub",
); );
byWithdrawalWithIdx = new Index<any, PlanchetRecord>(
this,
"planchetsByWithdrawalWithIdxIndex",
["withdrawSessionId", "coinIndex"],
);
} }
export const coins = new CoinsStore(); export const coins = new CoinsStore();
@ -1115,8 +1200,8 @@ export namespace Stores {
export const config = new ConfigStore(); export const config = new ConfigStore();
export const currencies = new CurrenciesStore(); export const currencies = new CurrenciesStore();
export const denominations = new DenominationsStore(); export const denominations = new DenominationsStore();
export const exchanges = new ExchangeStore(); export const exchanges = new ExchangesStore();
export const precoins = new PreCoinsStore(); export const planchets = new PlanchetsStore();
export const proposals = new ProposalsStore(); export const proposals = new ProposalsStore();
export const refresh = new Store<RefreshSessionRecord>("refresh", { export const refresh = new Store<RefreshSessionRecord>("refresh", {
keyPath: "refreshSessionId", keyPath: "refreshSessionId",
@ -1125,7 +1210,8 @@ export namespace Stores {
export const purchases = new PurchasesStore(); export const purchases = new PurchasesStore();
export const tips = new TipsStore(); export const tips = new TipsStore();
export const senderWires = new SenderWiresStore(); export const senderWires = new SenderWiresStore();
export const withdrawals = new WithdrawalsStore(); export const withdrawalSession = new WithdrawalSessionsStore();
export const bankWithdrawUris = new BankWithdrawUrisStore();
} }
/* tslint:enable:completed-docs */ /* tslint:enable:completed-docs */

View File

@ -45,6 +45,37 @@ function makeId(length: number): string {
export class Bank { export class Bank {
constructor(private bankBaseUrl: string) {} constructor(private bankBaseUrl: string) {}
async generateWithdrawUri(bankUser: BankUser, amount: string): Promise<string> {
const body = {
amount,
};
const reqUrl = new URI("api/withdraw-headless-uri")
.absoluteTo(this.bankBaseUrl)
.href();
const resp = await Axios({
method: "post",
url: reqUrl,
data: body,
responseType: "json",
headers: {
"X-Taler-Bank-Username": bankUser.username,
"X-Taler-Bank-Password": bankUser.password,
},
});
if (resp.status != 200) {
throw Error("failed to create bank reserve");
}
const withdrawUri = resp.data["taler_withdraw_uri"];
if (!withdrawUri) {
throw Error("Bank's response did not include withdraw URI");
}
return withdrawUri;
}
async createReserve( async createReserve(
bankUser: BankUser, bankUser: BankUser,
amount: string, amount: string,

View File

@ -29,6 +29,7 @@ export let STRING: Converter<string> = new Converter<string>();
export interface OptionArgs<T> { export interface OptionArgs<T> {
help?: string; help?: string;
default?: T; default?: T;
onPresentHandler?: (v: T) => void;
} }
export interface ArgumentArgs<T> { export interface ArgumentArgs<T> {
@ -269,9 +270,6 @@ export class CommandGroup<GN extends keyof any, TG> {
} }
printHelp(progName: string, parents: CommandGroup<any, any>[]) { printHelp(progName: string, parents: CommandGroup<any, any>[]) {
const chain: CommandGroup<any, any>[] = Array.prototype.concat(parents, [
this,
]);
let usageSpec = ""; let usageSpec = "";
for (let p of parents) { for (let p of parents) {
usageSpec += (p.name ?? progName) + " "; usageSpec += (p.name ?? progName) + " ";
@ -352,6 +350,7 @@ export class CommandGroup<GN extends keyof any, TG> {
process.exit(-1); process.exit(-1);
throw Error("not reached"); throw Error("not reached");
} }
foundOptions[d.name] = true;
myArgs[d.name] = true; myArgs[d.name] = true;
} else { } else {
if (r.value === undefined) { if (r.value === undefined) {
@ -380,6 +379,7 @@ export class CommandGroup<GN extends keyof any, TG> {
} }
if (opt.isFlag) { if (opt.isFlag) {
myArgs[opt.name] = true; myArgs[opt.name] = true;
foundOptions[opt.name] = true;
} else { } else {
if (si == optShort.length - 1) { if (si == optShort.length - 1) {
if (i === unparsedArgs.length - 1) { if (i === unparsedArgs.length - 1) {
@ -449,6 +449,13 @@ export class CommandGroup<GN extends keyof any, TG> {
} }
} }
for (let option of this.options) {
const ph = option.args.onPresentHandler;
if (ph && foundOptions[option.name]) {
ph(myArgs[option.name]);
}
}
if (parsedArgs[this.argKey].help) { if (parsedArgs[this.argKey].help) {
this.printHelp(progname, parents); this.printHelp(progname, parents);
process.exit(-1); process.exit(-1);
@ -546,7 +553,7 @@ export class Program<PN extends keyof any, T> {
name: N, name: N,
flagspec: string[], flagspec: string[],
args: OptionArgs<boolean> = {}, args: OptionArgs<boolean> = {},
): Program<N, T & SubRecord<PN, N, boolean>> { ): Program<PN, T & SubRecord<PN, N, boolean>> {
this.mainCommand.flag(name, flagspec, args); this.mainCommand.flag(name, flagspec, args);
return this as any; return this as any;
} }

View File

@ -34,35 +34,30 @@ import { Bank } from "./bank";
import fs = require("fs"); import fs = require("fs");
import { NodeCryptoWorkerFactory } from "../crypto/nodeProcessWorker"; import { NodeCryptoWorkerFactory } from "../crypto/nodeProcessWorker";
import { Logger } from "../logging";
const logger = new Logger("helpers.ts");
const enableTracing = false;
class ConsoleBadge implements Badge { class ConsoleBadge implements Badge {
startBusy(): void { startBusy(): void {
enableTracing && console.log("NOTIFICATION: busy");
} }
stopBusy(): void { stopBusy(): void {
enableTracing && console.log("NOTIFICATION: busy end");
} }
showNotification(): void { showNotification(): void {
enableTracing && console.log("NOTIFICATION: show");
} }
clearNotification(): void { clearNotification(): void {
enableTracing && console.log("NOTIFICATION: cleared");
} }
} }
export class NodeHttpLib implements HttpRequestLibrary { export class NodeHttpLib implements HttpRequestLibrary {
async get(url: string): Promise<import("../http").HttpResponse> { async get(url: string): Promise<import("../http").HttpResponse> {
enableTracing && console.log("making GET request to", url);
try { try {
const resp = await Axios({ const resp = await Axios({
method: "get", method: "get",
url: url, url: url,
responseType: "json", responseType: "json",
}); });
enableTracing && console.log("got response", resp.data);
enableTracing && console.log("resp type", typeof resp.data);
return { return {
responseJson: resp.data, responseJson: resp.data,
status: resp.status, status: resp.status,
@ -76,7 +71,6 @@ export class NodeHttpLib implements HttpRequestLibrary {
url: string, url: string,
body: any, body: any,
): Promise<import("../http").HttpResponse> { ): Promise<import("../http").HttpResponse> {
enableTracing && console.log("making POST request to", url);
try { try {
const resp = await Axios({ const resp = await Axios({
method: "post", method: "post",
@ -84,8 +78,6 @@ export class NodeHttpLib implements HttpRequestLibrary {
responseType: "json", responseType: "json",
data: body, data: body,
}); });
enableTracing && console.log("got response", resp.data);
enableTracing && console.log("resp type", typeof resp.data);
return { return {
responseJson: resp.data, responseJson: resp.data,
status: resp.status, status: resp.status,
@ -149,7 +141,6 @@ export async function getDefaultNodeWallet(
} }
myBackend.afterCommitCallback = async () => { myBackend.afterCommitCallback = async () => {
console.log("DATABASE COMMITTED");
// Allow caller to stop persisting the wallet. // Allow caller to stop persisting the wallet.
if (args.persistentStoragePath === undefined) { if (args.persistentStoragePath === undefined) {
return; return;
@ -219,7 +210,7 @@ export async function withdrawTestBalance(
const bankUser = await bank.registerRandomUser(); const bankUser = await bank.registerRandomUser();
console.log("bank user", bankUser); logger.trace(`Registered bank user ${JSON.stringify(bankUser)}`)
const exchangePaytoUri = await myWallet.getExchangePaytoUri( const exchangePaytoUri = await myWallet.getExchangePaytoUri(
exchangeBaseUrl, exchangeBaseUrl,
@ -234,6 +225,5 @@ export async function withdrawTestBalance(
); );
await myWallet.confirmReserve({ reservePub: reserveResponse.reservePub }); await myWallet.confirmReserve({ reservePub: reserveResponse.reservePub });
await myWallet.runUntilReserveDepleted(reservePub); await myWallet.runUntilReserveDepleted(reservePub);
} }

View File

@ -30,10 +30,60 @@ import URI = require("urijs");
* Connection to the *internal* merchant backend. * Connection to the *internal* merchant backend.
*/ */
export class MerchantBackendConnection { export class MerchantBackendConnection {
constructor( async refund(
public merchantBaseUrl: string, orderId: string,
public apiKey: string, reason: string,
) {} refundAmount: string,
): Promise<void> {
const reqUrl = new URI("refund").absoluteTo(this.merchantBaseUrl).href();
const refundReq = {
order_id: orderId,
reason,
refund: refundAmount,
};
const resp = await axios({
method: "post",
url: reqUrl,
data: refundReq,
responseType: "json",
headers: {
Authorization: `ApiKey ${this.apiKey}`,
},
});
if (resp.status != 200) {
throw Error("failed to do refund");
}
console.log("response", resp.data);
const refundUri = resp.data.taler_refund_uri;
if (!refundUri) {
throw Error("no refund URI in response");
}
return refundUri;
}
constructor(public merchantBaseUrl: string, public apiKey: string) {}
async authorizeTip(amount: string, justification: string) {
const reqUrl = new URI("tip-authorize").absoluteTo(this.merchantBaseUrl).href();
const tipReq = {
amount,
justification,
};
const resp = await axios({
method: "post",
url: reqUrl,
data: tipReq,
responseType: "json",
headers: {
Authorization: `ApiKey ${this.apiKey}`,
},
});
const tipUri = resp.data.taler_tip_uri;
if (!tipUri) {
throw Error("response does not contain tip URI");
}
return tipUri;
}
async createOrder( async createOrder(
amount: string, amount: string,

View File

@ -26,11 +26,16 @@ import { BridgeIDBFactory, MemoryBackend } from "idb-bridge";
import { Logger } from "../logging"; import { Logger } from "../logging";
import * as Amounts from "../amounts"; import * as Amounts from "../amounts";
import { decodeCrock } from "../crypto/talerCrypto"; import { decodeCrock } from "../crypto/talerCrypto";
import { Bank } from "./bank";
const logger = new Logger("taler-wallet-cli.ts"); const logger = new Logger("taler-wallet-cli.ts");
const walletDbPath = os.homedir + "/" + ".talerwalletdb.json"; const walletDbPath = os.homedir + "/" + ".talerwalletdb.json";
function assertUnreachable(x: never): never {
throw new Error("Didn't expect to get here");
}
async function doPay( async function doPay(
wallet: Wallet, wallet: Wallet,
payUrl: string, payUrl: string,
@ -78,7 +83,7 @@ async function doPay(
} }
if (pay) { if (pay) {
const payRes = await wallet.confirmPay(result.proposalId!, undefined); const payRes = await wallet.confirmPay(result.proposalId, undefined);
console.log("paid!"); console.log("paid!");
} else { } else {
console.log("not paying"); console.log("not paying");
@ -93,6 +98,12 @@ function applyVerbose(verbose: boolean) {
} }
} }
function printVersion() {
const info = require("../../../package.json");
console.log(`${info.version}`);
process.exit(0);
}
const walletCli = clk const walletCli = clk
.program("wallet", { .program("wallet", {
help: "Command line interface for the GNU Taler wallet.", help: "Command line interface for the GNU Taler wallet.",
@ -101,6 +112,9 @@ const walletCli = clk
help: help:
"Inhibit running certain operations, useful for debugging and testing.", "Inhibit running certain operations, useful for debugging and testing.",
}) })
.flag("version", ["-v", "--version"], {
onPresentHandler: printVersion,
})
.flag("verbose", ["-V", "--verbose"], { .flag("verbose", ["-V", "--verbose"], {
help: "Enable verbose output.", help: "Enable verbose output.",
}); });
@ -133,12 +147,21 @@ async function withWallet<T>(
} }
walletCli walletCli
.subcommand("", "balance", { help: "Show wallet balance." }) .subcommand("balance", "balance", { help: "Show wallet balance." })
.flag("json", ["--json"], {
help: "Show raw JSON.",
})
.action(async args => { .action(async args => {
console.log("balance command called");
await withWallet(args, async wallet => { await withWallet(args, async wallet => {
const balance = await wallet.getBalances(); const balance = await wallet.getBalances();
if (args.balance.json) {
console.log(JSON.stringify(balance, undefined, 2)); console.log(JSON.stringify(balance, undefined, 2));
} else {
const currencies = Object.keys(balance.byCurrency).sort();
for (const c of currencies) {
console.log(Amounts.toString(balance.byCurrency[c].available));
}
}
}); });
}); });
@ -205,15 +228,8 @@ walletCli
process.exit(1); process.exit(1);
return; return;
} }
const { confirmTransferUrl } = await wallet.acceptWithdrawal( const res = await wallet.acceptWithdrawal(uri, selectedExchange);
uri, await wallet.processReserve(res.reservePub);
selectedExchange,
);
if (confirmTransferUrl) {
console.log("please confirm the transfer at", confirmTransferUrl);
}
} else {
console.error("unrecognized URI");
} }
}); });
}); });
@ -258,13 +274,39 @@ const advancedCli = walletCli.subcommand("advancedArgs", "advanced", {
advancedCli advancedCli
.subcommand("decode", "decode", { .subcommand("decode", "decode", {
help: "Decode base32-crockford", help: "Decode base32-crockford.",
}) })
.action(args => { .action(args => {
const enc = fs.readFileSync(0, 'utf8'); const enc = fs.readFileSync(0, "utf8");
fs.writeFileSync(1, decodeCrock(enc.trim())) fs.writeFileSync(1, decodeCrock(enc.trim()));
}); });
advancedCli
.subcommand("payPrepare", "pay-prepare", {
help: "Claim an order but don't pay yet.",
})
.requiredArgument("url", clk.STRING)
.action(async args => {
await withWallet(args, async wallet => {
const res = await wallet.preparePay(args.payPrepare.url);
switch (res.status) {
case "error":
console.log("error:", res.error);
break;
case "insufficient-balance":
console.log("insufficient balance");
break;
case "paid":
console.log("already paid");
break;
case "payment-possible":
console.log("payment possible");
break;
default:
assertUnreachable(res);
}
});
});
advancedCli advancedCli
.subcommand("refresh", "force-refresh", { .subcommand("refresh", "force-refresh", {
@ -288,7 +330,9 @@ advancedCli
console.log(`coin ${coin.coinPub}`); console.log(`coin ${coin.coinPub}`);
console.log(` status ${coin.status}`); console.log(` status ${coin.status}`);
console.log(` exchange ${coin.exchangeBaseUrl}`); console.log(` exchange ${coin.exchangeBaseUrl}`);
console.log(` remaining amount ${Amounts.toString(coin.currentAmount)}`); console.log(
` remaining amount ${Amounts.toString(coin.currentAmount)}`,
);
} }
}); });
}); });
@ -324,12 +368,11 @@ testCli
return; return;
} }
console.log("taler pay URI:", talerPayUri); console.log("taler pay URI:", talerPayUri);
await withWallet(args, async (wallet) => { await withWallet(args, async wallet => {
await doPay(wallet, talerPayUri, { alwaysYes: true }); await doPay(wallet, talerPayUri, { alwaysYes: true });
}); });
}); });
testCli testCli
.subcommand("integrationtestCmd", "integrationtest", { .subcommand("integrationtestCmd", "integrationtest", {
help: "Run integration test with bank, exchange and merchant.", help: "Run integration test with bank, exchange and merchant.",
@ -377,7 +420,74 @@ testCli
}); });
testCli testCli
.subcommand("testMerchantQrcodeCmd", "test-merchant-qrcode") .subcommand("genTipUri", "gen-tip-uri", {
help: "Generate a taler://tip URI.",
})
.requiredOption("amount", ["-a", "--amount"], clk.STRING, {
default: "TESTKUDOS:10",
})
.action(async args => {
const merchantBackend = new MerchantBackendConnection(
"https://backend.test.taler.net/",
"sandbox",
);
const tipUri = await merchantBackend.authorizeTip("TESTKUDOS:10", "test");
console.log(tipUri);
});
testCli
.subcommand("genRefundUri", "gen-refund-uri", {
help: "Generate a taler://refund URI.",
})
.requiredOption("amount", ["-a", "--amount"], clk.STRING, {
default: "TESTKUDOS:5",
})
.requiredOption("refundAmount", ["-r", "--refund"], clk.STRING, {
default: "TESTKUDOS:3",
})
.requiredOption("summary", ["-s", "--summary"], clk.STRING, {
default: "Test Payment (for refund)",
})
.action(async args => {
const cmdArgs = args.genRefundUri;
const merchantBackend = new MerchantBackendConnection(
"https://backend.test.taler.net/",
"sandbox",
);
const orderResp = await merchantBackend.createOrder(
cmdArgs.amount,
cmdArgs.summary,
"",
);
console.log("created new order with order ID", orderResp.orderId);
const checkPayResp = await merchantBackend.checkPayment(orderResp.orderId);
const talerPayUri = checkPayResp.taler_pay_uri;
if (!talerPayUri) {
console.error("fatal: no taler pay URI received from backend");
process.exit(1);
return;
}
await withWallet(args, async wallet => {
await doPay(wallet, talerPayUri, { alwaysYes: true });
});
const refundUri = await merchantBackend.refund(
orderResp.orderId,
"test refund",
cmdArgs.refundAmount,
);
console.log(refundUri);
});
testCli
.subcommand("genPayUri", "gen-pay-uri", {
help: "Generate a taler://pay URI.",
})
.flag("qrcode", ["--qr"], {
help: "Show a QR code with the taler://pay URI",
})
.flag("wait", ["--wait"], {
help: "Wait until payment has completed",
})
.requiredOption("amount", ["-a", "--amount"], clk.STRING, { .requiredOption("amount", ["-a", "--amount"], clk.STRING, {
default: "TESTKUDOS:1", default: "TESTKUDOS:1",
}) })
@ -385,8 +495,7 @@ testCli
default: "Test Payment", default: "Test Payment",
}) })
.action(async args => { .action(async args => {
const cmdArgs = args.testMerchantQrcodeCmd; const cmdArgs = args.genPayUri;
applyVerbose(args.wallet.verbose);
console.log("creating order"); console.log("creating order");
const merchantBackend = new MerchantBackendConnection( const merchantBackend = new MerchantBackendConnection(
"https://backend.test.taler.net/", "https://backend.test.taler.net/",
@ -399,7 +508,6 @@ testCli
); );
console.log("created new order with order ID", orderResp.orderId); console.log("created new order with order ID", orderResp.orderId);
const checkPayResp = await merchantBackend.checkPayment(orderResp.orderId); const checkPayResp = await merchantBackend.checkPayment(orderResp.orderId);
const qrcode = qrcodeGenerator(0, "M");
const talerPayUri = checkPayResp.taler_pay_uri; const talerPayUri = checkPayResp.taler_pay_uri;
if (!talerPayUri) { if (!talerPayUri) {
console.error("fatal: no taler pay URI received from backend"); console.error("fatal: no taler pay URI received from backend");
@ -407,9 +515,13 @@ testCli
return; return;
} }
console.log("taler pay URI:", talerPayUri); console.log("taler pay URI:", talerPayUri);
if (cmdArgs.qrcode) {
const qrcode = qrcodeGenerator(0, "M");
qrcode.addData(talerPayUri); qrcode.addData(talerPayUri);
qrcode.make(); qrcode.make();
console.log(qrcode.createASCII()); console.log(qrcode.createASCII());
}
if (cmdArgs.wait) {
console.log("waiting for payment ..."); console.log("waiting for payment ...");
while (1) { while (1) {
await asyncSleep(500); await asyncSleep(500);
@ -421,6 +533,7 @@ testCli
break; break;
} }
} }
}
}); });
testCli testCli

View File

@ -47,6 +47,8 @@ function fakeCwd(current: string, value: string, feeDeposit: string): types.Coin
denomSig: "(mock)", denomSig: "(mock)",
exchangeBaseUrl: "(mock)", exchangeBaseUrl: "(mock)",
reservePub: "(mock)", reservePub: "(mock)",
coinIndex: -1,
withdrawSessionId: "",
status: dbTypes.CoinStatus.Fresh, status: dbTypes.CoinStatus.Fresh,
}, },
denom: { denom: {

File diff suppressed because it is too large Load Diff

View File

@ -465,14 +465,14 @@ export type PreparePayResult =
export interface PreparePayResultPaymentPossible { export interface PreparePayResultPaymentPossible {
status: "payment-possible"; status: "payment-possible";
proposalId: number; proposalId: string;
contractTerms: ContractTerms; contractTerms: ContractTerms;
totalFees: AmountJson; totalFees: AmountJson;
} }
export interface PreparePayResultInsufficientBalance { export interface PreparePayResultInsufficientBalance {
status: "insufficient-balance"; status: "insufficient-balance";
proposalId: number; proposalId: string;
contractTerms: ContractTerms; contractTerms: ContractTerms;
} }
@ -523,8 +523,10 @@ export interface WalletDiagnostics {
export interface PendingWithdrawOperation { export interface PendingWithdrawOperation {
type: "withdraw"; type: "withdraw";
stage: string;
reservePub: string; reservePub: string;
withdrawSessionId: string;
numCoinsWithdrawn: number;
numCoinsTotal: number;
} }
export interface PendingRefreshOperation { export interface PendingRefreshOperation {
@ -561,22 +563,47 @@ export interface PendingReserveOperation {
stage: string; stage: string;
timestampCreated: Timestamp; timestampCreated: Timestamp;
reserveType: string; reserveType: string;
reservePub: string;
bankWithdrawConfirmUrl?: string;
} }
export interface PendingRefreshOperation { export interface PendingRefreshOperation {
type: "refresh"; type: "refresh";
lastError?: OperationError; lastError?: OperationError;
refreshSessionId: string;
oldCoinPub: string; oldCoinPub: string;
refreshStatus: string; refreshStatus: string;
refreshOutputSize: number; refreshOutputSize: number;
} }
export interface PendingPlanchetOperation {
type: "planchet";
coinPub: string;
reservePub: string;
lastError?: OperationError;
}
export interface PendingDirtyCoinOperation {
type: "dirty-coin";
coinPub: string;
}
export interface PendingProposalOperation {
type: "proposal";
merchantBaseUrl: string;
proposalTimestamp: Timestamp;
proposalId: string;
}
export type PendingOperationInfo = export type PendingOperationInfo =
| PendingWithdrawOperation | PendingWithdrawOperation
| PendingReserveOperation | PendingReserveOperation
| PendingBugOperation | PendingBugOperation
| PendingPlanchetOperation
| PendingDirtyCoinOperation
| PendingExchangeUpdateOperation | PendingExchangeUpdateOperation
| PendingRefreshOperation; | PendingRefreshOperation
| PendingProposalOperation;
export interface PendingOperationsResponse { export interface PendingOperationsResponse {
pendingOperations: PendingOperationInfo[]; pendingOperations: PendingOperationInfo[];
@ -614,3 +641,17 @@ export function getTimestampNow(): Timestamp {
t_ms: new Date().getTime(), t_ms: new Date().getTime(),
}; };
} }
export interface PlanchetCreationResult {
coinPub: string;
coinPriv: string;
reservePub: string;
denomPubHash: string;
denomPub: string;
blindingKey: string;
withdrawSig: string;
coinEv: string;
exchangeBaseUrl: string;
coinValue: AmountJson;
}

View File

@ -66,7 +66,7 @@ export interface MessageMap {
response: void; response: void;
}; };
"confirm-pay": { "confirm-pay": {
request: { proposalId: number; sessionId?: string }; request: { proposalId: string; sessionId?: string };
response: walletTypes.ConfirmPayResult; response: walletTypes.ConfirmPayResult;
}; };
"exchange-info": { "exchange-info": {
@ -113,9 +113,9 @@ export interface MessageMap {
request: { reservePub: string }; request: { reservePub: string };
response: dbTypes.ReserveRecord[]; response: dbTypes.ReserveRecord[];
}; };
"get-precoins": { "get-planchets": {
request: { exchangeBaseUrl: string }; request: { exchangeBaseUrl: string };
response: dbTypes.PreCoinRecord[]; response: dbTypes.PlanchetRecord[];
}; };
"get-denoms": { "get-denoms": {
request: { exchangeBaseUrl: string }; request: { exchangeBaseUrl: string };

View File

@ -57,7 +57,7 @@ function Payback() {
<div> <div>
{reserves.map(r => ( {reserves.map(r => (
<div> <div>
<h2>Reserve for ${renderAmount(r.currentAmount!)}</h2> <h2>Reserve for ${renderAmount(r.withdrawRemainingAmount)}</h2>
<ul> <ul>
<li>Exchange: ${r.exchangeBaseUrl}</li> <li>Exchange: ${r.exchangeBaseUrl}</li>
</ul> </ul>

View File

@ -28,7 +28,7 @@ import {
CurrencyRecord, CurrencyRecord,
DenominationRecord, DenominationRecord,
ExchangeRecord, ExchangeRecord,
PreCoinRecord, PlanchetRecord,
ReserveRecord, ReserveRecord,
} from "../dbTypes"; } from "../dbTypes";
import { import {
@ -174,10 +174,10 @@ export function getCoins(exchangeBaseUrl: string): Promise<CoinRecord[]> {
/** /**
* Get all precoins withdrawn from the given exchange. * Get all planchets withdrawn from the given exchange.
*/ */
export function getPreCoins(exchangeBaseUrl: string): Promise<PreCoinRecord[]> { export function getPlanchets(exchangeBaseUrl: string): Promise<PlanchetRecord[]> {
return callBackend("get-precoins", { exchangeBaseUrl }); return callBackend("get-planchets", { exchangeBaseUrl });
} }
@ -207,7 +207,7 @@ export function payback(coinPub: string): Promise<void> {
/** /**
* Pay for a proposal. * Pay for a proposal.
*/ */
export function confirmPay(proposalId: number, sessionId: string | undefined): Promise<ConfirmPayResult> { export function confirmPay(proposalId: string, sessionId: string | undefined): Promise<ConfirmPayResult> {
return callBackend("confirm-pay", { proposalId, sessionId }); return callBackend("confirm-pay", { proposalId, sessionId });
} }

View File

@ -117,8 +117,8 @@ async function handleMessage(
return needsWallet().confirmReserve(req); return needsWallet().confirmReserve(req);
} }
case "confirm-pay": { case "confirm-pay": {
if (typeof detail.proposalId !== "number") { if (typeof detail.proposalId !== "string") {
throw Error("proposalId must be number"); throw Error("proposalId must be string");
} }
return needsWallet().confirmPay(detail.proposalId, detail.sessionId); return needsWallet().confirmPay(detail.proposalId, detail.sessionId);
} }
@ -178,11 +178,11 @@ async function handleMessage(
} }
return needsWallet().getCoinsForExchange(detail.exchangeBaseUrl); return needsWallet().getCoinsForExchange(detail.exchangeBaseUrl);
} }
case "get-precoins": { case "get-planchets": {
if (typeof detail.exchangeBaseUrl !== "string") { if (typeof detail.exchangeBaseUrl !== "string") {
return Promise.reject(Error("exchangBaseUrl missing")); return Promise.reject(Error("exchangBaseUrl missing"));
} }
return needsWallet().getPreCoins(detail.exchangeBaseUrl); return needsWallet().getPlanchets(detail.exchangeBaseUrl);
} }
case "get-denoms": { case "get-denoms": {
if (typeof detail.exchangeBaseUrl !== "string") { if (typeof detail.exchangeBaseUrl !== "string") {
@ -658,8 +658,8 @@ export async function wxMain() {
if (!wallet) { if (!wallet) {
console.warn("wallet not available while handling header"); console.warn("wallet not available while handling header");
} }
if (details.statusCode === 402) { if (details.statusCode === 402 || details.statusCode === 202) {
console.log(`got 402 from ${details.url}`); console.log(`got 402/202 from ${details.url}`);
for (let header of details.responseHeaders || []) { for (let header of details.responseHeaders || []) {
if (header.name.toLowerCase() === "taler") { if (header.name.toLowerCase() === "taler") {
const talerUri = header.value || ""; const talerUri = header.value || "";
@ -705,6 +705,15 @@ export async function wxMain() {
talerRefundUri: talerUri, talerRefundUri: talerUri,
}, },
); );
} else if (talerUri.startsWith("taler://notify-reserve/")) {
Promise.resolve().then(() => {
const w = currentWallet;
if (!w) {
return;
}
w.handleNotifyReserve();
});
} else { } else {
console.warn("Unknown action in taler:// URI, ignoring."); console.warn("Unknown action in taler:// URI, ignoring.");
} }

View File

@ -3417,10 +3417,10 @@ iconv-lite@0.4.24, iconv-lite@^0.4.4, iconv-lite@~0.4.13:
dependencies: dependencies:
safer-buffer ">= 2.1.2 < 3" safer-buffer ">= 2.1.2 < 3"
idb-bridge@^0.0.14: idb-bridge@^0.0.15:
version "0.0.14" version "0.0.15"
resolved "https://registry.yarnpkg.com/idb-bridge/-/idb-bridge-0.0.14.tgz#5fd50cd68b574df0eb6b1a960cef0cb984a21ded" resolved "https://registry.yarnpkg.com/idb-bridge/-/idb-bridge-0.0.15.tgz#3fddc91b9aab775fae273d02b272205c6090d270"
integrity sha512-jc9ZYGhhIrW6nh/pWyycGWzCmsLTFQ0iMY61lN+y9YcIOCxREpAkZxdfmhwNL7H0RvsYp7iJv0GH7ujs7HPC+g== integrity sha512-xuZM/i4vCm/NkqyrKNJDEuBaZK7M2kyj+1F4hDGqtEJZSmQMSV3v9A6Ie3fR12VXDKIbMr7uV22eWjIKwSosOA==
ieee754@^1.1.4: ieee754@^1.1.4:
version "1.1.13" version "1.1.13"