diff options
author | Florian Dold <florian.dold@gmail.com> | 2017-11-30 04:07:36 +0100 |
---|---|---|
committer | Florian Dold <florian.dold@gmail.com> | 2017-12-01 03:00:09 +0100 |
commit | b8ccc7c990a1542cf80578b41972f9a5b0870af9 (patch) | |
tree | 6f16319f9ce3133c4c4617129a516e692cfc3ac1 | |
parent | bc2c4aff8e657c7d5709433f137299491b98d257 (diff) |
partial implementation of tipping
-rw-r--r-- | img/spinner-bars.svg | 53 | ||||
-rw-r--r-- | src/crypto/cryptoApi.ts | 5 | ||||
-rw-r--r-- | src/crypto/cryptoWorker.ts | 31 | ||||
-rw-r--r-- | src/i18n/de.po | 113 | ||||
-rw-r--r-- | src/i18n/en-US.po | 113 | ||||
-rw-r--r-- | src/i18n/fr.po | 113 | ||||
-rw-r--r-- | src/i18n/it.po | 113 | ||||
-rw-r--r-- | src/i18n/strings.ts | 192 | ||||
-rw-r--r-- | src/i18n/taler-wallet-webex.pot | 113 | ||||
-rw-r--r-- | src/query.ts | 70 | ||||
-rw-r--r-- | src/types.ts | 224 | ||||
-rw-r--r-- | src/wallet.ts | 183 | ||||
-rw-r--r-- | src/webex/messages.ts | 16 | ||||
-rw-r--r-- | src/webex/notify.ts | 85 | ||||
-rw-r--r-- | src/webex/pages/confirm-create-reserve.tsx | 135 | ||||
-rw-r--r-- | src/webex/pages/tip.html | 24 | ||||
-rw-r--r-- | src/webex/pages/tip.tsx | 155 | ||||
-rw-r--r-- | src/webex/renderHtml.tsx | 145 | ||||
-rw-r--r-- | src/webex/style/wallet.css | 15 | ||||
-rw-r--r-- | src/webex/wxApi.ts | 23 | ||||
-rw-r--r-- | src/webex/wxBackend.ts | 25 | ||||
-rw-r--r-- | tsconfig.json | 1 | ||||
-rw-r--r-- | webpack.config.js | 1 |
23 files changed, 1393 insertions, 555 deletions
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 @@ +<!-- By Sam Herbert (@sherb), for everyone. More @ http://goo.gl/7AJzbL --> +<svg width="135" height="140" viewBox="0 0 135 140" xmlns="http://www.w3.org/2000/svg" fill="#fff"> + <rect y="10" width="15" height="120" rx="6"> + <animate attributeName="height" + begin="0.5s" dur="1s" + values="120;110;100;90;80;70;60;50;40;140;120" calcMode="linear" + repeatCount="indefinite" /> + <animate attributeName="y" + begin="0.5s" dur="1s" + values="10;15;20;25;30;35;40;45;50;0;10" calcMode="linear" + repeatCount="indefinite" /> + </rect> + <rect x="30" y="10" width="15" height="120" rx="6"> + <animate attributeName="height" + begin="0.25s" dur="1s" + values="120;110;100;90;80;70;60;50;40;140;120" calcMode="linear" + repeatCount="indefinite" /> + <animate attributeName="y" + begin="0.25s" dur="1s" + values="10;15;20;25;30;35;40;45;50;0;10" calcMode="linear" + repeatCount="indefinite" /> + </rect> + <rect x="60" width="15" height="140" rx="6"> + <animate attributeName="height" + begin="0s" dur="1s" + values="120;110;100;90;80;70;60;50;40;140;120" calcMode="linear" + repeatCount="indefinite" /> + <animate attributeName="y" + begin="0s" dur="1s" + values="10;15;20;25;30;35;40;45;50;0;10" calcMode="linear" + repeatCount="indefinite" /> + </rect> + <rect x="90" y="10" width="15" height="120" rx="6"> + <animate attributeName="height" + begin="0.25s" dur="1s" + values="120;110;100;90;80;70;60;50;40;140;120" calcMode="linear" + repeatCount="indefinite" /> + <animate attributeName="y" + begin="0.25s" dur="1s" + values="10;15;20;25;30;35;40;45;50;0;10" calcMode="linear" + repeatCount="indefinite" /> + </rect> + <rect x="120" y="10" width="15" height="120" rx="6"> + <animate attributeName="height" + begin="0.5s" dur="1s" + values="120;110;100;90;80;70;60;50;40;140;120" calcMode="linear" + repeatCount="indefinite" /> + <animate attributeName="y" + begin="0.5s" dur="1s" + values="10;15;20;25;30;35;40;45;50;0;10" calcMode="linear" + repeatCount="indefinite" /> + </rect> +</svg> 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<PreCoinRecord>("createPreCoin", 1, denom, reserve); } + createTipPlanchet(denom: DenominationRecord): Promise<TipPlanchet> { + return this.doRpc<TipPlanchet>("createTipPlanchet", 1, denom); + } + hashString(str: string): Promise<string> { return this.doRpc<string>("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 @@ -51,6 +51,20 @@ export class Store<T> { /** + * 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. */ export class Index<S extends IDBValidKey, T> { @@ -59,7 +73,16 @@ export class Index<S extends IDBValidKey, T> { */ storeName: string; - constructor(s: Store<T>, public indexName: string, public keyPath: string | string[]) { + /** + * Options to use for the index. + */ + options: IndexOptions; + + constructor(s: Store<T>, 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<T>(store: Store<T>, 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<T>(store: Store<T>, 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<T>(store: Store<T>, 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<string, ExchangeRecord>(this, "pubKey", "masterPublicKey"); + pubKeyIndex = new Index<string, ExchangeRecord>(this, "pubKeyIndex", "masterPublicKey"); } class NonceStore extends Store<NonceRecord> { @@ -521,7 +525,7 @@ export namespace Stores { } exchangeBaseUrlIndex = new Index<string, CoinRecord>(this, "exchangeBaseUrl", "exchangeBaseUrl"); - denomPubIndex = new Index<string, CoinRecord>(this, "denomPub", "denomPub"); + denomPubIndex = new Index<string, CoinRecord>(this, "denomPubIndex", "denomPub"); } class ProposalsStore extends Store<ProposalRecord> { @@ -531,7 +535,7 @@ export namespace Stores { keyPath: "id", }); } - timestampIndex = new Index<string, ProposalRecord>(this, "timestamp", "timestamp"); + timestampIndex = new Index<string, ProposalRecord>(this, "timestampIndex", "timestamp"); } class PurchasesStore extends Store<PurchaseRecord> { @@ -539,9 +543,9 @@ export namespace Stores { super("purchases", {keyPath: "contractTermsHash"}); } - fulfillmentUrlIndex = new Index<string, PurchaseRecord>(this, "fulfillment_url", "contractTerms.fulfillment_url"); - orderIdIndex = new Index<string, PurchaseRecord>(this, "order_id", "contractTerms.order_id"); - timestampIndex = new Index<string, PurchaseRecord>(this, "timestamp", "timestamp"); + fulfillmentUrlIndex = new Index<string, PurchaseRecord>(this, "fulfillmentUrlIndex", "contractTerms.fulfillment_url"); + orderIdIndex = new Index<string, PurchaseRecord>(this, "orderIdIndex", "contractTerms.order_id"); + timestampIndex = new Index<string, PurchaseRecord>(this, "timestampIndex", "timestamp"); } class DenominationsStore extends Store<DenominationRecord> { @@ -551,9 +555,9 @@ export namespace Stores { {keyPath: ["exchangeBaseUrl", "denomPub"] as any as IDBKeyPath}); } - denomPubHashIndex = new Index<string, DenominationRecord>(this, "denomPubHash", "denomPubHash"); - exchangeBaseUrlIndex = new Index<string, DenominationRecord>(this, "exchangeBaseUrl", "exchangeBaseUrl"); - denomPubIndex = new Index<string, DenominationRecord>(this, "denomPub", "denomPub"); + denomPubHashIndex = new Index<string, DenominationRecord>(this, "denomPubHashIndex", "denomPubHash"); + exchangeBaseUrlIndex = new Index<string, DenominationRecord>(this, "exchangeBaseUrlIndex", "exchangeBaseUrl"); + denomPubIndex = new Index<string, DenominationRecord>(this, "denomPubIndex", "denomPub"); } class CurrenciesStore extends Store<CurrencyRecord> { @@ -578,9 +582,16 @@ export namespace Stores { constructor() { super("reserves", {keyPath: "reserve_pub"}); } - timestampCreatedIndex = new Index<string, ReserveRecord>(this, "timestampCreated", "created"); - timestampConfirmedIndex = new Index<string, ReserveRecord>(this, "timestampConfirmed", "timestamp_confirmed"); - timestampDepletedIndex = new Index<string, ReserveRecord>(this, "timestampDepleted", "timestamp_depleted"); + timestampCreatedIndex = new Index<string, ReserveRecord>(this, "timestampCreatedIndex", "created"); + timestampConfirmedIndex = new Index<string, ReserveRecord>(this, "timestampConfirmedIndex", "timestamp_confirmed"); + timestampDepletedIndex = new Index<string, ReserveRecord>(this, "timestampDepletedIndex", "timestamp_depleted"); + } + + class TipsStore extends Store<TipRecord> { + constructor() { + super("tips", {keyPath: ["tipId", "merchantDomain"] as any as IDBKeyPath}); + } + coinPubIndex = new Index<string, TipRecord>(this, "coinPubIndex", "coinPubs", { multiEntry: true }); } export const coins = new CoinsStore(); @@ -596,6 +607,7 @@ export namespace Stores { export const refresh = new Store<RefreshSessionRecord>("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<CoinRecord> { - const reserve = await this.q().get<ReserveRecord>(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<TipPlanchetDetail[]> { + 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<void> { + 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<void> { + 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<TipStatus> { + 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<any> { // 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 ( - <p> - Details will be displayed when a valid exchange provider URL is entered. - </p> - ); - } - if (rci.exchangeInfo.auditors.length === 0) { - return ( - <p> - The exchange is not audited by any auditors. - </p> - ); - } - return ( - <div> - {rci.exchangeInfo.auditors.map((a) => ( - <div> - <h3>Auditor {a.auditor_url}</h3> - <p>Public key: {a.auditor_pub}</p> - <p>Trusted: {rci.trustedAuditorPubs.indexOf(a.auditor_pub) >= 0 ? "yes" : "no"}</p> - <p>Audits {a.denomination_keys.length} of {rci.numOfferedDenoms} denominations</p> - </div> - ))} - </div> - ); -} - -function renderReserveCreationDetails(rci: ReserveCreationInfo|null) { - if (!rci) { - return ( - <p> - Details will be displayed when a valid exchange provider URL is entered. - </p> - ); - } - - 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 ( - <tr> - <td>{countByPub[denom.denomPub] + "x"}</td> - <td>{renderAmount(denom.value)}</td> - <td>{renderAmount(denom.feeWithdraw)}</td> - <td>{renderAmount(denom.feeRefresh)}</td> - <td>{renderAmount(denom.feeDeposit)}</td> - </tr> - ); - } - - function wireFee(s: string) { - return [ - <thead> - <tr> - <th colSpan={3}>Wire Method {s}</th> - </tr> - <tr> - <th>Applies Until</th> - <th>Wire Fee</th> - <th>Closing Fee</th> - </tr> - </thead>, - <tbody> - {rci!.wireFees.feesForType[s].map((f) => ( - <tr> - <td>{moment.unix(f.endStamp).format("llll")}</td> - <td>{renderAmount(f.wireFee)}</td> - <td>{renderAmount(f.closingFee)}</td> - </tr> - ))} - </tbody>, - ]; - } - - const withdrawFee = renderAmount(rci.withdrawFee); - const overhead = renderAmount(rci.overhead); - - return ( - <div> - <h3>Overview</h3> - <p>{i18n.str`Withdrawal fees:`} {withdrawFee}</p> - <p>{i18n.str`Rounding loss:`} {overhead}</p> - <p>{i18n.str`Earliest expiration (for deposit): ${moment.unix(rci.earliestDepositExpiration).fromNow()}`}</p> - <h3>Coin Fees</h3> - <table className="pure-table"> - <thead> - <tr> - <th>{i18n.str`# Coins`}</th> - <th>{i18n.str`Value`}</th> - <th>{i18n.str`Withdraw Fee`}</th> - <th>{i18n.str`Refresh Fee`}</th> - <th>{i18n.str`Deposit Fee`}</th> - </tr> - </thead> - <tbody> - {uniq.map(row)} - </tbody> - </table> - <h3>Wire Fees</h3> - <table className="pure-table"> - {Object.keys(rci.wireFees.feesForType).map(wireFee)} - </table> - </div> - ); -} interface ExchangeSelectionProps { @@ -428,12 +306,7 @@ class ExchangeSelection extends ImplicitStateComponent<ExchangeSelectionProps> { </button> </p> {this.renderUpdateStatus()} - <Collapsible initiallyCollapsed={true} title="Fee and Spending Details"> - {renderReserveCreationDetails(this.reserveCreationInfo())} - </Collapsible> - <Collapsible initiallyCollapsed={true} title="Auditor Details"> - {renderAuditorDetails(this.reserveCreationInfo())} - </Collapsible> + <WithdrawDetailView rci={this.reserveCreationInfo()} /> </div> ); } 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 @@ +<!DOCTYPE html> +<html> + +<head> + <meta charset="UTF-8"> + <title>Taler Wallet: Received Tip</title> + + <link rel="icon" href="/img/icon.png"> + <link rel="stylesheet" type="text/css" href="../style/pure.css"> + <link rel="stylesheet" type="text/css" href="../style/wallet.css"> + + <script src="/dist/page-common-bundle.js"></script> + <script src="/dist/tip-bundle.js"></script> + +</head> + +<body> + <section id="main"> + <h1>GNU Taler Wallet</h1> + <div id="container"></div> + </section> +</body> + +</html> 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 <http://www.gnu.org/licenses/> + */ + + +/** + * 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<TipDisplayProps, TipDisplayState> { + 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 <p>Waiting for info about exchange ...</p> + } + const totalCost = Amounts.add(rci.overhead, rci.withdrawFee).amount; + return ( + <div> + <p> + The tip is handled by the exchange <strong>{rci.exchangeInfo.baseUrl}</strong>.{" "} + The exchange provider will charge + {" "} + <strong>{renderAmount(totalCost)}</strong> + {" "}. + </p> + <WithdrawDetailView rci={rci} /> + </div> + ); + } + + accept() { + this.setState({ working: true}); + acceptTip(this.props.merchantDomain, this.props.tipId); + } + + renderButtons() { + return ( + <form className="pure-form"> + <button + className="pure-button pure-button-primary" + type="button" + onClick={() => this.accept()}> + { this.state.working ? <span><object className="svg-icon svg-baseline" data="/img/spinner-bars.svg" /> </span> : null } + Accept tip + </button> + {" "} + <button className="pure-button" type="button" onClick={() => { window.close(); }}>Discard tip</button> + </form> + ); + } + + render(): JSX.Element { + const ts = this.state.tipStatus; + if (!ts) { + return <p>Processing ...</p>; + } + return ( + <div> + <h2>Tip Received!</h2> + <p>You received a tip of <strong>{renderAmount(ts.tip.amount)}</strong> from <strong>{this.props.merchantDomain}</strong>.</p> + {ts.tip.accepted + ? <p>You've accepted this tip!</p> + : this.renderButtons() + } + {this.renderExchangeInfo(ts)} + </div> + ); + } +} + +async function main() { + try { + const url = new URI(document.location.href); + const query: any = URI.parseQuery(url.query()); + + let merchantDomain = query.merchant_domain; + let tipId = query.tip_id; + let props: TipDisplayProps = { tipId, merchantDomain }; + + ReactDOM.render(<TipDisplay {...props} />, + document.getElementById("container")!); + + } catch (e) { + // TODO: provide more context information, maybe factor it out into a + // TODO:generic error reporting function or component. + document.body.innerText = i18n.str`Fatal error: "${e.message}".`; + console.error(`got error "${e.message}"`, e); + } +} + +document.addEventListener("DOMContentLoaded", () => { + main(); +}); diff --git a/src/webex/renderHtml.tsx b/src/webex/renderHtml.tsx index 792ba2f2c..d4c536fa9 100644 --- a/src/webex/renderHtml.tsx +++ b/src/webex/renderHtml.tsx @@ -27,8 +27,14 @@ import { AmountJson, Amounts, + DenominationRecord, + ReserveCreationInfo, } from "../types"; +import * as moment from "moment"; + +import * as i18n from "../i18n"; + import * as React from "react"; @@ -101,3 +107,142 @@ export class Collapsible extends React.Component<CollapsibleProps, CollapsibleSt ); } } + + +function AuditorDetailsView(props: {rci: ReserveCreationInfo|null}): JSX.Element { + const rci = props.rci; + console.log("rci", rci); + if (!rci) { + return ( + <p> + Details will be displayed when a valid exchange provider URL is entered. + </p> + ); + } + if (rci.exchangeInfo.auditors.length === 0) { + return ( + <p> + The exchange is not audited by any auditors. + </p> + ); + } + return ( + <div> + {rci.exchangeInfo.auditors.map((a) => ( + <div> + <h3>Auditor {a.auditor_url}</h3> + <p>Public key: {a.auditor_pub}</p> + <p>Trusted: {rci.trustedAuditorPubs.indexOf(a.auditor_pub) >= 0 ? "yes" : "no"}</p> + <p>Audits {a.denomination_keys.length} of {rci.numOfferedDenoms} denominations</p> + </div> + ))} + </div> + ); +} + +function FeeDetailsView(props: {rci: ReserveCreationInfo|null}): JSX.Element { + const rci = props.rci; + if (!rci) { + return ( + <p> + Details will be displayed when a valid exchange provider URL is entered. + </p> + ); + } + + 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 ( + <tr> + <td>{countByPub[denom.denomPub] + "x"}</td> + <td>{renderAmount(denom.value)}</td> + <td>{renderAmount(denom.feeWithdraw)}</td> + <td>{renderAmount(denom.feeRefresh)}</td> + <td>{renderAmount(denom.feeDeposit)}</td> + </tr> + ); + } + + function wireFee(s: string) { + return [ + <thead> + <tr> + <th colSpan={3}>Wire Method {s}</th> + </tr> + <tr> + <th>Applies Until</th> + <th>Wire Fee</th> + <th>Closing Fee</th> + </tr> + </thead>, + <tbody> + {rci!.wireFees.feesForType[s].map((f) => ( + <tr> + <td>{moment.unix(f.endStamp).format("llll")}</td> + <td>{renderAmount(f.wireFee)}</td> + <td>{renderAmount(f.closingFee)}</td> + </tr> + ))} + </tbody>, + ]; + } + + const withdrawFee = renderAmount(rci.withdrawFee); + const overhead = renderAmount(rci.overhead); + + return ( + <div> + <h3>Overview</h3> + <p>{i18n.str`Withdrawal fees:`} {withdrawFee}</p> + <p>{i18n.str`Rounding loss:`} {overhead}</p> + <p>{i18n.str`Earliest expiration (for deposit): ${moment.unix(rci.earliestDepositExpiration).fromNow()}`}</p> + <h3>Coin Fees</h3> + <table className="pure-table"> + <thead> + <tr> + <th>{i18n.str`# Coins`}</th> + <th>{i18n.str`Value`}</th> + <th>{i18n.str`Withdraw Fee`}</th> + <th>{i18n.str`Refresh Fee`}</th> + <th>{i18n.str`Deposit Fee`}</th> + </tr> + </thead> + <tbody> + {uniq.map(row)} + </tbody> + </table> + <h3>Wire Fees</h3> + <table className="pure-table"> + {Object.keys(rci.wireFees.feesForType).map(wireFee)} + </table> + </div> + ); +} + + +export function WithdrawDetailView(props: {rci: ReserveCreationInfo | null}): JSX.Element { + const rci = props.rci; + return ( + <div> + <Collapsible initiallyCollapsed={true} title="Fee and Spending Details"> + <FeeDetailsView rci={rci} /> + </Collapsible> + <Collapsible initiallyCollapsed={true} title="Auditor Details"> + <AuditorDetailsView rci={rci} /> + </Collapsible> + </div> + ); +} diff --git a/src/webex/style/wallet.css b/src/webex/style/wallet.css index 61dd611e9..dde17e890 100644 --- a/src/webex/style/wallet.css +++ b/src/webex/style/wallet.css @@ -251,3 +251,18 @@ a.opener { .opener-collapsed::before { content: "\25b6 " } + +.svg-icon { + display: inline-flex; + align-self: center; + position: relative; + height: 1em; + width: 1em; +} +.svg-icon svg { + height:1em; + width:1em; +} +object.svg-icon.svg-baseline { + transform: translate(0, 0.125em); +} diff --git a/src/webex/wxApi.ts b/src/webex/wxApi.ts index 7afc116ba..e362fc34a 100644 --- a/src/webex/wxApi.ts +++ b/src/webex/wxApi.ts @@ -37,6 +37,9 @@ import { ReserveCreationInfo, ReserveRecord, SenderWireInfos, + TipResponse, + TipPlanchetDetail, + TipStatus, WalletBalance, } from "../types"; @@ -358,3 +361,23 @@ export function getPurchase(contractTermsHash: string): Promise<PurchaseRecord> export function getFullRefundFees(args: { refundPermissions: RefundPermission[] }): Promise<AmountJson> { return callBackend("get-full-refund-fees", { refundPermissions: args.refundPermissions }); } + + +/** + * Get or generate planchets to give the merchant that wants to tip us. + */ +export function getTipPlanchets(merchantDomain: string, tipId: string, amount: AmountJson, deadline: number, exchangeUrl: string): Promise<TipPlanchetDetail[]> { + return callBackend("get-tip-planchets", { merchantDomain, tipId, amount, deadline, exchangeUrl }); +} + +export function getTipStatus(merchantDomain: string, tipId: string): Promise<TipStatus> { + return callBackend("get-tip-status", { merchantDomain, tipId }); +} + +export function acceptTip(merchantDomain: string, tipId: string): Promise<TipStatus> { + return callBackend("accept-tip", { merchantDomain, tipId }); +} + +export function processTipResponse(merchantDomain: string, tipId: string, tipResponse: TipResponse): Promise<void> { + return callBackend("process-tip-response", { merchantDomain, tipId, tipResponse }); +} diff --git a/src/webex/wxBackend.ts b/src/webex/wxBackend.ts index 7393c8880..a17f516a8 100644 --- a/src/webex/wxBackend.ts +++ b/src/webex/wxBackend.ts @@ -31,12 +31,16 @@ import { Store, } from "../query"; import { + AcceptTipRequest, AmountJson, ConfirmReserveRequest, CreateReserveRequest, + GetTipPlanchetsRequest, Notifier, + ProcessTipResponseRequest, ProposalRecord, ReturnCoinsRequest, + TipStatusRequest, } from "../types"; import { Stores, @@ -44,6 +48,7 @@ import { Wallet, } from "../wallet"; + import { ChromeBadge } from "./chromeBadge"; import { MessageType } from "./messages"; import * as wxApi from "./wxApi"; @@ -316,6 +321,22 @@ function handleMessage(sender: MessageSender, } case "get-full-refund-fees": return needsWallet().getFullRefundFees(detail.refundPermissions); + case "get-tip-status": { + const req = TipStatusRequest.checked(detail); + return needsWallet().getTipStatus(req.merchantDomain, req.tipId); + } + case "accept-tip": { + const req = AcceptTipRequest.checked(detail); + return needsWallet().acceptTip(req.merchantDomain, req.tipId); + } + case "process-tip-response": { + const req = ProcessTipResponseRequest.checked(detail); + return needsWallet().processTipResponse(req.merchantDomain, req.tipId, req.tipResponse); + } + case "get-tip-planchets": { + const req = GetTipPlanchetsRequest.checked(detail); + return needsWallet().getTipPlanchets(req.merchantDomain, req.tipId, req.amount, req.deadline, req.exchangeUrl); + } default: // Exhaustiveness check. // See https://www.typescriptlang.org/docs/handbook/advanced-types.html @@ -409,6 +430,7 @@ function handleHttpPayment(headerList: chrome.webRequest.HttpHeader[], url: stri contract_url: headers["x-taler-contract-url"], offer_url: headers["x-taler-offer-url"], refund_url: headers["x-taler-refund-url"], + tip: headers["x-taler-tip"], }; const talerHeaderFound = Object.keys(fields).filter((x: any) => (fields as any)[x]).length !== 0; @@ -424,6 +446,7 @@ function handleHttpPayment(headerList: chrome.webRequest.HttpHeader[], url: stri contract_url: fields.contract_url, offer_url: fields.offer_url, refund_url: fields.refund_url, + tip: fields.tip, }; console.log("got pay detail", payDetail); @@ -728,7 +751,7 @@ function openTalerDb(): Promise<IDBDatabase> { for (const indexName in (si as any)) { if ((si as any)[indexName] instanceof Index) { const ii: Index<any, any> = (si as any)[indexName]; - s.createIndex(ii.indexName, ii.keyPath); + s.createIndex(ii.indexName, ii.keyPath, ii.options); } } } diff --git a/tsconfig.json b/tsconfig.json index baceaa729..3e7c68371 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -65,6 +65,7 @@ "src/webex/pages/reset-required.tsx", "src/webex/pages/return-coins.tsx", "src/webex/pages/show-db.ts", + "src/webex/pages/tip.tsx", "src/webex/pages/tree.tsx", "src/webex/renderHtml.tsx", "src/webex/wxApi.ts", diff --git a/webpack.config.js b/webpack.config.js index af586dc53..b4d1b9079 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -78,6 +78,7 @@ module.exports = function (env) { "return-coins": "./src/webex/pages/return-coins.tsx", "refund": "./src/webex/pages/refund.tsx", "show-db": "./src/webex/pages/show-db.ts", + "tip": "./src/webex/pages/tip.tsx", "tree": "./src/webex/pages/tree.tsx", }, plugins: [ |