aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFlorian Dold <florian.dold@gmail.com>2018-01-17 03:49:54 +0100
committerFlorian Dold <florian.dold@gmail.com>2018-01-17 03:49:54 +0100
commitc62ba4986fbfcb8637a3befadf3d3eddbd5348ca (patch)
treee88b71758cff696df0c8f2bb163a7c7c3957f871
parent894a09a51c1111257be56809f1d3daf0146f8509 (diff)
downloadwallet-core-c62ba4986fbfcb8637a3befadf3d3eddbd5348ca.tar.xz
implement new mobile-compatible payment logic
-rw-r--r--src/checkable.ts25
-rw-r--r--src/dbTypes.ts51
-rw-r--r--src/i18n/de.po8
-rw-r--r--src/i18n/en-US.po8
-rw-r--r--src/i18n/fr.po8
-rw-r--r--src/i18n/it.po8
-rw-r--r--src/i18n/taler-wallet-webex.pot8
-rw-r--r--src/talerTypes.ts56
-rw-r--r--src/wallet.ts213
-rw-r--r--src/walletTypes.ts15
-rw-r--r--src/webex/messages.ts36
-rw-r--r--src/webex/notify.ts282
-rw-r--r--src/webex/pages/confirm-contract.tsx83
-rw-r--r--src/webex/wxApi.ts63
-rw-r--r--src/webex/wxBackend.ts183
15 files changed, 501 insertions, 546 deletions
diff --git a/src/checkable.ts b/src/checkable.ts
index 124eb6587..159e5a85e 100644
--- a/src/checkable.ts
+++ b/src/checkable.ts
@@ -15,8 +15,6 @@
*/
-"use strict";
-
/**
* Decorators for validating JSON objects and converting them to a typed
* object.
@@ -55,6 +53,7 @@ export namespace Checkable {
propertyKey: any;
checker: any;
type?: any;
+ typeThunk?: () => any;
elementChecker?: any;
elementProp?: any;
keyProp?: any;
@@ -167,11 +166,18 @@ export namespace Checkable {
function checkValue(target: any, prop: Prop, path: Path): any {
- const type = prop.type;
- const typeName = type.name || "??";
- if (!type) {
- throw Error(`assertion failed (prop is ${JSON.stringify(prop)})`);
+ let type;
+ if (prop.type) {
+ type = prop.type;
+ } else if (prop.typeThunk) {
+ type = prop.typeThunk();
+ if (!type) {
+ throw Error(`assertion failed: typeThunk returned null (prop is ${JSON.stringify(prop)})`);
+ }
+ } else {
+ throw Error(`assertion failed: type/typeThunk missing (prop is ${JSON.stringify(prop)})`);
}
+ const typeName = type.name || "??";
const v = target;
if (!v || typeof v !== "object") {
throw new SchemaError(
@@ -236,16 +242,13 @@ export namespace Checkable {
/**
* Target property must be a Checkable object of the given type.
*/
- export function Value(type: any) {
- if (!type) {
- throw Error("Type does not exist yet (wrong order of definitions?)");
- }
+ export function Value(typeThunk: () => any) {
function deco(target: object, propertyKey: string | symbol): void {
const chk = getCheckableInfo(target);
chk.props.push({
checker: checkValue,
propertyKey,
- type,
+ typeThunk,
});
}
diff --git a/src/dbTypes.ts b/src/dbTypes.ts
index b5040bee4..86f3e0a1e 100644
--- a/src/dbTypes.ts
+++ b/src/dbTypes.ts
@@ -49,7 +49,7 @@ import {
* In the future we might consider adding migration functions for
* each version increment.
*/
-export const WALLET_DB_VERSION = 24;
+export const WALLET_DB_VERSION = 25;
/**
@@ -206,7 +206,7 @@ export class DenominationRecord {
/**
* Value of one coin of the denomination.
*/
- @Checkable.Value(AmountJson)
+ @Checkable.Value(() => AmountJson)
value: AmountJson;
/**
@@ -225,25 +225,25 @@ export class DenominationRecord {
/**
* Fee for withdrawing.
*/
- @Checkable.Value(AmountJson)
+ @Checkable.Value(() => AmountJson)
feeWithdraw: AmountJson;
/**
* Fee for depositing.
*/
- @Checkable.Value(AmountJson)
+ @Checkable.Value(() => AmountJson)
feeDeposit: AmountJson;
/**
* Fee for refreshing.
*/
- @Checkable.Value(AmountJson)
+ @Checkable.Value(() => AmountJson)
feeRefresh: AmountJson;
/**
* Fee for refunding.
*/
- @Checkable.Value(AmountJson)
+ @Checkable.Value(() => AmountJson)
feeRefund: AmountJson;
/**
@@ -491,15 +491,22 @@ export interface CoinRecord {
status: CoinStatus;
}
+
/**
* Proposal record, stored in the wallet's database.
*/
@Checkable.Class()
-export class ProposalRecord {
+export class ProposalDownloadRecord {
+ /**
+ * URL where the proposal was downloaded.
+ */
+ @Checkable.String
+ url: string;
+
/**
* The contract that was offered by the merchant.
*/
- @Checkable.Value(ContractTerms)
+ @Checkable.Value(() => ContractTerms)
contractTerms: ContractTerms;
/**
@@ -528,10 +535,16 @@ export class ProposalRecord {
timestamp: number;
/**
+ * Private key for the nonce.
+ */
+ @Checkable.String
+ noncePriv: string;
+
+ /**
* Verify that a value matches the schema of this class and convert it into a
* member.
*/
- static checked: (obj: any) => ProposalRecord;
+ static checked: (obj: any) => ProposalDownloadRecord;
}
@@ -789,15 +802,6 @@ export interface SenderWireRecord {
/**
- * Nonce record as stored in the wallet's database.
- */
-export interface NonceRecord {
- priv: string;
- pub: string;
-}
-
-
-/**
* Configuration key/value entries to configure
* the wallet.
*/
@@ -869,12 +873,6 @@ export namespace Stores {
pubKeyIndex = new Index<string, ExchangeRecord>(this, "pubKeyIndex", "masterPublicKey");
}
- class NonceStore extends Store<NonceRecord> {
- constructor() {
- super("nonces", { keyPath: "pub" });
- }
- }
-
class CoinsStore extends Store<CoinRecord> {
constructor() {
super("coins", { keyPath: "coinPub" });
@@ -884,14 +882,14 @@ export namespace Stores {
denomPubIndex = new Index<string, CoinRecord>(this, "denomPubIndex", "denomPub");
}
- class ProposalsStore extends Store<ProposalRecord> {
+ class ProposalsStore extends Store<ProposalDownloadRecord> {
constructor() {
super("proposals", {
autoIncrement: true,
keyPath: "id",
});
}
- timestampIndex = new Index<string, ProposalRecord>(this, "timestampIndex", "timestamp");
+ timestampIndex = new Index<string, ProposalDownloadRecord>(this, "timestampIndex", "timestamp");
}
class PurchasesStore extends Store<PurchaseRecord> {
@@ -965,7 +963,6 @@ export namespace Stores {
export const denominations = new DenominationsStore();
export const exchangeWireFees = new ExchangeWireFeesStore();
export const exchanges = new ExchangeStore();
- export const nonces = new NonceStore();
export const precoins = new Store<PreCoinRecord>("precoins", {keyPath: "coinPub"});
export const proposals = new ProposalsStore();
export const refresh = new Store<RefreshSessionRecord>("refresh", {keyPath: "id", autoIncrement: true});
diff --git a/src/i18n/de.po b/src/i18n/de.po
index 21ff8dfe0..5f163a0d3 100644
--- a/src/i18n/de.po
+++ b/src/i18n/de.po
@@ -42,13 +42,13 @@ msgstr ""
msgid "Exchanges in the wallet:"
msgstr ""
-#: src/webex/pages/confirm-contract.tsx:159
+#: src/webex/pages/confirm-contract.tsx:175
#, c-format
msgid "You have insufficient funds of the requested currency in your wallet."
msgstr ""
#. tslint:disable-next-line:max-line-length
-#: src/webex/pages/confirm-contract.tsx:161
+#: src/webex/pages/confirm-contract.tsx:177
#, c-format
msgid ""
"You do not have any funds from an exchange that is accepted by this "
@@ -56,12 +56,12 @@ msgid ""
"wallet."
msgstr ""
-#: src/webex/pages/confirm-contract.tsx:217
+#: src/webex/pages/confirm-contract.tsx:236
#, c-format
msgid "The merchant%1$s offers you to purchase:\n"
msgstr ""
-#: src/webex/pages/confirm-contract.tsx:238
+#: src/webex/pages/confirm-contract.tsx:257
#, fuzzy, c-format
msgid "Confirm payment"
msgstr "Bezahlung bestätigen"
diff --git a/src/i18n/en-US.po b/src/i18n/en-US.po
index 3307229b1..0dfa852ca 100644
--- a/src/i18n/en-US.po
+++ b/src/i18n/en-US.po
@@ -42,13 +42,13 @@ msgstr ""
msgid "Exchanges in the wallet:"
msgstr ""
-#: src/webex/pages/confirm-contract.tsx:159
+#: src/webex/pages/confirm-contract.tsx:175
#, c-format
msgid "You have insufficient funds of the requested currency in your wallet."
msgstr ""
#. tslint:disable-next-line:max-line-length
-#: src/webex/pages/confirm-contract.tsx:161
+#: src/webex/pages/confirm-contract.tsx:177
#, c-format
msgid ""
"You do not have any funds from an exchange that is accepted by this "
@@ -56,12 +56,12 @@ msgid ""
"wallet."
msgstr ""
-#: src/webex/pages/confirm-contract.tsx:217
+#: src/webex/pages/confirm-contract.tsx:236
#, c-format
msgid "The merchant%1$s offers you to purchase:\n"
msgstr ""
-#: src/webex/pages/confirm-contract.tsx:238
+#: src/webex/pages/confirm-contract.tsx:257
#, c-format
msgid "Confirm payment"
msgstr ""
diff --git a/src/i18n/fr.po b/src/i18n/fr.po
index b955dc6a1..55677763b 100644
--- a/src/i18n/fr.po
+++ b/src/i18n/fr.po
@@ -42,13 +42,13 @@ msgstr ""
msgid "Exchanges in the wallet:"
msgstr ""
-#: src/webex/pages/confirm-contract.tsx:159
+#: src/webex/pages/confirm-contract.tsx:175
#, c-format
msgid "You have insufficient funds of the requested currency in your wallet."
msgstr ""
#. tslint:disable-next-line:max-line-length
-#: src/webex/pages/confirm-contract.tsx:161
+#: src/webex/pages/confirm-contract.tsx:177
#, c-format
msgid ""
"You do not have any funds from an exchange that is accepted by this "
@@ -56,12 +56,12 @@ msgid ""
"wallet."
msgstr ""
-#: src/webex/pages/confirm-contract.tsx:217
+#: src/webex/pages/confirm-contract.tsx:236
#, c-format
msgid "The merchant%1$s offers you to purchase:\n"
msgstr ""
-#: src/webex/pages/confirm-contract.tsx:238
+#: src/webex/pages/confirm-contract.tsx:257
#, c-format
msgid "Confirm payment"
msgstr ""
diff --git a/src/i18n/it.po b/src/i18n/it.po
index b955dc6a1..55677763b 100644
--- a/src/i18n/it.po
+++ b/src/i18n/it.po
@@ -42,13 +42,13 @@ msgstr ""
msgid "Exchanges in the wallet:"
msgstr ""
-#: src/webex/pages/confirm-contract.tsx:159
+#: src/webex/pages/confirm-contract.tsx:175
#, c-format
msgid "You have insufficient funds of the requested currency in your wallet."
msgstr ""
#. tslint:disable-next-line:max-line-length
-#: src/webex/pages/confirm-contract.tsx:161
+#: src/webex/pages/confirm-contract.tsx:177
#, c-format
msgid ""
"You do not have any funds from an exchange that is accepted by this "
@@ -56,12 +56,12 @@ msgid ""
"wallet."
msgstr ""
-#: src/webex/pages/confirm-contract.tsx:217
+#: src/webex/pages/confirm-contract.tsx:236
#, c-format
msgid "The merchant%1$s offers you to purchase:\n"
msgstr ""
-#: src/webex/pages/confirm-contract.tsx:238
+#: src/webex/pages/confirm-contract.tsx:257
#, c-format
msgid "Confirm payment"
msgstr ""
diff --git a/src/i18n/taler-wallet-webex.pot b/src/i18n/taler-wallet-webex.pot
index b955dc6a1..55677763b 100644
--- a/src/i18n/taler-wallet-webex.pot
+++ b/src/i18n/taler-wallet-webex.pot
@@ -42,13 +42,13 @@ msgstr ""
msgid "Exchanges in the wallet:"
msgstr ""
-#: src/webex/pages/confirm-contract.tsx:159
+#: src/webex/pages/confirm-contract.tsx:175
#, c-format
msgid "You have insufficient funds of the requested currency in your wallet."
msgstr ""
#. tslint:disable-next-line:max-line-length
-#: src/webex/pages/confirm-contract.tsx:161
+#: src/webex/pages/confirm-contract.tsx:177
#, c-format
msgid ""
"You do not have any funds from an exchange that is accepted by this "
@@ -56,12 +56,12 @@ msgid ""
"wallet."
msgstr ""
-#: src/webex/pages/confirm-contract.tsx:217
+#: src/webex/pages/confirm-contract.tsx:236
#, c-format
msgid "The merchant%1$s offers you to purchase:\n"
msgstr ""
-#: src/webex/pages/confirm-contract.tsx:238
+#: src/webex/pages/confirm-contract.tsx:257
#, c-format
msgid "Confirm payment"
msgstr ""
diff --git a/src/talerTypes.ts b/src/talerTypes.ts
index 27bf7b43b..d593c3d34 100644
--- a/src/talerTypes.ts
+++ b/src/talerTypes.ts
@@ -38,7 +38,7 @@ export class Denomination {
/**
* Value of one coin of the denomination.
*/
- @Checkable.Value(AmountJson)
+ @Checkable.Value(() => AmountJson)
value: AmountJson;
/**
@@ -50,25 +50,25 @@ export class Denomination {
/**
* Fee for withdrawing.
*/
- @Checkable.Value(AmountJson)
+ @Checkable.Value(() => AmountJson)
fee_withdraw: AmountJson;
/**
* Fee for depositing.
*/
- @Checkable.Value(AmountJson)
+ @Checkable.Value(() => AmountJson)
fee_deposit: AmountJson;
/**
* Fee for refreshing.
*/
- @Checkable.Value(AmountJson)
+ @Checkable.Value(() => AmountJson)
fee_refresh: AmountJson;
/**
* Fee for refunding.
*/
- @Checkable.Value(AmountJson)
+ @Checkable.Value(() => AmountJson)
fee_refund: AmountJson;
/**
@@ -151,7 +151,7 @@ export class Auditor {
/**
* List of signatures for denominations by the auditor.
*/
- @Checkable.List(Checkable.Value(AuditorDenomSig))
+ @Checkable.List(Checkable.Value(() => AuditorDenomSig))
denomination_keys: AuditorDenomSig[];
}
@@ -204,7 +204,7 @@ export class PaybackConfirmation {
* How much will the exchange pay back (needed by wallet in
* case coin was partially spent and wallet got restored from backup)
*/
- @Checkable.Value(AmountJson)
+ @Checkable.Value(() => AmountJson)
amount: AmountJson;
/**
@@ -336,7 +336,7 @@ export class ContractTerms {
/**
* Total amount payable.
*/
- @Checkable.Value(AmountJson)
+ @Checkable.Value(() => AmountJson)
amount: AmountJson;
/**
@@ -360,7 +360,7 @@ export class ContractTerms {
/**
* Maximum deposit fee covered by the merchant.
*/
- @Checkable.Value(AmountJson)
+ @Checkable.Value(() => AmountJson)
max_fee: AmountJson;
/**
@@ -378,7 +378,7 @@ export class ContractTerms {
/**
* List of accepted exchanges.
*/
- @Checkable.List(Checkable.Value(ExchangeHandle))
+ @Checkable.List(Checkable.Value(() => ExchangeHandle))
exchanges: ExchangeHandle[];
/**
@@ -428,7 +428,7 @@ export class ContractTerms {
/**
* Maximum wire fee that the merchant agrees to pay for.
*/
- @Checkable.Optional(Checkable.Value(AmountJson))
+ @Checkable.Optional(Checkable.Value(() => AmountJson))
max_wire_fee?: AmountJson;
/**
@@ -578,7 +578,7 @@ export class TipResponse {
/**
* The order of the signatures matches the planchets list.
*/
- @Checkable.List(Checkable.Value(ReserveSigSingleton))
+ @Checkable.List(Checkable.Value(() => ReserveSigSingleton))
reserve_sigs: ReserveSigSingleton[];
/**
@@ -620,7 +620,7 @@ export class TipToken {
/**
* Amount of tip.
*/
- @Checkable.Value(AmountJson)
+ @Checkable.Value(() => AmountJson)
amount: AmountJson;
/**
@@ -659,7 +659,7 @@ export class KeysJson {
/**
* List of offered denominations.
*/
- @Checkable.List(Checkable.Value(Denomination))
+ @Checkable.List(Checkable.Value(() => Denomination))
denoms: Denomination[];
/**
@@ -671,7 +671,7 @@ export class KeysJson {
/**
* The list of auditors (partially) auditing the exchange.
*/
- @Checkable.List(Checkable.Value(Auditor))
+ @Checkable.List(Checkable.Value(() => Auditor))
auditors: Auditor[];
/**
@@ -683,7 +683,7 @@ export class KeysJson {
/**
* List of paybacks for compromised denominations.
*/
- @Checkable.Optional(Checkable.List(Checkable.Value(Payback)))
+ @Checkable.Optional(Checkable.List(Checkable.Value(() => Payback)))
payback?: Payback[];
/**
@@ -715,13 +715,13 @@ export class WireFeesJson {
/**
* Cost of a wire transfer.
*/
- @Checkable.Value(AmountJson)
+ @Checkable.Value(() => AmountJson)
wire_fee: AmountJson;
/**
* Cost of clising a reserve.
*/
- @Checkable.Value(AmountJson)
+ @Checkable.Value(() => AmountJson)
closing_fee: AmountJson;
/**
@@ -765,7 +765,7 @@ export class WireDetailJson {
/**
* Fees associated with the wire transfer method.
*/
- @Checkable.List(Checkable.Value(WireFeesJson))
+ @Checkable.List(Checkable.Value(() => WireFeesJson))
fees: WireFeesJson[];
/**
@@ -788,3 +788,21 @@ export type WireDetail = object & { type: string };
export function isWireDetail(x: any): x is WireDetail {
return x && typeof x === "object" && typeof x.type === "string";
}
+
+/**
+ * Proposal returned from the contract URL.
+ */
+@Checkable.Class({extra: true})
+export class Proposal {
+ @Checkable.Value(() => ContractTerms)
+ contract_terms: ContractTerms;
+
+ @Checkable.String
+ sig: string;
+
+ /**
+ * Verify that a value matches the schema of this class and convert it into a
+ * member.
+ */
+ static checked: (obj: any) => Proposal;
+}
diff --git a/src/wallet.ts b/src/wallet.ts
index 8a63e45e2..24fab9f86 100644
--- a/src/wallet.ts
+++ b/src/wallet.ts
@@ -49,6 +49,8 @@ import * as Amounts from "./amounts";
import URI = require("urijs");
+import axios from "axios";
+
import {
CoinRecord,
CoinStatus,
@@ -59,7 +61,7 @@ import {
ExchangeRecord,
ExchangeWireFeesRecord,
PreCoinRecord,
- ProposalRecord,
+ ProposalDownloadRecord,
PurchaseRecord,
RefreshPreCoinRecord,
RefreshSessionRecord,
@@ -76,9 +78,11 @@ import {
KeysJson,
PayReq,
PaybackConfirmation,
+ Proposal,
RefundPermission,
TipPlanchetDetail,
TipResponse,
+ TipToken,
WireDetailJson,
isWireDetail,
} from "./talerTypes";
@@ -109,7 +113,7 @@ interface SpeculativePayData {
payCoinInfo: PayCoinInfo;
exchangeUrl: string;
proposalId: number;
- proposal: ProposalRecord;
+ proposal: ProposalDownloadRecord;
}
@@ -624,9 +628,9 @@ export class Wallet {
* Record all information that is necessary to
* pay for a proposal in the wallet's database.
*/
- private async recordConfirmPay(proposal: ProposalRecord,
+ private async recordConfirmPay(proposal: ProposalDownloadRecord,
payCoinInfo: PayCoinInfo,
- chosenExchange: string): Promise<void> {
+ chosenExchange: string): Promise<PurchaseRecord> {
const payReq: PayReq = {
coins: payCoinInfo.sigs,
merchant_pub: proposal.contractTerms.merchant_pub,
@@ -651,15 +655,42 @@ export class Wallet {
.finish();
this.badge.showNotification();
this.notifier.notify();
+ return t;
}
/**
- * Save a proposal in the database and return an id for it to
- * retrieve it later.
+ * Download a proposal and store it in the database.
+ * Returns an id for it to retrieve it later.
*/
- async saveProposal(proposal: ProposalRecord): Promise<number> {
- const id = await this.q().putWithResult(Stores.proposals, proposal);
+ async downloadProposal(url: string): Promise<number> {
+ const { priv, pub } = await this.cryptoApi.createEddsaKeypair();
+ const parsed_url = new URI(url);
+ url = parsed_url.setQuery({ nonce: pub }).href();
+ console.log("downloading contract from '" + url + "'");
+ let resp;
+ try {
+ resp = await axios.get(url, { validateStatus: (s) => s === 200 });
+ } catch (e) {
+ console.log("contract download failed", e);
+ throw e;
+ }
+ console.log("got response", resp);
+
+ const proposal = Proposal.checked(resp.data);
+
+ const contractTermsHash = await this.hashContract(proposal.contract_terms);
+
+ const proposalRecord: ProposalDownloadRecord = {
+ contractTerms: proposal.contract_terms,
+ contractTermsHash,
+ merchantSig: proposal.sig,
+ noncePriv: priv,
+ timestamp: (new Date()).getTime(),
+ url,
+ };
+
+ const id = await this.q().putWithResult(Stores.proposals, proposalRecord);
this.notifier.notify();
if (typeof id !== "number") {
throw Error("db schema wrong");
@@ -667,24 +698,50 @@ export class Wallet {
return id;
}
+ async submitPay(purchase: PurchaseRecord, sessionId: string | undefined): Promise<ConfirmPayResult> {
+ let resp;
+ const payReq = { ...purchase.payReq, session_id: sessionId };
+ try {
+ const config = {
+ headers: { "Content-Type": "application/json;charset=UTF-8" },
+ timeout: 5000, /* 5 seconds */
+ validateStatus: (s: number) => s === 200,
+ };
+ resp = await axios.post(purchase.contractTerms.pay_url, payReq, config);
+ } catch (e) {
+ // Gives the user the option to retry / abort and refresh
+ console.log("payment failed", e);
+ throw e;
+ }
+ const merchantResp = resp.data;
+ console.log("got success from pay_url");
+ await this.paymentSucceeded(purchase.contractTermsHash, merchantResp.sig);
+ const fu = new URI(purchase.contractTerms.fulfillment_url);
+ fu.addSearch("order_id", purchase.contractTerms.order_id);
+ if (merchantResp.session_sig) {
+ fu.addSearch("session_sig", merchantResp.session_sig);
+ }
+ const nextUrl = fu.href();
+ return { nextUrl };
+ }
+
/**
* Add a contract to the wallet and sign coins,
* but do not send them yet.
*/
- async confirmPay(proposalId: number): Promise<ConfirmPayResult> {
- console.log("executing confirmPay");
- const proposal: ProposalRecord|undefined = await this.q().get(Stores.proposals, proposalId);
+ async confirmPay(proposalId: number, sessionId: string | undefined): Promise<ConfirmPayResult> {
+ console.log(`executing confirmPay with proposalId ${proposalId} and sessionId ${sessionId}`);
+ const proposal: ProposalDownloadRecord|undefined = await this.q().get(Stores.proposals, proposalId);
if (!proposal) {
throw Error(`proposal with id ${proposalId} not found`);
}
- const purchase = await this.q().get(Stores.purchases, proposal.contractTermsHash);
+ let purchase = await this.q().get(Stores.purchases, proposal.contractTermsHash);
if (purchase) {
- // Already payed ...
- return "paid";
+ return this.submitPay(purchase, sessionId);
}
const res = await this.getCoinsForPayment({
@@ -702,22 +759,24 @@ export class Wallet {
console.log("coin selection result", res);
if (!res) {
+ // Should not happen, since checkPay should be called first
console.log("not confirming payment, insufficient coins");
- return "insufficient-balance";
+ throw Error("insufficient balance");
}
const sd = await this.getSpeculativePayData(proposalId);
if (!sd) {
const { exchangeUrl, cds } = res;
const payCoinInfo = await this.cryptoApi.signDeposit(proposal.contractTerms, cds);
- await this.recordConfirmPay(proposal, payCoinInfo, exchangeUrl);
+ purchase = await this.recordConfirmPay(proposal, payCoinInfo, exchangeUrl);
} else {
- await this.recordConfirmPay(sd.proposal, sd.payCoinInfo, sd.exchangeUrl);
+ purchase = await this.recordConfirmPay(sd.proposal, sd.payCoinInfo, sd.exchangeUrl);
}
- return "paid";
+ return this.submitPay(purchase, sessionId);
}
+
/**
* Get the speculative pay data, but only if coins have not changed in between.
*/
@@ -803,7 +862,7 @@ export class Wallet {
* Retrieve information required to pay for a contract, where the
* contract is identified via the fulfillment url.
*/
- async queryPayment(url: string): Promise<QueryPaymentResult> {
+ async queryPaymentByFulfillmentUrl(url: string): Promise<QueryPaymentResult> {
console.log("query for payment", url);
const t = await this.q().getIndexed(Stores.purchases.fulfillmentUrlIndex, url);
@@ -823,6 +882,30 @@ export class Wallet {
};
}
+ /**
+ * Retrieve information required to pay for a contract, where the
+ * contract is identified via the contract terms hash.
+ */
+ async queryPaymentByContractTermsHash(contractTermsHash: string): Promise<QueryPaymentResult> {
+ console.log("query for payment", contractTermsHash);
+
+ const t = await this.q().get(Stores.purchases, contractTermsHash);
+
+ if (!t) {
+ console.log("query for payment failed");
+ return {
+ found: false,
+ };
+ }
+ console.log("query for payment succeeded:", t);
+ return {
+ contractTerms: t.contractTerms,
+ contractTermsHash: t.contractTermsHash,
+ found: true,
+ payReq: t.payReq,
+ };
+ }
+
/**
* First fetch information requred to withdraw from the reserve,
@@ -2020,7 +2103,7 @@ export class Wallet {
// FIXME: do pagination instead of generating the full history
- const proposals = await this.q().iter<ProposalRecord>(Stores.proposals).toArray();
+ const proposals = await this.q().iter<ProposalDownloadRecord>(Stores.proposals).toArray();
for (const p of proposals) {
history.push({
detail: {
@@ -2111,7 +2194,7 @@ export class Wallet {
return denoms;
}
- async getProposal(proposalId: number): Promise<ProposalRecord|undefined> {
+ async getProposal(proposalId: number): Promise<ProposalDownloadRecord|undefined> {
const proposal = await this.q().get(Stores.proposals, proposalId);
return proposal;
}
@@ -2162,18 +2245,6 @@ export class Wallet {
}
- /**
- * Generate a nonce in form of an EdDSA public key.
- * Store the private key in our DB, so we can prove ownership.
- */
- async generateNonce(): Promise<string> {
- const {priv, pub} = await this.cryptoApi.createEddsaKeypair();
- await this.q()
- .put(Stores.nonces, {priv, pub})
- .finish();
- return pub;
- }
-
async getCurrencyRecord(currency: string): Promise<CurrencyRecord|undefined> {
return this.q().get(Stores.currencies, currency);
}
@@ -2466,10 +2537,25 @@ export class Wallet {
}
}
- async acceptRefund(refundPermissions: RefundPermission[]): Promise<void> {
+ async acceptRefund(refundUrl: string): Promise<string> {
+ console.log("processing refund");
+ let resp;
+ try {
+ const config = {
+ validateStatus: (s: number) => s === 200,
+ };
+ resp = await axios.get(refundUrl, config);
+ } catch (e) {
+ console.log("error downloading refund permission", e);
+ throw e;
+ }
+
+ // FIXME: validate schema
+ const refundPermissions = resp.data;
+
if (!refundPermissions.length) {
console.warn("got empty refund list");
- return;
+ throw Error("empty refund");
}
const hc = refundPermissions[0].h_contract_terms;
if (!hc) {
@@ -2513,6 +2599,8 @@ export class Wallet {
// Start submitting it but don't wait for it here.
this.submitRefunds(hc);
+
+ return refundPermissions[0].h_contract_terms;
}
async submitRefunds(contractTermsHash: string): Promise<void> {
@@ -2646,6 +2734,54 @@ export class Wallet {
return planchetDetail;
}
+
+ async processTip(tipToken: TipToken): Promise<void> {
+ console.log("got tip token", tipToken);
+
+ const deadlineSec = getTalerStampSec(tipToken.expiration);
+ if (!deadlineSec) {
+ throw Error("tipping failed (invalid expiration)");
+ }
+
+ const merchantDomain = new URI(document.location.href).origin();
+ let walletResp;
+ walletResp = await this.getTipPlanchets(merchantDomain,
+ tipToken.tip_id,
+ tipToken.amount,
+ deadlineSec,
+ tipToken.exchange_url,
+ tipToken.next_url);
+
+ const planchets = walletResp;
+
+ if (!planchets) {
+ console.log("failed tip", walletResp);
+ throw Error("processing tip failed");
+ }
+
+ 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) {
+ console.log("tipping failed", e);
+ throw e;
+ }
+
+ try {
+ this.processTipResponse(merchantDomain, tipToken.tip_id, merchantResp.data);
+ } catch (e) {
+ console.log("processTipResponse failed", e);
+ throw e;
+ }
+
+ return;
+ }
+
/**
* Accept a merchant's response to a tip pickup and start withdrawing the coins.
* These coins will not appear in the wallet yet.
@@ -2725,6 +2861,11 @@ export class Wallet {
return tipStatus;
}
+
+ getNextUrlFromResourceUrl(resourceUrl: string): string | undefined {
+ return;
+ }
+
/**
* Remove unreferenced / expired data from the wallet's database
* based on the current system time.
@@ -2745,7 +2886,7 @@ export class Wallet {
};
await this.q().deleteIf(Stores.reserves, gcReserve).finish();
- const gcProposal = (d: ProposalRecord, n: number) => {
+ const gcProposal = (d: ProposalDownloadRecord, n: number) => {
// Delete proposal after 60 minutes or 5 minutes before pay deadline,
// whatever comes first.
const deadlinePayMilli = getTalerStampSec(d.contractTerms.pay_deadline)! * 1000;
diff --git a/src/walletTypes.ts b/src/walletTypes.ts
index 3c7bff1eb..d1a4f8746 100644
--- a/src/walletTypes.ts
+++ b/src/walletTypes.ts
@@ -246,9 +246,11 @@ export interface CheckPayResult {
/**
- * Possible results for confirmPay.
+ * Result for confirmPay
*/
-export type ConfirmPayResult = "paid" | "insufficient-balance";
+export interface ConfirmPayResult {
+ nextUrl: string;
+}
/**
@@ -299,6 +301,7 @@ export interface QueryPaymentFound {
found: true;
contractTermsHash: string;
contractTerms: ContractTerms;
+ lastSessionSig?: string;
payReq: PayReq;
}
@@ -329,7 +332,7 @@ export class CreateReserveRequest {
/**
* The initial amount for the reserve.
*/
- @Checkable.Value(AmountJson)
+ @Checkable.Value(() => AmountJson)
amount: AmountJson;
/**
@@ -380,7 +383,7 @@ export class ReturnCoinsRequest {
/**
* The amount to wire.
*/
- @Checkable.Value(AmountJson)
+ @Checkable.Value(() => AmountJson)
amount: AmountJson;
/**
@@ -511,7 +514,7 @@ export class ProcessTipResponseRequest {
/**
* Tip response from the merchant.
*/
- @Checkable.Value(TipResponse)
+ @Checkable.Value(() => TipResponse)
tipResponse: TipResponse;
/**
@@ -543,7 +546,7 @@ export class GetTipPlanchetsRequest {
/**
* Amount of the tip.
*/
- @Checkable.Optional(Checkable.Value(AmountJson))
+ @Checkable.Optional(Checkable.Value(() => AmountJson))
amount: AmountJson;
/**
diff --git a/src/webex/messages.ts b/src/webex/messages.ts
index 0d0329808..0fcd6047e 100644
--- a/src/webex/messages.ts
+++ b/src/webex/messages.ts
@@ -44,10 +44,6 @@ export interface MessageMap {
};
response: void;
};
- "get-tab-cookie": {
- request: { }
- response: any;
- };
"ping": {
request: { };
response: void;
@@ -67,12 +63,8 @@ export interface MessageMap {
request: { reservePub: string };
response: void;
};
- "generate-nonce": {
- request: { }
- response: string;
- };
"confirm-pay": {
- request: { proposalId: number; };
+ request: { proposalId: number; sessionId?: string };
response: walletTypes.ConfirmPayResult;
};
"check-pay": {
@@ -95,10 +87,6 @@ export interface MessageMap {
request: { contract: object };
response: string;
};
- "save-proposal": {
- request: { proposal: dbTypes.ProposalRecord };
- response: void;
- };
"reserve-creation-info": {
request: { baseUrl: string, amount: AmountJson };
response: walletTypes.ReserveCreationInfo;
@@ -109,7 +97,7 @@ export interface MessageMap {
};
"get-proposal": {
request: { proposalId: number };
- response: dbTypes.ProposalRecord | undefined;
+ response: dbTypes.ProposalDownloadRecord | undefined;
};
"get-coins": {
request: { exchangeBaseUrl: string };
@@ -155,14 +143,6 @@ export interface MessageMap {
request: { coinPub: string };
response: void;
};
- "payment-failed": {
- request: { contractTermsHash: string };
- response: void;
- };
- "payment-succeeded": {
- request: { contractTermsHash: string; merchantSig: string };
- response: void;
- };
"check-upgrade": {
request: { };
response: void;
@@ -183,10 +163,6 @@ export interface MessageMap {
request: { reportUid: string };
response: void;
};
- "accept-refund": {
- request: any;
- response: void;
- };
"get-purchase": {
request: any;
response: void;
@@ -215,6 +191,14 @@ export interface MessageMap {
request: { };
response: void;
};
+ "taler-pay": {
+ request: any;
+ response: void;
+ };
+ "download-proposal": {
+ request: any;
+ response: void;
+ };
}
/**
diff --git a/src/webex/notify.ts b/src/webex/notify.ts
index a7d393a65..e163a6272 100644
--- a/src/webex/notify.ts
+++ b/src/webex/notify.ts
@@ -28,13 +28,6 @@ import URI = require("urijs");
import wxApi = require("./wxApi");
-import { getTalerStampSec } from "../helpers";
-import { TipToken } from "../talerTypes";
-import { QueryPaymentResult } from "../walletTypes";
-
-
-import axios from "axios";
-
declare var cloneInto: any;
let logVerbose: boolean = false;
@@ -103,42 +96,6 @@ function setStyles(installed: boolean) {
}
-async function handlePaymentResponse(maybeFoundResponse: QueryPaymentResult) {
- if (!maybeFoundResponse.found) {
- console.log("pay-failed", {hint: "payment not found in the wallet"});
- return;
- }
- const walletResp = maybeFoundResponse;
-
- logVerbose && console.log("handling taler-notify-payment: ", walletResp);
- let resp;
- try {
- const config = {
- headers: { "Content-Type": "application/json;charset=UTF-8" },
- timeout: 5000, /* 5 seconds */
- validateStatus: (s: number) => s === 200,
- };
- resp = await axios.post(walletResp.contractTerms.pay_url, walletResp.payReq, config);
- } catch (e) {
- // Gives the user the option to retry / abort and refresh
- wxApi.logAndDisplayError({
- contractTerms: walletResp.contractTerms,
- message: e.message,
- name: "pay-post-failed",
- response: e.response,
- });
- throw e;
- }
- const merchantResp = resp.data;
- logVerbose && console.log("got success from pay_url");
- await wxApi.paymentSucceeded(walletResp.contractTermsHash, merchantResp.sig);
- const nextUrl = walletResp.contractTerms.fulfillment_url;
- logVerbose && console.log("taler-payment-succeeded done, going to", nextUrl);
- window.location.href = nextUrl;
- window.location.reload(true);
-}
-
-
function onceOnComplete(cb: () => void) {
if (document.readyState === "complete") {
cb();
@@ -153,234 +110,29 @@ function onceOnComplete(cb: () => void) {
function init() {
- // Only place where we don't use the nicer RPC wrapper, since the wallet
- // backend might not be ready (during install, upgrade, etc.)
- chrome.runtime.sendMessage({type: "get-tab-cookie"}, (resp) => {
- logVerbose && console.log("got response for get-tab-cookie");
- if (chrome.runtime.lastError) {
- logVerbose && console.log("extension not yet ready");
- window.setTimeout(init, 200);
- return;
- }
- onceOnComplete(() => {
- if (document.documentElement.getAttribute("data-taler-nojs")) {
- initStyle();
- setStyles(true);
- }
- });
- registerHandlers();
- // Hack to know when the extension is unloaded
- const port = chrome.runtime.connect();
-
- port.onDisconnect.addListener(() => {
- logVerbose && console.log("chrome runtime disconnected, removing handlers");
- if (document.documentElement.getAttribute("data-taler-nojs")) {
- setStyles(false);
- }
- for (const handler of handlers) {
- document.removeEventListener(handler.type, handler.listener);
- }
- });
-
- if (resp && resp.type === "pay") {
- logVerbose && console.log("doing taler.pay with", resp.payDetail);
- talerPay(resp.payDetail).then(handlePaymentResponse);
+ onceOnComplete(() => {
+ if (document.documentElement.getAttribute("data-taler-nojs")) {
+ initStyle();
+ setStyles(true);
}
});
-}
-
-type HandlerFn = (detail: any, sendResponse: (msg: any) => void) => void;
-
-async function downloadContract(url: string, nonce: string): Promise<any> {
- const parsed_url = new URI(url);
- url = parsed_url.setQuery({nonce}).href();
- console.log("downloading contract from '" + url + "'");
- let resp;
- try {
- resp = await axios.get(url, { validateStatus: (s) => s === 200 });
- } catch (e) {
- wxApi.logAndDisplayError({
- message: e.message,
- name: "contract-download-failed",
- response: e.response,
- sameTab: true,
- });
- throw e;
- }
- console.log("got response", resp);
- return resp.data;
-}
-
-async function processProposal(proposal: any) {
-
- if (!proposal.contract_terms) {
- console.error("field proposal.contract_terms field missing");
- return;
- }
-
- const contractHash = await wxApi.hashContract(proposal.contract_terms);
-
- const proposalId = await wxApi.saveProposal({
- contractTerms: proposal.contract_terms,
- contractTermsHash: contractHash,
- merchantSig: proposal.sig,
- timestamp: (new Date()).getTime(),
- });
-
- const uri = new URI(chrome.extension.getURL("/src/webex/pages/confirm-contract.html"));
- const params = {
- proposalId: proposalId.toString(),
- };
- const target = uri.query(params).href();
- document.location.replace(target);
-}
-
-
-/**
- * Handle a payment request (coming either from an HTTP 402 or
- * the JS wallet API).
- */
-function talerPay(msg: any): Promise<any> {
- // Use a promise directly instead of of an async
- // function since some paths never resolve the promise.
- return new Promise(async(resolve, reject) => {
- if (msg.tip) {
- const tipToken = TipToken.checked(JSON.parse(msg.tip));
-
- console.log("got tip token", tipToken);
-
- const deadlineSec = getTalerStampSec(tipToken.expiration);
- if (!deadlineSec) {
- wxApi.logAndDisplayError({
- message: "invalid expiration",
- name: "tipping-failed",
- sameTab: true,
- });
- return;
- }
-
- const merchantDomain = new URI(document.location.href).origin();
- let walletResp;
- try {
- walletResp = await wxApi.getTipPlanchets(merchantDomain,
- tipToken.tip_id,
- tipToken.amount,
- deadlineSec,
- tipToken.exchange_url,
- tipToken.next_url);
- } catch (e) {
- wxApi.logAndDisplayError({
- message: e.message,
- name: "tipping-failed",
- response: e.response,
- sameTab: true,
- });
- throw e;
- }
-
- const planchets = walletResp;
-
- if (!planchets) {
- wxApi.logAndDisplayError({
- detail: walletResp,
- message: "processing tip failed",
- name: "tipping-failed",
- sameTab: true,
- });
- return;
- }
-
- let merchantResp;
-
- try {
- const config = {
- validateStatus: (s: number) => s === 200,
- };
- const req = { planchets, tip_id: tipToken.tip_id };
- merchantResp = await axios.post(tipToken.pickup_url, req, config);
- } catch (e) {
- wxApi.logAndDisplayError({
- message: e.message,
- name: "tipping-failed",
- response: e.response,
- sameTab: true,
- });
- throw e;
- }
-
- try {
- wxApi.processTipResponse(merchantDomain, tipToken.tip_id, merchantResp.data);
- } catch (e) {
- wxApi.logAndDisplayError({
- message: e.message,
- name: "tipping-failed",
- response: e.response,
- sameTab: true,
- });
- throw e;
- }
-
- // Go to tip dialog page, where the user can confirm the tip or
- // decline if they are not happy with the exchange.
- const uri = new URI(chrome.extension.getURL("/src/webex/pages/tip.html"));
- const params = { tip_id: tipToken.tip_id, merchant_domain: merchantDomain };
- const redirectUrl = uri.query(params).href();
- window.location.href = redirectUrl;
-
- return;
- }
-
- if (msg.refund_url) {
- console.log("processing refund");
- let resp;
- try {
- const config = {
- validateStatus: (s: number) => s === 200,
- };
- resp = await axios.get(msg.refund_url, config);
- } catch (e) {
- wxApi.logAndDisplayError({
- message: e.message,
- name: "refund-download-failed",
- response: e.response,
- sameTab: true,
- });
- throw e;
- }
- await wxApi.acceptRefund(resp.data);
- const hc = resp.data.refund_permissions[0].h_contract_terms;
- document.location.href = chrome.extension.getURL(`/src/webex/pages/refund.html?contractTermsHash=${hc}`);
- return;
- }
-
- // current URL without fragment
- const url = new URI(document.location.href).fragment("").href();
- const res = await wxApi.queryPayment(url);
- logVerbose && console.log("taler-pay: got response", res);
- if (res && res.found && res.payReq) {
- resolve(res);
- return;
+ registerHandlers();
+ // Hack to know when the extension is unloaded
+ const port = chrome.runtime.connect();
+
+ port.onDisconnect.addListener(() => {
+ logVerbose && console.log("chrome runtime disconnected, removing handlers");
+ if (document.documentElement.getAttribute("data-taler-nojs")) {
+ setStyles(false);
}
- if (msg.contract_url) {
- const nonce = await wxApi.generateNonce();
- const proposal = await downloadContract(msg.contract_url, nonce);
- if (proposal.contract_terms.nonce !== nonce) {
- console.error("stale contract");
- return;
- }
- await processProposal(proposal);
- return;
- }
-
- if (msg.offer_url) {
- document.location.href = msg.offer_url;
- return;
+ for (const handler of handlers) {
+ document.removeEventListener(handler.type, handler.listener);
}
-
- console.log("can't proceed with payment, no way to get contract specified");
});
}
+type HandlerFn = (detail: any, sendResponse: (msg: any) => void) => void;
+
function registerHandlers() {
/**
@@ -457,7 +209,7 @@ function registerHandlers() {
});
addHandler("taler-pay", async(msg: any, sendResponse: any) => {
- const resp = await talerPay(msg);
+ const resp = await wxApi.talerPay(msg);
sendResponse(resp);
});
}
diff --git a/src/webex/pages/confirm-contract.tsx b/src/webex/pages/confirm-contract.tsx
index 83de738b9..090737475 100644
--- a/src/webex/pages/confirm-contract.tsx
+++ b/src/webex/pages/confirm-contract.tsx
@@ -27,7 +27,7 @@ import * as i18n from "../../i18n";
import {
ExchangeRecord,
- ProposalRecord,
+ ProposalDownloadRecord,
} from "../../dbTypes";
import { ContractTerms } from "../../talerTypes";
import {
@@ -102,12 +102,15 @@ class Details extends React.Component<DetailProps, DetailState> {
}
interface ContractPromptProps {
- proposalId: number;
+ proposalId?: number;
+ contractUrl?: string;
+ sessionId?: string;
}
interface ContractPromptState {
- proposal: ProposalRecord|null;
- error: string|null;
+ proposalId: number | undefined;
+ proposal: ProposalDownloadRecord | null;
+ error: string | null;
payDisabled: boolean;
alreadyPaid: boolean;
exchanges: null|ExchangeRecord[];
@@ -130,6 +133,7 @@ class ContractPrompt extends React.Component<ContractPromptProps, ContractPrompt
holdCheck: false,
payDisabled: true,
proposal: null,
+ proposalId: props.proposalId,
};
}
@@ -142,11 +146,19 @@ class ContractPrompt extends React.Component<ContractPromptProps, ContractPrompt
}
async update() {
- const proposal = await wxApi.getProposal(this.props.proposalId);
- this.setState({proposal} as any);
+ let proposalId = this.props.proposalId;
+ if (proposalId === undefined) {
+ if (this.props.contractUrl === undefined) {
+ // Nothing we can do ...
+ return;
+ }
+ proposalId = await wxApi.downloadProposal(this.props.contractUrl);
+ }
+ const proposal = await wxApi.getProposal(proposalId);
+ this.setState({ proposal, proposalId });
this.checkPayment();
const exchanges = await wxApi.getExchanges();
- this.setState({exchanges} as any);
+ this.setState({ exchanges });
}
async checkPayment() {
@@ -154,7 +166,11 @@ class ContractPrompt extends React.Component<ContractPromptProps, ContractPrompt
if (this.state.holdCheck) {
return;
}
- const payStatus = await wxApi.checkPay(this.props.proposalId);
+ const proposalId = this.state.proposalId;
+ if (proposalId === undefined) {
+ return;
+ }
+ const payStatus = await wxApi.checkPay(proposalId);
if (payStatus.status === "insufficient-balance") {
const msgInsufficient = i18n.str`You have insufficient funds of the requested currency in your wallet.`;
// tslint:disable-next-line:max-line-length
@@ -163,18 +179,18 @@ class ContractPrompt extends React.Component<ContractPromptProps, ContractPrompt
const acceptedExchangePubs = this.state.proposal.contractTerms.exchanges.map((e) => e.master_pub);
const ex = this.state.exchanges.find((e) => acceptedExchangePubs.indexOf(e.masterPublicKey) >= 0);
if (ex) {
- this.setState({error: msgInsufficient});
+ this.setState({ error: msgInsufficient });
} else {
- this.setState({error: msgNoMatch});
+ this.setState({ error: msgNoMatch });
}
} else {
- this.setState({error: msgInsufficient});
+ this.setState({ error: msgInsufficient });
}
- this.setState({payDisabled: true});
+ this.setState({ payDisabled: true });
} else if (payStatus.status === "paid") {
- this.setState({alreadyPaid: true, payDisabled: false, error: null, payStatus});
+ this.setState({ alreadyPaid: true, payDisabled: false, error: null, payStatus });
} else {
- this.setState({payDisabled: false, error: null, payStatus});
+ this.setState({ payDisabled: false, error: null, payStatus });
}
}
@@ -184,21 +200,24 @@ class ContractPrompt extends React.Component<ContractPromptProps, ContractPrompt
if (!proposal) {
return;
}
- const payStatus = await wxApi.confirmPay(this.props.proposalId);
- switch (payStatus) {
- case "insufficient-balance":
- this.checkPayment();
- return;
- case "paid":
- console.log("contract", proposal.contractTerms);
- document.location.href = proposal.contractTerms.fulfillment_url;
- break;
+ const proposalId = proposal.id;
+ if (proposalId === undefined) {
+ console.error("proposal has no id");
+ return;
}
- this.setState({holdCheck: true});
+ const payResult = await wxApi.confirmPay(proposalId, this.props.sessionId);
+ document.location.href = payResult.nextUrl;
+ this.setState({ holdCheck: true });
}
render() {
+ if (this.props.contractUrl === undefined && this.props.proposalId === undefined) {
+ return <span>Error: either contractUrl or proposalId must be given</span>;
+ }
+ if (this.state.proposalId === undefined) {
+ return <span>Downloading contract terms</span>;
+ }
if (!this.state.proposal) {
return <span>...</span>;
}
@@ -255,8 +274,18 @@ class ContractPrompt extends React.Component<ContractPromptProps, ContractPrompt
document.addEventListener("DOMContentLoaded", () => {
const url = new URI(document.location.href);
const query: any = URI.parseQuery(url.query());
- const proposalId = JSON.parse(query.proposalId);
- ReactDOM.render(<ContractPrompt proposalId={proposalId}/>, document.getElementById(
- "contract")!);
+ let proposalId;
+ try {
+ proposalId = JSON.parse(query.proposalId);
+ } catch {
+ // ignore error
+ }
+
+ const sessionId = query.sessionId;
+ const contractUrl = query.contractUrl;
+
+ ReactDOM.render(
+ <ContractPrompt {...{ proposalId, contractUrl, sessionId }}/>,
+ document.getElementById("contract")!);
});
diff --git a/src/webex/wxApi.ts b/src/webex/wxApi.ts
index 2f7a13c48..efebf21d1 100644
--- a/src/webex/wxApi.ts
+++ b/src/webex/wxApi.ts
@@ -217,8 +217,8 @@ export function checkPay(proposalId: number): Promise<CheckPayResult> {
/**
* Pay for a proposal.
*/
-export function confirmPay(proposalId: number): Promise<ConfirmPayResult> {
- return callBackend("confirm-pay", { proposalId });
+export function confirmPay(proposalId: number, sessionId: string | undefined): Promise<ConfirmPayResult> {
+ return callBackend("confirm-pay", { proposalId, sessionId });
}
/**
@@ -228,15 +228,6 @@ export function hashContract(contract: object): Promise<string> {
return callBackend("hash-contract", { contract });
}
-
-/**
- * Save a proposal in the wallet. Returns the proposal id that
- * the proposal is stored under.
- */
-export function saveProposal(proposal: any): Promise<number> {
- return callBackend("save-proposal", { proposal });
-}
-
/**
* Mark a reserve as confirmed.
*/
@@ -252,36 +243,6 @@ export function queryPayment(url: string): Promise<QueryPaymentResult> {
}
/**
- * Mark a payment as succeeded.
- */
-export function paymentSucceeded(contractTermsHash: string, merchantSig: string): Promise<void> {
- return callBackend("payment-succeeded", { contractTermsHash, merchantSig });
-}
-
-/**
- * Mark a payment as succeeded.
- */
-export function paymentFailed(contractTermsHash: string): Promise<void> {
- return callBackend("payment-failed", { contractTermsHash });
-}
-
-/**
- * Get the payment cookie for the current tab, or undefined if no payment
- * cookie was set.
- */
-export function getTabCookie(): Promise<any> {
- return callBackend("get-tab-cookie", { });
-}
-
-/**
- * Generate a contract nonce (EdDSA key pair), store it in the wallet's
- * database and return the public key.
- */
-export function generateNonce(): Promise<string> {
- return callBackend("generate-nonce", { });
-}
-
-/**
* Check upgrade information
*/
export function checkUpgrade(): Promise<UpgradeResponse> {
@@ -344,12 +305,6 @@ export function getReport(reportUid: string): Promise<any> {
return callBackend("get-report", { reportUid });
}
-/**
- * Apply a refund that we got from the merchant.
- */
-export function acceptRefund(refundData: any): Promise<number> {
- return callBackend("accept-refund", refundData);
-}
/**
* Look up a purchase in the wallet database from
@@ -407,3 +362,17 @@ export function processTipResponse(merchantDomain: string, tipId: string, tipRes
export function clearNotification(): Promise<void> {
return callBackend("clear-notification", { });
}
+
+/**
+ * Trigger taler payment processing (for payment, tipping and refunds).
+ */
+export function talerPay(msg: any): Promise<void> {
+ return callBackend("taler-pay", msg);
+}
+
+/**
+ * Download a contract.
+ */
+export function downloadProposal(url: string): Promise<number> {
+ return callBackend("download-proposal", { url });
+}
diff --git a/src/webex/wxBackend.ts b/src/webex/wxBackend.ts
index 02a1543e5..c0b42a768 100644
--- a/src/webex/wxBackend.ts
+++ b/src/webex/wxBackend.ts
@@ -33,7 +33,6 @@ import {
import { AmountJson } from "../amounts";
-import { ProposalRecord } from "../dbTypes";
import {
AcceptTipRequest,
ConfirmReserveRequest,
@@ -41,6 +40,7 @@ import {
GetTipPlanchetsRequest,
Notifier,
ProcessTipResponseRequest,
+ QueryPaymentFound,
ReturnCoinsRequest,
TipStatusRequest,
} from "../walletTypes";
@@ -62,6 +62,7 @@ import * as wxApi from "./wxApi";
import URI = require("urijs");
import Port = chrome.runtime.Port;
import MessageSender = chrome.runtime.MessageSender;
+import { TipToken } from "../talerTypes";
const DB_NAME = "taler";
@@ -93,15 +94,6 @@ function handleMessage(sender: MessageSender,
const db = needsWallet().db;
return importDb(db, detail.dump);
}
- case "get-tab-cookie": {
- if (!sender || !sender.tab || !sender.tab.id) {
- return Promise.resolve();
- }
- const id: number = sender.tab.id;
- const info: any = paymentRequestCookies[id] as any;
- delete paymentRequestCookies[id];
- return Promise.resolve(info);
- }
case "ping": {
return Promise.resolve();
}
@@ -138,14 +130,11 @@ function handleMessage(sender: MessageSender,
const req = ConfirmReserveRequest.checked(d);
return needsWallet().confirmReserve(req);
}
- case "generate-nonce": {
- return needsWallet().generateNonce();
- }
case "confirm-pay": {
if (typeof detail.proposalId !== "number") {
throw Error("proposalId must be number");
}
- return needsWallet().confirmPay(detail.proposalId);
+ return needsWallet().confirmPay(detail.proposalId, detail.sessionId);
}
case "check-pay": {
if (typeof detail.proposalId !== "number") {
@@ -166,7 +155,7 @@ function handleMessage(sender: MessageSender,
return Promise.resolve(msg);
}
}
- return needsWallet().queryPayment(detail.url);
+ return needsWallet().queryPaymentByFulfillmentUrl(detail.url);
}
case "exchange-info": {
if (!detail.baseUrl) {
@@ -188,11 +177,6 @@ function handleMessage(sender: MessageSender,
return hash;
});
}
- case "save-proposal": {
- console.log("handling save-proposal", detail);
- const checkedRecord = ProposalRecord.checked(detail.proposal);
- return needsWallet().saveProposal(checkedRecord);
- }
case "reserve-creation-info": {
if (!detail.baseUrl || typeof detail.baseUrl !== "string") {
return Promise.resolve({ error: "bad url" });
@@ -261,25 +245,6 @@ function handleMessage(sender: MessageSender,
}
return needsWallet().payback(detail.coinPub);
}
- case "payment-failed": {
- // For now we just update exchanges (maybe the exchange did something
- // wrong and the keys were messed up).
- // FIXME: in the future we should look at what actually went wrong.
- console.error("payment reported as failed");
- needsWallet().updateExchanges();
- return Promise.resolve();
- }
- case "payment-succeeded": {
- const contractTermsHash = detail.contractTermsHash;
- const merchantSig = detail.merchantSig;
- if (!contractTermsHash) {
- return Promise.reject(Error("contractHash missing"));
- }
- if (!merchantSig) {
- return Promise.reject(Error("merchantSig missing"));
- }
- return needsWallet().paymentSucceeded(contractTermsHash, merchantSig);
- }
case "get-sender-wire-infos": {
return needsWallet().getSenderWireInfos();
}
@@ -316,8 +281,6 @@ function handleMessage(sender: MessageSender,
return;
case "get-report":
return logging.getReport(detail.reportUid);
- case "accept-refund":
- return needsWallet().acceptRefund(detail.refund_permissions);
case "get-purchase": {
const contractTermsHash = detail.contractTermsHash;
if (!contractTermsHash) {
@@ -351,6 +314,28 @@ function handleMessage(sender: MessageSender,
case "clear-notification": {
return needsWallet().clearNotification();
}
+ case "download-proposal": {
+ return needsWallet().downloadProposal(detail.url);
+ }
+ case "taler-pay": {
+ const senderUrl = sender.url;
+ if (!senderUrl) {
+ console.log("can't trigger payment, no sender URL");
+ return;
+ }
+ const tab = sender.tab;
+ if (!tab) {
+ console.log("can't trigger payment, no sender tab");
+ return;
+ }
+ const tabId = tab.id;
+ if (typeof tabId !== "string") {
+ console.log("can't trigger payment, no sender tab id");
+ return;
+ }
+ talerPay(detail, senderUrl, tabId);
+ return;
+ }
default:
// Exhaustiveness check.
// See https://www.typescriptlang.org/docs/handbook/advanced-types.html
@@ -417,13 +402,67 @@ class ChromeNotifier implements Notifier {
}
-/**
- * Mapping from tab ID to payment information (if any).
- *
- * Used to pass information from an intercepted HTTP header to the content
- * script on the page.
- */
-const paymentRequestCookies: { [n: number]: any } = {};
+async function talerPay(fields: any, url: string, tabId: number): Promise<string | undefined> {
+ if (!currentWallet) {
+ console.log("can't handle payment, no wallet");
+ return undefined;
+ }
+
+ const w = currentWallet;
+
+ const goToPayment = (p: QueryPaymentFound): string => {
+ const nextUrl = new URI(p.contractTerms.fulfillment_url);
+ nextUrl.addSearch("order_id", p.contractTerms.order_id);
+ if (p.lastSessionSig) {
+ nextUrl.addSearch("session_sig", p.lastSessionSig);
+ }
+ return url;
+ };
+
+ if (fields.resource_url) {
+ const p = await w.queryPaymentByFulfillmentUrl(fields.resource_url);
+ if (p.found) {
+ return goToPayment(p);
+ }
+ }
+ if (fields.contract_hash) {
+ const p = await w.queryPaymentByContractTermsHash(fields.contract_hash);
+ if (p.found) {
+ goToPayment(p);
+ return goToPayment(p);
+ }
+ }
+ if (fields.contract_url) {
+ const proposalId = await w.downloadProposal(fields.contract_url);
+ const uri = new URI(chrome.extension.getURL("/src/webex/pages/confirm-contract.html"));
+ if (fields.session_id) {
+ uri.addSearch("sessionId", fields.session_id);
+ }
+ uri.addSearch("proposalId", proposalId);
+ const redirectUrl = uri.href();
+ return redirectUrl;
+ }
+ if (fields.offer_url) {
+ return fields.offer_url;
+ }
+ if (fields.refund_url) {
+ console.log("processing refund");
+ const hc = await w.acceptRefund(fields.refund_url);
+ return chrome.extension.getURL(`/src/webex/pages/refund.html?contractTermsHash=${hc}`);
+ }
+ if (fields.tip) {
+ const tipToken = TipToken.checked(fields.tip);
+ w.processTip(tipToken);
+ // Go to tip dialog page, where the user can confirm the tip or
+ // decline if they are not happy with the exchange.
+ const merchantDomain = new URI(url).origin();
+ 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();
+ return redirectUrl;
+ }
+ return undefined;
+}
/**
@@ -433,6 +472,11 @@ const paymentRequestCookies: { [n: number]: any } = {};
* in this tab.
*/
function handleHttpPayment(headerList: chrome.webRequest.HttpHeader[], url: string, tabId: number): any {
+ if (!currentWallet) {
+ console.log("can't handle payment, no wallet");
+ return;
+ }
+
const headers: { [s: string]: string } = {};
for (const kv of headerList) {
if (kv.value) {
@@ -441,9 +485,12 @@ function handleHttpPayment(headerList: chrome.webRequest.HttpHeader[], url: stri
}
const fields = {
+ contract_hash: headers["x-taler-contract-hash"],
contract_url: headers["x-taler-contract-url"],
offer_url: headers["x-taler-offer-url"],
refund_url: headers["x-taler-refund-url"],
+ resource_url: headers["x-taler-resource-url"],
+ session_id: headers["x-taler-session-id"],
tip: headers["x-taler-tip"],
};
@@ -456,21 +503,33 @@ function handleHttpPayment(headerList: chrome.webRequest.HttpHeader[], url: stri
return;
}
- const payDetail = {
- contract_url: fields.contract_url,
- offer_url: fields.offer_url,
- refund_url: fields.refund_url,
- tip: fields.tip,
- };
+ console.log("got pay detail", fields);
- console.log("got pay detail", payDetail);
+ // Fast path for existing payment
+ if (fields.resource_url) {
+ const nextUrl = currentWallet.getNextUrlFromResourceUrl(fields.resource_url);
+ if (nextUrl) {
+ return { redirectUrl: nextUrl };
+ }
+ }
+ // Fast path for new contract
+ if (!fields.contract_hash && fields.contract_url) {
+ const uri = new URI(chrome.extension.getURL("/src/webex/pages/confirm-contract.html"));
+ uri.addSearch("contractUrl", fields.contract_url);
+ if (fields.session_id) {
+ uri.addSearch("sessionId", fields.session_id);
+ }
+ return { redirectUrl: uri.href() };
+ }
- // This cookie will be read by the injected content script
- // in the tab that displays the page.
- paymentRequestCookies[tabId] = {
- payDetail,
- type: "pay",
- };
+ // We need to do some asynchronous operation, we can't directly redirect
+ talerPay(fields, url, tabId).then((nextUrl) => {
+ if (nextUrl) {
+ chrome.tabs.update(tabId, { url: nextUrl });
+ }
+ });
+
+ return;
}
@@ -541,7 +600,7 @@ function handleBankRequest(wallet: Wallet, headerList: chrome.webRequest.HttpHea
const redirectUrl = uri.query(params).href();
console.log("redirecting to", redirectUrl);
// FIXME: use direct redirect when https://bugzilla.mozilla.org/show_bug.cgi?id=707624 is fixed
- chrome.tabs.update(tabId, {url: redirectUrl});
+ chrome.tabs.update(tabId, { url: redirectUrl });
return;
}