partial implementation of tipping

This commit is contained in:
Florian Dold 2017-11-30 04:07:36 +01:00
parent bc2c4aff8e
commit b8ccc7c990
No known key found for this signature in database
GPG Key ID: D2E4F00F29D02A4B
23 changed files with 1393 additions and 555 deletions

53
img/spinner-bars.svg Normal file
View File

@ -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>

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@ -34,6 +34,7 @@ import {
PreCoinRecord, PreCoinRecord,
RefreshSessionRecord, RefreshSessionRecord,
ReserveRecord, ReserveRecord,
TipPlanchet,
WireFee, WireFee,
} from "../types"; } from "../types";
@ -253,6 +254,10 @@ export class CryptoApi {
return this.doRpc<PreCoinRecord>("createPreCoin", 1, denom, reserve); 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> { hashString(str: string): Promise<string> {
return this.doRpc<string>("hashString", 1, str); return this.doRpc<string>("hashString", 1, str);
} }

View File

@ -40,6 +40,7 @@ import {
RefreshPreCoinRecord, RefreshPreCoinRecord,
RefreshSessionRecord, RefreshSessionRecord,
ReserveRecord, ReserveRecord,
TipPlanchet,
WireFee, WireFee,
} from "../types"; } from "../types";
@ -103,6 +104,7 @@ namespace RpcFunctions {
coinValue: denom.value, coinValue: denom.value,
denomPub: denomPub.encode().toCrock(), denomPub: denomPub.encode().toCrock(),
exchangeBaseUrl: reserve.exchange_base_url, exchangeBaseUrl: reserve.exchange_base_url,
isFromTip: false,
reservePub: reservePub.toCrock(), reservePub: reservePub.toCrock(),
withdrawSig: sig.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. * Create and sign a message to request payback for a coin.
*/ */

View File

@ -66,67 +66,27 @@ msgstr ""
msgid "Confirm payment" msgid "Confirm payment"
msgstr "Bezahlung bestätigen" msgstr "Bezahlung bestätigen"
#: src/webex/pages/confirm-create-reserve.tsx:178 #: src/webex/pages/confirm-create-reserve.tsx:121
#, 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
#, c-format #, c-format
msgid "Select" msgid "Select"
msgstr "" msgstr ""
#: src/webex/pages/confirm-create-reserve.tsx:259 #: src/webex/pages/confirm-create-reserve.tsx:137
#, c-format #, c-format
msgid "Error: URL may not be relative" msgid "Error: URL may not be relative"
msgstr "" msgstr ""
#: src/webex/pages/confirm-create-reserve.tsx:327 #: src/webex/pages/confirm-create-reserve.tsx:205
#, c-format #, c-format
msgid "The exchange is trusted by the wallet.\n" msgid "The exchange is trusted by the wallet.\n"
msgstr "" msgstr ""
#: src/webex/pages/confirm-create-reserve.tsx:333 #: src/webex/pages/confirm-create-reserve.tsx:211
#, c-format #, c-format
msgid "The exchange is audited by a trusted auditor.\n" msgid "The exchange is audited by a trusted auditor.\n"
msgstr "" msgstr ""
#: src/webex/pages/confirm-create-reserve.tsx:339 #: src/webex/pages/confirm-create-reserve.tsx:217
#, c-format #, c-format
msgid "" msgid ""
"Warning: The exchange is neither directly trusted nor audited by a trusted " "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" "If you withdraw from this exchange, it will be trusted in the future.\n"
msgstr "" msgstr ""
#: src/webex/pages/confirm-create-reserve.tsx:348 #: src/webex/pages/confirm-create-reserve.tsx:226
#, c-format #, c-format
msgid "" msgid ""
"Using exchange provider%1$s.\n" "Using exchange provider%1$s.\n"
@ -142,58 +102,59 @@ msgid ""
" %2$s in fees.\n" " %2$s in fees.\n"
msgstr "" msgstr ""
#: src/webex/pages/confirm-create-reserve.tsx:362 #: src/webex/pages/confirm-create-reserve.tsx:240
#, c-format #, c-format
msgid "" msgid ""
"Waiting for a response from\n" "Waiting for a response from\n"
" %1$s" " %1$s"
msgstr "" msgstr ""
#: src/webex/pages/confirm-create-reserve.tsx:379 #: src/webex/pages/confirm-create-reserve.tsx:257
#, c-format #, c-format
msgid "" msgid ""
"Information about fees will be available when an exchange provider is " "Information about fees will be available when an exchange provider is "
"selected." "selected."
msgstr "" msgstr ""
#: src/webex/pages/confirm-create-reserve.tsx:422 #: src/webex/pages/confirm-create-reserve.tsx:300
#, c-format #, c-format
msgid "Accept fees and withdraw" msgid "Accept fees and withdraw"
msgstr "" msgstr ""
#: src/webex/pages/confirm-create-reserve.tsx:427 #: src/webex/pages/confirm-create-reserve.tsx:305
#, c-format #, c-format
msgid "Change Exchange Provider" msgid "Change Exchange Provider"
msgstr "" msgstr ""
#: src/webex/pages/confirm-create-reserve.tsx:484 #: src/webex/pages/confirm-create-reserve.tsx:357
#, c-format #, c-format
msgid "You are about to withdraw %1$s from your bank account into your wallet." msgid "You are about to withdraw %1$s from your bank account into your wallet."
msgstr "" msgstr ""
#: src/webex/pages/confirm-create-reserve.tsx:569 #: src/webex/pages/confirm-create-reserve.tsx:442
#, c-format #, c-format
msgid "" msgid ""
"Oops, something went wrong. The wallet responded with error status (%1$s)." "Oops, something went wrong. The wallet responded with error status (%1$s)."
msgstr "" msgstr ""
#: src/webex/pages/confirm-create-reserve.tsx:578 #: src/webex/pages/confirm-create-reserve.tsx:451
#, c-format #, c-format
msgid "Checking URL, please wait ..." msgid "Checking URL, please wait ..."
msgstr "" msgstr ""
#: src/webex/pages/confirm-create-reserve.tsx:592 #: src/webex/pages/confirm-create-reserve.tsx:465
#, c-format #, c-format
msgid "Can't parse amount: %1$s" msgid "Can't parse amount: %1$s"
msgstr "" msgstr ""
#: src/webex/pages/confirm-create-reserve.tsx:599 #: src/webex/pages/confirm-create-reserve.tsx:472
#, c-format #, c-format
msgid "Can't parse wire_types: %1$s" msgid "Can't parse wire_types: %1$s"
msgstr "" msgstr ""
#. #-#-#-#-# - (PACKAGE VERSION) #-#-#-#-#
#. TODO:generic error reporting function or component. #. 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 #, c-format
msgid "Fatal error: \"%1$s\"." msgid "Fatal error: \"%1$s\"."
msgstr "" msgstr ""
@ -324,6 +285,46 @@ msgstr "Bezahlung bestätigen"
msgid "Cancel" msgid "Cancel"
msgstr "Saldo" 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 #: src/wire.ts:38
#, c-format #, c-format
msgid "Invalid Wire" msgid "Invalid Wire"

View File

@ -66,67 +66,27 @@ msgstr ""
msgid "Confirm payment" msgid "Confirm payment"
msgstr "" msgstr ""
#: src/webex/pages/confirm-create-reserve.tsx:178 #: src/webex/pages/confirm-create-reserve.tsx:121
#, 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
#, c-format #, c-format
msgid "Select" msgid "Select"
msgstr "" msgstr ""
#: src/webex/pages/confirm-create-reserve.tsx:259 #: src/webex/pages/confirm-create-reserve.tsx:137
#, c-format #, c-format
msgid "Error: URL may not be relative" msgid "Error: URL may not be relative"
msgstr "" msgstr ""
#: src/webex/pages/confirm-create-reserve.tsx:327 #: src/webex/pages/confirm-create-reserve.tsx:205
#, c-format #, c-format
msgid "The exchange is trusted by the wallet.\n" msgid "The exchange is trusted by the wallet.\n"
msgstr "" msgstr ""
#: src/webex/pages/confirm-create-reserve.tsx:333 #: src/webex/pages/confirm-create-reserve.tsx:211
#, c-format #, c-format
msgid "The exchange is audited by a trusted auditor.\n" msgid "The exchange is audited by a trusted auditor.\n"
msgstr "" msgstr ""
#: src/webex/pages/confirm-create-reserve.tsx:339 #: src/webex/pages/confirm-create-reserve.tsx:217
#, c-format #, c-format
msgid "" msgid ""
"Warning: The exchange is neither directly trusted nor audited by a trusted " "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" "If you withdraw from this exchange, it will be trusted in the future.\n"
msgstr "" msgstr ""
#: src/webex/pages/confirm-create-reserve.tsx:348 #: src/webex/pages/confirm-create-reserve.tsx:226
#, c-format #, c-format
msgid "" msgid ""
"Using exchange provider%1$s.\n" "Using exchange provider%1$s.\n"
@ -142,58 +102,59 @@ msgid ""
" %2$s in fees.\n" " %2$s in fees.\n"
msgstr "" msgstr ""
#: src/webex/pages/confirm-create-reserve.tsx:362 #: src/webex/pages/confirm-create-reserve.tsx:240
#, c-format #, c-format
msgid "" msgid ""
"Waiting for a response from\n" "Waiting for a response from\n"
" %1$s" " %1$s"
msgstr "" msgstr ""
#: src/webex/pages/confirm-create-reserve.tsx:379 #: src/webex/pages/confirm-create-reserve.tsx:257
#, c-format #, c-format
msgid "" msgid ""
"Information about fees will be available when an exchange provider is " "Information about fees will be available when an exchange provider is "
"selected." "selected."
msgstr "" msgstr ""
#: src/webex/pages/confirm-create-reserve.tsx:422 #: src/webex/pages/confirm-create-reserve.tsx:300
#, c-format #, c-format
msgid "Accept fees and withdraw" msgid "Accept fees and withdraw"
msgstr "" msgstr ""
#: src/webex/pages/confirm-create-reserve.tsx:427 #: src/webex/pages/confirm-create-reserve.tsx:305
#, c-format #, c-format
msgid "Change Exchange Provider" msgid "Change Exchange Provider"
msgstr "" msgstr ""
#: src/webex/pages/confirm-create-reserve.tsx:484 #: src/webex/pages/confirm-create-reserve.tsx:357
#, c-format #, c-format
msgid "You are about to withdraw %1$s from your bank account into your wallet." msgid "You are about to withdraw %1$s from your bank account into your wallet."
msgstr "" msgstr ""
#: src/webex/pages/confirm-create-reserve.tsx:569 #: src/webex/pages/confirm-create-reserve.tsx:442
#, c-format #, c-format
msgid "" msgid ""
"Oops, something went wrong. The wallet responded with error status (%1$s)." "Oops, something went wrong. The wallet responded with error status (%1$s)."
msgstr "" msgstr ""
#: src/webex/pages/confirm-create-reserve.tsx:578 #: src/webex/pages/confirm-create-reserve.tsx:451
#, c-format #, c-format
msgid "Checking URL, please wait ..." msgid "Checking URL, please wait ..."
msgstr "" msgstr ""
#: src/webex/pages/confirm-create-reserve.tsx:592 #: src/webex/pages/confirm-create-reserve.tsx:465
#, c-format #, c-format
msgid "Can't parse amount: %1$s" msgid "Can't parse amount: %1$s"
msgstr "" msgstr ""
#: src/webex/pages/confirm-create-reserve.tsx:599 #: src/webex/pages/confirm-create-reserve.tsx:472
#, c-format #, c-format
msgid "Can't parse wire_types: %1$s" msgid "Can't parse wire_types: %1$s"
msgstr "" msgstr ""
#. #-#-#-#-# - (PACKAGE VERSION) #-#-#-#-#
#. TODO:generic error reporting function or component. #. 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 #, c-format
msgid "Fatal error: \"%1$s\"." msgid "Fatal error: \"%1$s\"."
msgstr "" msgstr ""
@ -321,6 +282,46 @@ msgstr ""
msgid "Cancel" msgid "Cancel"
msgstr "" 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 #: src/wire.ts:38
#, c-format #, c-format
msgid "Invalid Wire" msgid "Invalid Wire"

View File

@ -66,67 +66,27 @@ msgstr ""
msgid "Confirm payment" msgid "Confirm payment"
msgstr "" msgstr ""
#: src/webex/pages/confirm-create-reserve.tsx:178 #: src/webex/pages/confirm-create-reserve.tsx:121
#, 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
#, c-format #, c-format
msgid "Select" msgid "Select"
msgstr "" msgstr ""
#: src/webex/pages/confirm-create-reserve.tsx:259 #: src/webex/pages/confirm-create-reserve.tsx:137
#, c-format #, c-format
msgid "Error: URL may not be relative" msgid "Error: URL may not be relative"
msgstr "" msgstr ""
#: src/webex/pages/confirm-create-reserve.tsx:327 #: src/webex/pages/confirm-create-reserve.tsx:205
#, c-format #, c-format
msgid "The exchange is trusted by the wallet.\n" msgid "The exchange is trusted by the wallet.\n"
msgstr "" msgstr ""
#: src/webex/pages/confirm-create-reserve.tsx:333 #: src/webex/pages/confirm-create-reserve.tsx:211
#, c-format #, c-format
msgid "The exchange is audited by a trusted auditor.\n" msgid "The exchange is audited by a trusted auditor.\n"
msgstr "" msgstr ""
#: src/webex/pages/confirm-create-reserve.tsx:339 #: src/webex/pages/confirm-create-reserve.tsx:217
#, c-format #, c-format
msgid "" msgid ""
"Warning: The exchange is neither directly trusted nor audited by a trusted " "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" "If you withdraw from this exchange, it will be trusted in the future.\n"
msgstr "" msgstr ""
#: src/webex/pages/confirm-create-reserve.tsx:348 #: src/webex/pages/confirm-create-reserve.tsx:226
#, c-format #, c-format
msgid "" msgid ""
"Using exchange provider%1$s.\n" "Using exchange provider%1$s.\n"
@ -142,58 +102,59 @@ msgid ""
" %2$s in fees.\n" " %2$s in fees.\n"
msgstr "" msgstr ""
#: src/webex/pages/confirm-create-reserve.tsx:362 #: src/webex/pages/confirm-create-reserve.tsx:240
#, c-format #, c-format
msgid "" msgid ""
"Waiting for a response from\n" "Waiting for a response from\n"
" %1$s" " %1$s"
msgstr "" msgstr ""
#: src/webex/pages/confirm-create-reserve.tsx:379 #: src/webex/pages/confirm-create-reserve.tsx:257
#, c-format #, c-format
msgid "" msgid ""
"Information about fees will be available when an exchange provider is " "Information about fees will be available when an exchange provider is "
"selected." "selected."
msgstr "" msgstr ""
#: src/webex/pages/confirm-create-reserve.tsx:422 #: src/webex/pages/confirm-create-reserve.tsx:300
#, c-format #, c-format
msgid "Accept fees and withdraw" msgid "Accept fees and withdraw"
msgstr "" msgstr ""
#: src/webex/pages/confirm-create-reserve.tsx:427 #: src/webex/pages/confirm-create-reserve.tsx:305
#, c-format #, c-format
msgid "Change Exchange Provider" msgid "Change Exchange Provider"
msgstr "" msgstr ""
#: src/webex/pages/confirm-create-reserve.tsx:484 #: src/webex/pages/confirm-create-reserve.tsx:357
#, c-format #, c-format
msgid "You are about to withdraw %1$s from your bank account into your wallet." msgid "You are about to withdraw %1$s from your bank account into your wallet."
msgstr "" msgstr ""
#: src/webex/pages/confirm-create-reserve.tsx:569 #: src/webex/pages/confirm-create-reserve.tsx:442
#, c-format #, c-format
msgid "" msgid ""
"Oops, something went wrong. The wallet responded with error status (%1$s)." "Oops, something went wrong. The wallet responded with error status (%1$s)."
msgstr "" msgstr ""
#: src/webex/pages/confirm-create-reserve.tsx:578 #: src/webex/pages/confirm-create-reserve.tsx:451
#, c-format #, c-format
msgid "Checking URL, please wait ..." msgid "Checking URL, please wait ..."
msgstr "" msgstr ""
#: src/webex/pages/confirm-create-reserve.tsx:592 #: src/webex/pages/confirm-create-reserve.tsx:465
#, c-format #, c-format
msgid "Can't parse amount: %1$s" msgid "Can't parse amount: %1$s"
msgstr "" msgstr ""
#: src/webex/pages/confirm-create-reserve.tsx:599 #: src/webex/pages/confirm-create-reserve.tsx:472
#, c-format #, c-format
msgid "Can't parse wire_types: %1$s" msgid "Can't parse wire_types: %1$s"
msgstr "" msgstr ""
#. #-#-#-#-# - (PACKAGE VERSION) #-#-#-#-#
#. TODO:generic error reporting function or component. #. 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 #, c-format
msgid "Fatal error: \"%1$s\"." msgid "Fatal error: \"%1$s\"."
msgstr "" msgstr ""
@ -321,6 +282,46 @@ msgstr ""
msgid "Cancel" msgid "Cancel"
msgstr "" 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 #: src/wire.ts:38
#, c-format #, c-format
msgid "Invalid Wire" msgid "Invalid Wire"

View File

@ -66,67 +66,27 @@ msgstr ""
msgid "Confirm payment" msgid "Confirm payment"
msgstr "" msgstr ""
#: src/webex/pages/confirm-create-reserve.tsx:178 #: src/webex/pages/confirm-create-reserve.tsx:121
#, 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
#, c-format #, c-format
msgid "Select" msgid "Select"
msgstr "" msgstr ""
#: src/webex/pages/confirm-create-reserve.tsx:259 #: src/webex/pages/confirm-create-reserve.tsx:137
#, c-format #, c-format
msgid "Error: URL may not be relative" msgid "Error: URL may not be relative"
msgstr "" msgstr ""
#: src/webex/pages/confirm-create-reserve.tsx:327 #: src/webex/pages/confirm-create-reserve.tsx:205
#, c-format #, c-format
msgid "The exchange is trusted by the wallet.\n" msgid "The exchange is trusted by the wallet.\n"
msgstr "" msgstr ""
#: src/webex/pages/confirm-create-reserve.tsx:333 #: src/webex/pages/confirm-create-reserve.tsx:211
#, c-format #, c-format
msgid "The exchange is audited by a trusted auditor.\n" msgid "The exchange is audited by a trusted auditor.\n"
msgstr "" msgstr ""
#: src/webex/pages/confirm-create-reserve.tsx:339 #: src/webex/pages/confirm-create-reserve.tsx:217
#, c-format #, c-format
msgid "" msgid ""
"Warning: The exchange is neither directly trusted nor audited by a trusted " "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" "If you withdraw from this exchange, it will be trusted in the future.\n"
msgstr "" msgstr ""
#: src/webex/pages/confirm-create-reserve.tsx:348 #: src/webex/pages/confirm-create-reserve.tsx:226
#, c-format #, c-format
msgid "" msgid ""
"Using exchange provider%1$s.\n" "Using exchange provider%1$s.\n"
@ -142,58 +102,59 @@ msgid ""
" %2$s in fees.\n" " %2$s in fees.\n"
msgstr "" msgstr ""
#: src/webex/pages/confirm-create-reserve.tsx:362 #: src/webex/pages/confirm-create-reserve.tsx:240
#, c-format #, c-format
msgid "" msgid ""
"Waiting for a response from\n" "Waiting for a response from\n"
" %1$s" " %1$s"
msgstr "" msgstr ""
#: src/webex/pages/confirm-create-reserve.tsx:379 #: src/webex/pages/confirm-create-reserve.tsx:257
#, c-format #, c-format
msgid "" msgid ""
"Information about fees will be available when an exchange provider is " "Information about fees will be available when an exchange provider is "
"selected." "selected."
msgstr "" msgstr ""
#: src/webex/pages/confirm-create-reserve.tsx:422 #: src/webex/pages/confirm-create-reserve.tsx:300
#, c-format #, c-format
msgid "Accept fees and withdraw" msgid "Accept fees and withdraw"
msgstr "" msgstr ""
#: src/webex/pages/confirm-create-reserve.tsx:427 #: src/webex/pages/confirm-create-reserve.tsx:305
#, c-format #, c-format
msgid "Change Exchange Provider" msgid "Change Exchange Provider"
msgstr "" msgstr ""
#: src/webex/pages/confirm-create-reserve.tsx:484 #: src/webex/pages/confirm-create-reserve.tsx:357
#, c-format #, c-format
msgid "You are about to withdraw %1$s from your bank account into your wallet." msgid "You are about to withdraw %1$s from your bank account into your wallet."
msgstr "" msgstr ""
#: src/webex/pages/confirm-create-reserve.tsx:569 #: src/webex/pages/confirm-create-reserve.tsx:442
#, c-format #, c-format
msgid "" msgid ""
"Oops, something went wrong. The wallet responded with error status (%1$s)." "Oops, something went wrong. The wallet responded with error status (%1$s)."
msgstr "" msgstr ""
#: src/webex/pages/confirm-create-reserve.tsx:578 #: src/webex/pages/confirm-create-reserve.tsx:451
#, c-format #, c-format
msgid "Checking URL, please wait ..." msgid "Checking URL, please wait ..."
msgstr "" msgstr ""
#: src/webex/pages/confirm-create-reserve.tsx:592 #: src/webex/pages/confirm-create-reserve.tsx:465
#, c-format #, c-format
msgid "Can't parse amount: %1$s" msgid "Can't parse amount: %1$s"
msgstr "" msgstr ""
#: src/webex/pages/confirm-create-reserve.tsx:599 #: src/webex/pages/confirm-create-reserve.tsx:472
#, c-format #, c-format
msgid "Can't parse wire_types: %1$s" msgid "Can't parse wire_types: %1$s"
msgstr "" msgstr ""
#. #-#-#-#-# - (PACKAGE VERSION) #-#-#-#-#
#. TODO:generic error reporting function or component. #. 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 #, c-format
msgid "Fatal error: \"%1$s\"." msgid "Fatal error: \"%1$s\"."
msgstr "" msgstr ""
@ -321,6 +282,46 @@ msgstr ""
msgid "Cancel" msgid "Cancel"
msgstr "" 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 #: src/wire.ts:38
#, c-format #, c-format
msgid "Invalid Wire" msgid "Invalid Wire"

View File

@ -45,30 +45,6 @@ strings['de'] = {
"Confirm payment": [ "Confirm payment": [
"Bezahlung bestätigen" "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": [ "Select": [
"" ""
], ],
@ -186,6 +162,30 @@ strings['de'] = {
"Cancel": [ "Cancel": [
"Saldo" "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": [ "Invalid Wire": [
"" ""
], ],
@ -231,30 +231,6 @@ strings['en-US'] = {
"Confirm payment": [ "Confirm payment": [
"" ""
], ],
"Withdrawal fees:": [
""
],
"Rounding loss:": [
""
],
"Earliest expiration (for deposit): %1$s": [
""
],
"# Coins": [
""
],
"Value": [
""
],
"Withdraw Fee": [
""
],
"Refresh Fee": [
""
],
"Deposit Fee": [
""
],
"Select": [ "Select": [
"" ""
], ],
@ -372,6 +348,30 @@ strings['en-US'] = {
"Cancel": [ "Cancel": [
"" ""
], ],
"Withdrawal fees:": [
""
],
"Rounding loss:": [
""
],
"Earliest expiration (for deposit): %1$s": [
""
],
"# Coins": [
""
],
"Value": [
""
],
"Withdraw Fee": [
""
],
"Refresh Fee": [
""
],
"Deposit Fee": [
""
],
"Invalid Wire": [ "Invalid Wire": [
"" ""
], ],
@ -417,30 +417,6 @@ strings['fr'] = {
"Confirm payment": [ "Confirm payment": [
"" ""
], ],
"Withdrawal fees:": [
""
],
"Rounding loss:": [
""
],
"Earliest expiration (for deposit): %1$s": [
""
],
"# Coins": [
""
],
"Value": [
""
],
"Withdraw Fee": [
""
],
"Refresh Fee": [
""
],
"Deposit Fee": [
""
],
"Select": [ "Select": [
"" ""
], ],
@ -558,6 +534,30 @@ strings['fr'] = {
"Cancel": [ "Cancel": [
"" ""
], ],
"Withdrawal fees:": [
""
],
"Rounding loss:": [
""
],
"Earliest expiration (for deposit): %1$s": [
""
],
"# Coins": [
""
],
"Value": [
""
],
"Withdraw Fee": [
""
],
"Refresh Fee": [
""
],
"Deposit Fee": [
""
],
"Invalid Wire": [ "Invalid Wire": [
"" ""
], ],
@ -603,30 +603,6 @@ strings['it'] = {
"Confirm payment": [ "Confirm payment": [
"" ""
], ],
"Withdrawal fees:": [
""
],
"Rounding loss:": [
""
],
"Earliest expiration (for deposit): %1$s": [
""
],
"# Coins": [
""
],
"Value": [
""
],
"Withdraw Fee": [
""
],
"Refresh Fee": [
""
],
"Deposit Fee": [
""
],
"Select": [ "Select": [
"" ""
], ],
@ -744,6 +720,30 @@ strings['it'] = {
"Cancel": [ "Cancel": [
"" ""
], ],
"Withdrawal fees:": [
""
],
"Rounding loss:": [
""
],
"Earliest expiration (for deposit): %1$s": [
""
],
"# Coins": [
""
],
"Value": [
""
],
"Withdraw Fee": [
""
],
"Refresh Fee": [
""
],
"Deposit Fee": [
""
],
"Invalid Wire": [ "Invalid Wire": [
"" ""
], ],

View File

@ -66,67 +66,27 @@ msgstr ""
msgid "Confirm payment" msgid "Confirm payment"
msgstr "" msgstr ""
#: src/webex/pages/confirm-create-reserve.tsx:178 #: src/webex/pages/confirm-create-reserve.tsx:121
#, 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
#, c-format #, c-format
msgid "Select" msgid "Select"
msgstr "" msgstr ""
#: src/webex/pages/confirm-create-reserve.tsx:259 #: src/webex/pages/confirm-create-reserve.tsx:137
#, c-format #, c-format
msgid "Error: URL may not be relative" msgid "Error: URL may not be relative"
msgstr "" msgstr ""
#: src/webex/pages/confirm-create-reserve.tsx:327 #: src/webex/pages/confirm-create-reserve.tsx:205
#, c-format #, c-format
msgid "The exchange is trusted by the wallet.\n" msgid "The exchange is trusted by the wallet.\n"
msgstr "" msgstr ""
#: src/webex/pages/confirm-create-reserve.tsx:333 #: src/webex/pages/confirm-create-reserve.tsx:211
#, c-format #, c-format
msgid "The exchange is audited by a trusted auditor.\n" msgid "The exchange is audited by a trusted auditor.\n"
msgstr "" msgstr ""
#: src/webex/pages/confirm-create-reserve.tsx:339 #: src/webex/pages/confirm-create-reserve.tsx:217
#, c-format #, c-format
msgid "" msgid ""
"Warning: The exchange is neither directly trusted nor audited by a trusted " "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" "If you withdraw from this exchange, it will be trusted in the future.\n"
msgstr "" msgstr ""
#: src/webex/pages/confirm-create-reserve.tsx:348 #: src/webex/pages/confirm-create-reserve.tsx:226
#, c-format #, c-format
msgid "" msgid ""
"Using exchange provider%1$s.\n" "Using exchange provider%1$s.\n"
@ -142,58 +102,59 @@ msgid ""
" %2$s in fees.\n" " %2$s in fees.\n"
msgstr "" msgstr ""
#: src/webex/pages/confirm-create-reserve.tsx:362 #: src/webex/pages/confirm-create-reserve.tsx:240
#, c-format #, c-format
msgid "" msgid ""
"Waiting for a response from\n" "Waiting for a response from\n"
" %1$s" " %1$s"
msgstr "" msgstr ""
#: src/webex/pages/confirm-create-reserve.tsx:379 #: src/webex/pages/confirm-create-reserve.tsx:257
#, c-format #, c-format
msgid "" msgid ""
"Information about fees will be available when an exchange provider is " "Information about fees will be available when an exchange provider is "
"selected." "selected."
msgstr "" msgstr ""
#: src/webex/pages/confirm-create-reserve.tsx:422 #: src/webex/pages/confirm-create-reserve.tsx:300
#, c-format #, c-format
msgid "Accept fees and withdraw" msgid "Accept fees and withdraw"
msgstr "" msgstr ""
#: src/webex/pages/confirm-create-reserve.tsx:427 #: src/webex/pages/confirm-create-reserve.tsx:305
#, c-format #, c-format
msgid "Change Exchange Provider" msgid "Change Exchange Provider"
msgstr "" msgstr ""
#: src/webex/pages/confirm-create-reserve.tsx:484 #: src/webex/pages/confirm-create-reserve.tsx:357
#, c-format #, c-format
msgid "You are about to withdraw %1$s from your bank account into your wallet." msgid "You are about to withdraw %1$s from your bank account into your wallet."
msgstr "" msgstr ""
#: src/webex/pages/confirm-create-reserve.tsx:569 #: src/webex/pages/confirm-create-reserve.tsx:442
#, c-format #, c-format
msgid "" msgid ""
"Oops, something went wrong. The wallet responded with error status (%1$s)." "Oops, something went wrong. The wallet responded with error status (%1$s)."
msgstr "" msgstr ""
#: src/webex/pages/confirm-create-reserve.tsx:578 #: src/webex/pages/confirm-create-reserve.tsx:451
#, c-format #, c-format
msgid "Checking URL, please wait ..." msgid "Checking URL, please wait ..."
msgstr "" msgstr ""
#: src/webex/pages/confirm-create-reserve.tsx:592 #: src/webex/pages/confirm-create-reserve.tsx:465
#, c-format #, c-format
msgid "Can't parse amount: %1$s" msgid "Can't parse amount: %1$s"
msgstr "" msgstr ""
#: src/webex/pages/confirm-create-reserve.tsx:599 #: src/webex/pages/confirm-create-reserve.tsx:472
#, c-format #, c-format
msgid "Can't parse wire_types: %1$s" msgid "Can't parse wire_types: %1$s"
msgstr "" msgstr ""
#. #-#-#-#-# - (PACKAGE VERSION) #-#-#-#-#
#. TODO:generic error reporting function or component. #. 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 #, c-format
msgid "Fatal error: \"%1$s\"." msgid "Fatal error: \"%1$s\"."
msgstr "" msgstr ""
@ -321,6 +282,46 @@ msgstr ""
msgid "Cancel" msgid "Cancel"
msgstr "" 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 #: src/wire.ts:38
#, c-format #, c-format
msgid "Invalid Wire" msgid "Invalid Wire"

View File

@ -50,6 +50,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. * Definition of an index.
*/ */
@ -59,7 +73,16 @@ export class Index<S extends IDBValidKey, T> {
*/ */
storeName: string; 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; 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 { mutate<T>(store: Store<T>, key: any, f: (v: T|undefined) => T|undefined): QueryRoot {
this.checkFinished(); this.checkFinished();
const doPut = (tx: IDBTransaction) => { const doPut = (tx: IDBTransaction) => {
const reqGet = tx.objectStore(store.name).get(key); const req = tx.objectStore(store.name).openCursor(IDBKeyRange.only(key));
reqGet.onsuccess = () => { req.onsuccess = () => {
const r = reqGet.result; const cursor = req.result;
let m: T|undefined; if (cursor) {
try { const value = cursor.value;
m = f(r); let modifiedValue: T|undefined;
} catch (e) { try {
if (e === AbortTransaction) { modifiedValue = f(value);
tx.abort(); } catch (e) {
return; if (e === AbortTransaction) {
tx.abort();
return;
}
throw e;
} }
throw e; if (modifiedValue !== undefined && modifiedValue !== null) {
} cursor.update(modifiedValue);
if (m !== undefined && m !== null) { }
tx.objectStore(store.name).put(m); cursor.continue();
} }
}; };
}; };
@ -702,8 +732,6 @@ export class QueryRoot {
/** /**
* Add all object from an iterable to the given object store. * 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 { putAll<T>(store: Store<T>, iterable: T[]): QueryRoot {
this.checkFinished(); this.checkFinished();
@ -822,13 +850,13 @@ export class QueryRoot {
/** /**
* Delete an object by from the given object store. * 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(); this.checkFinished();
const doDelete = (tx: IDBTransaction) => { const doDelete = (tx: IDBTransaction) => {
tx.objectStore(storeName).delete(key); tx.objectStore(store.name).delete(key);
}; };
this.scheduleFinish(); this.scheduleFinish();
this.addWork(doDelete, storeName, true); this.addWork(doDelete, store.name, true);
return this; return this;
} }

View File

@ -598,6 +598,12 @@ export interface PreCoinRecord {
coinEv: string; coinEv: string;
exchangeBaseUrl: string; exchangeBaseUrl: string;
coinValue: AmountJson; 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. * Coin was dirty but can't be refreshed.
*/ */
Useless, 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; 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;
}

View File

@ -81,6 +81,10 @@ import {
ReserveRecord, ReserveRecord,
ReturnCoinsRequest, ReturnCoinsRequest,
SenderWireInfos, SenderWireInfos,
TipPlanchetDetail,
TipRecord,
TipResponse,
TipStatus,
WalletBalance, WalletBalance,
WalletBalanceEntry, WalletBalanceEntry,
WireFee, WireFee,
@ -324,7 +328,7 @@ export const WALLET_PROTOCOL_VERSION = "0:0:0";
* In the future we might consider adding migration functions for * In the future we might consider adding migration functions for
* each version increment. * each version increment.
*/ */
export const WALLET_DB_VERSION = 20; export const WALLET_DB_VERSION = 21;
const builtinCurrencies: CurrencyRecord[] = [ const builtinCurrencies: CurrencyRecord[] = [
{ {
@ -506,7 +510,7 @@ export namespace Stores {
super("exchanges", {keyPath: "baseUrl"}); 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> { class NonceStore extends Store<NonceRecord> {
@ -521,7 +525,7 @@ export namespace Stores {
} }
exchangeBaseUrlIndex = new Index<string, CoinRecord>(this, "exchangeBaseUrl", "exchangeBaseUrl"); 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> { class ProposalsStore extends Store<ProposalRecord> {
@ -531,7 +535,7 @@ export namespace Stores {
keyPath: "id", keyPath: "id",
}); });
} }
timestampIndex = new Index<string, ProposalRecord>(this, "timestamp", "timestamp"); timestampIndex = new Index<string, ProposalRecord>(this, "timestampIndex", "timestamp");
} }
class PurchasesStore extends Store<PurchaseRecord> { class PurchasesStore extends Store<PurchaseRecord> {
@ -539,9 +543,9 @@ export namespace Stores {
super("purchases", {keyPath: "contractTermsHash"}); super("purchases", {keyPath: "contractTermsHash"});
} }
fulfillmentUrlIndex = new Index<string, PurchaseRecord>(this, "fulfillment_url", "contractTerms.fulfillment_url"); fulfillmentUrlIndex = new Index<string, PurchaseRecord>(this, "fulfillmentUrlIndex", "contractTerms.fulfillment_url");
orderIdIndex = new Index<string, PurchaseRecord>(this, "order_id", "contractTerms.order_id"); orderIdIndex = new Index<string, PurchaseRecord>(this, "orderIdIndex", "contractTerms.order_id");
timestampIndex = new Index<string, PurchaseRecord>(this, "timestamp", "timestamp"); timestampIndex = new Index<string, PurchaseRecord>(this, "timestampIndex", "timestamp");
} }
class DenominationsStore extends Store<DenominationRecord> { class DenominationsStore extends Store<DenominationRecord> {
@ -551,9 +555,9 @@ export namespace Stores {
{keyPath: ["exchangeBaseUrl", "denomPub"] as any as IDBKeyPath}); {keyPath: ["exchangeBaseUrl", "denomPub"] as any as IDBKeyPath});
} }
denomPubHashIndex = new Index<string, DenominationRecord>(this, "denomPubHash", "denomPubHash"); denomPubHashIndex = new Index<string, DenominationRecord>(this, "denomPubHashIndex", "denomPubHash");
exchangeBaseUrlIndex = new Index<string, DenominationRecord>(this, "exchangeBaseUrl", "exchangeBaseUrl"); exchangeBaseUrlIndex = new Index<string, DenominationRecord>(this, "exchangeBaseUrlIndex", "exchangeBaseUrl");
denomPubIndex = new Index<string, DenominationRecord>(this, "denomPub", "denomPub"); denomPubIndex = new Index<string, DenominationRecord>(this, "denomPubIndex", "denomPub");
} }
class CurrenciesStore extends Store<CurrencyRecord> { class CurrenciesStore extends Store<CurrencyRecord> {
@ -578,9 +582,16 @@ export namespace Stores {
constructor() { constructor() {
super("reserves", {keyPath: "reserve_pub"}); super("reserves", {keyPath: "reserve_pub"});
} }
timestampCreatedIndex = new Index<string, ReserveRecord>(this, "timestampCreated", "created"); timestampCreatedIndex = new Index<string, ReserveRecord>(this, "timestampCreatedIndex", "created");
timestampConfirmedIndex = new Index<string, ReserveRecord>(this, "timestampConfirmed", "timestamp_confirmed"); timestampConfirmedIndex = new Index<string, ReserveRecord>(this, "timestampConfirmedIndex", "timestamp_confirmed");
timestampDepletedIndex = new Index<string, ReserveRecord>(this, "timestampDepleted", "timestamp_depleted"); 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(); 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 refresh = new Store<RefreshSessionRecord>("refresh", {keyPath: "id", autoIncrement: true});
export const reserves = new ReservesStore(); export const reserves = new ReservesStore();
export const purchases = new PurchasesStore(); export const purchases = new PurchasesStore();
export const tips = new TipsStore();
} }
/* tslint:enable:completed-docs */ /* tslint:enable:completed-docs */
@ -1126,7 +1138,7 @@ export class Wallet {
() => this.processPreCoin(preCoin, Math.min(retryDelayMs * 2, 5 * 60 * 1000))); () => this.processPreCoin(preCoin, Math.min(retryDelayMs * 2, 5 * 60 * 1000)));
return; return;
} }
console.log("executing processPreCoin"); console.log("executing processPreCoin", preCoin);
this.processPreCoinConcurrent++; this.processPreCoinConcurrent++;
try { try {
const exchange = await this.q().get(Stores.exchanges, const exchange = await this.q().get(Stores.exchanges,
@ -1143,6 +1155,7 @@ export class Wallet {
} }
const coin = await this.withdrawExecute(preCoin); const coin = await this.withdrawExecute(preCoin);
console.log("processPreCoin: got coin", coin);
const mutateReserve = (r: ReserveRecord) => { const mutateReserve = (r: ReserveRecord) => {
@ -1160,10 +1173,28 @@ export class Wallet {
await this.q() await this.q()
.mutate(Stores.reserves, preCoin.reservePub, mutateReserve) .mutate(Stores.reserves, preCoin.reservePub, mutateReserve)
.delete("precoins", coin.coinPub) .delete(Stores.precoins, coin.coinPub)
.add(Stores.coins, coin) .add(Stores.coins, coin)
.finish(); .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(); this.notifier.notify();
} catch (e) { } catch (e) {
console.error("Failed to withdraw coin from precoin, retrying in", console.error("Failed to withdraw coin from precoin, retrying in",
@ -1266,19 +1297,12 @@ export class Wallet {
private async withdrawExecute(pc: PreCoinRecord): Promise<CoinRecord> { 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 = {}; const wd: any = {};
wd.denom_pub = pc.denomPub; wd.denom_pub = pc.denomPub;
wd.reserve_pub = pc.reservePub; wd.reserve_pub = pc.reservePub;
wd.reserve_sig = pc.withdrawSig; wd.reserve_sig = pc.withdrawSig;
wd.coin_ev = pc.coinEv; 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); const resp = await this.http.postJson(reqUrl.href(), wd);
if (resp.status !== 200) { if (resp.status !== 200) {
@ -1289,8 +1313,8 @@ export class Wallet {
} }
const r = JSON.parse(resp.responseText); const r = JSON.parse(resp.responseText);
const denomSig = await this.cryptoApi.rsaUnblind(r.ev_sig, const denomSig = await this.cryptoApi.rsaUnblind(r.ev_sig,
pc.blindingKey, pc.blindingKey,
pc.denomPub); pc.denomPub);
const coin: CoinRecord = { const coin: CoinRecord = {
blindingKey: pc.blindingKey, blindingKey: pc.blindingKey,
coinPriv: pc.coinPriv, coinPriv: pc.coinPriv,
@ -2809,4 +2833,113 @@ export class Wallet {
} }
return feeAcc; 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;
}
} }

View File

@ -192,6 +192,22 @@ export interface MessageMap {
request: { refundPermissions: types.RefundPermission[] }; request: { refundPermissions: types.RefundPermission[] };
response: void; 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;
};
} }
/** /**

View File

@ -28,7 +28,9 @@ import URI = require("urijs");
import wxApi = require("./wxApi"); import wxApi = require("./wxApi");
import { QueryPaymentResult } from "../types"; import { getTalerStampSec } from "../helpers";
import { TipToken, QueryPaymentResult } from "../types";
import axios from "axios"; import axios from "axios";
@ -260,6 +262,87 @@ function talerPay(msg: any): Promise<any> {
// Use a promise directly instead of of an async // Use a promise directly instead of of an async
// function since some paths never resolve the promise. // function since some paths never resolve the promise.
return new Promise(async(resolve, reject) => { 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) { if (msg.refund_url) {
console.log("processing refund"); console.log("processing refund");
let resp; let resp;

View File

@ -22,18 +22,17 @@
* @author Florian Dold * @author Florian Dold
*/ */
import {canonicalizeBaseUrl} from "../../helpers"; import { canonicalizeBaseUrl } from "../../helpers";
import * as i18n from "../../i18n"; import * as i18n from "../../i18n";
import { import {
AmountJson, AmountJson,
Amounts, Amounts,
CreateReserveResponse, CreateReserveResponse,
CurrencyRecord, CurrencyRecord,
DenominationRecord,
ReserveCreationInfo, ReserveCreationInfo,
} from "../../types"; } from "../../types";
import {ImplicitStateComponent, StateHolder} from "../components"; import { ImplicitStateComponent, StateHolder } from "../components";
import { import {
createReserve, createReserve,
getCurrency, getCurrency,
@ -41,9 +40,8 @@ import {
getReserveCreationInfo, getReserveCreationInfo,
} from "../wxApi"; } from "../wxApi";
import {Collapsible, renderAmount} from "../renderHtml"; import { renderAmount, WithdrawDetailView } from "../renderHtml";
import * as moment from "moment";
import * as React from "react"; import * as React from "react";
import * as ReactDOM from "react-dom"; import * as ReactDOM from "react-dom";
import URI = require("urijs"); 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 { interface ExchangeSelectionProps {
@ -428,12 +306,7 @@ class ExchangeSelection extends ImplicitStateComponent<ExchangeSelectionProps> {
</button> </button>
</p> </p>
{this.renderUpdateStatus()} {this.renderUpdateStatus()}
<Collapsible initiallyCollapsed={true} title="Fee and Spending Details"> <WithdrawDetailView rci={this.reserveCreationInfo()} />
{renderReserveCreationDetails(this.reserveCreationInfo())}
</Collapsible>
<Collapsible initiallyCollapsed={true} title="Auditor Details">
{renderAuditorDetails(this.reserveCreationInfo())}
</Collapsible>
</div> </div>
); );
} }

24
src/webex/pages/tip.html Normal file
View File

@ -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>

155
src/webex/pages/tip.tsx Normal file
View File

@ -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();
});

View File

@ -27,8 +27,14 @@
import { import {
AmountJson, AmountJson,
Amounts, Amounts,
DenominationRecord,
ReserveCreationInfo,
} from "../types"; } from "../types";
import * as moment from "moment";
import * as i18n from "../i18n";
import * as React from "react"; 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>
);
}

View File

@ -251,3 +251,18 @@ a.opener {
.opener-collapsed::before { .opener-collapsed::before {
content: "\25b6 " 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);
}

View File

@ -37,6 +37,9 @@ import {
ReserveCreationInfo, ReserveCreationInfo,
ReserveRecord, ReserveRecord,
SenderWireInfos, SenderWireInfos,
TipResponse,
TipPlanchetDetail,
TipStatus,
WalletBalance, WalletBalance,
} from "../types"; } from "../types";
@ -358,3 +361,23 @@ export function getPurchase(contractTermsHash: string): Promise<PurchaseRecord>
export function getFullRefundFees(args: { refundPermissions: RefundPermission[] }): Promise<AmountJson> { export function getFullRefundFees(args: { refundPermissions: RefundPermission[] }): Promise<AmountJson> {
return callBackend("get-full-refund-fees", { refundPermissions: args.refundPermissions }); 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 });
}

View File

@ -31,12 +31,16 @@ import {
Store, Store,
} from "../query"; } from "../query";
import { import {
AcceptTipRequest,
AmountJson, AmountJson,
ConfirmReserveRequest, ConfirmReserveRequest,
CreateReserveRequest, CreateReserveRequest,
GetTipPlanchetsRequest,
Notifier, Notifier,
ProcessTipResponseRequest,
ProposalRecord, ProposalRecord,
ReturnCoinsRequest, ReturnCoinsRequest,
TipStatusRequest,
} from "../types"; } from "../types";
import { import {
Stores, Stores,
@ -44,6 +48,7 @@ import {
Wallet, Wallet,
} from "../wallet"; } from "../wallet";
import { ChromeBadge } from "./chromeBadge"; import { ChromeBadge } from "./chromeBadge";
import { MessageType } from "./messages"; import { MessageType } from "./messages";
import * as wxApi from "./wxApi"; import * as wxApi from "./wxApi";
@ -316,6 +321,22 @@ function handleMessage(sender: MessageSender,
} }
case "get-full-refund-fees": case "get-full-refund-fees":
return needsWallet().getFullRefundFees(detail.refundPermissions); 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: default:
// Exhaustiveness check. // Exhaustiveness check.
// See https://www.typescriptlang.org/docs/handbook/advanced-types.html // 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"], contract_url: headers["x-taler-contract-url"],
offer_url: headers["x-taler-offer-url"], offer_url: headers["x-taler-offer-url"],
refund_url: headers["x-taler-refund-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; 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, contract_url: fields.contract_url,
offer_url: fields.offer_url, offer_url: fields.offer_url,
refund_url: fields.refund_url, refund_url: fields.refund_url,
tip: fields.tip,
}; };
console.log("got pay detail", payDetail); console.log("got pay detail", payDetail);
@ -728,7 +751,7 @@ function openTalerDb(): Promise<IDBDatabase> {
for (const indexName in (si as any)) { for (const indexName in (si as any)) {
if ((si as any)[indexName] instanceof Index) { if ((si as any)[indexName] instanceof Index) {
const ii: Index<any, any> = (si as any)[indexName]; const ii: Index<any, any> = (si as any)[indexName];
s.createIndex(ii.indexName, ii.keyPath); s.createIndex(ii.indexName, ii.keyPath, ii.options);
} }
} }
} }

View File

@ -65,6 +65,7 @@
"src/webex/pages/reset-required.tsx", "src/webex/pages/reset-required.tsx",
"src/webex/pages/return-coins.tsx", "src/webex/pages/return-coins.tsx",
"src/webex/pages/show-db.ts", "src/webex/pages/show-db.ts",
"src/webex/pages/tip.tsx",
"src/webex/pages/tree.tsx", "src/webex/pages/tree.tsx",
"src/webex/renderHtml.tsx", "src/webex/renderHtml.tsx",
"src/webex/wxApi.ts", "src/webex/wxApi.ts",

View File

@ -78,6 +78,7 @@ module.exports = function (env) {
"return-coins": "./src/webex/pages/return-coins.tsx", "return-coins": "./src/webex/pages/return-coins.tsx",
"refund": "./src/webex/pages/refund.tsx", "refund": "./src/webex/pages/refund.tsx",
"show-db": "./src/webex/pages/show-db.ts", "show-db": "./src/webex/pages/show-db.ts",
"tip": "./src/webex/pages/tip.tsx",
"tree": "./src/webex/pages/tree.tsx", "tree": "./src/webex/pages/tree.tsx",
}, },
plugins: [ plugins: [