aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--img/spinner-bars.svg53
-rw-r--r--src/crypto/cryptoApi.ts5
-rw-r--r--src/crypto/cryptoWorker.ts31
-rw-r--r--src/i18n/de.po113
-rw-r--r--src/i18n/en-US.po113
-rw-r--r--src/i18n/fr.po113
-rw-r--r--src/i18n/it.po113
-rw-r--r--src/i18n/strings.ts192
-rw-r--r--src/i18n/taler-wallet-webex.pot113
-rw-r--r--src/query.ts70
-rw-r--r--src/types.ts224
-rw-r--r--src/wallet.ts183
-rw-r--r--src/webex/messages.ts16
-rw-r--r--src/webex/notify.ts85
-rw-r--r--src/webex/pages/confirm-create-reserve.tsx135
-rw-r--r--src/webex/pages/tip.html24
-rw-r--r--src/webex/pages/tip.tsx155
-rw-r--r--src/webex/renderHtml.tsx145
-rw-r--r--src/webex/style/wallet.css15
-rw-r--r--src/webex/wxApi.ts23
-rw-r--r--src/webex/wxBackend.ts25
-rw-r--r--tsconfig.json1
-rw-r--r--webpack.config.js1
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: [