From b8ccc7c990a1542cf80578b41972f9a5b0870af9 Mon Sep 17 00:00:00 2001 From: Florian Dold Date: Thu, 30 Nov 2017 04:07:36 +0100 Subject: partial implementation of tipping --- img/spinner-bars.svg | 53 +++++++ src/crypto/cryptoApi.ts | 5 + src/crypto/cryptoWorker.ts | 31 ++++ src/i18n/de.po | 113 +++++++-------- src/i18n/en-US.po | 113 +++++++-------- src/i18n/fr.po | 113 +++++++-------- src/i18n/it.po | 113 +++++++-------- src/i18n/strings.ts | 192 ++++++++++++------------- src/i18n/taler-wallet-webex.pot | 113 +++++++-------- src/query.ts | 70 ++++++--- src/types.ts | 224 +++++++++++++++++++++++++++++ src/wallet.ts | 183 +++++++++++++++++++---- src/webex/messages.ts | 16 +++ src/webex/notify.ts | 85 ++++++++++- src/webex/pages/confirm-create-reserve.tsx | 135 +---------------- src/webex/pages/tip.html | 24 ++++ src/webex/pages/tip.tsx | 155 ++++++++++++++++++++ src/webex/renderHtml.tsx | 145 +++++++++++++++++++ src/webex/style/wallet.css | 15 ++ src/webex/wxApi.ts | 23 +++ src/webex/wxBackend.ts | 25 +++- tsconfig.json | 1 + webpack.config.js | 1 + 23 files changed, 1393 insertions(+), 555 deletions(-) create mode 100644 img/spinner-bars.svg create mode 100644 src/webex/pages/tip.html create mode 100644 src/webex/pages/tip.tsx diff --git a/img/spinner-bars.svg b/img/spinner-bars.svg new file mode 100644 index 000000000..f6f7dfcb3 --- /dev/null +++ b/img/spinner-bars.svg @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/crypto/cryptoApi.ts b/src/crypto/cryptoApi.ts index 00013f0d3..5300c1370 100644 --- a/src/crypto/cryptoApi.ts +++ b/src/crypto/cryptoApi.ts @@ -34,6 +34,7 @@ import { PreCoinRecord, RefreshSessionRecord, ReserveRecord, + TipPlanchet, WireFee, } from "../types"; @@ -253,6 +254,10 @@ export class CryptoApi { return this.doRpc("createPreCoin", 1, denom, reserve); } + createTipPlanchet(denom: DenominationRecord): Promise { + return this.doRpc("createTipPlanchet", 1, denom); + } + hashString(str: string): Promise { return this.doRpc("hashString", 1, str); } diff --git a/src/crypto/cryptoWorker.ts b/src/crypto/cryptoWorker.ts index 0a93fcb07..5ec7c18e5 100644 --- a/src/crypto/cryptoWorker.ts +++ b/src/crypto/cryptoWorker.ts @@ -40,6 +40,7 @@ import { RefreshPreCoinRecord, RefreshSessionRecord, ReserveRecord, + TipPlanchet, WireFee, } from "../types"; @@ -103,6 +104,7 @@ namespace RpcFunctions { coinValue: denom.value, denomPub: denomPub.encode().toCrock(), exchangeBaseUrl: reserve.exchange_base_url, + isFromTip: false, reservePub: reservePub.toCrock(), withdrawSig: sig.toCrock(), }; @@ -110,6 +112,35 @@ namespace RpcFunctions { } + export function createTipPlanchet(denom: DenominationRecord): TipPlanchet { + const denomPub = native.RsaPublicKey.fromCrock(denom.denomPub); + const coinPriv = native.EddsaPrivateKey.create(); + const coinPub = coinPriv.getPublicKey(); + const blindingFactor = native.RsaBlindingKeySecret.create(); + const pubHash: native.HashCode = coinPub.hash(); + const ev = native.rsaBlind(pubHash, blindingFactor, denomPub); + + if (!ev) { + throw Error("couldn't blind (malicious exchange key?)"); + } + + if (!denom.feeWithdraw) { + throw Error("Field fee_withdraw missing"); + } + + const tipPlanchet: TipPlanchet = { + blindingKey: blindingFactor.toCrock(), + coinEv: ev.toCrock(), + coinPriv: coinPriv.toCrock(), + coinPub: coinPub.toCrock(), + coinValue: denom.value, + denomPubHash: denomPub.encode().hash().toCrock(), + denomPub: denomPub.encode().toCrock(), + }; + return tipPlanchet; + } + + /** * Create and sign a message to request payback for a coin. */ diff --git a/src/i18n/de.po b/src/i18n/de.po index 253abb9e2..23b5ad535 100644 --- a/src/i18n/de.po +++ b/src/i18n/de.po @@ -66,67 +66,27 @@ msgstr "" msgid "Confirm payment" msgstr "Bezahlung bestätigen" -#: src/webex/pages/confirm-create-reserve.tsx:178 -#, fuzzy, c-format -msgid "Withdrawal fees:" -msgstr "Abheben bei %1$s" - -#: src/webex/pages/confirm-create-reserve.tsx:179 -#, c-format -msgid "Rounding loss:" -msgstr "" - -#: src/webex/pages/confirm-create-reserve.tsx:180 -#, c-format -msgid "Earliest expiration (for deposit): %1$s" -msgstr "" - -#: src/webex/pages/confirm-create-reserve.tsx:185 -#, c-format -msgid "# Coins" -msgstr "" - -#: src/webex/pages/confirm-create-reserve.tsx:186 -#, c-format -msgid "Value" -msgstr "" - -#: src/webex/pages/confirm-create-reserve.tsx:187 -#, fuzzy, c-format -msgid "Withdraw Fee" -msgstr "Abheben bei %1$s" - -#: src/webex/pages/confirm-create-reserve.tsx:188 -#, c-format -msgid "Refresh Fee" -msgstr "" - -#: src/webex/pages/confirm-create-reserve.tsx:189 -#, c-format -msgid "Deposit Fee" -msgstr "" - -#: src/webex/pages/confirm-create-reserve.tsx:243 +#: src/webex/pages/confirm-create-reserve.tsx:121 #, c-format msgid "Select" msgstr "" -#: src/webex/pages/confirm-create-reserve.tsx:259 +#: src/webex/pages/confirm-create-reserve.tsx:137 #, c-format msgid "Error: URL may not be relative" msgstr "" -#: src/webex/pages/confirm-create-reserve.tsx:327 +#: src/webex/pages/confirm-create-reserve.tsx:205 #, c-format msgid "The exchange is trusted by the wallet.\n" msgstr "" -#: src/webex/pages/confirm-create-reserve.tsx:333 +#: src/webex/pages/confirm-create-reserve.tsx:211 #, c-format msgid "The exchange is audited by a trusted auditor.\n" msgstr "" -#: src/webex/pages/confirm-create-reserve.tsx:339 +#: src/webex/pages/confirm-create-reserve.tsx:217 #, c-format msgid "" "Warning: The exchange is neither directly trusted nor audited by a trusted " @@ -134,7 +94,7 @@ msgid "" "If you withdraw from this exchange, it will be trusted in the future.\n" msgstr "" -#: src/webex/pages/confirm-create-reserve.tsx:348 +#: src/webex/pages/confirm-create-reserve.tsx:226 #, c-format msgid "" "Using exchange provider%1$s.\n" @@ -142,58 +102,59 @@ msgid "" " %2$s in fees.\n" msgstr "" -#: src/webex/pages/confirm-create-reserve.tsx:362 +#: src/webex/pages/confirm-create-reserve.tsx:240 #, c-format msgid "" "Waiting for a response from\n" " %1$s" msgstr "" -#: src/webex/pages/confirm-create-reserve.tsx:379 +#: src/webex/pages/confirm-create-reserve.tsx:257 #, c-format msgid "" "Information about fees will be available when an exchange provider is " "selected." msgstr "" -#: src/webex/pages/confirm-create-reserve.tsx:422 +#: src/webex/pages/confirm-create-reserve.tsx:300 #, c-format msgid "Accept fees and withdraw" msgstr "" -#: src/webex/pages/confirm-create-reserve.tsx:427 +#: src/webex/pages/confirm-create-reserve.tsx:305 #, c-format msgid "Change Exchange Provider" msgstr "" -#: src/webex/pages/confirm-create-reserve.tsx:484 +#: src/webex/pages/confirm-create-reserve.tsx:357 #, c-format msgid "You are about to withdraw %1$s from your bank account into your wallet." msgstr "" -#: src/webex/pages/confirm-create-reserve.tsx:569 +#: src/webex/pages/confirm-create-reserve.tsx:442 #, c-format msgid "" "Oops, something went wrong. The wallet responded with error status (%1$s)." msgstr "" -#: src/webex/pages/confirm-create-reserve.tsx:578 +#: src/webex/pages/confirm-create-reserve.tsx:451 #, c-format msgid "Checking URL, please wait ..." msgstr "" -#: src/webex/pages/confirm-create-reserve.tsx:592 +#: src/webex/pages/confirm-create-reserve.tsx:465 #, c-format msgid "Can't parse amount: %1$s" msgstr "" -#: src/webex/pages/confirm-create-reserve.tsx:599 +#: src/webex/pages/confirm-create-reserve.tsx:472 #, c-format msgid "Can't parse wire_types: %1$s" msgstr "" +#. #-#-#-#-# - (PACKAGE VERSION) #-#-#-#-# #. TODO:generic error reporting function or component. -#: src/webex/pages/confirm-create-reserve.tsx:625 +#: src/webex/pages/confirm-create-reserve.tsx:498 src/webex/pages/tip.tsx:148 #, c-format msgid "Fatal error: \"%1$s\"." msgstr "" @@ -324,6 +285,46 @@ msgstr "Bezahlung bestätigen" msgid "Cancel" msgstr "Saldo" +#: src/webex/renderHtml.tsx:209 +#, fuzzy, c-format +msgid "Withdrawal fees:" +msgstr "Abheben bei %1$s" + +#: src/webex/renderHtml.tsx:210 +#, c-format +msgid "Rounding loss:" +msgstr "" + +#: src/webex/renderHtml.tsx:211 +#, c-format +msgid "Earliest expiration (for deposit): %1$s" +msgstr "" + +#: src/webex/renderHtml.tsx:216 +#, c-format +msgid "# Coins" +msgstr "" + +#: src/webex/renderHtml.tsx:217 +#, c-format +msgid "Value" +msgstr "" + +#: src/webex/renderHtml.tsx:218 +#, fuzzy, c-format +msgid "Withdraw Fee" +msgstr "Abheben bei %1$s" + +#: src/webex/renderHtml.tsx:219 +#, c-format +msgid "Refresh Fee" +msgstr "" + +#: src/webex/renderHtml.tsx:220 +#, c-format +msgid "Deposit Fee" +msgstr "" + #: src/wire.ts:38 #, c-format msgid "Invalid Wire" diff --git a/src/i18n/en-US.po b/src/i18n/en-US.po index 85926f211..6ceaf22aa 100644 --- a/src/i18n/en-US.po +++ b/src/i18n/en-US.po @@ -66,67 +66,27 @@ msgstr "" msgid "Confirm payment" msgstr "" -#: src/webex/pages/confirm-create-reserve.tsx:178 -#, c-format -msgid "Withdrawal fees:" -msgstr "" - -#: src/webex/pages/confirm-create-reserve.tsx:179 -#, c-format -msgid "Rounding loss:" -msgstr "" - -#: src/webex/pages/confirm-create-reserve.tsx:180 -#, c-format -msgid "Earliest expiration (for deposit): %1$s" -msgstr "" - -#: src/webex/pages/confirm-create-reserve.tsx:185 -#, c-format -msgid "# Coins" -msgstr "" - -#: src/webex/pages/confirm-create-reserve.tsx:186 -#, c-format -msgid "Value" -msgstr "" - -#: src/webex/pages/confirm-create-reserve.tsx:187 -#, c-format -msgid "Withdraw Fee" -msgstr "" - -#: src/webex/pages/confirm-create-reserve.tsx:188 -#, c-format -msgid "Refresh Fee" -msgstr "" - -#: src/webex/pages/confirm-create-reserve.tsx:189 -#, c-format -msgid "Deposit Fee" -msgstr "" - -#: src/webex/pages/confirm-create-reserve.tsx:243 +#: src/webex/pages/confirm-create-reserve.tsx:121 #, c-format msgid "Select" msgstr "" -#: src/webex/pages/confirm-create-reserve.tsx:259 +#: src/webex/pages/confirm-create-reserve.tsx:137 #, c-format msgid "Error: URL may not be relative" msgstr "" -#: src/webex/pages/confirm-create-reserve.tsx:327 +#: src/webex/pages/confirm-create-reserve.tsx:205 #, c-format msgid "The exchange is trusted by the wallet.\n" msgstr "" -#: src/webex/pages/confirm-create-reserve.tsx:333 +#: src/webex/pages/confirm-create-reserve.tsx:211 #, c-format msgid "The exchange is audited by a trusted auditor.\n" msgstr "" -#: src/webex/pages/confirm-create-reserve.tsx:339 +#: src/webex/pages/confirm-create-reserve.tsx:217 #, c-format msgid "" "Warning: The exchange is neither directly trusted nor audited by a trusted " @@ -134,7 +94,7 @@ msgid "" "If you withdraw from this exchange, it will be trusted in the future.\n" msgstr "" -#: src/webex/pages/confirm-create-reserve.tsx:348 +#: src/webex/pages/confirm-create-reserve.tsx:226 #, c-format msgid "" "Using exchange provider%1$s.\n" @@ -142,58 +102,59 @@ msgid "" " %2$s in fees.\n" msgstr "" -#: src/webex/pages/confirm-create-reserve.tsx:362 +#: src/webex/pages/confirm-create-reserve.tsx:240 #, c-format msgid "" "Waiting for a response from\n" " %1$s" msgstr "" -#: src/webex/pages/confirm-create-reserve.tsx:379 +#: src/webex/pages/confirm-create-reserve.tsx:257 #, c-format msgid "" "Information about fees will be available when an exchange provider is " "selected." msgstr "" -#: src/webex/pages/confirm-create-reserve.tsx:422 +#: src/webex/pages/confirm-create-reserve.tsx:300 #, c-format msgid "Accept fees and withdraw" msgstr "" -#: src/webex/pages/confirm-create-reserve.tsx:427 +#: src/webex/pages/confirm-create-reserve.tsx:305 #, c-format msgid "Change Exchange Provider" msgstr "" -#: src/webex/pages/confirm-create-reserve.tsx:484 +#: src/webex/pages/confirm-create-reserve.tsx:357 #, c-format msgid "You are about to withdraw %1$s from your bank account into your wallet." msgstr "" -#: src/webex/pages/confirm-create-reserve.tsx:569 +#: src/webex/pages/confirm-create-reserve.tsx:442 #, c-format msgid "" "Oops, something went wrong. The wallet responded with error status (%1$s)." msgstr "" -#: src/webex/pages/confirm-create-reserve.tsx:578 +#: src/webex/pages/confirm-create-reserve.tsx:451 #, c-format msgid "Checking URL, please wait ..." msgstr "" -#: src/webex/pages/confirm-create-reserve.tsx:592 +#: src/webex/pages/confirm-create-reserve.tsx:465 #, c-format msgid "Can't parse amount: %1$s" msgstr "" -#: src/webex/pages/confirm-create-reserve.tsx:599 +#: src/webex/pages/confirm-create-reserve.tsx:472 #, c-format msgid "Can't parse wire_types: %1$s" msgstr "" +#. #-#-#-#-# - (PACKAGE VERSION) #-#-#-#-# #. TODO:generic error reporting function or component. -#: src/webex/pages/confirm-create-reserve.tsx:625 +#: src/webex/pages/confirm-create-reserve.tsx:498 src/webex/pages/tip.tsx:148 #, c-format msgid "Fatal error: \"%1$s\"." msgstr "" @@ -321,6 +282,46 @@ msgstr "" msgid "Cancel" msgstr "" +#: src/webex/renderHtml.tsx:209 +#, c-format +msgid "Withdrawal fees:" +msgstr "" + +#: src/webex/renderHtml.tsx:210 +#, c-format +msgid "Rounding loss:" +msgstr "" + +#: src/webex/renderHtml.tsx:211 +#, c-format +msgid "Earliest expiration (for deposit): %1$s" +msgstr "" + +#: src/webex/renderHtml.tsx:216 +#, c-format +msgid "# Coins" +msgstr "" + +#: src/webex/renderHtml.tsx:217 +#, c-format +msgid "Value" +msgstr "" + +#: src/webex/renderHtml.tsx:218 +#, c-format +msgid "Withdraw Fee" +msgstr "" + +#: src/webex/renderHtml.tsx:219 +#, c-format +msgid "Refresh Fee" +msgstr "" + +#: src/webex/renderHtml.tsx:220 +#, c-format +msgid "Deposit Fee" +msgstr "" + #: src/wire.ts:38 #, c-format msgid "Invalid Wire" diff --git a/src/i18n/fr.po b/src/i18n/fr.po index f8d2023b7..41c6e6d6e 100644 --- a/src/i18n/fr.po +++ b/src/i18n/fr.po @@ -66,67 +66,27 @@ msgstr "" msgid "Confirm payment" msgstr "" -#: src/webex/pages/confirm-create-reserve.tsx:178 -#, c-format -msgid "Withdrawal fees:" -msgstr "" - -#: src/webex/pages/confirm-create-reserve.tsx:179 -#, c-format -msgid "Rounding loss:" -msgstr "" - -#: src/webex/pages/confirm-create-reserve.tsx:180 -#, c-format -msgid "Earliest expiration (for deposit): %1$s" -msgstr "" - -#: src/webex/pages/confirm-create-reserve.tsx:185 -#, c-format -msgid "# Coins" -msgstr "" - -#: src/webex/pages/confirm-create-reserve.tsx:186 -#, c-format -msgid "Value" -msgstr "" - -#: src/webex/pages/confirm-create-reserve.tsx:187 -#, c-format -msgid "Withdraw Fee" -msgstr "" - -#: src/webex/pages/confirm-create-reserve.tsx:188 -#, c-format -msgid "Refresh Fee" -msgstr "" - -#: src/webex/pages/confirm-create-reserve.tsx:189 -#, c-format -msgid "Deposit Fee" -msgstr "" - -#: src/webex/pages/confirm-create-reserve.tsx:243 +#: src/webex/pages/confirm-create-reserve.tsx:121 #, c-format msgid "Select" msgstr "" -#: src/webex/pages/confirm-create-reserve.tsx:259 +#: src/webex/pages/confirm-create-reserve.tsx:137 #, c-format msgid "Error: URL may not be relative" msgstr "" -#: src/webex/pages/confirm-create-reserve.tsx:327 +#: src/webex/pages/confirm-create-reserve.tsx:205 #, c-format msgid "The exchange is trusted by the wallet.\n" msgstr "" -#: src/webex/pages/confirm-create-reserve.tsx:333 +#: src/webex/pages/confirm-create-reserve.tsx:211 #, c-format msgid "The exchange is audited by a trusted auditor.\n" msgstr "" -#: src/webex/pages/confirm-create-reserve.tsx:339 +#: src/webex/pages/confirm-create-reserve.tsx:217 #, c-format msgid "" "Warning: The exchange is neither directly trusted nor audited by a trusted " @@ -134,7 +94,7 @@ msgid "" "If you withdraw from this exchange, it will be trusted in the future.\n" msgstr "" -#: src/webex/pages/confirm-create-reserve.tsx:348 +#: src/webex/pages/confirm-create-reserve.tsx:226 #, c-format msgid "" "Using exchange provider%1$s.\n" @@ -142,58 +102,59 @@ msgid "" " %2$s in fees.\n" msgstr "" -#: src/webex/pages/confirm-create-reserve.tsx:362 +#: src/webex/pages/confirm-create-reserve.tsx:240 #, c-format msgid "" "Waiting for a response from\n" " %1$s" msgstr "" -#: src/webex/pages/confirm-create-reserve.tsx:379 +#: src/webex/pages/confirm-create-reserve.tsx:257 #, c-format msgid "" "Information about fees will be available when an exchange provider is " "selected." msgstr "" -#: src/webex/pages/confirm-create-reserve.tsx:422 +#: src/webex/pages/confirm-create-reserve.tsx:300 #, c-format msgid "Accept fees and withdraw" msgstr "" -#: src/webex/pages/confirm-create-reserve.tsx:427 +#: src/webex/pages/confirm-create-reserve.tsx:305 #, c-format msgid "Change Exchange Provider" msgstr "" -#: src/webex/pages/confirm-create-reserve.tsx:484 +#: src/webex/pages/confirm-create-reserve.tsx:357 #, c-format msgid "You are about to withdraw %1$s from your bank account into your wallet." msgstr "" -#: src/webex/pages/confirm-create-reserve.tsx:569 +#: src/webex/pages/confirm-create-reserve.tsx:442 #, c-format msgid "" "Oops, something went wrong. The wallet responded with error status (%1$s)." msgstr "" -#: src/webex/pages/confirm-create-reserve.tsx:578 +#: src/webex/pages/confirm-create-reserve.tsx:451 #, c-format msgid "Checking URL, please wait ..." msgstr "" -#: src/webex/pages/confirm-create-reserve.tsx:592 +#: src/webex/pages/confirm-create-reserve.tsx:465 #, c-format msgid "Can't parse amount: %1$s" msgstr "" -#: src/webex/pages/confirm-create-reserve.tsx:599 +#: src/webex/pages/confirm-create-reserve.tsx:472 #, c-format msgid "Can't parse wire_types: %1$s" msgstr "" +#. #-#-#-#-# - (PACKAGE VERSION) #-#-#-#-# #. TODO:generic error reporting function or component. -#: src/webex/pages/confirm-create-reserve.tsx:625 +#: src/webex/pages/confirm-create-reserve.tsx:498 src/webex/pages/tip.tsx:148 #, c-format msgid "Fatal error: \"%1$s\"." msgstr "" @@ -321,6 +282,46 @@ msgstr "" msgid "Cancel" msgstr "" +#: src/webex/renderHtml.tsx:209 +#, c-format +msgid "Withdrawal fees:" +msgstr "" + +#: src/webex/renderHtml.tsx:210 +#, c-format +msgid "Rounding loss:" +msgstr "" + +#: src/webex/renderHtml.tsx:211 +#, c-format +msgid "Earliest expiration (for deposit): %1$s" +msgstr "" + +#: src/webex/renderHtml.tsx:216 +#, c-format +msgid "# Coins" +msgstr "" + +#: src/webex/renderHtml.tsx:217 +#, c-format +msgid "Value" +msgstr "" + +#: src/webex/renderHtml.tsx:218 +#, c-format +msgid "Withdraw Fee" +msgstr "" + +#: src/webex/renderHtml.tsx:219 +#, c-format +msgid "Refresh Fee" +msgstr "" + +#: src/webex/renderHtml.tsx:220 +#, c-format +msgid "Deposit Fee" +msgstr "" + #: src/wire.ts:38 #, c-format msgid "Invalid Wire" diff --git a/src/i18n/it.po b/src/i18n/it.po index f8d2023b7..41c6e6d6e 100644 --- a/src/i18n/it.po +++ b/src/i18n/it.po @@ -66,67 +66,27 @@ msgstr "" msgid "Confirm payment" msgstr "" -#: src/webex/pages/confirm-create-reserve.tsx:178 -#, c-format -msgid "Withdrawal fees:" -msgstr "" - -#: src/webex/pages/confirm-create-reserve.tsx:179 -#, c-format -msgid "Rounding loss:" -msgstr "" - -#: src/webex/pages/confirm-create-reserve.tsx:180 -#, c-format -msgid "Earliest expiration (for deposit): %1$s" -msgstr "" - -#: src/webex/pages/confirm-create-reserve.tsx:185 -#, c-format -msgid "# Coins" -msgstr "" - -#: src/webex/pages/confirm-create-reserve.tsx:186 -#, c-format -msgid "Value" -msgstr "" - -#: src/webex/pages/confirm-create-reserve.tsx:187 -#, c-format -msgid "Withdraw Fee" -msgstr "" - -#: src/webex/pages/confirm-create-reserve.tsx:188 -#, c-format -msgid "Refresh Fee" -msgstr "" - -#: src/webex/pages/confirm-create-reserve.tsx:189 -#, c-format -msgid "Deposit Fee" -msgstr "" - -#: src/webex/pages/confirm-create-reserve.tsx:243 +#: src/webex/pages/confirm-create-reserve.tsx:121 #, c-format msgid "Select" msgstr "" -#: src/webex/pages/confirm-create-reserve.tsx:259 +#: src/webex/pages/confirm-create-reserve.tsx:137 #, c-format msgid "Error: URL may not be relative" msgstr "" -#: src/webex/pages/confirm-create-reserve.tsx:327 +#: src/webex/pages/confirm-create-reserve.tsx:205 #, c-format msgid "The exchange is trusted by the wallet.\n" msgstr "" -#: src/webex/pages/confirm-create-reserve.tsx:333 +#: src/webex/pages/confirm-create-reserve.tsx:211 #, c-format msgid "The exchange is audited by a trusted auditor.\n" msgstr "" -#: src/webex/pages/confirm-create-reserve.tsx:339 +#: src/webex/pages/confirm-create-reserve.tsx:217 #, c-format msgid "" "Warning: The exchange is neither directly trusted nor audited by a trusted " @@ -134,7 +94,7 @@ msgid "" "If you withdraw from this exchange, it will be trusted in the future.\n" msgstr "" -#: src/webex/pages/confirm-create-reserve.tsx:348 +#: src/webex/pages/confirm-create-reserve.tsx:226 #, c-format msgid "" "Using exchange provider%1$s.\n" @@ -142,58 +102,59 @@ msgid "" " %2$s in fees.\n" msgstr "" -#: src/webex/pages/confirm-create-reserve.tsx:362 +#: src/webex/pages/confirm-create-reserve.tsx:240 #, c-format msgid "" "Waiting for a response from\n" " %1$s" msgstr "" -#: src/webex/pages/confirm-create-reserve.tsx:379 +#: src/webex/pages/confirm-create-reserve.tsx:257 #, c-format msgid "" "Information about fees will be available when an exchange provider is " "selected." msgstr "" -#: src/webex/pages/confirm-create-reserve.tsx:422 +#: src/webex/pages/confirm-create-reserve.tsx:300 #, c-format msgid "Accept fees and withdraw" msgstr "" -#: src/webex/pages/confirm-create-reserve.tsx:427 +#: src/webex/pages/confirm-create-reserve.tsx:305 #, c-format msgid "Change Exchange Provider" msgstr "" -#: src/webex/pages/confirm-create-reserve.tsx:484 +#: src/webex/pages/confirm-create-reserve.tsx:357 #, c-format msgid "You are about to withdraw %1$s from your bank account into your wallet." msgstr "" -#: src/webex/pages/confirm-create-reserve.tsx:569 +#: src/webex/pages/confirm-create-reserve.tsx:442 #, c-format msgid "" "Oops, something went wrong. The wallet responded with error status (%1$s)." msgstr "" -#: src/webex/pages/confirm-create-reserve.tsx:578 +#: src/webex/pages/confirm-create-reserve.tsx:451 #, c-format msgid "Checking URL, please wait ..." msgstr "" -#: src/webex/pages/confirm-create-reserve.tsx:592 +#: src/webex/pages/confirm-create-reserve.tsx:465 #, c-format msgid "Can't parse amount: %1$s" msgstr "" -#: src/webex/pages/confirm-create-reserve.tsx:599 +#: src/webex/pages/confirm-create-reserve.tsx:472 #, c-format msgid "Can't parse wire_types: %1$s" msgstr "" +#. #-#-#-#-# - (PACKAGE VERSION) #-#-#-#-# #. TODO:generic error reporting function or component. -#: src/webex/pages/confirm-create-reserve.tsx:625 +#: src/webex/pages/confirm-create-reserve.tsx:498 src/webex/pages/tip.tsx:148 #, c-format msgid "Fatal error: \"%1$s\"." msgstr "" @@ -321,6 +282,46 @@ msgstr "" msgid "Cancel" msgstr "" +#: src/webex/renderHtml.tsx:209 +#, c-format +msgid "Withdrawal fees:" +msgstr "" + +#: src/webex/renderHtml.tsx:210 +#, c-format +msgid "Rounding loss:" +msgstr "" + +#: src/webex/renderHtml.tsx:211 +#, c-format +msgid "Earliest expiration (for deposit): %1$s" +msgstr "" + +#: src/webex/renderHtml.tsx:216 +#, c-format +msgid "# Coins" +msgstr "" + +#: src/webex/renderHtml.tsx:217 +#, c-format +msgid "Value" +msgstr "" + +#: src/webex/renderHtml.tsx:218 +#, c-format +msgid "Withdraw Fee" +msgstr "" + +#: src/webex/renderHtml.tsx:219 +#, c-format +msgid "Refresh Fee" +msgstr "" + +#: src/webex/renderHtml.tsx:220 +#, c-format +msgid "Deposit Fee" +msgstr "" + #: src/wire.ts:38 #, c-format msgid "Invalid Wire" diff --git a/src/i18n/strings.ts b/src/i18n/strings.ts index ddf93e688..737458c48 100644 --- a/src/i18n/strings.ts +++ b/src/i18n/strings.ts @@ -45,30 +45,6 @@ strings['de'] = { "Confirm payment": [ "Bezahlung bestätigen" ], - "Withdrawal fees:": [ - "Abheben bei %1$s" - ], - "Rounding loss:": [ - "" - ], - "Earliest expiration (for deposit): %1$s": [ - "" - ], - "# Coins": [ - "" - ], - "Value": [ - "" - ], - "Withdraw Fee": [ - "Abheben bei %1$s" - ], - "Refresh Fee": [ - "" - ], - "Deposit Fee": [ - "" - ], "Select": [ "" ], @@ -186,6 +162,30 @@ strings['de'] = { "Cancel": [ "Saldo" ], + "Withdrawal fees:": [ + "Abheben bei %1$s" + ], + "Rounding loss:": [ + "" + ], + "Earliest expiration (for deposit): %1$s": [ + "" + ], + "# Coins": [ + "" + ], + "Value": [ + "" + ], + "Withdraw Fee": [ + "Abheben bei %1$s" + ], + "Refresh Fee": [ + "" + ], + "Deposit Fee": [ + "" + ], "Invalid Wire": [ "" ], @@ -231,30 +231,6 @@ strings['en-US'] = { "Confirm payment": [ "" ], - "Withdrawal fees:": [ - "" - ], - "Rounding loss:": [ - "" - ], - "Earliest expiration (for deposit): %1$s": [ - "" - ], - "# Coins": [ - "" - ], - "Value": [ - "" - ], - "Withdraw Fee": [ - "" - ], - "Refresh Fee": [ - "" - ], - "Deposit Fee": [ - "" - ], "Select": [ "" ], @@ -372,6 +348,30 @@ strings['en-US'] = { "Cancel": [ "" ], + "Withdrawal fees:": [ + "" + ], + "Rounding loss:": [ + "" + ], + "Earliest expiration (for deposit): %1$s": [ + "" + ], + "# Coins": [ + "" + ], + "Value": [ + "" + ], + "Withdraw Fee": [ + "" + ], + "Refresh Fee": [ + "" + ], + "Deposit Fee": [ + "" + ], "Invalid Wire": [ "" ], @@ -417,30 +417,6 @@ strings['fr'] = { "Confirm payment": [ "" ], - "Withdrawal fees:": [ - "" - ], - "Rounding loss:": [ - "" - ], - "Earliest expiration (for deposit): %1$s": [ - "" - ], - "# Coins": [ - "" - ], - "Value": [ - "" - ], - "Withdraw Fee": [ - "" - ], - "Refresh Fee": [ - "" - ], - "Deposit Fee": [ - "" - ], "Select": [ "" ], @@ -558,6 +534,30 @@ strings['fr'] = { "Cancel": [ "" ], + "Withdrawal fees:": [ + "" + ], + "Rounding loss:": [ + "" + ], + "Earliest expiration (for deposit): %1$s": [ + "" + ], + "# Coins": [ + "" + ], + "Value": [ + "" + ], + "Withdraw Fee": [ + "" + ], + "Refresh Fee": [ + "" + ], + "Deposit Fee": [ + "" + ], "Invalid Wire": [ "" ], @@ -603,30 +603,6 @@ strings['it'] = { "Confirm payment": [ "" ], - "Withdrawal fees:": [ - "" - ], - "Rounding loss:": [ - "" - ], - "Earliest expiration (for deposit): %1$s": [ - "" - ], - "# Coins": [ - "" - ], - "Value": [ - "" - ], - "Withdraw Fee": [ - "" - ], - "Refresh Fee": [ - "" - ], - "Deposit Fee": [ - "" - ], "Select": [ "" ], @@ -744,6 +720,30 @@ strings['it'] = { "Cancel": [ "" ], + "Withdrawal fees:": [ + "" + ], + "Rounding loss:": [ + "" + ], + "Earliest expiration (for deposit): %1$s": [ + "" + ], + "# Coins": [ + "" + ], + "Value": [ + "" + ], + "Withdraw Fee": [ + "" + ], + "Refresh Fee": [ + "" + ], + "Deposit Fee": [ + "" + ], "Invalid Wire": [ "" ], diff --git a/src/i18n/taler-wallet-webex.pot b/src/i18n/taler-wallet-webex.pot index f8d2023b7..41c6e6d6e 100644 --- a/src/i18n/taler-wallet-webex.pot +++ b/src/i18n/taler-wallet-webex.pot @@ -66,67 +66,27 @@ msgstr "" msgid "Confirm payment" msgstr "" -#: src/webex/pages/confirm-create-reserve.tsx:178 -#, c-format -msgid "Withdrawal fees:" -msgstr "" - -#: src/webex/pages/confirm-create-reserve.tsx:179 -#, c-format -msgid "Rounding loss:" -msgstr "" - -#: src/webex/pages/confirm-create-reserve.tsx:180 -#, c-format -msgid "Earliest expiration (for deposit): %1$s" -msgstr "" - -#: src/webex/pages/confirm-create-reserve.tsx:185 -#, c-format -msgid "# Coins" -msgstr "" - -#: src/webex/pages/confirm-create-reserve.tsx:186 -#, c-format -msgid "Value" -msgstr "" - -#: src/webex/pages/confirm-create-reserve.tsx:187 -#, c-format -msgid "Withdraw Fee" -msgstr "" - -#: src/webex/pages/confirm-create-reserve.tsx:188 -#, c-format -msgid "Refresh Fee" -msgstr "" - -#: src/webex/pages/confirm-create-reserve.tsx:189 -#, c-format -msgid "Deposit Fee" -msgstr "" - -#: src/webex/pages/confirm-create-reserve.tsx:243 +#: src/webex/pages/confirm-create-reserve.tsx:121 #, c-format msgid "Select" msgstr "" -#: src/webex/pages/confirm-create-reserve.tsx:259 +#: src/webex/pages/confirm-create-reserve.tsx:137 #, c-format msgid "Error: URL may not be relative" msgstr "" -#: src/webex/pages/confirm-create-reserve.tsx:327 +#: src/webex/pages/confirm-create-reserve.tsx:205 #, c-format msgid "The exchange is trusted by the wallet.\n" msgstr "" -#: src/webex/pages/confirm-create-reserve.tsx:333 +#: src/webex/pages/confirm-create-reserve.tsx:211 #, c-format msgid "The exchange is audited by a trusted auditor.\n" msgstr "" -#: src/webex/pages/confirm-create-reserve.tsx:339 +#: src/webex/pages/confirm-create-reserve.tsx:217 #, c-format msgid "" "Warning: The exchange is neither directly trusted nor audited by a trusted " @@ -134,7 +94,7 @@ msgid "" "If you withdraw from this exchange, it will be trusted in the future.\n" msgstr "" -#: src/webex/pages/confirm-create-reserve.tsx:348 +#: src/webex/pages/confirm-create-reserve.tsx:226 #, c-format msgid "" "Using exchange provider%1$s.\n" @@ -142,58 +102,59 @@ msgid "" " %2$s in fees.\n" msgstr "" -#: src/webex/pages/confirm-create-reserve.tsx:362 +#: src/webex/pages/confirm-create-reserve.tsx:240 #, c-format msgid "" "Waiting for a response from\n" " %1$s" msgstr "" -#: src/webex/pages/confirm-create-reserve.tsx:379 +#: src/webex/pages/confirm-create-reserve.tsx:257 #, c-format msgid "" "Information about fees will be available when an exchange provider is " "selected." msgstr "" -#: src/webex/pages/confirm-create-reserve.tsx:422 +#: src/webex/pages/confirm-create-reserve.tsx:300 #, c-format msgid "Accept fees and withdraw" msgstr "" -#: src/webex/pages/confirm-create-reserve.tsx:427 +#: src/webex/pages/confirm-create-reserve.tsx:305 #, c-format msgid "Change Exchange Provider" msgstr "" -#: src/webex/pages/confirm-create-reserve.tsx:484 +#: src/webex/pages/confirm-create-reserve.tsx:357 #, c-format msgid "You are about to withdraw %1$s from your bank account into your wallet." msgstr "" -#: src/webex/pages/confirm-create-reserve.tsx:569 +#: src/webex/pages/confirm-create-reserve.tsx:442 #, c-format msgid "" "Oops, something went wrong. The wallet responded with error status (%1$s)." msgstr "" -#: src/webex/pages/confirm-create-reserve.tsx:578 +#: src/webex/pages/confirm-create-reserve.tsx:451 #, c-format msgid "Checking URL, please wait ..." msgstr "" -#: src/webex/pages/confirm-create-reserve.tsx:592 +#: src/webex/pages/confirm-create-reserve.tsx:465 #, c-format msgid "Can't parse amount: %1$s" msgstr "" -#: src/webex/pages/confirm-create-reserve.tsx:599 +#: src/webex/pages/confirm-create-reserve.tsx:472 #, c-format msgid "Can't parse wire_types: %1$s" msgstr "" +#. #-#-#-#-# - (PACKAGE VERSION) #-#-#-#-# #. TODO:generic error reporting function or component. -#: src/webex/pages/confirm-create-reserve.tsx:625 +#: src/webex/pages/confirm-create-reserve.tsx:498 src/webex/pages/tip.tsx:148 #, c-format msgid "Fatal error: \"%1$s\"." msgstr "" @@ -321,6 +282,46 @@ msgstr "" msgid "Cancel" msgstr "" +#: src/webex/renderHtml.tsx:209 +#, c-format +msgid "Withdrawal fees:" +msgstr "" + +#: src/webex/renderHtml.tsx:210 +#, c-format +msgid "Rounding loss:" +msgstr "" + +#: src/webex/renderHtml.tsx:211 +#, c-format +msgid "Earliest expiration (for deposit): %1$s" +msgstr "" + +#: src/webex/renderHtml.tsx:216 +#, c-format +msgid "# Coins" +msgstr "" + +#: src/webex/renderHtml.tsx:217 +#, c-format +msgid "Value" +msgstr "" + +#: src/webex/renderHtml.tsx:218 +#, c-format +msgid "Withdraw Fee" +msgstr "" + +#: src/webex/renderHtml.tsx:219 +#, c-format +msgid "Refresh Fee" +msgstr "" + +#: src/webex/renderHtml.tsx:220 +#, c-format +msgid "Deposit Fee" +msgstr "" + #: src/wire.ts:38 #, c-format msgid "Invalid Wire" diff --git a/src/query.ts b/src/query.ts index 653e91a1b..554f937a5 100644 --- a/src/query.ts +++ b/src/query.ts @@ -50,6 +50,20 @@ export class Store { } +/** + * Options for an index. + */ +export interface IndexOptions { + /** + * If true and the path resolves to an array, create an index entry for + * each member of the array (instead of one index entry containing the full array). + * + * Defaults to false. + */ + multiEntry?: boolean; +} + + /** * Definition of an index. */ @@ -59,7 +73,16 @@ export class Index { */ storeName: string; - constructor(s: Store, public indexName: string, public keyPath: string | string[]) { + /** + * Options to use for the index. + */ + options: IndexOptions; + + constructor(s: Store, public indexName: string, public keyPath: string | string[], options?: IndexOptions) { + const defaultOptions = { + multiEntry: false, + }; + this.options = { ...defaultOptions, ...(options || {}) }; this.storeName = s.name; } @@ -671,26 +694,33 @@ export class QueryRoot { /** - * Get, modify and store an element inside a transaction. + * Update objects inside a transaction. + * + * If the mutation function throws AbortTransaction, the whole transaction will be aborted. + * If the mutation function returns undefined or null, no modification will be made. */ mutate(store: Store, key: any, f: (v: T|undefined) => T|undefined): QueryRoot { this.checkFinished(); const doPut = (tx: IDBTransaction) => { - const reqGet = tx.objectStore(store.name).get(key); - reqGet.onsuccess = () => { - const r = reqGet.result; - let m: T|undefined; - try { - m = f(r); - } catch (e) { - if (e === AbortTransaction) { - tx.abort(); - return; + const req = tx.objectStore(store.name).openCursor(IDBKeyRange.only(key)); + req.onsuccess = () => { + const cursor = req.result; + if (cursor) { + const value = cursor.value; + let modifiedValue: T|undefined; + try { + modifiedValue = f(value); + } catch (e) { + if (e === AbortTransaction) { + tx.abort(); + return; + } + throw e; } - throw e; - } - if (m !== undefined && m !== null) { - tx.objectStore(store.name).put(m); + if (modifiedValue !== undefined && modifiedValue !== null) { + cursor.update(modifiedValue); + } + cursor.continue(); } }; }; @@ -702,8 +732,6 @@ export class QueryRoot { /** * Add all object from an iterable to the given object store. - * Fails if the object's key is already present - * in the object store. */ putAll(store: Store, iterable: T[]): QueryRoot { this.checkFinished(); @@ -822,13 +850,13 @@ export class QueryRoot { /** * Delete an object by from the given object store. */ - delete(storeName: string, key: any): QueryRoot { + delete(store: Store, key: any): QueryRoot { this.checkFinished(); const doDelete = (tx: IDBTransaction) => { - tx.objectStore(storeName).delete(key); + tx.objectStore(store.name).delete(key); }; this.scheduleFinish(); - this.addWork(doDelete, storeName, true); + this.addWork(doDelete, store.name, true); return this; } diff --git a/src/types.ts b/src/types.ts index 20517b7a2..03ba597fe 100644 --- a/src/types.ts +++ b/src/types.ts @@ -598,6 +598,12 @@ export interface PreCoinRecord { coinEv: string; exchangeBaseUrl: string; coinValue: AmountJson; + /** + * Set to true if this pre-coin came from a tip. + * Until the tip is marked as "accepted", the resulting + * coin will not be used for payments. + */ + isFromTip: boolean; } /** @@ -836,6 +842,10 @@ export enum CoinStatus { * Coin was dirty but can't be refreshed. */ Useless, + /** + * The coin was withdrawn for a tip that the user hasn't accepted yet. + */ + TainedByTip, } @@ -1782,3 +1792,217 @@ export interface CoinWithDenom { */ denom: DenominationRecord; } + + +/** + * Planchet detail sent to the merchant. + */ +export interface TipPlanchetDetail { + /** + * Hashed denomination public key. + */ + denom_pub_hash: string; + + /** + * Coin's blinded public key. + */ + coin_ev: string; +} + + +export interface TipPickupRequest { + /** + * Identifier of the tip. + */ + tip_id: string; + + /** + * List of planchets the wallet wants to use for the tip. + */ + planchets: TipPlanchetDetail[]; +} + +@Checkable.Class() +export class ReserveSigSingleton { + @Checkable.String + reserve_sig: string; + + static checked: (obj: any) => ReserveSigSingleton; +} + +/** + * Response of the merchant + * to the TipPickupRequest. + */ +@Checkable.Class() +export class TipResponse { + /** + * Public key of the reserve + */ + @Checkable.String + reserve_pub: string; + + /** + * The order of the signatures matches the planchets list. + */ + @Checkable.List(Checkable.Value(ReserveSigSingleton)) + reserve_sigs: ReserveSigSingleton[]; + + static checked: (obj: any) => TipResponse; +} + + +/** + * Tipping planchet stored in the database. + */ +export interface TipPlanchet { + blindingKey: string; + coinEv: string; + coinPriv: string; + coinPub: string; + coinValue: AmountJson; + denomPubHash: string; + denomPub: string; +} + +/** + * Status of a tip we got from a merchant. + */ +export interface TipRecord { + /** + * Has the user accepted the tip? Only after the tip has been accepted coins + * withdrawn from the tip may be used. + */ + accepted: boolean; + + /** + * The tipped amount. + */ + amount: AmountJson; + + /** + * Coin public keys from the planchets. + * This field is redundant and used for indexing the record via + * a multi-entry index to look up tip records by coin public key. + */ + coinPubs: string[]; + + /** + * Timestamp, the tip can't be picked up anymore after this deadline. + */ + deadline: number; + + /** + * The exchange that will sign our coins, chosen by the merchant. + */ + exchangeUrl: string; + + /** + * Domain of the merchant, necessary to uniquely identify the tip since + * merchants can freely choose the ID and a malicious merchant might cause a + * collision. + */ + merchantDomain: string; + + /** + * Planchets, the members included in TipPlanchetDetail will be sent to the + * merchant. + */ + planchets: TipPlanchet[]; + + /** + * Response if the merchant responded, + * undefined otherwise. + */ + response?: TipResponse[]; + + /** + * Identifier for the tip, chosen by the merchant. + */ + tipId: string; +} + + +export interface TipStatus { + tip: TipRecord; + rci?: ReserveCreationInfo; +} + + +@Checkable.Class() +export class TipStatusRequest { + @Checkable.String + tipId: string; + + @Checkable.String + merchantDomain: string; + + static checked: (obj: any) => TipStatusRequest; +} + + +@Checkable.Class() +export class AcceptTipRequest { + @Checkable.String + tipId: string; + + @Checkable.String + merchantDomain: string; + + static checked: (obj: any) => AcceptTipRequest; +} + + +@Checkable.Class() +export class ProcessTipResponseRequest { + @Checkable.String + tipId: string; + + @Checkable.String + merchantDomain: string; + + @Checkable.Value(TipResponse) + tipResponse: TipResponse; + + static checked: (obj: any) => ProcessTipResponseRequest; +} + +@Checkable.Class() +export class GetTipPlanchetsRequest { + @Checkable.String + tipId: string; + + @Checkable.String + merchantDomain: string; + + @Checkable.Optional(Checkable.Value(AmountJson)) + amount: AmountJson; + + @Checkable.Number + deadline: number; + + @Checkable.String + exchangeUrl: string; + + static checked: (obj: any) => GetTipPlanchetsRequest; +} + +@Checkable.Class() +export class TipToken { + @Checkable.String + expiration: string; + + @Checkable.String + exchange_url: string; + + @Checkable.String + pickup_url: string; + + @Checkable.String + tip_id: string; + + @Checkable.Value(AmountJson) + amount: AmountJson; + + static checked: (obj: any) => TipToken; +} diff --git a/src/wallet.ts b/src/wallet.ts index 703c7b4aa..14c614e6c 100644 --- a/src/wallet.ts +++ b/src/wallet.ts @@ -81,6 +81,10 @@ import { ReserveRecord, ReturnCoinsRequest, SenderWireInfos, + TipPlanchetDetail, + TipRecord, + TipResponse, + TipStatus, WalletBalance, WalletBalanceEntry, WireFee, @@ -324,7 +328,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 = 20; +export const WALLET_DB_VERSION = 21; const builtinCurrencies: CurrencyRecord[] = [ { @@ -506,7 +510,7 @@ export namespace Stores { super("exchanges", {keyPath: "baseUrl"}); } - pubKeyIndex = new Index(this, "pubKey", "masterPublicKey"); + pubKeyIndex = new Index(this, "pubKeyIndex", "masterPublicKey"); } class NonceStore extends Store { @@ -521,7 +525,7 @@ export namespace Stores { } exchangeBaseUrlIndex = new Index(this, "exchangeBaseUrl", "exchangeBaseUrl"); - denomPubIndex = new Index(this, "denomPub", "denomPub"); + denomPubIndex = new Index(this, "denomPubIndex", "denomPub"); } class ProposalsStore extends Store { @@ -531,7 +535,7 @@ export namespace Stores { keyPath: "id", }); } - timestampIndex = new Index(this, "timestamp", "timestamp"); + timestampIndex = new Index(this, "timestampIndex", "timestamp"); } class PurchasesStore extends Store { @@ -539,9 +543,9 @@ export namespace Stores { super("purchases", {keyPath: "contractTermsHash"}); } - fulfillmentUrlIndex = new Index(this, "fulfillment_url", "contractTerms.fulfillment_url"); - orderIdIndex = new Index(this, "order_id", "contractTerms.order_id"); - timestampIndex = new Index(this, "timestamp", "timestamp"); + fulfillmentUrlIndex = new Index(this, "fulfillmentUrlIndex", "contractTerms.fulfillment_url"); + orderIdIndex = new Index(this, "orderIdIndex", "contractTerms.order_id"); + timestampIndex = new Index(this, "timestampIndex", "timestamp"); } class DenominationsStore extends Store { @@ -551,9 +555,9 @@ export namespace Stores { {keyPath: ["exchangeBaseUrl", "denomPub"] as any as IDBKeyPath}); } - denomPubHashIndex = new Index(this, "denomPubHash", "denomPubHash"); - exchangeBaseUrlIndex = new Index(this, "exchangeBaseUrl", "exchangeBaseUrl"); - denomPubIndex = new Index(this, "denomPub", "denomPub"); + denomPubHashIndex = new Index(this, "denomPubHashIndex", "denomPubHash"); + exchangeBaseUrlIndex = new Index(this, "exchangeBaseUrlIndex", "exchangeBaseUrl"); + denomPubIndex = new Index(this, "denomPubIndex", "denomPub"); } class CurrenciesStore extends Store { @@ -578,9 +582,16 @@ export namespace Stores { 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"); + timestampCreatedIndex = new Index(this, "timestampCreatedIndex", "created"); + timestampConfirmedIndex = new Index(this, "timestampConfirmedIndex", "timestamp_confirmed"); + timestampDepletedIndex = new Index(this, "timestampDepletedIndex", "timestamp_depleted"); + } + + class TipsStore extends Store { + constructor() { + super("tips", {keyPath: ["tipId", "merchantDomain"] as any as IDBKeyPath}); + } + coinPubIndex = new Index(this, "coinPubIndex", "coinPubs", { multiEntry: true }); } export const coins = new CoinsStore(); @@ -596,6 +607,7 @@ export namespace Stores { export const refresh = new Store("refresh", {keyPath: "id", autoIncrement: true}); export const reserves = new ReservesStore(); export const purchases = new PurchasesStore(); + export const tips = new TipsStore(); } /* tslint:enable:completed-docs */ @@ -1126,7 +1138,7 @@ export class Wallet { () => this.processPreCoin(preCoin, Math.min(retryDelayMs * 2, 5 * 60 * 1000))); return; } - console.log("executing processPreCoin"); + console.log("executing processPreCoin", preCoin); this.processPreCoinConcurrent++; try { const exchange = await this.q().get(Stores.exchanges, @@ -1143,6 +1155,7 @@ export class Wallet { } const coin = await this.withdrawExecute(preCoin); + console.log("processPreCoin: got coin", coin); const mutateReserve = (r: ReserveRecord) => { @@ -1160,10 +1173,28 @@ export class Wallet { await this.q() .mutate(Stores.reserves, preCoin.reservePub, mutateReserve) - .delete("precoins", coin.coinPub) + .delete(Stores.precoins, coin.coinPub) .add(Stores.coins, coin) .finish(); + if (coin.status === CoinStatus.TainedByTip) { + let tip = await this.q().getIndexed(Stores.tips.coinPubIndex, coin.coinPub); + if (!tip) { + throw Error(`inconsistent DB: tip for coin pub ${coin.coinPub} not found.`); + } + + if (tip.accepted) { + // Transactionall set coin to fresh. + const mutateCoin = (c: CoinRecord) => { + if (c.status === CoinStatus.TainedByTip) { + c.status = CoinStatus.Fresh; + } + return c; + } + await this.q().mutate(Stores.coins, coin.coinPub, mutateCoin) + } + } + this.notifier.notify(); } catch (e) { console.error("Failed to withdraw coin from precoin, retrying in", @@ -1266,19 +1297,12 @@ export class Wallet { private async withdrawExecute(pc: PreCoinRecord): Promise { - const reserve = await this.q().get(Stores.reserves, - pc.reservePub); - - if (!reserve) { - throw Error("db inconsistent"); - } - const wd: any = {}; wd.denom_pub = pc.denomPub; wd.reserve_pub = pc.reservePub; wd.reserve_sig = pc.withdrawSig; wd.coin_ev = pc.coinEv; - const reqUrl = (new URI("reserve/withdraw")).absoluteTo(reserve.exchange_base_url); + const reqUrl = (new URI("reserve/withdraw")).absoluteTo(pc.exchangeBaseUrl); const resp = await this.http.postJson(reqUrl.href(), wd); if (resp.status !== 200) { @@ -1289,8 +1313,8 @@ export class Wallet { } const r = JSON.parse(resp.responseText); const denomSig = await this.cryptoApi.rsaUnblind(r.ev_sig, - pc.blindingKey, - pc.denomPub); + pc.blindingKey, + pc.denomPub); const coin: CoinRecord = { blindingKey: pc.blindingKey, coinPriv: pc.coinPriv, @@ -2809,4 +2833,113 @@ export class Wallet { } return feeAcc; } + + /** + * Get planchets for a tip. Creates new planchets if they don't exist already + * for this tip. The tip is uniquely identified by the merchant's domain and the tip id. + */ + async getTipPlanchets(merchantDomain: string, tipId: string, amount: AmountJson, deadline: number, exchangeUrl: string): Promise { + let tipRecord = await this.q().get(Stores.tips, [tipId, merchantDomain]); + if (!tipRecord) { + await this.updateExchangeFromUrl(exchangeUrl); + const denomsForWithdraw = await this.getVerifiedWithdrawDenomList(exchangeUrl, amount); + const planchets = await Promise.all(denomsForWithdraw.map(d => this.cryptoApi.createTipPlanchet(d))); + const coinPubs: string[] = planchets.map(x => x.coinPub); + tipRecord = { + accepted: false, + amount, + coinPubs, + deadline, + exchangeUrl, + merchantDomain, + planchets, + tipId, + }; + await this.q().put(Stores.tips, tipRecord).finish(); + } + // Planchets in the form that the merchant expects + const planchetDetail: TipPlanchetDetail[]= tipRecord.planchets.map((p) => ({ + denom_pub_hash: p.denomPubHash, + coin_ev: p.coinEv, + })); + return planchetDetail; + } + + /** + * Accept a merchant's response to a tip pickup and start withdrawing the coins. + * These coins will not appear in the wallet yet. + */ + async processTipResponse(merchantDomain: string, tipId: string, response: TipResponse): Promise { + let tipRecord = await this.q().get(Stores.tips, [tipId, merchantDomain]); + if (!tipRecord) { + throw Error("tip not found"); + } + console.log("processing tip response", response); + if (response.reserve_sigs.length !== tipRecord.planchets.length) { + throw Error("number of tip responses does not match requested planchets"); + } + + for (let i = 0; i < tipRecord.planchets.length; i++) { + let planchet = tipRecord.planchets[i]; + let preCoin = { + coinPub: planchet.coinPub, + coinPriv: planchet.coinPriv, + coinEv: planchet.coinEv, + coinValue: planchet.coinValue, + reservePub: response.reserve_pub, + denomPub: planchet.denomPub, + blindingKey: planchet.blindingKey, + withdrawSig: response.reserve_sigs[i].reserve_sig, + exchangeBaseUrl: tipRecord.exchangeUrl, + isFromTip: true, + }; + await this.q().put(Stores.precoins, preCoin); + this.processPreCoin(preCoin); + } + } + + /** + * Start using the coins from a tip. + */ + async acceptTip(merchantDomain: string, tipId: string): Promise { + const tipRecord = await this.q().get(Stores.tips, [tipId, merchantDomain]); + if (!tipRecord) { + throw Error("tip not found"); + } + tipRecord.accepted = true; + + // Create one transactional query, within this transaction + // both the tip will be marked as accepted and coins + // already withdrawn will be untainted. + const q = this.q(); + + q.put(Stores.tips, tipRecord); + + const updateCoin = (c: CoinRecord) => { + if (c.status === CoinStatus.TainedByTip) { + c.status = CoinStatus.Fresh; + } + return c; + }; + + for (const coinPub of tipRecord.coinPubs) { + q.mutate(Stores.coins, coinPub, updateCoin); + } + + await q.finish(); + this.notifier.notify(); + } + + async getTipStatus(merchantDomain: string, tipId: string): Promise { + const tipRecord = await this.q().get(Stores.tips, [tipId, merchantDomain]); + if (!tipRecord) { + throw Error("tip not found"); + } + const rci = await this.getReserveCreationInfo(tipRecord.exchangeUrl, tipRecord.amount); + const tipStatus: TipStatus = { + rci, + tip: tipRecord, + }; + return tipStatus; + } } diff --git a/src/webex/messages.ts b/src/webex/messages.ts index 0ca903154..7cc6c4259 100644 --- a/src/webex/messages.ts +++ b/src/webex/messages.ts @@ -192,6 +192,22 @@ export interface MessageMap { request: { refundPermissions: types.RefundPermission[] }; response: void; }; + "get-tip-planchets": { + request: types.GetTipPlanchetsRequest; + response: void; + }; + "process-tip-response": { + request: types.ProcessTipResponseRequest; + response: void; + }; + "accept-tip": { + request: types.AcceptTipRequest; + response: void; + }; + "get-tip-status": { + request: types.TipStatusRequest; + response: void; + }; } /** diff --git a/src/webex/notify.ts b/src/webex/notify.ts index cc8086ceb..ecc04e8a2 100644 --- a/src/webex/notify.ts +++ b/src/webex/notify.ts @@ -28,7 +28,9 @@ import URI = require("urijs"); import wxApi = require("./wxApi"); -import { QueryPaymentResult } from "../types"; +import { getTalerStampSec } from "../helpers"; +import { TipToken, QueryPaymentResult } from "../types"; + import axios from "axios"; @@ -260,6 +262,87 @@ function talerPay(msg: any): Promise { // Use a promise directly instead of of an async // function since some paths never resolve the promise. return new Promise(async(resolve, reject) => { + if (msg.tip) { + const tipToken = TipToken.checked(JSON.parse(msg.tip)); + + console.log("got tip token", tipToken); + + const deadlineSec = getTalerStampSec(tipToken.expiration); + if (!deadlineSec) { + wxApi.logAndDisplayError({ + message: "invalid expiration", + name: "tipping-failed", + sameTab: true, + }); + return; + } + + const merchantDomain = new URI(document.location.href).origin(); + let walletResp; + try { + walletResp = await wxApi.getTipPlanchets(merchantDomain, tipToken.tip_id, tipToken.amount, deadlineSec, tipToken.exchange_url); + } catch (e) { + wxApi.logAndDisplayError({ + message: e.message, + name: "tipping-failed", + response: e.response, + sameTab: true, + }); + throw e; + } + + let planchets = walletResp; + + if (!planchets) { + wxApi.logAndDisplayError({ + message: "processing tip failed", + detail: walletResp, + name: "tipping-failed", + sameTab: true, + }); + return; + } + + let merchantResp; + + try { + const config = { + validateStatus: (s: number) => s === 200, + }; + const req = { planchets, tip_id: tipToken.tip_id }; + merchantResp = await axios.post(tipToken.pickup_url, req, config); + } catch (e) { + wxApi.logAndDisplayError({ + message: e.message, + name: "tipping-failed", + response: e.response, + sameTab: true, + }); + throw e; + } + + try { + wxApi.processTipResponse(merchantDomain, tipToken.tip_id, merchantResp.data); + } catch (e) { + wxApi.logAndDisplayError({ + message: e.message, + name: "tipping-failed", + response: e.response, + sameTab: true, + }); + throw e; + } + + // Go to tip dialog page, where the user can confirm the tip or + // decline if they are not happy with the exchange. + const uri = new URI(chrome.extension.getURL("/src/webex/pages/tip.html")); + const params = { tip_id: tipToken.tip_id, merchant_domain: merchantDomain }; + const redirectUrl = uri.query(params).href(); + window.location.href = redirectUrl; + + return; + } + if (msg.refund_url) { console.log("processing refund"); let resp; diff --git a/src/webex/pages/confirm-create-reserve.tsx b/src/webex/pages/confirm-create-reserve.tsx index 0e1cb17df..53b0d635f 100644 --- a/src/webex/pages/confirm-create-reserve.tsx +++ b/src/webex/pages/confirm-create-reserve.tsx @@ -22,18 +22,17 @@ * @author Florian Dold */ -import {canonicalizeBaseUrl} from "../../helpers"; +import { canonicalizeBaseUrl } from "../../helpers"; import * as i18n from "../../i18n"; import { AmountJson, Amounts, CreateReserveResponse, CurrencyRecord, - DenominationRecord, ReserveCreationInfo, } from "../../types"; -import {ImplicitStateComponent, StateHolder} from "../components"; +import { ImplicitStateComponent, StateHolder } from "../components"; import { createReserve, getCurrency, @@ -41,9 +40,8 @@ import { getReserveCreationInfo, } from "../wxApi"; -import {Collapsible, renderAmount} from "../renderHtml"; +import { renderAmount, WithdrawDetailView } from "../renderHtml"; -import * as moment from "moment"; import * as React from "react"; import * as ReactDOM from "react-dom"; import URI = require("urijs"); @@ -80,126 +78,6 @@ class EventTrigger { } -function renderAuditorDetails(rci: ReserveCreationInfo|null) { - console.log("rci", rci); - if (!rci) { - return ( -

