From 03782f8aea043042aaa069de0b91cdb80fbb4679 Mon Sep 17 00:00:00 2001 From: Florian Dold Date: Sun, 15 Oct 2017 18:30:02 +0200 Subject: derive history from db instead of storing it --- src/checkable.ts | 5 +- src/crypto/cryptoApi-test.ts | 4 +- src/i18n/de.po | 38 +++--- src/i18n/en-US.po | 38 +++--- src/i18n/fr.po | 38 +++--- src/i18n/it.po | 38 +++--- src/i18n/taler-wallet-webex.pot | 38 +++--- src/types.ts | 58 +++++---- src/wallet.ts | 266 +++++++++++++++++++++++----------------- src/webex/messages.ts | 4 - src/webex/notify.ts | 16 +-- src/webex/pages/popup.tsx | 23 ++-- src/webex/pages/refund.tsx | 6 +- src/webex/pages/tree.tsx | 2 +- src/webex/wxApi.ts | 9 +- src/webex/wxBackend.ts | 12 +- tsconfig.json | 4 +- 17 files changed, 310 insertions(+), 289 deletions(-) diff --git a/src/checkable.ts b/src/checkable.ts index e6ef99336..0e437839e 100644 --- a/src/checkable.ts +++ b/src/checkable.ts @@ -183,7 +183,7 @@ export namespace Checkable { if (innerProp.optional) { continue; } - throw new SchemaError(`Property ${innerProp.propertyKey} missing on ${path}`); + throw new SchemaError(`Property ${innerProp.propertyKey} missing on ${path} of ${type.name||"??"}`); } if (!remainingPropNames.delete(innerProp.propertyKey)) { throw new SchemaError("assertion failed"); @@ -195,8 +195,7 @@ export namespace Checkable { } if (!prop.extraAllowed && remainingPropNames.size !== 0) { - throw new SchemaError("superfluous properties " + JSON.stringify(Array.from( - remainingPropNames.values()))); + throw new SchemaError(`superfluous properties ${JSON.stringify(Array.from(remainingPropNames.values()))} of ${type.name||"??"}`); } return obj; } diff --git a/src/crypto/cryptoApi-test.ts b/src/crypto/cryptoApi-test.ts index 7e391cd91..d96d69e40 100644 --- a/src/crypto/cryptoApi-test.ts +++ b/src/crypto/cryptoApi-test.ts @@ -81,16 +81,16 @@ test("precoin creation", async (t) => { const crypto = new CryptoApi(); const {priv, pub} = await crypto.createEddsaKeypair(); const r: ReserveRecord = { - confirmed: false, created: 0, current_amount: null, exchange_base_url: "https://example.com/exchange", hasPayback: false, - last_query: null, precoin_amount: {currency: "PUDOS", value: 0, fraction: 0}, requested_amount: {currency: "PUDOS", value: 0, fraction: 0}, reserve_priv: priv, reserve_pub: pub, + timestamp_confirmed: 0, + timestamp_depleted: 0, }; const precoin = await crypto.createPreCoin(denomValid1, r); diff --git a/src/i18n/de.po b/src/i18n/de.po index 38ce00c45..1318654af 100644 --- a/src/i18n/de.po +++ b/src/i18n/de.po @@ -198,105 +198,105 @@ msgstr "" msgid "Fatal error: \"%1$s\"." msgstr "" -#: src/webex/pages/popup.tsx:161 +#: src/webex/pages/popup.tsx:160 #, c-format msgid "Balance" msgstr "Saldo" -#: src/webex/pages/popup.tsx:164 +#: src/webex/pages/popup.tsx:163 #, c-format msgid "History" msgstr "Verlauf" -#: src/webex/pages/popup.tsx:167 +#: src/webex/pages/popup.tsx:166 #, c-format msgid "Debug" msgstr "Debug" -#: src/webex/pages/popup.tsx:247 +#: src/webex/pages/popup.tsx:246 #, c-format msgid "help" msgstr "" -#: src/webex/pages/popup.tsx:252 +#: src/webex/pages/popup.tsx:251 #, fuzzy, c-format msgid "" "You have no balance to show. Need some\n" " %1$s getting started?\n" msgstr "Sie haben kein Digitalgeld. Wollen Sie %1$s? abheben?" -#: src/webex/pages/popup.tsx:269 +#: src/webex/pages/popup.tsx:268 #, c-format msgid "%1$s incoming\n" msgstr "" -#: src/webex/pages/popup.tsx:282 +#: src/webex/pages/popup.tsx:281 #, c-format msgid "%1$s being spent\n" msgstr "" -#: src/webex/pages/popup.tsx:308 +#: src/webex/pages/popup.tsx:307 #, c-format msgid "Error: could not retrieve balance information." msgstr "" -#: src/webex/pages/popup.tsx:335 +#: src/webex/pages/popup.tsx:334 #, c-format msgid "Payback" msgstr "" -#: src/webex/pages/popup.tsx:336 +#: src/webex/pages/popup.tsx:335 #, c-format msgid "Return Electronic Cash to Bank Account" msgstr "" -#: src/webex/pages/popup.tsx:337 +#: src/webex/pages/popup.tsx:336 #, c-format msgid "Manage Trusted Auditors and Exchanges" msgstr "" -#: src/webex/pages/popup.tsx:349 +#: src/webex/pages/popup.tsx:348 #, fuzzy, c-format msgid "" "Bank requested reserve (%1$s) for\n" " %2$s.\n" msgstr "Bank bestätig anlegen der Reserve (%1$s) bei %2$s" -#: src/webex/pages/popup.tsx:360 +#: src/webex/pages/popup.tsx:359 #, fuzzy, c-format msgid "" "Started to withdraw\n" " %1$s from%2$s(%3$s).\n" msgstr "Reserve (%1$s) mit %2$s bei %3$s erzeugt" -#: src/webex/pages/popup.tsx:370 +#: src/webex/pages/popup.tsx:369 #, c-format msgid "Merchant%1$soffered contract%2$s;\n" msgstr "" -#: src/webex/pages/popup.tsx:380 +#: src/webex/pages/popup.tsx:379 #, fuzzy, c-format msgid "Withdrew%1$sfrom%2$s(%3$s).\n" msgstr "Reserve (%1$s) mit %2$s bei %3$s erzeugt" -#: src/webex/pages/popup.tsx:390 +#: src/webex/pages/popup.tsx:389 #, fuzzy, c-format msgid "" "Paid%1$sto merchant%2$s.\n" " (%3$s)\n" msgstr "Reserve (%1$s) mit %2$s bei %3$s erzeugt" -#: src/webex/pages/popup.tsx:399 +#: src/webex/pages/popup.tsx:398 #, c-format msgid "Unknown event (%1$s)" msgstr "" -#: src/webex/pages/popup.tsx:442 +#: src/webex/pages/popup.tsx:441 #, c-format msgid "Error: could not retrieve event history" msgstr "" -#: src/webex/pages/popup.tsx:476 +#: src/webex/pages/popup.tsx:466 #, c-format msgid "Your wallet has no events recorded." msgstr "Ihre Geldbörse verzeichnet keine Vorkommnisse." diff --git a/src/i18n/en-US.po b/src/i18n/en-US.po index 66d4bd118..1418b0f58 100644 --- a/src/i18n/en-US.po +++ b/src/i18n/en-US.po @@ -198,105 +198,105 @@ msgstr "" msgid "Fatal error: \"%1$s\"." msgstr "" -#: src/webex/pages/popup.tsx:161 +#: src/webex/pages/popup.tsx:160 #, c-format msgid "Balance" msgstr "" -#: src/webex/pages/popup.tsx:164 +#: src/webex/pages/popup.tsx:163 #, c-format msgid "History" msgstr "" -#: src/webex/pages/popup.tsx:167 +#: src/webex/pages/popup.tsx:166 #, c-format msgid "Debug" msgstr "" -#: src/webex/pages/popup.tsx:247 +#: src/webex/pages/popup.tsx:246 #, c-format msgid "help" msgstr "" -#: src/webex/pages/popup.tsx:252 +#: src/webex/pages/popup.tsx:251 #, c-format msgid "" "You have no balance to show. Need some\n" " %1$s getting started?\n" msgstr "" -#: src/webex/pages/popup.tsx:269 +#: src/webex/pages/popup.tsx:268 #, c-format msgid "%1$s incoming\n" msgstr "" -#: src/webex/pages/popup.tsx:282 +#: src/webex/pages/popup.tsx:281 #, c-format msgid "%1$s being spent\n" msgstr "" -#: src/webex/pages/popup.tsx:308 +#: src/webex/pages/popup.tsx:307 #, c-format msgid "Error: could not retrieve balance information." msgstr "" -#: src/webex/pages/popup.tsx:335 +#: src/webex/pages/popup.tsx:334 #, c-format msgid "Payback" msgstr "" -#: src/webex/pages/popup.tsx:336 +#: src/webex/pages/popup.tsx:335 #, c-format msgid "Return Electronic Cash to Bank Account" msgstr "" -#: src/webex/pages/popup.tsx:337 +#: src/webex/pages/popup.tsx:336 #, c-format msgid "Manage Trusted Auditors and Exchanges" msgstr "" -#: src/webex/pages/popup.tsx:349 +#: src/webex/pages/popup.tsx:348 #, c-format msgid "" "Bank requested reserve (%1$s) for\n" " %2$s.\n" msgstr "" -#: src/webex/pages/popup.tsx:360 +#: src/webex/pages/popup.tsx:359 #, c-format msgid "" "Started to withdraw\n" " %1$s from%2$s(%3$s).\n" msgstr "" -#: src/webex/pages/popup.tsx:370 +#: src/webex/pages/popup.tsx:369 #, c-format msgid "Merchant%1$soffered contract%2$s;\n" msgstr "" -#: src/webex/pages/popup.tsx:380 +#: src/webex/pages/popup.tsx:379 #, c-format msgid "Withdrew%1$sfrom%2$s(%3$s).\n" msgstr "" -#: src/webex/pages/popup.tsx:390 +#: src/webex/pages/popup.tsx:389 #, c-format msgid "" "Paid%1$sto merchant%2$s.\n" " (%3$s)\n" msgstr "" -#: src/webex/pages/popup.tsx:399 +#: src/webex/pages/popup.tsx:398 #, c-format msgid "Unknown event (%1$s)" msgstr "" -#: src/webex/pages/popup.tsx:442 +#: src/webex/pages/popup.tsx:441 #, c-format msgid "Error: could not retrieve event history" msgstr "" -#: src/webex/pages/popup.tsx:476 +#: src/webex/pages/popup.tsx:466 #, c-format msgid "Your wallet has no events recorded." msgstr "" diff --git a/src/i18n/fr.po b/src/i18n/fr.po index d804bd204..33e2a1755 100644 --- a/src/i18n/fr.po +++ b/src/i18n/fr.po @@ -198,105 +198,105 @@ msgstr "" msgid "Fatal error: \"%1$s\"." msgstr "" -#: src/webex/pages/popup.tsx:161 +#: src/webex/pages/popup.tsx:160 #, c-format msgid "Balance" msgstr "" -#: src/webex/pages/popup.tsx:164 +#: src/webex/pages/popup.tsx:163 #, c-format msgid "History" msgstr "" -#: src/webex/pages/popup.tsx:167 +#: src/webex/pages/popup.tsx:166 #, c-format msgid "Debug" msgstr "" -#: src/webex/pages/popup.tsx:247 +#: src/webex/pages/popup.tsx:246 #, c-format msgid "help" msgstr "" -#: src/webex/pages/popup.tsx:252 +#: src/webex/pages/popup.tsx:251 #, c-format msgid "" "You have no balance to show. Need some\n" " %1$s getting started?\n" msgstr "" -#: src/webex/pages/popup.tsx:269 +#: src/webex/pages/popup.tsx:268 #, c-format msgid "%1$s incoming\n" msgstr "" -#: src/webex/pages/popup.tsx:282 +#: src/webex/pages/popup.tsx:281 #, c-format msgid "%1$s being spent\n" msgstr "" -#: src/webex/pages/popup.tsx:308 +#: src/webex/pages/popup.tsx:307 #, c-format msgid "Error: could not retrieve balance information." msgstr "" -#: src/webex/pages/popup.tsx:335 +#: src/webex/pages/popup.tsx:334 #, c-format msgid "Payback" msgstr "" -#: src/webex/pages/popup.tsx:336 +#: src/webex/pages/popup.tsx:335 #, c-format msgid "Return Electronic Cash to Bank Account" msgstr "" -#: src/webex/pages/popup.tsx:337 +#: src/webex/pages/popup.tsx:336 #, c-format msgid "Manage Trusted Auditors and Exchanges" msgstr "" -#: src/webex/pages/popup.tsx:349 +#: src/webex/pages/popup.tsx:348 #, c-format msgid "" "Bank requested reserve (%1$s) for\n" " %2$s.\n" msgstr "" -#: src/webex/pages/popup.tsx:360 +#: src/webex/pages/popup.tsx:359 #, c-format msgid "" "Started to withdraw\n" " %1$s from%2$s(%3$s).\n" msgstr "" -#: src/webex/pages/popup.tsx:370 +#: src/webex/pages/popup.tsx:369 #, c-format msgid "Merchant%1$soffered contract%2$s;\n" msgstr "" -#: src/webex/pages/popup.tsx:380 +#: src/webex/pages/popup.tsx:379 #, c-format msgid "Withdrew%1$sfrom%2$s(%3$s).\n" msgstr "" -#: src/webex/pages/popup.tsx:390 +#: src/webex/pages/popup.tsx:389 #, c-format msgid "" "Paid%1$sto merchant%2$s.\n" " (%3$s)\n" msgstr "" -#: src/webex/pages/popup.tsx:399 +#: src/webex/pages/popup.tsx:398 #, c-format msgid "Unknown event (%1$s)" msgstr "" -#: src/webex/pages/popup.tsx:442 +#: src/webex/pages/popup.tsx:441 #, c-format msgid "Error: could not retrieve event history" msgstr "" -#: src/webex/pages/popup.tsx:476 +#: src/webex/pages/popup.tsx:466 #, c-format msgid "Your wallet has no events recorded." msgstr "" diff --git a/src/i18n/it.po b/src/i18n/it.po index d804bd204..33e2a1755 100644 --- a/src/i18n/it.po +++ b/src/i18n/it.po @@ -198,105 +198,105 @@ msgstr "" msgid "Fatal error: \"%1$s\"." msgstr "" -#: src/webex/pages/popup.tsx:161 +#: src/webex/pages/popup.tsx:160 #, c-format msgid "Balance" msgstr "" -#: src/webex/pages/popup.tsx:164 +#: src/webex/pages/popup.tsx:163 #, c-format msgid "History" msgstr "" -#: src/webex/pages/popup.tsx:167 +#: src/webex/pages/popup.tsx:166 #, c-format msgid "Debug" msgstr "" -#: src/webex/pages/popup.tsx:247 +#: src/webex/pages/popup.tsx:246 #, c-format msgid "help" msgstr "" -#: src/webex/pages/popup.tsx:252 +#: src/webex/pages/popup.tsx:251 #, c-format msgid "" "You have no balance to show. Need some\n" " %1$s getting started?\n" msgstr "" -#: src/webex/pages/popup.tsx:269 +#: src/webex/pages/popup.tsx:268 #, c-format msgid "%1$s incoming\n" msgstr "" -#: src/webex/pages/popup.tsx:282 +#: src/webex/pages/popup.tsx:281 #, c-format msgid "%1$s being spent\n" msgstr "" -#: src/webex/pages/popup.tsx:308 +#: src/webex/pages/popup.tsx:307 #, c-format msgid "Error: could not retrieve balance information." msgstr "" -#: src/webex/pages/popup.tsx:335 +#: src/webex/pages/popup.tsx:334 #, c-format msgid "Payback" msgstr "" -#: src/webex/pages/popup.tsx:336 +#: src/webex/pages/popup.tsx:335 #, c-format msgid "Return Electronic Cash to Bank Account" msgstr "" -#: src/webex/pages/popup.tsx:337 +#: src/webex/pages/popup.tsx:336 #, c-format msgid "Manage Trusted Auditors and Exchanges" msgstr "" -#: src/webex/pages/popup.tsx:349 +#: src/webex/pages/popup.tsx:348 #, c-format msgid "" "Bank requested reserve (%1$s) for\n" " %2$s.\n" msgstr "" -#: src/webex/pages/popup.tsx:360 +#: src/webex/pages/popup.tsx:359 #, c-format msgid "" "Started to withdraw\n" " %1$s from%2$s(%3$s).\n" msgstr "" -#: src/webex/pages/popup.tsx:370 +#: src/webex/pages/popup.tsx:369 #, c-format msgid "Merchant%1$soffered contract%2$s;\n" msgstr "" -#: src/webex/pages/popup.tsx:380 +#: src/webex/pages/popup.tsx:379 #, c-format msgid "Withdrew%1$sfrom%2$s(%3$s).\n" msgstr "" -#: src/webex/pages/popup.tsx:390 +#: src/webex/pages/popup.tsx:389 #, c-format msgid "" "Paid%1$sto merchant%2$s.\n" " (%3$s)\n" msgstr "" -#: src/webex/pages/popup.tsx:399 +#: src/webex/pages/popup.tsx:398 #, c-format msgid "Unknown event (%1$s)" msgstr "" -#: src/webex/pages/popup.tsx:442 +#: src/webex/pages/popup.tsx:441 #, c-format msgid "Error: could not retrieve event history" msgstr "" -#: src/webex/pages/popup.tsx:476 +#: src/webex/pages/popup.tsx:466 #, c-format msgid "Your wallet has no events recorded." msgstr "" diff --git a/src/i18n/taler-wallet-webex.pot b/src/i18n/taler-wallet-webex.pot index d804bd204..33e2a1755 100644 --- a/src/i18n/taler-wallet-webex.pot +++ b/src/i18n/taler-wallet-webex.pot @@ -198,105 +198,105 @@ msgstr "" msgid "Fatal error: \"%1$s\"." msgstr "" -#: src/webex/pages/popup.tsx:161 +#: src/webex/pages/popup.tsx:160 #, c-format msgid "Balance" msgstr "" -#: src/webex/pages/popup.tsx:164 +#: src/webex/pages/popup.tsx:163 #, c-format msgid "History" msgstr "" -#: src/webex/pages/popup.tsx:167 +#: src/webex/pages/popup.tsx:166 #, c-format msgid "Debug" msgstr "" -#: src/webex/pages/popup.tsx:247 +#: src/webex/pages/popup.tsx:246 #, c-format msgid "help" msgstr "" -#: src/webex/pages/popup.tsx:252 +#: src/webex/pages/popup.tsx:251 #, c-format msgid "" "You have no balance to show. Need some\n" " %1$s getting started?\n" msgstr "" -#: src/webex/pages/popup.tsx:269 +#: src/webex/pages/popup.tsx:268 #, c-format msgid "%1$s incoming\n" msgstr "" -#: src/webex/pages/popup.tsx:282 +#: src/webex/pages/popup.tsx:281 #, c-format msgid "%1$s being spent\n" msgstr "" -#: src/webex/pages/popup.tsx:308 +#: src/webex/pages/popup.tsx:307 #, c-format msgid "Error: could not retrieve balance information." msgstr "" -#: src/webex/pages/popup.tsx:335 +#: src/webex/pages/popup.tsx:334 #, c-format msgid "Payback" msgstr "" -#: src/webex/pages/popup.tsx:336 +#: src/webex/pages/popup.tsx:335 #, c-format msgid "Return Electronic Cash to Bank Account" msgstr "" -#: src/webex/pages/popup.tsx:337 +#: src/webex/pages/popup.tsx:336 #, c-format msgid "Manage Trusted Auditors and Exchanges" msgstr "" -#: src/webex/pages/popup.tsx:349 +#: src/webex/pages/popup.tsx:348 #, c-format msgid "" "Bank requested reserve (%1$s) for\n" " %2$s.\n" msgstr "" -#: src/webex/pages/popup.tsx:360 +#: src/webex/pages/popup.tsx:359 #, c-format msgid "" "Started to withdraw\n" " %1$s from%2$s(%3$s).\n" msgstr "" -#: src/webex/pages/popup.tsx:370 +#: src/webex/pages/popup.tsx:369 #, c-format msgid "Merchant%1$soffered contract%2$s;\n" msgstr "" -#: src/webex/pages/popup.tsx:380 +#: src/webex/pages/popup.tsx:379 #, c-format msgid "Withdrew%1$sfrom%2$s(%3$s).\n" msgstr "" -#: src/webex/pages/popup.tsx:390 +#: src/webex/pages/popup.tsx:389 #, c-format msgid "" "Paid%1$sto merchant%2$s.\n" " (%3$s)\n" msgstr "" -#: src/webex/pages/popup.tsx:399 +#: src/webex/pages/popup.tsx:398 #, c-format msgid "Unknown event (%1$s)" msgstr "" -#: src/webex/pages/popup.tsx:442 +#: src/webex/pages/popup.tsx:441 #, c-format msgid "Error: could not retrieve event history" msgstr "" -#: src/webex/pages/popup.tsx:476 +#: src/webex/pages/popup.tsx:466 #, c-format msgid "Your wallet has no events recorded." msgstr "" diff --git a/src/types.ts b/src/types.ts index 90fe7cf9c..4dea8f3b4 100644 --- a/src/types.ts +++ b/src/types.ts @@ -84,43 +84,53 @@ export interface ReserveRecord { * The reserve public key. */ reserve_pub: string; + /** * The reserve private key. */ reserve_priv: string; + /** * The exchange base URL. */ exchange_base_url: string; + /** * Time when the reserve was created. */ created: number; + /** - * Time when the reserve was last queried, - * or 'null' if it was never queried. + * Time when the reserve was depleted. + * Set to 0 if not depleted yet. + */ + timestamp_depleted: number; + + /** + * Time when the reserve was confirmed. + * + * Set to 0 if not confirmed yet. */ - last_query: number | null; + timestamp_confirmed: number; + /** * Current amount left in the reserve */ current_amount: AmountJson | null; + /** * Amount requested when the reserve was created. * When a reserve is re-used (rare!) the current_amount can * be higher than the requested_amount */ requested_amount: AmountJson; + /** * What's the current amount that sits * in precoins? */ precoin_amount: AmountJson; - /** - * The bank conformed that the reserve will eventually - * be filled with money. - */ - confirmed: boolean; + /** * We got some payback to this reserve. We'll cease to automatically * withdraw money from it. @@ -1188,6 +1198,9 @@ export class ProposalRecord { @Checkable.Optional(Checkable.Number) id?: number; + @Checkable.Number + timestamp: number; + /** * Verify that a value matches the schema of this class and convert it into a * member. @@ -1482,18 +1495,6 @@ export interface CheckPayResult { export type ConfirmPayResult = "paid" | "insufficient-balance"; -/** - * Level of detail at which a history - * entry should be shown. - */ -export enum HistoryLevel { - Trace = 1, - Developer = 2, - Expert = 3, - User = 4, -} - - /* * Activity history record. */ @@ -1518,11 +1519,6 @@ export interface HistoryRecord { * Details used when rendering the history record. */ detail: any; - - /** - * Level of detail of the history entry. - */ - level: HistoryLevel; } @@ -1701,6 +1697,18 @@ export interface PurchaseRecord { refundsPending: { [refundSig: string]: RefundPermission }; refundsDone: { [refundSig: string]: RefundPermission }; + + /** + * When was the purchase made? + * Refers to the time that the user accepted. + */ + timestamp: number; + + /** + * When was the last refund made? + * Set to 0 if no refund was made on the purchase. + */ + timestamp_refund: number; } diff --git a/src/wallet.ts b/src/wallet.ts index f194755e8..2f848c870 100644 --- a/src/wallet.ts +++ b/src/wallet.ts @@ -66,7 +66,6 @@ import { ExchangeHandle, ExchangeRecord, ExchangeWireFeesRecord, - HistoryLevel, HistoryRecord, Notifier, PayCoinInfo, @@ -321,7 +320,7 @@ export const WALLET_PROTOCOL_VERSION = "0:0:0"; * In the future we might consider adding migration functions for * each version increment. */ -export const WALLET_DB_VERSION = 19; +export const WALLET_DB_VERSION = 20; const builtinCurrencies: CurrencyRecord[] = [ { @@ -518,17 +517,6 @@ export namespace Stores { denomPubIndex = new Index(this, "denomPub", "denomPub"); } - class HistoryStore extends Store { - constructor() { - super("history", { - autoIncrement: true, - keyPath: "id", - }); - } - - timestampIndex = new Index(this, "timestamp", "timestamp"); - } - class ProposalsStore extends Store { constructor() { super("proposals", { @@ -536,6 +524,7 @@ export namespace Stores { keyPath: "id", }); } + timestampIndex = new Index(this, "timestamp", "timestamp"); } class PurchasesStore extends Store { @@ -545,6 +534,7 @@ export namespace Stores { fulfillmentUrlIndex = new Index(this, "fulfillment_url", "contractTerms.fulfillment_url"); orderIdIndex = new Index(this, "order_id", "contractTerms.order_id"); + timestampIndex = new Index(this, "timestamp", "timestamp"); } class DenominationsStore extends Store { @@ -577,6 +567,15 @@ export namespace Stores { } } + class ReservesStore extends Store { + constructor() { + super("reserves", {keyPath: "reserve_pub"}); + } + timestampCreatedIndex = new Index(this, "timestampCreated", "created"); + timestampConfirmedIndex = new Index(this, "timestampConfirmed", "timestamp_confirmed"); + timestampDepletedIndex = new Index(this, "timestampDepleted", "timestamp_depleted"); + } + export const coins = new CoinsStore(); export const coinsReturns = new Store("coinsReturns", {keyPath: "contractTermsHash"}); export const config = new ConfigStore(); @@ -584,12 +583,11 @@ export namespace Stores { export const denominations = new DenominationsStore(); export const exchangeWireFees = new ExchangeWireFeesStore(); export const exchanges = new ExchangeStore(); - export const history = new HistoryStore(); export const nonces = new NonceStore(); export const precoins = new Store("precoins", {keyPath: "coinPub"}); export const proposals = new ProposalsStore(); export const refresh = new Store("refresh", {keyPath: "id", autoIncrement: true}); - export const reserves = new Store("reserves", {keyPath: "reserve_pub"}); + export const reserves = new ReservesStore(); export const purchases = new PurchasesStore(); } @@ -952,24 +950,12 @@ export class Wallet { payReq, refundsDone: {}, refundsPending: {}, - }; - - const historyEntry: HistoryRecord = { - detail: { - amount: proposal.contractTerms.amount, - contractTermsHash: proposal.contractTermsHash, - fulfillmentUrl: proposal.contractTerms.fulfillment_url, - merchantName: proposal.contractTerms.merchant.name, - }, - level: HistoryLevel.User, - subjectId: `contract-${proposal.contractTermsHash}`, timestamp: (new Date()).getTime(), - type: "pay", + timestamp_refund: 0, }; await this.q() .put(Stores.purchases, t) - .put(Stores.history, historyEntry) .putAll(Stores.coins, payCoinInfo.map((pci) => pci.updatedCoin)) .finish(); @@ -977,12 +963,6 @@ export class Wallet { } - async putHistory(historyEntry: HistoryRecord): Promise { - await this.q().put(Stores.history, historyEntry).finish(); - this.notifier.notify(); - } - - /** * Save a proposal in the database and return an id for it to * retrieve it later. @@ -1115,23 +1095,7 @@ export class Wallet { try { const reserve = await this.updateReserve(reserveRecord.reserve_pub); - const n = await this.depleteReserve(reserve); - - if (n !== 0) { - const depleted: HistoryRecord = { - detail: { - currentAmount: reserveRecord.current_amount, - exchangeBaseUrl: reserveRecord.exchange_base_url, - requestedAmount: reserveRecord.requested_amount, - reservePub: reserveRecord.reserve_pub, - }, - level: HistoryLevel.User, - subjectId: `reserve-progress-${reserveRecord.reserve_pub}`, - timestamp: (new Date()).getTime(), - type: "depleted-reserve", - }; - await this.q().put(Stores.history, depleted).finish(); - } + await this.depleteReserve(reserve); } catch (e) { // random, exponential backoff truncated at 3 minutes const nextDelay = Math.min(2 * retryDelayMs + retryDelayMs * Math.random(), 3000 * 60); @@ -1143,8 +1107,12 @@ export class Wallet { } + /** + * Given a planchet, withdraw a coin from the exchange. + */ private async processPreCoin(preCoin: PreCoinRecord, retryDelayMs = 200): Promise { + // Throttle concurrent executions of this function, so we don't withdraw too many coins at once. if (this.processPreCoinConcurrent >= 4 || this.processPreCoinThrottle[preCoin.exchangeBaseUrl]) { console.log("delaying processPreCoin"); this.timerGroup.after(retryDelayMs, () => this.processPreCoin(preCoin, Math.min(retryDelayMs * 2, 5 * 60 * 1000))); @@ -1156,7 +1124,7 @@ export class Wallet { const exchange = await this.q().get(Stores.exchanges, preCoin.exchangeBaseUrl); if (!exchange) { - console.error("db inconsistend: exchange for precoin not found"); + console.error("db inconsistent: exchange for precoin not found"); return; } const denom = await this.q().get(Stores.denominations, @@ -1182,20 +1150,10 @@ export class Wallet { return r; }; - const historyEntry: HistoryRecord = { - detail: { - coinPub: coin.coinPub, - }, - level: HistoryLevel.Expert, - timestamp: (new Date()).getTime(), - type: "withdraw", - }; - await this.q() .mutate(Stores.reserves, preCoin.reservePub, mutateReserve) .delete("precoins", coin.coinPub) .add(Stores.coins, coin) - .add(Stores.history, historyEntry) .finish(); this.notifier.notify(); @@ -1228,28 +1186,17 @@ export class Wallet { const canonExchange = canonicalizeBaseUrl(req.exchange); const reserveRecord: ReserveRecord = { - confirmed: false, created: now, current_amount: null, exchange_base_url: canonExchange, hasPayback: false, - last_query: null, precoin_amount: Amounts.getZero(req.amount.currency), requested_amount: req.amount, reserve_priv: keypair.priv, reserve_pub: keypair.pub, senderWire: req.senderWire, - }; - - const historyEntry = { - detail: { - requestedAmount: req.amount, - reservePub: reserveRecord.reserve_pub, - }, - level: HistoryLevel.Expert, - subjectId: `reserve-progress-${reserveRecord.reserve_pub}`, - timestamp: now, - type: "create-reserve", + timestamp_confirmed: 0, + timestamp_depleted: 0, }; const exchangeInfo = await this.updateExchangeFromUrl(req.exchange); @@ -1271,7 +1218,6 @@ export class Wallet { await this.q() .put(Stores.currencies, currencyRecord) .put(Stores.reserves, reserveRecord) - .put(Stores.history, historyEntry) .finish(); const r: CreateReserveResponse = { @@ -1301,21 +1247,9 @@ export class Wallet { return; } console.log("reserve confirmed"); - const historyEntry: HistoryRecord = { - detail: { - exchangeBaseUrl: reserve.exchange_base_url, - requestedAmount: reserve.requested_amount, - reservePub: req.reservePub, - }, - level: HistoryLevel.User, - subjectId: `reserve-progress-${reserve.reserve_pub}`, - timestamp: now, - type: "confirm-reserve", - }; - reserve.confirmed = true; + reserve.timestamp_confirmed = now; await this.q() .put(Stores.reserves, reserve) - .put(Stores.history, historyEntry) .finish(); this.notifier.notify(); @@ -1366,8 +1300,11 @@ export class Wallet { /** * Withdraw coins from a reserve until it is empty. + * + * When finished, marks the reserve as depleted by setting + * the depleted timestamp. */ - private async depleteReserve(reserve: ReserveRecord): Promise { + private async depleteReserve(reserve: ReserveRecord): Promise { console.log("depleting reserve"); if (!reserve.current_amount) { throw Error("can't withdraw when amount is unknown"); @@ -1377,6 +1314,7 @@ export class Wallet { throw Error("can't withdraw when amount is unknown"); } const denomsForWithdraw = await this.getVerifiedWithdrawDenomList(reserve.exchange_base_url, withdrawAmount); + const smallestAmount = await this.getVerifiedSmallestWithdrawAmount(reserve.exchange_base_url); console.log(`withdrawing ${denomsForWithdraw.length} coins`); @@ -1398,6 +1336,11 @@ export class Wallet { } r.current_amount = result.amount; + // Reserve is depleted if the amount left is too small to withdraw + if (Amounts.cmp(r.current_amount, smallestAmount) < 0) { + r.timestamp_depleted = (new Date()).getTime(); + } + console.log(`after creating precoin: current ${amountToPretty(r.current_amount)}, precoin: ${amountToPretty( r.precoin_amount)})}`); @@ -1413,7 +1356,6 @@ export class Wallet { }); await Promise.all(ps); - return ps.length; } @@ -1437,24 +1379,9 @@ export class Wallet { if (!reserveInfo) { throw Error(); } - const oldAmount = reserve.current_amount; - const newAmount = reserveInfo.balance; reserve.current_amount = reserveInfo.balance; - const historyEntry = { - detail: { - newAmount, - oldAmount, - requestedAmount: reserve.requested_amount, - reservePub, - }, - level: HistoryLevel.Developer, - subjectId: `reserve-progress-${reserve.reserve_pub}`, - timestamp: (new Date()).getTime(), - type: "reserve-update", - }; await this.q() .put(Stores.reserves, reserve) - .put(Stores.history, historyEntry) .finish(); this.notifier.notify(); return reserve; @@ -1489,6 +1416,52 @@ export class Wallet { ); } + + /** + * Compute the smallest withdrawable amount possible, based on verified denominations. + * + * Writes to the DB in order to record the result from verifying + * denominations. + */ + async getVerifiedSmallestWithdrawAmount(exchangeBaseUrl: string): Promise { + const exchange = await this.q().get(Stores.exchanges, exchangeBaseUrl); + if (!exchange) { + throw Error(`exchange ${exchangeBaseUrl} not found`); + } + + const possibleDenoms = await ( + this.q().iterIndex(Stores.denominations.exchangeBaseUrlIndex, + exchange.baseUrl) + .filter((d) => d.status === DenominationStatus.Unverified || d.status === DenominationStatus.VerifiedGood) + .toArray() + ); + possibleDenoms.sort((d1, d2) => { + let a1 = Amounts.add(d1.feeWithdraw, d1.value).amount; + let a2 = Amounts.add(d2.feeWithdraw, d2.value).amount; + return Amounts.cmp(a1, a2); + }); + + for (let denom of possibleDenoms) { + if (denom.status === DenominationStatus.VerifiedGood) { + return Amounts.add(denom.feeWithdraw, denom.value).amount; + } + console.log(`verifying denom ${denom.denomPub.substr(0, 15)}`); + const valid = await this.cryptoApi.isValidDenom(denom, + exchange.masterPublicKey); + if (!valid) { + denom.status = DenominationStatus.VerifiedBad; + } else { + denom.status = DenominationStatus.VerifiedGood; + } + await this.q().put(Stores.denominations, denom).finish(); + if (valid) { + return Amounts.add(denom.feeWithdraw, denom.value).amount; + } + } + return Amounts.getZero(exchange.currency); + } + + /** * Get a list of denominations to withdraw from the given exchange for the * given amount, making sure that all denominations' signatures are verified. @@ -1895,7 +1868,7 @@ export class Wallet { } function collectPendingWithdraw(r: ReserveRecord, balance: WalletBalance) { - if (!r.confirmed) { + if (!r.timestamp_confirmed) { return balance; } let amount = r.current_amount; @@ -2253,16 +2226,79 @@ export class Wallet { * Retrive the full event history for this wallet. */ async getHistory(): Promise<{history: HistoryRecord[]}> { - function collect(x: any, acc: any) { - acc.push(x); - return acc; + const history: HistoryRecord[] = []; + + // FIXME: do pagination instead of generating the full history + + const proposals = await this.q().iter(Stores.proposals).toArray(); + for (let p of proposals) { + history.push({ + type: "offer-contract", + timestamp: p.timestamp, + detail: { + contractTermsHash: p.contractTermsHash, + merchantName: p.contractTerms.merchant.name, + }, + }); } - const history = await ( - this.q() - .iterIndex(Stores.history.timestampIndex) - .reduce(collect, [])); + const purchases = await this.q().iter(Stores.purchases).toArray(); + for (let p of purchases) { + history.push({ + type: "pay", + timestamp: p.timestamp, + detail: { + amount: p.contractTerms.amount, + contractTermsHash: p.contractTermsHash, + fulfillmentUrl: p.contractTerms.fulfillment_url, + merchantName: p.contractTerms.merchant.name, + }, + }); + if (p.timestamp_refund) { + const amountsPending = Object.keys(p.refundsPending).map((x) => p.refundsPending[x].refund_amount); + const amountsDone = Object.keys(p.refundsDone).map((x) => p.refundsDone[x].refund_amount); + const amounts: AmountJson[] = amountsPending.concat(amountsDone); + const amount = Amounts.add(Amounts.getZero(p.contractTerms.amount.currency), ...amounts).amount; + + history.push({ + type: "refund", + timestamp: p.timestamp_refund, + detail: { + refundAmount: amount, + contractTermsHash: p.contractTermsHash, + fulfillmentUrl: p.contractTerms.fulfillment_url, + merchantName: p.contractTerms.merchant.name, + }, + }); + } + } + const reserves: ReserveRecord[] = await this.q().iter(Stores.reserves).toArray(); + for (let r of reserves) { + history.push({ + type: "create-reserve", + timestamp: r.created, + detail: { + exchangeBaseUrl: r.exchange_base_url, + requestedAmount: r.requested_amount, + reservePub: r.reserve_pub, + }, + }); + if (r.timestamp_depleted) { + history.push({ + type: "depleted-reserve", + timestamp: r.timestamp_depleted, + detail: { + exchangeBaseUrl: r.exchange_base_url, + requestedAmount: r.requested_amount, + reservePub: r.reserve_pub, + }, + }); + } + } + + history.sort((h1, h2) => Math.sign(h1.timestamp - h2.timestamp)); + return {history}; } @@ -2656,6 +2692,8 @@ export class Wallet { return; } + t.timestamp_refund = (new Date()).getTime(); + for (const perm of refundPermissions) { if (!t.refundsPending[perm.merchant_sig] && !t.refundsDone[perm.merchant_sig]) { t.refundsPending[perm.merchant_sig] = perm; diff --git a/src/webex/messages.ts b/src/webex/messages.ts index 122bd8fe2..ca51abf1d 100644 --- a/src/webex/messages.ts +++ b/src/webex/messages.ts @@ -92,10 +92,6 @@ export interface MessageMap { request: { contract: object }; response: string; }; - "put-history-entry": { - request: { historyEntry: types.HistoryRecord }; - response: void; - }; "save-proposal": { request: { proposal: types.ProposalRecord }; response: void; diff --git a/src/webex/notify.ts b/src/webex/notify.ts index 5e024d619..7086ca95d 100644 --- a/src/webex/notify.ts +++ b/src/webex/notify.ts @@ -210,6 +210,7 @@ async function downloadContract(url: string, nonce: string): Promise { } async function processProposal(proposal: any) { + if (!proposal.data) { console.error("field proposal.data field missing"); return; @@ -235,17 +236,12 @@ async function processProposal(proposal: any) { // bad contract / name not included } - const historyEntry = { - detail: { - contractHash, - merchantName, - }, - subjectId: `contract-${contractHash}`, + let proposalId = await wxApi.saveProposal({ timestamp: (new Date()).getTime(), - type: "offer-contract", - }; - await wxApi.putHistory(historyEntry); - let proposalId = await wxApi.saveProposal(proposal); + contractTerms: proposal.data, + contractTermsHash: proposal.hash, + merchantSig: proposal.sig, + }); const uri = new URI(chrome.extension.getURL("/src/webex/pages/confirm-contract.html")); const params = { diff --git a/src/webex/pages/popup.tsx b/src/webex/pages/popup.tsx index 7d12d365e..4e4e9687c 100644 --- a/src/webex/pages/popup.tsx +++ b/src/webex/pages/popup.tsx @@ -29,7 +29,6 @@ import * as i18n from "../../i18n"; import { AmountJson, Amounts, - HistoryLevel, HistoryRecord, WalletBalance, WalletBalanceEntry, @@ -354,8 +353,7 @@ function formatHistoryItem(historyItem: HistoryRecord) { ); case "confirm-reserve": { - // FIXME: eventually remove compat fix - const exchange = d.exchangeBaseUrl ? (new URI(d.exchangeBaseUrl)).host() : "??"; + const exchange = (new URI(d.exchangeBaseUrl)).host(); const pub = abbrev(d.reservePub); return ( @@ -369,7 +367,7 @@ function formatHistoryItem(historyItem: HistoryRecord) { const link = chrome.extension.getURL("view-contract.html"); return ( - Merchant {abbrev(d.merchantName, 15)} offered contract {abbrev(d.contractHash)}; + Merchant {abbrev(d.merchantName, 15)} offered contract {abbrev(d.contractTermsHash)}; ); } @@ -395,6 +393,14 @@ function formatHistoryItem(historyItem: HistoryRecord) { ); } + case "refund": { + const merchantElem = {abbrev(d.merchantName, 15)}; + return ( + + Merchant {merchantElem} gave a refund over {renderAmount(d.refundAmount)}. + + ); + } default: return (

{i18n.str`Unknown event (${historyItem.type})`}

); } @@ -447,17 +453,8 @@ class WalletHistory extends React.Component { return ; } - const subjectMemo: {[s: string]: boolean} = {}; const listing: any[] = []; for (const record of history.reverse()) { - if (record.subjectId && subjectMemo[record.subjectId]) { - continue; - } - if (record.level !== undefined && record.level < HistoryLevel.User) { - continue; - } - subjectMemo[record.subjectId as string] = true; - const item = (
diff --git a/src/webex/pages/refund.tsx b/src/webex/pages/refund.tsx index d2c21c2f4..73bed30ee 100644 --- a/src/webex/pages/refund.tsx +++ b/src/webex/pages/refund.tsx @@ -63,10 +63,12 @@ const RefundDetail = ({purchase, fullRefundFees}: {purchase: types.PurchaseRecor amountDone = types.Amounts.add(amountDone, purchase.refundsDone[k].refund_amount).amount; } + const hasPending = amountPending.fraction !== 0 || amountPending.value !== 0; + return (
-

Refund fully received: (refund fees: )

-

Refund incoming:

+ {hasPending ?

Refund pending:

: null} +

Refund received: (refund fees: )

); }; diff --git a/src/webex/pages/tree.tsx b/src/webex/pages/tree.tsx index 2d542f01d..072150312 100644 --- a/src/webex/pages/tree.tsx +++ b/src/webex/pages/tree.tsx @@ -61,7 +61,7 @@ class ReserveView extends React.Component {
  • Created: {(new Date(r.created * 1000).toString())}
  • Current: {r.current_amount ? renderAmount(r.current_amount!) : "null"}
  • Requested: {renderAmount(r.requested_amount)}
  • -
  • Confirmed: {r.confirmed}
  • +
  • Confirmed: {r.timestamp_confirmed}
  • ); diff --git a/src/webex/wxApi.ts b/src/webex/wxApi.ts index 096d855e0..c1c6eb2d3 100644 --- a/src/webex/wxApi.ts +++ b/src/webex/wxApi.ts @@ -226,7 +226,7 @@ export function hashContract(contract: object): Promise { * the proposal is stored under. */ export function saveProposal(proposal: any): Promise { - return callBackend("save-proposal", proposal); + return callBackend("save-proposal", { proposal }); } /** @@ -243,13 +243,6 @@ export function queryPayment(url: string): Promise { return callBackend("query-payment", { url }); } -/** - * Add a new history item. - */ -export function putHistory(historyEntry: any): Promise { - return callBackend("put-history-entry", { historyEntry }); -} - /** * Mark a payment as succeeded. */ diff --git a/src/webex/wxBackend.ts b/src/webex/wxBackend.ts index 16da3d97d..e7f571b92 100644 --- a/src/webex/wxBackend.ts +++ b/src/webex/wxBackend.ts @@ -177,19 +177,9 @@ function handleMessage(sender: MessageSender, return hash; }); } - case "put-history-entry": { - if (!detail.historyEntry) { - return Promise.resolve({ error: "historyEntry missing" }); - } - return needsWallet().putHistory(detail.historyEntry); - } case "save-proposal": { console.log("handling save-proposal", detail); - const checkedRecord = ProposalRecord.checked({ - contractTerms: detail.data, - contractTermsHash: detail.hash, - merchantSig: detail.sig, - }); + const checkedRecord = ProposalRecord.checked(detail.proposal); return needsWallet().saveProposal(checkedRecord); } case "reserve-creation-info": { diff --git a/tsconfig.json b/tsconfig.json index b99b28b3e..ee2f58640 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -63,12 +63,14 @@ "src/webex/pages/logs.tsx", "src/webex/pages/payback.tsx", "src/webex/pages/popup.tsx", + "src/webex/pages/refund.tsx", "src/webex/pages/reset-required.tsx", "src/webex/pages/return-coins.tsx", "src/webex/pages/show-db.ts", "src/webex/pages/tree.tsx", "src/webex/renderHtml.tsx", "src/webex/wxApi.ts", - "src/webex/wxBackend.ts" + "src/webex/wxBackend.ts", + "src/wire.ts" ] } \ No newline at end of file -- cgit v1.2.3