From d5bba630a35fff72b11273fb5e62c2208f9e1f5b Mon Sep 17 00:00:00 2001 From: Florian Dold Date: Mon, 14 Aug 2017 04:16:12 +0200 Subject: implement returning coins to user's account --- src/checkable.ts | 5 +- src/crypto/cryptoApi.ts | 6 +- src/crypto/cryptoWorker.ts | 22 +- src/i18n/de.po | 97 ++++---- src/i18n/en-US.po | 97 ++++---- src/i18n/fr.po | 97 ++++---- src/i18n/it.po | 97 ++++---- src/i18n/strings.ts | 36 +++ src/i18n/taler-wallet-webex.pot | 97 ++++---- src/query.ts | 40 +-- src/types.ts | 192 ++++++++++++++- src/wallet.ts | 369 ++++++++++++++++++++++------ src/webex/messages.ts | 8 + src/webex/pages/confirm-create-reserve.html | 30 +-- src/webex/pages/popup.tsx | 44 ++-- src/webex/pages/return-coins.html | 19 ++ src/webex/pages/return-coins.tsx | 271 ++++++++++++++++++++ src/webex/style/wallet.css | 16 ++ src/webex/wxApi.ts | 27 +- src/webex/wxBackend.ts | 17 +- src/wire.ts | 53 ++++ 21 files changed, 1268 insertions(+), 372 deletions(-) create mode 100644 src/webex/pages/return-coins.html create mode 100644 src/webex/pages/return-coins.tsx create mode 100644 src/wire.ts (limited to 'src') diff --git a/src/checkable.ts b/src/checkable.ts index 802d8f32d..e6ef99336 100644 --- a/src/checkable.ts +++ b/src/checkable.ts @@ -217,12 +217,11 @@ export namespace Checkable { type: target, }, ["(root)"]); if (opts.validate) { - const instance = new target(); - if (typeof instance.validate !== "function") { + if (target.validate !== "function") { throw Error("invalid Checkable annotion: validate method required"); } // May throw exception - instance.validate.call(cv); + target.validate(cv); } return cv; }; diff --git a/src/crypto/cryptoApi.ts b/src/crypto/cryptoApi.ts index 139f8ae88..227c3d346 100644 --- a/src/crypto/cryptoApi.ts +++ b/src/crypto/cryptoApi.ts @@ -26,8 +26,8 @@ import { AmountJson, CoinRecord, + ContractTerms, DenominationRecord, - ProposalRecord, PayCoinInfo, PaybackRequest, PreCoinRecord, @@ -277,9 +277,9 @@ export class CryptoApi { return this.doRpc("isValidPaymentSignature", 1, sig, contractHash, merchantPub); } - signDeposit(proposal: ProposalRecord, + signDeposit(contractTerms: ContractTerms, cds: CoinWithDenom[]): Promise { - return this.doRpc("signDeposit", 3, proposal, cds); + return this.doRpc("signDeposit", 3, contractTerms, cds); } createEddsaKeypair(): Promise<{priv: string, pub: string}> { diff --git a/src/crypto/cryptoWorker.ts b/src/crypto/cryptoWorker.ts index 507a080ac..b05d7d184 100644 --- a/src/crypto/cryptoWorker.ts +++ b/src/crypto/cryptoWorker.ts @@ -28,8 +28,8 @@ import { CoinPaySig, CoinRecord, CoinStatus, + ContractTerms, DenominationRecord, - ProposalRecord, PayCoinInfo, PaybackRequest, PreCoinRecord, @@ -38,6 +38,9 @@ import { ReserveRecord, WireFee, } from "../types"; +import { + canonicalJson, +} from "../helpers"; import { CoinWithDenom, } from "../wallet"; @@ -227,16 +230,17 @@ namespace RpcFunctions { * Generate updated coins (to store in the database) * and deposit permissions for each given coin. */ - export function signDeposit(proposal: ProposalRecord, + export function signDeposit(contractTerms: ContractTerms, cds: CoinWithDenom[]): PayCoinInfo { const ret: PayCoinInfo = []; + const contractTermsHash = hashString(canonicalJson(contractTerms)); const feeList: AmountJson[] = cds.map((x) => x.denom.feeDeposit); let fees = Amounts.add(Amounts.getZero(feeList[0].currency), ...feeList).amount; // okay if saturates - fees = Amounts.sub(fees, proposal.contractTerms.max_fee).amount; - const total = Amounts.add(fees, proposal.contractTerms.amount).amount; + fees = Amounts.sub(fees, contractTerms.max_fee).amount; + const total = Amounts.add(fees, contractTerms.amount).amount; const amountSpent = native.Amount.getZero(cds[0].coin.currentAmount.currency); const amountRemaining = new native.Amount(total); @@ -273,11 +277,11 @@ namespace RpcFunctions { amount_with_fee: coinSpend.toNbo(), coin_pub: native.EddsaPublicKey.fromCrock(cd.coin.coinPub), deposit_fee: new native.Amount(cd.denom.feeDeposit).toNbo(), - h_contract: native.HashCode.fromCrock(proposal.contractTermsHash), - h_wire: native.HashCode.fromCrock(proposal.contractTerms.H_wire), - merchant: native.EddsaPublicKey.fromCrock(proposal.contractTerms.merchant_pub), - refund_deadline: native.AbsoluteTimeNbo.fromTalerString(proposal.contractTerms.refund_deadline), - timestamp: native.AbsoluteTimeNbo.fromTalerString(proposal.contractTerms.timestamp), + h_contract: native.HashCode.fromCrock(contractTermsHash), + h_wire: native.HashCode.fromCrock(contractTerms.H_wire), + merchant: native.EddsaPublicKey.fromCrock(contractTerms.merchant_pub), + refund_deadline: native.AbsoluteTimeNbo.fromTalerString(contractTerms.refund_deadline), + timestamp: native.AbsoluteTimeNbo.fromTalerString(contractTerms.timestamp), }); const coinSig = native.eddsaSign(d.toPurpose(), diff --git a/src/i18n/de.po b/src/i18n/de.po index 5bf73c8ab..887dc2c47 100644 --- a/src/i18n/de.po +++ b/src/i18n/de.po @@ -56,67 +56,67 @@ msgid "" "wallet." msgstr "" -#: src/webex/pages/confirm-create-reserve.tsx:212 +#: src/webex/pages/confirm-create-reserve.tsx:213 #, fuzzy, c-format msgid "Withdrawal fees:" msgstr "Abheben bei %1$s" -#: src/webex/pages/confirm-create-reserve.tsx:213 +#: src/webex/pages/confirm-create-reserve.tsx:214 #, c-format msgid "Rounding loss:" msgstr "" -#: src/webex/pages/confirm-create-reserve.tsx:214 +#: src/webex/pages/confirm-create-reserve.tsx:215 #, c-format msgid "Earliest expiration (for deposit): %1$s" msgstr "" -#: src/webex/pages/confirm-create-reserve.tsx:219 +#: src/webex/pages/confirm-create-reserve.tsx:220 #, c-format msgid "# Coins" msgstr "" -#: src/webex/pages/confirm-create-reserve.tsx:220 +#: src/webex/pages/confirm-create-reserve.tsx:221 #, c-format msgid "Value" msgstr "" -#: src/webex/pages/confirm-create-reserve.tsx:221 +#: src/webex/pages/confirm-create-reserve.tsx:222 #, fuzzy, c-format msgid "Withdraw Fee" msgstr "Abheben bei %1$s" -#: src/webex/pages/confirm-create-reserve.tsx:222 +#: src/webex/pages/confirm-create-reserve.tsx:223 #, c-format msgid "Refresh Fee" msgstr "" -#: src/webex/pages/confirm-create-reserve.tsx:223 +#: src/webex/pages/confirm-create-reserve.tsx:224 #, c-format msgid "Deposit Fee" msgstr "" -#: src/webex/pages/confirm-create-reserve.tsx:276 +#: src/webex/pages/confirm-create-reserve.tsx:278 #, c-format msgid "Select" msgstr "" -#: src/webex/pages/confirm-create-reserve.tsx:292 +#: src/webex/pages/confirm-create-reserve.tsx:294 #, c-format msgid "Error: URL may not be relative" msgstr "" -#: src/webex/pages/confirm-create-reserve.tsx:360 +#: src/webex/pages/confirm-create-reserve.tsx:362 #, c-format msgid "The exchange is trusted by the wallet.\n" msgstr "" -#: src/webex/pages/confirm-create-reserve.tsx:366 +#: src/webex/pages/confirm-create-reserve.tsx:368 #, c-format msgid "The exchange is audited by a trusted auditor.\n" msgstr "" -#: src/webex/pages/confirm-create-reserve.tsx:372 +#: src/webex/pages/confirm-create-reserve.tsx:374 #, c-format msgid "" "Warning: The exchange is neither directly trusted nor audited by a trusted " @@ -124,7 +124,7 @@ msgid "" "If you withdraw from this exchange, it will be trusted in the future.\n" msgstr "" -#: src/webex/pages/confirm-create-reserve.tsx:381 +#: src/webex/pages/confirm-create-reserve.tsx:383 #, c-format msgid "" "Using exchange provider%1$s.\n" @@ -132,151 +132,166 @@ msgid "" " %2$s in fees.\n" msgstr "" -#: src/webex/pages/confirm-create-reserve.tsx:395 +#: src/webex/pages/confirm-create-reserve.tsx:397 #, c-format msgid "" "Waiting for a response from\n" " %1$s" msgstr "" -#: src/webex/pages/confirm-create-reserve.tsx:406 +#: src/webex/pages/confirm-create-reserve.tsx:408 #, c-format msgid "A problem occured, see below. %1$s" msgstr "" -#: src/webex/pages/confirm-create-reserve.tsx:412 +#: src/webex/pages/confirm-create-reserve.tsx:414 #, c-format msgid "" "Information about fees will be available when an exchange provider is " "selected." msgstr "" -#: src/webex/pages/confirm-create-reserve.tsx:455 +#: src/webex/pages/confirm-create-reserve.tsx:457 #, c-format msgid "Accept fees and withdraw" msgstr "" -#: src/webex/pages/confirm-create-reserve.tsx:460 +#: src/webex/pages/confirm-create-reserve.tsx:462 #, c-format msgid "Change Exchange Provider" msgstr "" -#: src/webex/pages/confirm-create-reserve.tsx:517 +#: src/webex/pages/confirm-create-reserve.tsx:519 #, 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:600 +#: src/webex/pages/confirm-create-reserve.tsx:607 #, c-format msgid "" "Oops, something went wrong. The wallet responded with error status (%1$s)." msgstr "" -#: src/webex/pages/confirm-create-reserve.tsx:611 +#: src/webex/pages/confirm-create-reserve.tsx:616 #, c-format msgid "Checking URL, please wait ..." msgstr "" -#: src/webex/pages/confirm-create-reserve.tsx:625 +#: src/webex/pages/confirm-create-reserve.tsx:630 #, c-format msgid "Can't parse amount: %1$s" msgstr "" -#: src/webex/pages/confirm-create-reserve.tsx:632 +#: src/webex/pages/confirm-create-reserve.tsx:637 #, c-format msgid "Can't parse wire_types: %1$s" msgstr "" #. TODO:generic error reporting function or component. -#: src/webex/pages/confirm-create-reserve.tsx:652 +#: src/webex/pages/confirm-create-reserve.tsx:663 #, c-format msgid "Fatal error: \"%1$s\"." msgstr "" -#: src/webex/pages/popup.tsx:160 +#: src/webex/pages/popup.tsx:161 #, c-format msgid "Balance" msgstr "Saldo" -#: src/webex/pages/popup.tsx:163 +#: src/webex/pages/popup.tsx:164 #, c-format msgid "History" msgstr "Verlauf" -#: src/webex/pages/popup.tsx:166 +#: src/webex/pages/popup.tsx:167 #, c-format msgid "Debug" msgstr "Debug" -#: src/webex/pages/popup.tsx:242 +#: src/webex/pages/popup.tsx:247 #, c-format msgid "help" msgstr "" -#: src/webex/pages/popup.tsx:247 +#: src/webex/pages/popup.tsx:252 #, fuzzy, c-format msgid "" "You have no balance to show. Need some\n" " %1$s getting started?\n" msgstr "Sie haben kein Digitalgeld. Wollen Sie %1$s? abheben?" -#: src/webex/pages/popup.tsx:264 +#: src/webex/pages/popup.tsx:269 #, c-format msgid "%1$s incoming\n" msgstr "" -#: src/webex/pages/popup.tsx:277 +#: src/webex/pages/popup.tsx:282 #, c-format msgid "%1$s being spent\n" msgstr "" -#: src/webex/pages/popup.tsx:303 +#: src/webex/pages/popup.tsx:308 #, c-format msgid "Error: could not retrieve balance information." msgstr "" -#: src/webex/pages/popup.tsx:342 +#: src/webex/pages/popup.tsx:335 +#, c-format +msgid "Payback" +msgstr "" + +#: src/webex/pages/popup.tsx:336 +#, c-format +msgid "Return Electronic Cash to Bank Account" +msgstr "" + +#: src/webex/pages/popup.tsx:337 +#, c-format +msgid "Manage Trusted Auditors and Exchanges" +msgstr "" + +#: src/webex/pages/popup.tsx:349 #, fuzzy, c-format msgid "" "Bank requested reserve (%1$s) for\n" " %2$s.\n" msgstr "Bank bestätig anlegen der Reserve (%1$s) bei %2$s" -#: src/webex/pages/popup.tsx:353 +#: src/webex/pages/popup.tsx:360 #, fuzzy, c-format msgid "" "Started to withdraw\n" " %1$s from%2$s(%3$s).\n" msgstr "Reserve (%1$s) mit %2$s bei %3$s erzeugt" -#: src/webex/pages/popup.tsx:363 +#: src/webex/pages/popup.tsx:370 #, c-format msgid "Merchant%1$soffered contract%2$s;\n" msgstr "" -#: src/webex/pages/popup.tsx:373 +#: src/webex/pages/popup.tsx:380 #, fuzzy, c-format msgid "Withdrew%1$sfrom%2$s(%3$s).\n" msgstr "Reserve (%1$s) mit %2$s bei %3$s erzeugt" -#: src/webex/pages/popup.tsx:383 +#: src/webex/pages/popup.tsx:390 #, fuzzy, c-format msgid "" "Paid%1$sto merchant%2$s.\n" " (%3$s)\n" msgstr "Reserve (%1$s) mit %2$s bei %3$s erzeugt" -#: src/webex/pages/popup.tsx:392 +#: src/webex/pages/popup.tsx:399 #, c-format msgid "Unknown event (%1$s)" msgstr "" -#: src/webex/pages/popup.tsx:435 +#: src/webex/pages/popup.tsx:442 #, c-format msgid "Error: could not retrieve event history" msgstr "" -#: src/webex/pages/popup.tsx:469 +#: src/webex/pages/popup.tsx:476 #, c-format msgid "Your wallet has no events recorded." msgstr "Ihre Geldbörse verzeichnet keine Vorkommnisse." diff --git a/src/i18n/en-US.po b/src/i18n/en-US.po index e548014e6..ec879f580 100644 --- a/src/i18n/en-US.po +++ b/src/i18n/en-US.po @@ -56,67 +56,67 @@ msgid "" "wallet." msgstr "" -#: src/webex/pages/confirm-create-reserve.tsx:212 +#: src/webex/pages/confirm-create-reserve.tsx:213 #, c-format msgid "Withdrawal fees:" msgstr "" -#: src/webex/pages/confirm-create-reserve.tsx:213 +#: src/webex/pages/confirm-create-reserve.tsx:214 #, c-format msgid "Rounding loss:" msgstr "" -#: src/webex/pages/confirm-create-reserve.tsx:214 +#: src/webex/pages/confirm-create-reserve.tsx:215 #, c-format msgid "Earliest expiration (for deposit): %1$s" msgstr "" -#: src/webex/pages/confirm-create-reserve.tsx:219 +#: src/webex/pages/confirm-create-reserve.tsx:220 #, c-format msgid "# Coins" msgstr "" -#: src/webex/pages/confirm-create-reserve.tsx:220 +#: src/webex/pages/confirm-create-reserve.tsx:221 #, c-format msgid "Value" msgstr "" -#: src/webex/pages/confirm-create-reserve.tsx:221 +#: src/webex/pages/confirm-create-reserve.tsx:222 #, c-format msgid "Withdraw Fee" msgstr "" -#: src/webex/pages/confirm-create-reserve.tsx:222 +#: src/webex/pages/confirm-create-reserve.tsx:223 #, c-format msgid "Refresh Fee" msgstr "" -#: src/webex/pages/confirm-create-reserve.tsx:223 +#: src/webex/pages/confirm-create-reserve.tsx:224 #, c-format msgid "Deposit Fee" msgstr "" -#: src/webex/pages/confirm-create-reserve.tsx:276 +#: src/webex/pages/confirm-create-reserve.tsx:278 #, c-format msgid "Select" msgstr "" -#: src/webex/pages/confirm-create-reserve.tsx:292 +#: src/webex/pages/confirm-create-reserve.tsx:294 #, c-format msgid "Error: URL may not be relative" msgstr "" -#: src/webex/pages/confirm-create-reserve.tsx:360 +#: src/webex/pages/confirm-create-reserve.tsx:362 #, c-format msgid "The exchange is trusted by the wallet.\n" msgstr "" -#: src/webex/pages/confirm-create-reserve.tsx:366 +#: src/webex/pages/confirm-create-reserve.tsx:368 #, c-format msgid "The exchange is audited by a trusted auditor.\n" msgstr "" -#: src/webex/pages/confirm-create-reserve.tsx:372 +#: src/webex/pages/confirm-create-reserve.tsx:374 #, c-format msgid "" "Warning: The exchange is neither directly trusted nor audited by a trusted " @@ -124,7 +124,7 @@ msgid "" "If you withdraw from this exchange, it will be trusted in the future.\n" msgstr "" -#: src/webex/pages/confirm-create-reserve.tsx:381 +#: src/webex/pages/confirm-create-reserve.tsx:383 #, c-format msgid "" "Using exchange provider%1$s.\n" @@ -132,151 +132,166 @@ msgid "" " %2$s in fees.\n" msgstr "" -#: src/webex/pages/confirm-create-reserve.tsx:395 +#: src/webex/pages/confirm-create-reserve.tsx:397 #, c-format msgid "" "Waiting for a response from\n" " %1$s" msgstr "" -#: src/webex/pages/confirm-create-reserve.tsx:406 +#: src/webex/pages/confirm-create-reserve.tsx:408 #, c-format msgid "A problem occured, see below. %1$s" msgstr "" -#: src/webex/pages/confirm-create-reserve.tsx:412 +#: src/webex/pages/confirm-create-reserve.tsx:414 #, c-format msgid "" "Information about fees will be available when an exchange provider is " "selected." msgstr "" -#: src/webex/pages/confirm-create-reserve.tsx:455 +#: src/webex/pages/confirm-create-reserve.tsx:457 #, c-format msgid "Accept fees and withdraw" msgstr "" -#: src/webex/pages/confirm-create-reserve.tsx:460 +#: src/webex/pages/confirm-create-reserve.tsx:462 #, c-format msgid "Change Exchange Provider" msgstr "" -#: src/webex/pages/confirm-create-reserve.tsx:517 +#: src/webex/pages/confirm-create-reserve.tsx:519 #, 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:600 +#: src/webex/pages/confirm-create-reserve.tsx:607 #, c-format msgid "" "Oops, something went wrong. The wallet responded with error status (%1$s)." msgstr "" -#: src/webex/pages/confirm-create-reserve.tsx:611 +#: src/webex/pages/confirm-create-reserve.tsx:616 #, c-format msgid "Checking URL, please wait ..." msgstr "" -#: src/webex/pages/confirm-create-reserve.tsx:625 +#: src/webex/pages/confirm-create-reserve.tsx:630 #, c-format msgid "Can't parse amount: %1$s" msgstr "" -#: src/webex/pages/confirm-create-reserve.tsx:632 +#: src/webex/pages/confirm-create-reserve.tsx:637 #, c-format msgid "Can't parse wire_types: %1$s" msgstr "" #. TODO:generic error reporting function or component. -#: src/webex/pages/confirm-create-reserve.tsx:652 +#: src/webex/pages/confirm-create-reserve.tsx:663 #, c-format msgid "Fatal error: \"%1$s\"." msgstr "" -#: src/webex/pages/popup.tsx:160 +#: src/webex/pages/popup.tsx:161 #, c-format msgid "Balance" msgstr "" -#: src/webex/pages/popup.tsx:163 +#: src/webex/pages/popup.tsx:164 #, c-format msgid "History" msgstr "" -#: src/webex/pages/popup.tsx:166 +#: src/webex/pages/popup.tsx:167 #, c-format msgid "Debug" msgstr "" -#: src/webex/pages/popup.tsx:242 +#: src/webex/pages/popup.tsx:247 #, c-format msgid "help" msgstr "" -#: src/webex/pages/popup.tsx:247 +#: src/webex/pages/popup.tsx:252 #, c-format msgid "" "You have no balance to show. Need some\n" " %1$s getting started?\n" msgstr "" -#: src/webex/pages/popup.tsx:264 +#: src/webex/pages/popup.tsx:269 #, c-format msgid "%1$s incoming\n" msgstr "" -#: src/webex/pages/popup.tsx:277 +#: src/webex/pages/popup.tsx:282 #, c-format msgid "%1$s being spent\n" msgstr "" -#: src/webex/pages/popup.tsx:303 +#: src/webex/pages/popup.tsx:308 #, c-format msgid "Error: could not retrieve balance information." msgstr "" -#: src/webex/pages/popup.tsx:342 +#: src/webex/pages/popup.tsx:335 +#, c-format +msgid "Payback" +msgstr "" + +#: src/webex/pages/popup.tsx:336 +#, c-format +msgid "Return Electronic Cash to Bank Account" +msgstr "" + +#: src/webex/pages/popup.tsx:337 +#, c-format +msgid "Manage Trusted Auditors and Exchanges" +msgstr "" + +#: src/webex/pages/popup.tsx:349 #, c-format msgid "" "Bank requested reserve (%1$s) for\n" " %2$s.\n" msgstr "" -#: src/webex/pages/popup.tsx:353 +#: src/webex/pages/popup.tsx:360 #, c-format msgid "" "Started to withdraw\n" " %1$s from%2$s(%3$s).\n" msgstr "" -#: src/webex/pages/popup.tsx:363 +#: src/webex/pages/popup.tsx:370 #, c-format msgid "Merchant%1$soffered contract%2$s;\n" msgstr "" -#: src/webex/pages/popup.tsx:373 +#: src/webex/pages/popup.tsx:380 #, c-format msgid "Withdrew%1$sfrom%2$s(%3$s).\n" msgstr "" -#: src/webex/pages/popup.tsx:383 +#: src/webex/pages/popup.tsx:390 #, c-format msgid "" "Paid%1$sto merchant%2$s.\n" " (%3$s)\n" msgstr "" -#: src/webex/pages/popup.tsx:392 +#: src/webex/pages/popup.tsx:399 #, c-format msgid "Unknown event (%1$s)" msgstr "" -#: src/webex/pages/popup.tsx:435 +#: src/webex/pages/popup.tsx:442 #, c-format msgid "Error: could not retrieve event history" msgstr "" -#: src/webex/pages/popup.tsx:469 +#: src/webex/pages/popup.tsx:476 #, c-format msgid "Your wallet has no events recorded." msgstr "" diff --git a/src/i18n/fr.po b/src/i18n/fr.po index da860ad8f..705b1cba6 100644 --- a/src/i18n/fr.po +++ b/src/i18n/fr.po @@ -56,67 +56,67 @@ msgid "" "wallet." msgstr "" -#: src/webex/pages/confirm-create-reserve.tsx:212 +#: src/webex/pages/confirm-create-reserve.tsx:213 #, c-format msgid "Withdrawal fees:" msgstr "" -#: src/webex/pages/confirm-create-reserve.tsx:213 +#: src/webex/pages/confirm-create-reserve.tsx:214 #, c-format msgid "Rounding loss:" msgstr "" -#: src/webex/pages/confirm-create-reserve.tsx:214 +#: src/webex/pages/confirm-create-reserve.tsx:215 #, c-format msgid "Earliest expiration (for deposit): %1$s" msgstr "" -#: src/webex/pages/confirm-create-reserve.tsx:219 +#: src/webex/pages/confirm-create-reserve.tsx:220 #, c-format msgid "# Coins" msgstr "" -#: src/webex/pages/confirm-create-reserve.tsx:220 +#: src/webex/pages/confirm-create-reserve.tsx:221 #, c-format msgid "Value" msgstr "" -#: src/webex/pages/confirm-create-reserve.tsx:221 +#: src/webex/pages/confirm-create-reserve.tsx:222 #, c-format msgid "Withdraw Fee" msgstr "" -#: src/webex/pages/confirm-create-reserve.tsx:222 +#: src/webex/pages/confirm-create-reserve.tsx:223 #, c-format msgid "Refresh Fee" msgstr "" -#: src/webex/pages/confirm-create-reserve.tsx:223 +#: src/webex/pages/confirm-create-reserve.tsx:224 #, c-format msgid "Deposit Fee" msgstr "" -#: src/webex/pages/confirm-create-reserve.tsx:276 +#: src/webex/pages/confirm-create-reserve.tsx:278 #, c-format msgid "Select" msgstr "" -#: src/webex/pages/confirm-create-reserve.tsx:292 +#: src/webex/pages/confirm-create-reserve.tsx:294 #, c-format msgid "Error: URL may not be relative" msgstr "" -#: src/webex/pages/confirm-create-reserve.tsx:360 +#: src/webex/pages/confirm-create-reserve.tsx:362 #, c-format msgid "The exchange is trusted by the wallet.\n" msgstr "" -#: src/webex/pages/confirm-create-reserve.tsx:366 +#: src/webex/pages/confirm-create-reserve.tsx:368 #, c-format msgid "The exchange is audited by a trusted auditor.\n" msgstr "" -#: src/webex/pages/confirm-create-reserve.tsx:372 +#: src/webex/pages/confirm-create-reserve.tsx:374 #, c-format msgid "" "Warning: The exchange is neither directly trusted nor audited by a trusted " @@ -124,7 +124,7 @@ msgid "" "If you withdraw from this exchange, it will be trusted in the future.\n" msgstr "" -#: src/webex/pages/confirm-create-reserve.tsx:381 +#: src/webex/pages/confirm-create-reserve.tsx:383 #, c-format msgid "" "Using exchange provider%1$s.\n" @@ -132,151 +132,166 @@ msgid "" " %2$s in fees.\n" msgstr "" -#: src/webex/pages/confirm-create-reserve.tsx:395 +#: src/webex/pages/confirm-create-reserve.tsx:397 #, c-format msgid "" "Waiting for a response from\n" " %1$s" msgstr "" -#: src/webex/pages/confirm-create-reserve.tsx:406 +#: src/webex/pages/confirm-create-reserve.tsx:408 #, c-format msgid "A problem occured, see below. %1$s" msgstr "" -#: src/webex/pages/confirm-create-reserve.tsx:412 +#: src/webex/pages/confirm-create-reserve.tsx:414 #, c-format msgid "" "Information about fees will be available when an exchange provider is " "selected." msgstr "" -#: src/webex/pages/confirm-create-reserve.tsx:455 +#: src/webex/pages/confirm-create-reserve.tsx:457 #, c-format msgid "Accept fees and withdraw" msgstr "" -#: src/webex/pages/confirm-create-reserve.tsx:460 +#: src/webex/pages/confirm-create-reserve.tsx:462 #, c-format msgid "Change Exchange Provider" msgstr "" -#: src/webex/pages/confirm-create-reserve.tsx:517 +#: src/webex/pages/confirm-create-reserve.tsx:519 #, 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:600 +#: src/webex/pages/confirm-create-reserve.tsx:607 #, c-format msgid "" "Oops, something went wrong. The wallet responded with error status (%1$s)." msgstr "" -#: src/webex/pages/confirm-create-reserve.tsx:611 +#: src/webex/pages/confirm-create-reserve.tsx:616 #, c-format msgid "Checking URL, please wait ..." msgstr "" -#: src/webex/pages/confirm-create-reserve.tsx:625 +#: src/webex/pages/confirm-create-reserve.tsx:630 #, c-format msgid "Can't parse amount: %1$s" msgstr "" -#: src/webex/pages/confirm-create-reserve.tsx:632 +#: src/webex/pages/confirm-create-reserve.tsx:637 #, c-format msgid "Can't parse wire_types: %1$s" msgstr "" #. TODO:generic error reporting function or component. -#: src/webex/pages/confirm-create-reserve.tsx:652 +#: src/webex/pages/confirm-create-reserve.tsx:663 #, c-format msgid "Fatal error: \"%1$s\"." msgstr "" -#: src/webex/pages/popup.tsx:160 +#: src/webex/pages/popup.tsx:161 #, c-format msgid "Balance" msgstr "" -#: src/webex/pages/popup.tsx:163 +#: src/webex/pages/popup.tsx:164 #, c-format msgid "History" msgstr "" -#: src/webex/pages/popup.tsx:166 +#: src/webex/pages/popup.tsx:167 #, c-format msgid "Debug" msgstr "" -#: src/webex/pages/popup.tsx:242 +#: src/webex/pages/popup.tsx:247 #, c-format msgid "help" msgstr "" -#: src/webex/pages/popup.tsx:247 +#: src/webex/pages/popup.tsx:252 #, c-format msgid "" "You have no balance to show. Need some\n" " %1$s getting started?\n" msgstr "" -#: src/webex/pages/popup.tsx:264 +#: src/webex/pages/popup.tsx:269 #, c-format msgid "%1$s incoming\n" msgstr "" -#: src/webex/pages/popup.tsx:277 +#: src/webex/pages/popup.tsx:282 #, c-format msgid "%1$s being spent\n" msgstr "" -#: src/webex/pages/popup.tsx:303 +#: src/webex/pages/popup.tsx:308 #, c-format msgid "Error: could not retrieve balance information." msgstr "" -#: src/webex/pages/popup.tsx:342 +#: src/webex/pages/popup.tsx:335 +#, c-format +msgid "Payback" +msgstr "" + +#: src/webex/pages/popup.tsx:336 +#, c-format +msgid "Return Electronic Cash to Bank Account" +msgstr "" + +#: src/webex/pages/popup.tsx:337 +#, c-format +msgid "Manage Trusted Auditors and Exchanges" +msgstr "" + +#: src/webex/pages/popup.tsx:349 #, c-format msgid "" "Bank requested reserve (%1$s) for\n" " %2$s.\n" msgstr "" -#: src/webex/pages/popup.tsx:353 +#: src/webex/pages/popup.tsx:360 #, c-format msgid "" "Started to withdraw\n" " %1$s from%2$s(%3$s).\n" msgstr "" -#: src/webex/pages/popup.tsx:363 +#: src/webex/pages/popup.tsx:370 #, c-format msgid "Merchant%1$soffered contract%2$s;\n" msgstr "" -#: src/webex/pages/popup.tsx:373 +#: src/webex/pages/popup.tsx:380 #, c-format msgid "Withdrew%1$sfrom%2$s(%3$s).\n" msgstr "" -#: src/webex/pages/popup.tsx:383 +#: src/webex/pages/popup.tsx:390 #, c-format msgid "" "Paid%1$sto merchant%2$s.\n" " (%3$s)\n" msgstr "" -#: src/webex/pages/popup.tsx:392 +#: src/webex/pages/popup.tsx:399 #, c-format msgid "Unknown event (%1$s)" msgstr "" -#: src/webex/pages/popup.tsx:435 +#: src/webex/pages/popup.tsx:442 #, c-format msgid "Error: could not retrieve event history" msgstr "" -#: src/webex/pages/popup.tsx:469 +#: src/webex/pages/popup.tsx:476 #, c-format msgid "Your wallet has no events recorded." msgstr "" diff --git a/src/i18n/it.po b/src/i18n/it.po index da860ad8f..705b1cba6 100644 --- a/src/i18n/it.po +++ b/src/i18n/it.po @@ -56,67 +56,67 @@ msgid "" "wallet." msgstr "" -#: src/webex/pages/confirm-create-reserve.tsx:212 +#: src/webex/pages/confirm-create-reserve.tsx:213 #, c-format msgid "Withdrawal fees:" msgstr "" -#: src/webex/pages/confirm-create-reserve.tsx:213 +#: src/webex/pages/confirm-create-reserve.tsx:214 #, c-format msgid "Rounding loss:" msgstr "" -#: src/webex/pages/confirm-create-reserve.tsx:214 +#: src/webex/pages/confirm-create-reserve.tsx:215 #, c-format msgid "Earliest expiration (for deposit): %1$s" msgstr "" -#: src/webex/pages/confirm-create-reserve.tsx:219 +#: src/webex/pages/confirm-create-reserve.tsx:220 #, c-format msgid "# Coins" msgstr "" -#: src/webex/pages/confirm-create-reserve.tsx:220 +#: src/webex/pages/confirm-create-reserve.tsx:221 #, c-format msgid "Value" msgstr "" -#: src/webex/pages/confirm-create-reserve.tsx:221 +#: src/webex/pages/confirm-create-reserve.tsx:222 #, c-format msgid "Withdraw Fee" msgstr "" -#: src/webex/pages/confirm-create-reserve.tsx:222 +#: src/webex/pages/confirm-create-reserve.tsx:223 #, c-format msgid "Refresh Fee" msgstr "" -#: src/webex/pages/confirm-create-reserve.tsx:223 +#: src/webex/pages/confirm-create-reserve.tsx:224 #, c-format msgid "Deposit Fee" msgstr "" -#: src/webex/pages/confirm-create-reserve.tsx:276 +#: src/webex/pages/confirm-create-reserve.tsx:278 #, c-format msgid "Select" msgstr "" -#: src/webex/pages/confirm-create-reserve.tsx:292 +#: src/webex/pages/confirm-create-reserve.tsx:294 #, c-format msgid "Error: URL may not be relative" msgstr "" -#: src/webex/pages/confirm-create-reserve.tsx:360 +#: src/webex/pages/confirm-create-reserve.tsx:362 #, c-format msgid "The exchange is trusted by the wallet.\n" msgstr "" -#: src/webex/pages/confirm-create-reserve.tsx:366 +#: src/webex/pages/confirm-create-reserve.tsx:368 #, c-format msgid "The exchange is audited by a trusted auditor.\n" msgstr "" -#: src/webex/pages/confirm-create-reserve.tsx:372 +#: src/webex/pages/confirm-create-reserve.tsx:374 #, c-format msgid "" "Warning: The exchange is neither directly trusted nor audited by a trusted " @@ -124,7 +124,7 @@ msgid "" "If you withdraw from this exchange, it will be trusted in the future.\n" msgstr "" -#: src/webex/pages/confirm-create-reserve.tsx:381 +#: src/webex/pages/confirm-create-reserve.tsx:383 #, c-format msgid "" "Using exchange provider%1$s.\n" @@ -132,151 +132,166 @@ msgid "" " %2$s in fees.\n" msgstr "" -#: src/webex/pages/confirm-create-reserve.tsx:395 +#: src/webex/pages/confirm-create-reserve.tsx:397 #, c-format msgid "" "Waiting for a response from\n" " %1$s" msgstr "" -#: src/webex/pages/confirm-create-reserve.tsx:406 +#: src/webex/pages/confirm-create-reserve.tsx:408 #, c-format msgid "A problem occured, see below. %1$s" msgstr "" -#: src/webex/pages/confirm-create-reserve.tsx:412 +#: src/webex/pages/confirm-create-reserve.tsx:414 #, c-format msgid "" "Information about fees will be available when an exchange provider is " "selected." msgstr "" -#: src/webex/pages/confirm-create-reserve.tsx:455 +#: src/webex/pages/confirm-create-reserve.tsx:457 #, c-format msgid "Accept fees and withdraw" msgstr "" -#: src/webex/pages/confirm-create-reserve.tsx:460 +#: src/webex/pages/confirm-create-reserve.tsx:462 #, c-format msgid "Change Exchange Provider" msgstr "" -#: src/webex/pages/confirm-create-reserve.tsx:517 +#: src/webex/pages/confirm-create-reserve.tsx:519 #, 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:600 +#: src/webex/pages/confirm-create-reserve.tsx:607 #, c-format msgid "" "Oops, something went wrong. The wallet responded with error status (%1$s)." msgstr "" -#: src/webex/pages/confirm-create-reserve.tsx:611 +#: src/webex/pages/confirm-create-reserve.tsx:616 #, c-format msgid "Checking URL, please wait ..." msgstr "" -#: src/webex/pages/confirm-create-reserve.tsx:625 +#: src/webex/pages/confirm-create-reserve.tsx:630 #, c-format msgid "Can't parse amount: %1$s" msgstr "" -#: src/webex/pages/confirm-create-reserve.tsx:632 +#: src/webex/pages/confirm-create-reserve.tsx:637 #, c-format msgid "Can't parse wire_types: %1$s" msgstr "" #. TODO:generic error reporting function or component. -#: src/webex/pages/confirm-create-reserve.tsx:652 +#: src/webex/pages/confirm-create-reserve.tsx:663 #, c-format msgid "Fatal error: \"%1$s\"." msgstr "" -#: src/webex/pages/popup.tsx:160 +#: src/webex/pages/popup.tsx:161 #, c-format msgid "Balance" msgstr "" -#: src/webex/pages/popup.tsx:163 +#: src/webex/pages/popup.tsx:164 #, c-format msgid "History" msgstr "" -#: src/webex/pages/popup.tsx:166 +#: src/webex/pages/popup.tsx:167 #, c-format msgid "Debug" msgstr "" -#: src/webex/pages/popup.tsx:242 +#: src/webex/pages/popup.tsx:247 #, c-format msgid "help" msgstr "" -#: src/webex/pages/popup.tsx:247 +#: src/webex/pages/popup.tsx:252 #, c-format msgid "" "You have no balance to show. Need some\n" " %1$s getting started?\n" msgstr "" -#: src/webex/pages/popup.tsx:264 +#: src/webex/pages/popup.tsx:269 #, c-format msgid "%1$s incoming\n" msgstr "" -#: src/webex/pages/popup.tsx:277 +#: src/webex/pages/popup.tsx:282 #, c-format msgid "%1$s being spent\n" msgstr "" -#: src/webex/pages/popup.tsx:303 +#: src/webex/pages/popup.tsx:308 #, c-format msgid "Error: could not retrieve balance information." msgstr "" -#: src/webex/pages/popup.tsx:342 +#: src/webex/pages/popup.tsx:335 +#, c-format +msgid "Payback" +msgstr "" + +#: src/webex/pages/popup.tsx:336 +#, c-format +msgid "Return Electronic Cash to Bank Account" +msgstr "" + +#: src/webex/pages/popup.tsx:337 +#, c-format +msgid "Manage Trusted Auditors and Exchanges" +msgstr "" + +#: src/webex/pages/popup.tsx:349 #, c-format msgid "" "Bank requested reserve (%1$s) for\n" " %2$s.\n" msgstr "" -#: src/webex/pages/popup.tsx:353 +#: src/webex/pages/popup.tsx:360 #, c-format msgid "" "Started to withdraw\n" " %1$s from%2$s(%3$s).\n" msgstr "" -#: src/webex/pages/popup.tsx:363 +#: src/webex/pages/popup.tsx:370 #, c-format msgid "Merchant%1$soffered contract%2$s;\n" msgstr "" -#: src/webex/pages/popup.tsx:373 +#: src/webex/pages/popup.tsx:380 #, c-format msgid "Withdrew%1$sfrom%2$s(%3$s).\n" msgstr "" -#: src/webex/pages/popup.tsx:383 +#: src/webex/pages/popup.tsx:390 #, c-format msgid "" "Paid%1$sto merchant%2$s.\n" " (%3$s)\n" msgstr "" -#: src/webex/pages/popup.tsx:392 +#: src/webex/pages/popup.tsx:399 #, c-format msgid "Unknown event (%1$s)" msgstr "" -#: src/webex/pages/popup.tsx:435 +#: src/webex/pages/popup.tsx:442 #, c-format msgid "Error: could not retrieve event history" msgstr "" -#: src/webex/pages/popup.tsx:469 +#: src/webex/pages/popup.tsx:476 #, c-format msgid "Your wallet has no events recorded." msgstr "" diff --git a/src/i18n/strings.ts b/src/i18n/strings.ts index 242fecf5f..aa883403e 100644 --- a/src/i18n/strings.ts +++ b/src/i18n/strings.ts @@ -138,6 +138,15 @@ strings['de'] = { "Error: could not retrieve balance information.": [ "" ], + "Payback": [ + "" + ], + "Return Electronic Cash to Bank Account": [ + "" + ], + "Manage Trusted Auditors and Exchanges": [ + "" + ], "Bank requested reserve (%1$s) for\n %2$s.\n": [ "Bank bestätig anlegen der Reserve (%1$s) bei %2$s" ], @@ -294,6 +303,15 @@ strings['en-US'] = { "Error: could not retrieve balance information.": [ "" ], + "Payback": [ + "" + ], + "Return Electronic Cash to Bank Account": [ + "" + ], + "Manage Trusted Auditors and Exchanges": [ + "" + ], "Bank requested reserve (%1$s) for\n %2$s.\n": [ "" ], @@ -450,6 +468,15 @@ strings['fr'] = { "Error: could not retrieve balance information.": [ "" ], + "Payback": [ + "" + ], + "Return Electronic Cash to Bank Account": [ + "" + ], + "Manage Trusted Auditors and Exchanges": [ + "" + ], "Bank requested reserve (%1$s) for\n %2$s.\n": [ "" ], @@ -606,6 +633,15 @@ strings['it'] = { "Error: could not retrieve balance information.": [ "" ], + "Payback": [ + "" + ], + "Return Electronic Cash to Bank Account": [ + "" + ], + "Manage Trusted Auditors and Exchanges": [ + "" + ], "Bank requested reserve (%1$s) for\n %2$s.\n": [ "" ], diff --git a/src/i18n/taler-wallet-webex.pot b/src/i18n/taler-wallet-webex.pot index da860ad8f..705b1cba6 100644 --- a/src/i18n/taler-wallet-webex.pot +++ b/src/i18n/taler-wallet-webex.pot @@ -56,67 +56,67 @@ msgid "" "wallet." msgstr "" -#: src/webex/pages/confirm-create-reserve.tsx:212 +#: src/webex/pages/confirm-create-reserve.tsx:213 #, c-format msgid "Withdrawal fees:" msgstr "" -#: src/webex/pages/confirm-create-reserve.tsx:213 +#: src/webex/pages/confirm-create-reserve.tsx:214 #, c-format msgid "Rounding loss:" msgstr "" -#: src/webex/pages/confirm-create-reserve.tsx:214 +#: src/webex/pages/confirm-create-reserve.tsx:215 #, c-format msgid "Earliest expiration (for deposit): %1$s" msgstr "" -#: src/webex/pages/confirm-create-reserve.tsx:219 +#: src/webex/pages/confirm-create-reserve.tsx:220 #, c-format msgid "# Coins" msgstr "" -#: src/webex/pages/confirm-create-reserve.tsx:220 +#: src/webex/pages/confirm-create-reserve.tsx:221 #, c-format msgid "Value" msgstr "" -#: src/webex/pages/confirm-create-reserve.tsx:221 +#: src/webex/pages/confirm-create-reserve.tsx:222 #, c-format msgid "Withdraw Fee" msgstr "" -#: src/webex/pages/confirm-create-reserve.tsx:222 +#: src/webex/pages/confirm-create-reserve.tsx:223 #, c-format msgid "Refresh Fee" msgstr "" -#: src/webex/pages/confirm-create-reserve.tsx:223 +#: src/webex/pages/confirm-create-reserve.tsx:224 #, c-format msgid "Deposit Fee" msgstr "" -#: src/webex/pages/confirm-create-reserve.tsx:276 +#: src/webex/pages/confirm-create-reserve.tsx:278 #, c-format msgid "Select" msgstr "" -#: src/webex/pages/confirm-create-reserve.tsx:292 +#: src/webex/pages/confirm-create-reserve.tsx:294 #, c-format msgid "Error: URL may not be relative" msgstr "" -#: src/webex/pages/confirm-create-reserve.tsx:360 +#: src/webex/pages/confirm-create-reserve.tsx:362 #, c-format msgid "The exchange is trusted by the wallet.\n" msgstr "" -#: src/webex/pages/confirm-create-reserve.tsx:366 +#: src/webex/pages/confirm-create-reserve.tsx:368 #, c-format msgid "The exchange is audited by a trusted auditor.\n" msgstr "" -#: src/webex/pages/confirm-create-reserve.tsx:372 +#: src/webex/pages/confirm-create-reserve.tsx:374 #, c-format msgid "" "Warning: The exchange is neither directly trusted nor audited by a trusted " @@ -124,7 +124,7 @@ msgid "" "If you withdraw from this exchange, it will be trusted in the future.\n" msgstr "" -#: src/webex/pages/confirm-create-reserve.tsx:381 +#: src/webex/pages/confirm-create-reserve.tsx:383 #, c-format msgid "" "Using exchange provider%1$s.\n" @@ -132,151 +132,166 @@ msgid "" " %2$s in fees.\n" msgstr "" -#: src/webex/pages/confirm-create-reserve.tsx:395 +#: src/webex/pages/confirm-create-reserve.tsx:397 #, c-format msgid "" "Waiting for a response from\n" " %1$s" msgstr "" -#: src/webex/pages/confirm-create-reserve.tsx:406 +#: src/webex/pages/confirm-create-reserve.tsx:408 #, c-format msgid "A problem occured, see below. %1$s" msgstr "" -#: src/webex/pages/confirm-create-reserve.tsx:412 +#: src/webex/pages/confirm-create-reserve.tsx:414 #, c-format msgid "" "Information about fees will be available when an exchange provider is " "selected." msgstr "" -#: src/webex/pages/confirm-create-reserve.tsx:455 +#: src/webex/pages/confirm-create-reserve.tsx:457 #, c-format msgid "Accept fees and withdraw" msgstr "" -#: src/webex/pages/confirm-create-reserve.tsx:460 +#: src/webex/pages/confirm-create-reserve.tsx:462 #, c-format msgid "Change Exchange Provider" msgstr "" -#: src/webex/pages/confirm-create-reserve.tsx:517 +#: src/webex/pages/confirm-create-reserve.tsx:519 #, 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:600 +#: src/webex/pages/confirm-create-reserve.tsx:607 #, c-format msgid "" "Oops, something went wrong. The wallet responded with error status (%1$s)." msgstr "" -#: src/webex/pages/confirm-create-reserve.tsx:611 +#: src/webex/pages/confirm-create-reserve.tsx:616 #, c-format msgid "Checking URL, please wait ..." msgstr "" -#: src/webex/pages/confirm-create-reserve.tsx:625 +#: src/webex/pages/confirm-create-reserve.tsx:630 #, c-format msgid "Can't parse amount: %1$s" msgstr "" -#: src/webex/pages/confirm-create-reserve.tsx:632 +#: src/webex/pages/confirm-create-reserve.tsx:637 #, c-format msgid "Can't parse wire_types: %1$s" msgstr "" #. TODO:generic error reporting function or component. -#: src/webex/pages/confirm-create-reserve.tsx:652 +#: src/webex/pages/confirm-create-reserve.tsx:663 #, c-format msgid "Fatal error: \"%1$s\"." msgstr "" -#: src/webex/pages/popup.tsx:160 +#: src/webex/pages/popup.tsx:161 #, c-format msgid "Balance" msgstr "" -#: src/webex/pages/popup.tsx:163 +#: src/webex/pages/popup.tsx:164 #, c-format msgid "History" msgstr "" -#: src/webex/pages/popup.tsx:166 +#: src/webex/pages/popup.tsx:167 #, c-format msgid "Debug" msgstr "" -#: src/webex/pages/popup.tsx:242 +#: src/webex/pages/popup.tsx:247 #, c-format msgid "help" msgstr "" -#: src/webex/pages/popup.tsx:247 +#: src/webex/pages/popup.tsx:252 #, c-format msgid "" "You have no balance to show. Need some\n" " %1$s getting started?\n" msgstr "" -#: src/webex/pages/popup.tsx:264 +#: src/webex/pages/popup.tsx:269 #, c-format msgid "%1$s incoming\n" msgstr "" -#: src/webex/pages/popup.tsx:277 +#: src/webex/pages/popup.tsx:282 #, c-format msgid "%1$s being spent\n" msgstr "" -#: src/webex/pages/popup.tsx:303 +#: src/webex/pages/popup.tsx:308 #, c-format msgid "Error: could not retrieve balance information." msgstr "" -#: src/webex/pages/popup.tsx:342 +#: src/webex/pages/popup.tsx:335 +#, c-format +msgid "Payback" +msgstr "" + +#: src/webex/pages/popup.tsx:336 +#, c-format +msgid "Return Electronic Cash to Bank Account" +msgstr "" + +#: src/webex/pages/popup.tsx:337 +#, c-format +msgid "Manage Trusted Auditors and Exchanges" +msgstr "" + +#: src/webex/pages/popup.tsx:349 #, c-format msgid "" "Bank requested reserve (%1$s) for\n" " %2$s.\n" msgstr "" -#: src/webex/pages/popup.tsx:353 +#: src/webex/pages/popup.tsx:360 #, c-format msgid "" "Started to withdraw\n" " %1$s from%2$s(%3$s).\n" msgstr "" -#: src/webex/pages/popup.tsx:363 +#: src/webex/pages/popup.tsx:370 #, c-format msgid "Merchant%1$soffered contract%2$s;\n" msgstr "" -#: src/webex/pages/popup.tsx:373 +#: src/webex/pages/popup.tsx:380 #, c-format msgid "Withdrew%1$sfrom%2$s(%3$s).\n" msgstr "" -#: src/webex/pages/popup.tsx:383 +#: src/webex/pages/popup.tsx:390 #, c-format msgid "" "Paid%1$sto merchant%2$s.\n" " (%3$s)\n" msgstr "" -#: src/webex/pages/popup.tsx:392 +#: src/webex/pages/popup.tsx:399 #, c-format msgid "Unknown event (%1$s)" msgstr "" -#: src/webex/pages/popup.tsx:435 +#: src/webex/pages/popup.tsx:442 #, c-format msgid "Error: could not retrieve event history" msgstr "" -#: src/webex/pages/popup.tsx:469 +#: src/webex/pages/popup.tsx:476 #, c-format msgid "Your wallet has no events recorded." msgstr "" diff --git a/src/query.ts b/src/query.ts index 24db4de56..9a6162807 100644 --- a/src/query.ts +++ b/src/query.ts @@ -130,7 +130,11 @@ export interface QueryStream { */ first(): QueryValue; - then(onfulfill: any, onreject: any): any; + /** + * Run the query without returning a result. + * Useful for queries with side effects. + */ + run(): Promise; } @@ -225,7 +229,7 @@ export const AbortTransaction = Symbol("abort_transaction"); * function. */ export function openPromise(): any { - let resolve: ((value?: T | PromiseLike) => void) | null = null; + let resolve: ((x?: any) => void) | null = null; let reject: ((reason?: any) => void) | null = null; const promise = new Promise((res, rej) => { resolve = res; @@ -239,7 +243,7 @@ export function openPromise(): any { } -abstract class QueryStreamBase implements QueryStream, PromiseLike { +abstract class QueryStreamBase implements QueryStream { abstract subscribe(f: (isDone: boolean, value: any, tx: IDBTransaction) => void): void; @@ -250,11 +254,6 @@ abstract class QueryStreamBase implements QueryStream, PromiseLike { return new FirstQueryValue(this); } - then(onfulfilled: (value: void) => R | PromiseLike, - onrejected: (reason: any) => R | PromiseLike): PromiseLike { - return this.root.then(onfulfilled, onrejected); - } - flatMap(f: (x: T) => S[]): QueryStream { return new QueryStreamFlatMap(this, f); } @@ -279,8 +278,7 @@ abstract class QueryStreamBase implements QueryStream, PromiseLike { keyFn: (obj: T) => I): QueryStream> { this.root.addStoreAccess(store.name, false); return new QueryStreamKeyJoin(this, store.name, keyFn); - } - + } filter(f: (x: any) => boolean): QueryStream { return new QueryStreamFilter(this, f); } @@ -318,6 +316,21 @@ abstract class QueryStreamBase implements QueryStream, PromiseLike { .then(() => this.root.finish()) .then(() => promise); } + + run(): Promise { + const {resolve, promise} = openPromise(); + + this.subscribe((isDone, value) => { + if (isDone) { + resolve(); + return; + } + }); + + return Promise.resolve() + .then(() => this.root.finish()) + .then(() => promise); + } } type FilterFn = (e: any) => boolean; @@ -519,7 +532,7 @@ class IterQueryStream extends QueryStreamBase { /** * Root wrapper around an IndexedDB for queries with a fluent interface. */ -export class QueryRoot implements PromiseLike { +export class QueryRoot { private work: Array<((t: IDBTransaction) => void)> = []; private stores = new Set(); private kickoffPromise: Promise; @@ -537,11 +550,6 @@ export class QueryRoot implements PromiseLike { constructor(public db: IDBDatabase) { } - then(onfulfilled: (value: void) => R | PromiseLike, - onrejected: (reason: any) => R | PromiseLike): PromiseLike { - return this.finish().then(onfulfilled, onrejected); - } - private checkFinished() { if (this.finished) { throw Error("Can't add work to query after it was started"); diff --git a/src/types.ts b/src/types.ts index 68fde2690..9031b19b7 100644 --- a/src/types.ts +++ b/src/types.ts @@ -126,6 +126,12 @@ export interface ReserveRecord { * withdraw money from it. */ hasPayback: boolean; + + /** + * Wire information for the bank account that + * transfered funds for this reserve. + */ + senderWire?: object; } @@ -814,6 +820,61 @@ export enum CoinStatus { } + +/** + * State of returning a list of coins + * to the customer's bank account. + */ +export interface CoinsReturnRecord { + /** + * Coins that we're returning. + */ + coins: CoinPaySig[]; + + /** + * Responses to the deposit requests. + */ + responses: any; + + /** + * Ephemeral dummy merchant key for + * the coins returns operation. + */ + dummyMerchantPub: string; + + /** + * Ephemeral dummy merchant key for + * the coins returns operation. + */ + dummyMerchantPriv: string; + + /** + * Contract terms. + */ + contractTerms: string; + + /** + * Hash of contract terms. + */ + contractTermsHash: string; + + /** + * Wire info to send the money for the coins to. + */ + wire: object; + + /** + * Hash of the wire object. + */ + wireHash: string; + + /** + * All coins were deposited. + */ + finished: boolean; +} + + /** * CoinRecord as stored in the "coins" data store * of the wallet database. @@ -903,14 +964,19 @@ export class ExchangeHandle { /** - * Mapping from currency names to detailed balance - * information for that particular currency. + * Mapping from currency/exchange to detailed balance + * information. */ export interface WalletBalance { /** - * Mapping from currency name to defailed balance info. + * Mapping from currency name to detailed balance info. */ - [currency: string]: WalletBalanceEntry; + byExchange: { [exchangeBaseUrl: string]: WalletBalanceEntry }; + + /** + * Mapping from currency name to detailed balance info. + */ + byCurrency: { [currency: string]: WalletBalanceEntry }; } @@ -942,8 +1008,8 @@ export interface WalletBalanceEntry { */ @Checkable.Class({validate: true}) export class ContractTerms { - validate() { - if (this.exchanges.length === 0) { + static validate(x: ContractTerms) { + if (x.exchanges.length === 0) { throw Error("no exchanges in contract terms"); } } @@ -1361,6 +1427,18 @@ export namespace Amounts { value: Number.parseInt(res[2]), }; } + + export function toFloat(a: AmountJson): number { + return a.value + (a.fraction / fractionalBase); + } + + export function fromFloat(floatVal: number, currency: string) { + return { + currency, + fraction: (floatVal - Math.floor(floatVal)) * fractionalBase, + value: Math.floor(floatVal), + }; + } } @@ -1457,7 +1535,7 @@ export interface PayReq { order_id: string; /** - * Exchange that the coins are from. + * Exchange that the coins are from (base URL). */ exchange: string; } @@ -1484,3 +1562,103 @@ export interface QueryPaymentFound { contractTerms: ContractTerms; payReq: PayReq; } + +/** + * Information about all sender wire details known to the wallet, + * as well as exchanges that accept these wire types. + */ +export interface SenderWireInfos { + /** + * Mapping from exchange base url to list of accepted + * wire types. + */ + exchangeWireTypes: { [exchangeBaseUrl: string]: string[] }; + + /** + * Sender wire types stored in the wallet. + */ + senderWires: object[]; +} + + +/** + * Request to mark a reserve as confirmed. + */ +@Checkable.Class() +export class CreateReserveRequest { + /** + * The initial amount for the reserve. + */ + @Checkable.Value(AmountJson) + amount: AmountJson; + + /** + * Exchange URL where the bank should create the reserve. + */ + @Checkable.String + exchange: string; + + /** + * Wire details for the bank account that sent the funds to the exchange. + */ + @Checkable.Optional(Checkable.Any) + senderWire?: object; + + /** + * Verify that a value matches the schema of this class and convert it into a + * member. + */ + static checked: (obj: any) => CreateReserveRequest; +} + + +/** + * Request to mark a reserve as confirmed. + */ +@Checkable.Class() +export class ConfirmReserveRequest { + /** + * Public key of then reserve that should be marked + * as confirmed. + */ + @Checkable.String + reservePub: string; + + /** + * Verify that a value matches the schema of this class and convert it into a + * member. + */ + static checked: (obj: any) => ConfirmReserveRequest; +} + + +/** + * Wire coins to the user's own bank account. + */ +@Checkable.Class() +export class ReturnCoinsRequest { + /** + * The amount to wire. + */ + @Checkable.Value(AmountJson) + amount: AmountJson; + + /** + * The exchange to take the coins from. + */ + @Checkable.String + exchange: string; + + /** + * Wire details for the bank account of the customer that will + * receive the funds. + */ + @Checkable.Any + senderWire?: object; + + /** + * Verify that a value matches the schema of this class and convert it into a + * member. + */ + static checked: (obj: any) => ReturnCoinsRequest; +} diff --git a/src/wallet.ts b/src/wallet.ts index 5de3906dc..68d70b0bb 100644 --- a/src/wallet.ts +++ b/src/wallet.ts @@ -49,10 +49,13 @@ import { Amounts, Auditor, CheckPayResult, + CoinPaySig, CoinRecord, CoinStatus, ConfirmPayResult, + ConfirmReserveRequest, ContractTerms, + CreateReserveRequest, CreateReserveResponse, CurrencyRecord, Denomination, @@ -73,6 +76,8 @@ import { RefreshSessionRecord, ReserveCreationInfo, ReserveRecord, + ReturnCoinsRequest, + SenderWireInfos, WalletBalance, WalletBalanceEntry, WireFee, @@ -236,51 +241,6 @@ class WireDetailJson { } -/** - * Request to mark a reserve as confirmed. - */ -@Checkable.Class() -export class CreateReserveRequest { - /** - * The initial amount for the reserve. - */ - @Checkable.Value(AmountJson) - amount: AmountJson; - - /** - * Exchange URL where the bank should create the reserve. - */ - @Checkable.String - exchange: string; - - /** - * Verify that a value matches the schema of this class and convert it into a - * member. - */ - static checked: (obj: any) => CreateReserveRequest; -} - - -/** - * Request to mark a reserve as confirmed. - */ -@Checkable.Class() -export class ConfirmReserveRequest { - /** - * Public key of then reserve that should be marked - * as confirmed. - */ - @Checkable.String - reservePub: string; - - /** - * Verify that a value matches the schema of this class and convert it into a - * member. - */ - static checked: (obj: any) => ConfirmReserveRequest; -} - - interface TransactionRecord { contractTermsHash: string; contractTerms: ContractTerms; @@ -329,6 +289,48 @@ export interface ConfigRecord { } +/** + * Coin that we're depositing ourselves. + */ +export interface DepositCoin { + coinPaySig: CoinPaySig; + + /** + * Undefined if coin not deposited, otherwise signature + * from the exchange confirming the deposit. + */ + depositedSig?: string; +} + +export interface CoinsReturnRecord { + /** + * Hash of the contract for sending coins to our own bank account. + */ + contractTermsHash: string; + + contractTerms: ContractTerms; + + /** + * Private key where corresponding + * public key is used in the contract terms + * as merchant pub. + */ + merchantPriv: string; + + coins: DepositCoin[]; + + /** + * Exchange base URL to deposit coins at. + */ + exchange: string; + + /** + * Our own wire information for the deposit. + */ + wire: any; +} + + /** * Wallet protocol version spoken with the exchange * and merchant. @@ -343,7 +345,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 = 18; +export const WALLET_DB_VERSION = 19; const builtinCurrencies: CurrencyRecord[] = [ { @@ -421,6 +423,7 @@ export function selectPayCoins(cds: CoinWithDenom[], paymentAmount: AmountJson, Amounts.add(paymentAmount, denom.feeDeposit).amount) >= 0; isBelowFee = Amounts.cmp(accFee, depositFeeLimit) <= 0; + if ((coversAmount && isBelowFee) || coversAmountWithFee) { return cdsResult; } @@ -553,6 +556,7 @@ export namespace Stores { } export const coins = new CoinsStore(); + export const coinsReturns = new Store("coinsReturns", {keyPath: "contractTermsHash"}); export const config = new ConfigStore(); export const currencies = new CurrenciesStore(); export const denominations = new DenominationsStore(); @@ -700,6 +704,12 @@ export class Wallet { this.continueRefreshSession(r); }); + this.q() + .iter(Stores.coinsReturns) + .reduce((r: CoinsReturnRecord) => { + this.depositReturnedCoins(r); + }); + // FIXME: optimize via index this.q() .iter(Stores.coins) @@ -712,6 +722,58 @@ export class Wallet { } + private async getCoinsForReturn(exchangeBaseUrl: string, amount: AmountJson): Promise { + const exchange = await this.q().get(Stores.exchanges, exchangeBaseUrl); + if (!exchange) { + throw Error(`Exchange ${exchangeBaseUrl} not known to the wallet`); + } + + const coins: CoinRecord[] = await ( + this.q() + .iterIndex(Stores.coins.exchangeBaseUrlIndex, exchange.baseUrl) + .toArray() + ); + + if (!coins || !coins.length) { + return []; + } + + // Denomination of the first coin, we assume that all other + // coins have the same currency + const firstDenom = await this.q().get(Stores.denominations, + [ + exchange.baseUrl, + coins[0].denomPub, + ]); + if (!firstDenom) { + throw Error("db inconsistent"); + } + const currency = firstDenom.value.currency; + + const cds: CoinWithDenom[] = []; + for (const coin of coins) { + const denom = await this.q().get(Stores.denominations, + [exchange.baseUrl, coin.denomPub]); + if (!denom) { + throw Error("db inconsistent"); + } + if (denom.value.currency !== currency) { + console.warn(`same pubkey for different currencies at exchange ${exchange.baseUrl}`); + continue; + } + if (coin.suspended) { + continue; + } + if (coin.status !== CoinStatus.Fresh) { + continue; + } + cds.push({coin, denom}); + } + + return selectPayCoins(cds, amount, amount); + } + + /** * Get exchanges and associated coins that are still spendable, * but only if the sum the coins' remaining value exceeds the payment amount. @@ -769,6 +831,7 @@ export class Wallet { if (!coins || coins.length === 0) { continue; } + // Denomination of the first coin, we assume that all other // coins have the same currency const firstDenom = await this.q().get(Stores.denominations, @@ -903,7 +966,7 @@ export class Wallet { */ async confirmPay(proposalId: number): Promise { console.log("executing confirmPay"); - const proposal = await this.q().get(Stores.proposals, proposalId); + const proposal: ProposalRecord|undefined = await this.q().get(Stores.proposals, proposalId); if (!proposal) { throw Error(`proposal with id ${proposalId} not found`); @@ -936,7 +999,7 @@ export class Wallet { } const {exchangeUrl, cds} = res; - const ds = await this.cryptoApi.signDeposit(proposal, cds); + const ds = await this.cryptoApi.signDeposit(proposal.contractTerms, cds); await this.recordConfirmPay(proposal, ds, exchangeUrl); return "paid"; } @@ -1138,6 +1201,7 @@ export class Wallet { requested_amount: req.amount, reserve_priv: keypair.priv, reserve_pub: keypair.pub, + senderWire: req.senderWire, }; const historyEntry = { @@ -1755,22 +1819,26 @@ export class Wallet { /** - * Retrieve a mapping from currency name to the amount - * that is currenctly available for spending in the wallet. + * Get detailed balance information, sliced by exchange and by currency. */ async getBalances(): Promise { - function ensureEntry(balance: WalletBalance, currency: string) { - let entry: WalletBalanceEntry|undefined = balance[currency]; - const z = Amounts.getZero(currency); - if (!entry) { - balance[currency] = entry = { - available: z, - paybackAmount: z, - pendingIncoming: z, - pendingPayment: z, - }; + /** + * Add amount to a balance field, both for + * the slicing by exchange and currency. + */ + function addTo(balance: WalletBalance, field: keyof WalletBalanceEntry, amount: AmountJson, exchange: string): void { + const z = Amounts.getZero(amount.currency); + const balanceIdentity = {available: z, paybackAmount: z, pendingIncoming: z, pendingPayment: z}; + let entryCurr = balance.byCurrency[amount.currency]; + if (!entryCurr) { + balance.byCurrency[amount.currency] = entryCurr = { ...balanceIdentity }; + } + let entryEx = balance.byExchange[exchange]; + if (!entryEx) { + balance.byExchange[exchange] = entryEx = { ...balanceIdentity }; } - return entry; + entryCurr[field] = Amounts.add(entryCurr[field], amount).amount; + entryEx[field] = Amounts.add(entryEx[field], amount).amount; } function collectBalances(c: CoinRecord, balance: WalletBalance) { @@ -1780,9 +1848,8 @@ export class Wallet { if (!(c.status === CoinStatus.Dirty || c.status === CoinStatus.Fresh)) { return balance; } - const currency = c.currentAmount.currency; - const entry = ensureEntry(balance, currency); - entry.available = Amounts.add(entry.available, c.currentAmount).amount; + console.log("collecting balance"); + addTo(balance, "available", c.currentAmount, c.exchangeBaseUrl); return balance; } @@ -1790,15 +1857,13 @@ export class Wallet { if (!r.confirmed) { return balance; } - const entry = ensureEntry(balance, r.requested_amount.currency); let amount = r.current_amount; if (!amount) { amount = r.requested_amount; } amount = Amounts.add(amount, r.precoin_amount).amount; if (Amounts.cmp(smallestWithdraw[r.exchange_base_url], amount) < 0) { - entry.pendingIncoming = Amounts.add(entry.pendingIncoming, - amount).amount; + addTo(balance, "pendingIncoming", amount, r.exchange_base_url); } return balance; } @@ -1807,9 +1872,8 @@ export class Wallet { if (!r.hasPayback) { return balance; } - const entry = ensureEntry(balance, r.requested_amount.currency); if (Amounts.cmp(smallestWithdraw[r.exchange_base_url], r.current_amount!) < 0) { - entry.paybackAmount = Amounts.add(entry.paybackAmount, r.current_amount!).amount; + addTo(balance, "paybackAmount", r.current_amount!, r.exchange_base_url); } return balance; } @@ -1821,9 +1885,7 @@ export class Wallet { if (r.finished) { return balance; } - const entry = ensureEntry(balance, r.valueWithFee.currency); - entry.pendingIncoming = Amounts.add(entry.pendingIncoming, - r.valueOutput).amount; + addTo(balance, "pendingIncoming", r.valueOutput, r.exchangeBaseUrl); return balance; } @@ -1832,10 +1894,7 @@ export class Wallet { if (t.finished) { return balance; } - const entry = ensureEntry(balance, t.contractTerms.amount.currency); - entry.pendingPayment = Amounts.add(entry.pendingPayment, - t.contractTerms.amount).amount; - + addTo(balance, "pendingIncoming", t.contractTerms.amount, t.payReq.exchange); return balance; } @@ -1852,7 +1911,10 @@ export class Wallet { return sw; } - const balance = {}; + const balance = { + byExchange: {}, + byCurrency: {}, + }; // Mapping from exchange pub to smallest // possible amount we can withdraw let smallestWithdraw: {[baseUrl: string]: AmountJson} = {}; @@ -1876,7 +1938,6 @@ export class Wallet { .reduce(collectPayments, balance); await tx.finish(); return balance; - } @@ -2347,4 +2408,156 @@ export class Wallet { stop() { this.timerGroup.stopCurrentAndFutureTimers(); } + + async getSenderWireInfos(): Promise { + const m: { [url: string]: Set } = {}; + await this.q().iter(Stores.exchangeWireFees).map((x) => { + const s = m[x.exchangeBaseUrl] = m[x.exchangeBaseUrl] || new Set(); + Object.keys(x.feesForType).map((k) => s.add(k)); + }).run(); + console.log(m); + const exchangeWireTypes: { [url: string]: string[] } = {}; + Object.keys(m).map((e) => { exchangeWireTypes[e] = Array.from(m[e]); }); + + const senderWiresSet = new Set(); + await this.q().iter(Stores.reserves).map((x) => { + if (x.senderWire) { + senderWiresSet.add(JSON.stringify(x.senderWire)); + } + }).run(); + const senderWires = Array.from(senderWiresSet).map((x) => JSON.parse(x)); + + return { + exchangeWireTypes, + senderWires, + }; + } + + /** + * Trigger paying coins back into the user's account. + */ + async returnCoins(req: ReturnCoinsRequest): Promise { + console.log("got returnCoins request", req); + const wireType = (req.senderWire as any).type; + console.log("wireType", wireType); + if (!wireType || typeof wireType !== "string") { + console.error(`wire type must be a non-empty string, not ${wireType}`); + return; + } + const stampSecNow = Math.floor((new Date()).getTime() / 1000); + const exchange = await this.q().get(Stores.exchanges, req.exchange); + if (!exchange) { + console.error(`Exchange ${req.exchange} not known to the wallet`); + return; + } + const cds = await this.getCoinsForReturn(req.exchange, req.amount); + console.log(cds); + + if (!cds) { + throw Error("coin return impossible, can't select coins"); + } + + const { priv, pub } = await this.cryptoApi.createEddsaKeypair(); + + const wireHash = await this.cryptoApi.hashString(canonicalJson(req.senderWire)); + + const contractTerms: ContractTerms = { + H_wire: wireHash, + amount: req.amount, + auditors: [], + wire_method: wireType, + pay_deadline: `/Date(${stampSecNow + 60 * 5})/`, + locations: [], + max_fee: req.amount, + merchant: {}, + merchant_pub: pub, + exchanges: [ { master_pub: exchange.masterPublicKey, url: exchange.baseUrl } ], + products: [], + refund_deadline: `/Date(${stampSecNow + 60 * 5})/`, + timestamp: `/Date(${stampSecNow})/`, + order_id: "none", + pay_url: "", + fulfillment_url: "", + extra: {}, + }; + + const contractTermsHash = await this.cryptoApi.hashString(canonicalJson(contractTerms)); + + const payCoinInfo = await this.cryptoApi.signDeposit(contractTerms, cds); + + console.log("pci", payCoinInfo); + + const coins = payCoinInfo.map((pci) => ({ coinPaySig: pci.sig })); + + const coinsReturnRecord: CoinsReturnRecord = { + coins, + exchange: exchange.baseUrl, + contractTerms, + contractTermsHash, + merchantPriv: priv, + wire: req.senderWire, + } + + await this.q() + .put(Stores.coinsReturns, coinsReturnRecord) + .putAll(Stores.coins, payCoinInfo.map((pci) => pci.updatedCoin)) + .finish(); + + this.depositReturnedCoins(coinsReturnRecord); + } + + async depositReturnedCoins(coinsReturnRecord: CoinsReturnRecord): Promise { + for (const c of coinsReturnRecord.coins) { + if (c.depositedSig) { + continue; + } + const req = { + f: c.coinPaySig.f, + wire: coinsReturnRecord.wire, + H_wire: coinsReturnRecord.contractTerms.H_wire, + h_contract_terms: coinsReturnRecord.contractTermsHash, + coin_pub: c.coinPaySig.coin_pub, + denom_pub: c.coinPaySig.denom_pub, + ub_sig: c.coinPaySig.ub_sig, + timestamp: coinsReturnRecord.contractTerms.timestamp, + wire_transfer_deadline: coinsReturnRecord.contractTerms.pay_deadline, + pay_deadline: coinsReturnRecord.contractTerms.pay_deadline, + refund_deadline: coinsReturnRecord.contractTerms.refund_deadline, + merchant_pub: coinsReturnRecord.contractTerms.merchant_pub, + coin_sig: c.coinPaySig.coin_sig, + }; + console.log("req", req); + const reqUrl = (new URI("deposit")).absoluteTo(coinsReturnRecord.exchange); + const resp = await this.http.postJson(reqUrl.href(), req); + if (resp.status !== 200) { + console.error("deposit failed due to status code", resp); + continue; + } + const respJson = JSON.parse(resp.responseText); + if (respJson.status !== "DEPOSIT_OK") { + console.error("deposit failed", resp); + continue; + } + + if (!respJson.sig) { + console.error("invalid 'sig' field", resp); + continue; + } + + // FIXME: verify signature + + // For every successful deposit, we replace the old record with an updated one + const currentCrr = await this.q().get(Stores.coinsReturns, coinsReturnRecord.contractTermsHash); + if (!currentCrr) { + console.error("database inconsistent"); + continue; + } + for (const nc of currentCrr.coins) { + if (nc.coinPaySig.coin_pub === c.coinPaySig.coin_pub) { + nc.depositedSig = respJson.sig; + } + } + await this.q().put(Stores.coinsReturns, currentCrr); + } + } } diff --git a/src/webex/messages.ts b/src/webex/messages.ts index bf9ca00b0..d7ecd06a1 100644 --- a/src/webex/messages.ts +++ b/src/webex/messages.ts @@ -168,6 +168,14 @@ export interface MessageMap { request: { }; response: void; }; + "get-sender-wire-infos": { + request: { }; + response: void; + }; + "return-coins": { + request: { }; + response: void; + }; } /** diff --git a/src/webex/pages/confirm-create-reserve.html b/src/webex/pages/confirm-create-reserve.html index 493a6fb5f..17daf4dde 100644 --- a/src/webex/pages/confirm-create-reserve.html +++ b/src/webex/pages/confirm-create-reserve.html @@ -6,40 +6,12 @@ Taler Wallet: Select Taler Provider - + - - diff --git a/src/webex/pages/popup.tsx b/src/webex/pages/popup.tsx index f1f0353ad..7d12d365e 100644 --- a/src/webex/pages/popup.tsx +++ b/src/webex/pages/popup.tsx @@ -219,22 +219,26 @@ class WalletBalanceView extends React.Component { this.unmount = true; } - updateBalance() { - chrome.runtime.sendMessage({type: "balances"}, (resp) => { + async updateBalance() { + let balance: WalletBalance; + try { + balance = await wxApi.getBalance(); + } catch (e) { if (this.unmount) { return; } - if (resp.error) { - this.gotError = true; - console.error("could not retrieve balances", resp); - this.setState({}); - return; - } - this.gotError = false; - console.log("got wallet", resp); - this.balance = resp; + this.gotError = true; + console.error("could not retrieve balances", e); this.setState({}); - }); + return; + } + if (this.unmount) { + return; + } + this.gotError = false; + console.log("got balance", balance); + this.balance = balance; + this.setState({}); } renderEmpty(): JSX.Element { @@ -308,8 +312,8 @@ class WalletBalanceView extends React.Component { } console.log(wallet); let paybackAvailable = false; - const listing = Object.keys(wallet).map((key) => { - const entry: WalletBalanceEntry = wallet[key]; + const listing = Object.keys(wallet.byCurrency).map((key) => { + const entry: WalletBalanceEntry = wallet.byCurrency[key]; if (entry.paybackAmount.value !== 0 || entry.paybackAmount.fraction !== 0) { paybackAvailable = true; } @@ -321,14 +325,16 @@ class WalletBalanceView extends React.Component {

); }); - const link = chrome.extension.getURL("/src/webex/pages/auditors.html"); - const linkElem = Trusted Auditors and Exchanges; - const paybackLinkElem = Trusted Auditors and Exchanges; + const makeLink = (page: string, name: string) => { + const url = chrome.extension.getURL(`/src/webex/pages/${page}`); + return ; + }; return (
{listing.length > 0 ? listing : this.renderEmpty()} - {paybackAvailable && paybackLinkElem} - {linkElem} + {paybackAvailable && makeLink("payback", i18n.str`Payback`)} + {makeLink("return-coins.html#dissolve", i18n.str`Return Electronic Cash to Bank Account`)} + {makeLink("auditors.html", i18n.str`Manage Trusted Auditors and Exchanges`)}
); } diff --git a/src/webex/pages/return-coins.html b/src/webex/pages/return-coins.html new file mode 100644 index 000000000..c0ab218d2 --- /dev/null +++ b/src/webex/pages/return-coins.html @@ -0,0 +1,19 @@ + + + + + + Taler Wallet: Return Coins to Bank Account + + + + + + + + + + +
+ + diff --git a/src/webex/pages/return-coins.tsx b/src/webex/pages/return-coins.tsx new file mode 100644 index 000000000..1fdadd2e9 --- /dev/null +++ b/src/webex/pages/return-coins.tsx @@ -0,0 +1,271 @@ +/* + This file is part of TALER + (C) 2017 Inria + + 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 + */ + +/** + * View and edit auditors. + * + * @author Florian Dold + */ + + +/** + * Imports. + */ + +import { + AmountJson, + Amounts, + SenderWireInfos, + WalletBalance, +} from "../../types"; + +import * as i18n from "../../i18n"; + +import * as wire from "../../wire"; + +import { + getBalance, + getSenderWireInfos, + returnCoins, +} from "../wxApi"; + +import { renderAmount } from "../renderHtml"; + +import * as React from "react"; +import * as ReactDOM from "react-dom"; + +interface ReturnSelectionItemProps extends ReturnSelectionListProps { + exchangeUrl: string; + senderWireInfos: SenderWireInfos; +} + +interface ReturnSelectionItemState { + selectedValue: string; + supportedWires: object[]; + selectedWire: string; + currency: string; +} + +class ReturnSelectionItem extends React.Component { + constructor(props: ReturnSelectionItemProps) { + super(props); + const exchange = this.props.exchangeUrl; + const wireTypes = this.props.senderWireInfos.exchangeWireTypes; + const supportedWires = this.props.senderWireInfos.senderWires.filter((x) => { + return wireTypes[exchange] && wireTypes[exchange].indexOf((x as any).type) >= 0; + }); + this.state = { + currency: props.balance.byExchange[props.exchangeUrl].available.currency, + selectedValue: Amounts.toFloat(props.balance.byExchange[props.exchangeUrl].available).toString(), + selectedWire: "", + supportedWires, + }; + } + render(): JSX.Element { + const exchange = this.props.exchangeUrl; + const byExchange = this.props.balance.byExchange; + const wireTypes = this.props.senderWireInfos.exchangeWireTypes; + return ( +
+

Exchange {exchange}

+

Available amount: {renderAmount(byExchange[exchange].available)}

+

Supported wire methods: {wireTypes[exchange].length ? wireTypes[exchange].join(", ") : "none"}

+

Wire {""} + this.setState({selectedValue: evt.target.value})} + style={{textAlign: "center"}} + /> {this.props.balance.byExchange[exchange].available.currency} {""} + to account {""} + . +

+ {this.state.selectedWire + ? + : null} +
+ ); + } + + select() { + let val: number; + let selectedWire: number; + try { + val = Number.parseFloat(this.state.selectedValue); + selectedWire = Number.parseInt(this.state.selectedWire); + } catch (e) { + console.error(e); + return; + } + this.props.selectDetail({ + amount: Amounts.fromFloat(val, this.state.currency), + exchange: this.props.exchangeUrl, + senderWire: this.state.supportedWires[selectedWire], + }); + } +} + +interface ReturnSelectionListProps { + balance: WalletBalance; + senderWireInfos: SenderWireInfos; + selectDetail(d: SelectedDetail): void; +} + +class ReturnSelectionList extends React.Component { + render(): JSX.Element { + const byExchange = this.props.balance.byExchange; + const exchanges = Object.keys(byExchange); + if (!exchanges.length) { + return

Currently no funds available to transfer.

; + } + return ( +
+ {exchanges.map((e) => )} +
+ ); + } +} + +interface SelectedDetail { + amount: AmountJson; + senderWire: any; + exchange: string; +} + + +interface ReturnConfirmationProps { + detail: SelectedDetail; + cancel(): void; + confirm(): void; +} + +class ReturnConfirmation extends React.Component { + render() { + return ( +
+

Please confirm if you want to transmit {renderAmount(this.props.detail.amount)} at {""} + {this.props.detail.exchange} to account {""} + {wire.summarizeWire(this.props.detail.senderWire)}. +

+ + +
+ ); + } +} + +interface ReturnCoinsState { + balance: WalletBalance | undefined; + senderWireInfos: SenderWireInfos | undefined; + selectedReturn: SelectedDetail | undefined; + /** + * Last confirmed detail, so we can show a nice box. + */ + lastConfirmedDetail: SelectedDetail | undefined; +} + +class ReturnCoins extends React.Component { + constructor() { + super(); + const port = chrome.runtime.connect(); + port.onMessage.addListener((msg: any) => { + if (msg.notify) { + console.log("got notified"); + this.update(); + } + }); + this.update(); + this.state = {} as any; + } + + async update() { + const balance = await getBalance(); + const senderWireInfos = await getSenderWireInfos(); + console.log("got swi", senderWireInfos); + console.log("got bal", balance); + this.setState({ balance, senderWireInfos }); + } + + selectDetail(d: SelectedDetail) { + this.setState({selectedReturn: d}); + } + + async confirm() { + const selectedReturn = this.state.selectedReturn; + if (!selectedReturn) { + return; + } + await returnCoins(selectedReturn); + await this.update(); + this.setState({selectedReturn: undefined, lastConfirmedDetail: selectedReturn}); + } + + async cancel() { + this.setState({selectedReturn: undefined, lastConfirmedDetail: undefined}); + } + + render() { + const balance = this.state.balance; + const senderWireInfos = this.state.senderWireInfos; + if (!balance || !senderWireInfos) { + return ...; + } + if (this.state.selectedReturn) { + return ( +
+ this.cancel()} + confirm={() => this.confirm()} + /> +
+ ); + } + return ( +
+

Wire electronic cash back to own bank account

+

You can send coins back into your own bank account. Note that + you're acting as a merchant when doing this, and thus the same fees apply.

+ {this.state.lastConfirmedDetail + ?

Transfer of {renderAmount(this.state.lastConfirmedDetail.amount)} successfully initiated.

+ : null} + this.selectDetail(d)} + balance={balance} + senderWireInfos={senderWireInfos} /> +
+ ); + } +} + + +function main() { + ReactDOM.render(, document.getElementById("container")!); +} + +document.addEventListener("DOMContentLoaded", main); diff --git a/src/webex/style/wallet.css b/src/webex/style/wallet.css index 5773eb396..61dd611e9 100644 --- a/src/webex/style/wallet.css +++ b/src/webex/style/wallet.css @@ -1,3 +1,8 @@ +body { + font-size: 100%; + overflow-y: scroll; +} + #main { border: solid 1px black; border-radius: 10px; @@ -235,3 +240,14 @@ a.actionLink { font-weight: bold; background: #00FA9A; } + + +a.opener { + color: black; +} +.opener-open::before { + content: "\25bc" +} +.opener-collapsed::before { + content: "\25b6 " +} diff --git a/src/webex/wxApi.ts b/src/webex/wxApi.ts index 9d8ba4d1d..1371e27e4 100644 --- a/src/webex/wxApi.ts +++ b/src/webex/wxApi.ts @@ -31,9 +31,11 @@ import { DenominationRecord, ExchangeRecord, PreCoinRecord, + QueryPaymentResult, ReserveCreationInfo, ReserveRecord, - QueryPaymentResult, + SenderWireInfos, + WalletBalance, } from "../types"; import { MessageType, MessageMap } from "./messages"; @@ -296,3 +298,26 @@ export function createReserve(args: { amount: AmountJson, exchange: string, send export function resetDb(): Promise { return callBackend("reset-db", { }); } + +/** + * Get balances for all currencies/exchanges. + */ +export function getBalance(): Promise { + return callBackend("balances", { }); +} + + +/** + * Get possible sender wire infos for getting money + * wired from an exchange. + */ +export function getSenderWireInfos(): Promise { + return callBackend("get-sender-wire-infos", { }); +} + +/** + * Return coins to a bank account. + */ +export function returnCoins(args: { amount: AmountJson, exchange: string, senderWire: object }): Promise { + return callBackend("return-coins", args); +} diff --git a/src/webex/wxBackend.ts b/src/webex/wxBackend.ts index 261477386..974bcb3c2 100644 --- a/src/webex/wxBackend.ts +++ b/src/webex/wxBackend.ts @@ -32,12 +32,13 @@ import { } from "../query"; import { AmountJson, + ConfirmReserveRequest, + CreateReserveRequest, Notifier, ProposalRecord, + ReturnCoinsRequest, } from "../types"; import { - ConfirmReserveRequest, - CreateReserveRequest, Stores, WALLET_DB_VERSION, Wallet, @@ -278,6 +279,18 @@ function handleMessage(sender: MessageSender, } return needsWallet().paymentSucceeded(contractTermsHash, merchantSig); } + case "get-sender-wire-infos": { + return needsWallet().getSenderWireInfos(); + } + case "return-coins": { + const d = { + amount: detail.amount, + exchange: detail.exchange, + senderWire: detail.senderWire, + }; + const req = ReturnCoinsRequest.checked(d); + return needsWallet().returnCoins(req); + } case "check-upgrade": { let dbResetRequired = false; if (!currentWallet) { diff --git a/src/wire.ts b/src/wire.ts new file mode 100644 index 000000000..d61e8eab2 --- /dev/null +++ b/src/wire.ts @@ -0,0 +1,53 @@ +/* + 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 + */ + + +/** + * Display and manipulate wire information. + * + * Right now, all types are hard-coded. In the future, there might be plugins / configurable + * methods or support for the "payto://" URI scheme. + */ + +/** + * Imports. + */ +import * as i18n from "./i18n"; + +/** + * Short summary of the wire information. + * + * Might abbreviate and return the same summary for different + * wire details. + */ +export function summarizeWire(w: any): string { + if (!w.type) { + return i18n.str`Invalid Wire`; + } + switch (w.type.toLowerCase()) { + case "test": + if (!w.account_number && w.account_number !== 0) { + return i18n.str`Invalid Test Wire Detail`; + } + if (!w.bank_uri) { + return i18n.str`Invalid Test Wire Detail`; + } + return i18n.str`Test Wire Acct #${w.account_number} on ${w.bank_uri}`; + default: + return i18n.str`Unknown Wire Detail`; + } +} + -- cgit v1.2.3