- Details will be displayed when a valid exchange provider URL is entered. -

- ); - } - if (rci.exchangeInfo.auditors.length === 0) { - return ( -

- The exchange is not audited by any auditors. -

- ); - } - return ( -
- {rci.exchangeInfo.auditors.map((a) => ( -
-

Auditor {a.auditor_url}

-

Public key: {a.auditor_pub}

-

Trusted: {rci.trustedAuditorPubs.indexOf(a.auditor_pub) >= 0 ? "yes" : "no"}

-

Audits {a.denomination_keys.length} of {rci.numOfferedDenoms} denominations

-
- ))} -
- ); -} - -function renderReserveCreationDetails(rci: ReserveCreationInfo|null) { - if (!rci) { - return ( -

- Details will be displayed when a valid exchange provider URL is entered. -

- ); - } - - const denoms = rci.selectedDenoms; - - const countByPub: {[s: string]: number} = {}; - const uniq: DenominationRecord[] = []; - - denoms.forEach((x: DenominationRecord) => { - let c = countByPub[x.denomPub] || 0; - if (c === 0) { - uniq.push(x); - } - c += 1; - countByPub[x.denomPub] = c; - }); - - function row(denom: DenominationRecord) { - return ( - - {countByPub[denom.denomPub] + "x"} - {renderAmount(denom.value)} - {renderAmount(denom.feeWithdraw)} - {renderAmount(denom.feeRefresh)} - {renderAmount(denom.feeDeposit)} - - ); - } - - function wireFee(s: string) { - return [ - - - Wire Method {s} - - - Applies Until - Wire Fee - Closing Fee - - , - - {rci!.wireFees.feesForType[s].map((f) => ( - - {moment.unix(f.endStamp).format("llll")} - {renderAmount(f.wireFee)} - {renderAmount(f.closingFee)} - - ))} - , - ]; - } - - const withdrawFee = renderAmount(rci.withdrawFee); - const overhead = renderAmount(rci.overhead); - - return ( -
-

Overview

-

{i18n.str`Withdrawal fees:`} {withdrawFee}

-

{i18n.str`Rounding loss:`} {overhead}

-

{i18n.str`Earliest expiration (for deposit): ${moment.unix(rci.earliestDepositExpiration).fromNow()}`}

-

Coin Fees

- - - - - - - - - - - - {uniq.map(row)} - -
{i18n.str`# Coins`}{i18n.str`Value`}{i18n.str`Withdraw Fee`}{i18n.str`Refresh Fee`}{i18n.str`Deposit Fee`}
-

Wire Fees

- - {Object.keys(rci.wireFees.feesForType).map(wireFee)} -
-
- ); -} interface ExchangeSelectionProps { @@ -428,12 +306,7 @@ class ExchangeSelection extends ImplicitStateComponent {

{this.renderUpdateStatus()} - - {renderReserveCreationDetails(this.reserveCreationInfo())} - - - {renderAuditorDetails(this.reserveCreationInfo())} - + ); } diff --git a/src/webex/pages/tip.html b/src/webex/pages/tip.html new file mode 100644 index 000000000..72d91a123 --- /dev/null +++ b/src/webex/pages/tip.html @@ -0,0 +1,24 @@ + + + + + + Taler Wallet: Received Tip + + + + + + + + + + + +
+

GNU Taler Wallet

+
+
+ + + diff --git a/src/webex/pages/tip.tsx b/src/webex/pages/tip.tsx new file mode 100644 index 000000000..7f3a7c1fe --- /dev/null +++ b/src/webex/pages/tip.tsx @@ -0,0 +1,155 @@ +/* + This file is part of TALER + (C) 2017 GNUnet e.V. + + 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. + + 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 + TALER; see the file COPYING. If not, see + */ + + +/** + * Page shown to the user to confirm creation + * of a reserve, usually requested by the bank. + * + * @author Florian Dold + */ + +import * as React from "react"; +import * as ReactDOM from "react-dom"; +import URI = require("urijs"); + +import * as i18n from "../../i18n"; + +import { + acceptTip, + getTipStatus, +} from "../wxApi"; + +import { renderAmount, WithdrawDetailView } from "../renderHtml"; + +import { Amounts, TipStatus } from "../../types"; + +interface TipDisplayProps { + merchantDomain: string; + tipId: string; +} + +interface TipDisplayState { + tipStatus?: TipStatus; + working: boolean; +} + +class TipDisplay extends React.Component { + constructor(props: TipDisplayProps) { + super(props); + this.state = { working: false }; + } + + async update() { + let tipStatus = await getTipStatus(this.props.merchantDomain, this.props.tipId); + this.setState({ tipStatus }); + } + + componentDidMount() { + this.update(); + const port = chrome.runtime.connect(); + port.onMessage.addListener((msg: any) => { + if (msg.notify) { + console.log("got notified"); + this.update(); + } + }); + this.update(); + } + + renderExchangeInfo(ts: TipStatus) { + const rci = ts.rci; + if (!rci) { + return

Waiting for info about exchange ...

+ } + const totalCost = Amounts.add(rci.overhead, rci.withdrawFee).amount; + return ( +
+

+ The tip is handled by the exchange {rci.exchangeInfo.baseUrl}.{" "} + The exchange provider will charge + {" "} + {renderAmount(totalCost)} + {" "}. +

+ +
+ ); + } + + accept() { + this.setState({ working: true}); + acceptTip(this.props.merchantDomain, this.props.tipId); + } + + renderButtons() { + return ( +
+