aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFlorian Dold <florian.dold@gmail.com>2019-08-30 17:27:59 +0200
committerFlorian Dold <florian.dold@gmail.com>2019-08-30 17:27:59 +0200
commit5ec344290efd937fa82c0704bc7c204a0bf14c78 (patch)
tree7d9594180bbc7b5fa2b4a8dbe24272e7a82301f3
parentdefbf625bdef0f8a666b72b8ce99de5e01af6b91 (diff)
support for tipping protocol changes
-rw-r--r--src/dbTypes.ts175
-rw-r--r--src/headless/taler-wallet-cli.ts18
-rw-r--r--src/talerTypes.ts122
-rw-r--r--src/taleruri.ts51
-rw-r--r--src/wallet.ts258
-rw-r--r--src/walletTypes.ts8
-rw-r--r--src/webex/messages.ts6
-rw-r--r--src/webex/pages/tip.tsx203
-rw-r--r--src/webex/wxApi.ts11
-rw-r--r--src/webex/wxBackend.ts11
10 files changed, 441 insertions, 422 deletions
diff --git a/src/dbTypes.ts b/src/dbTypes.ts
index d9fd2e9d9..17e7a89b7 100644
--- a/src/dbTypes.ts
+++ b/src/dbTypes.ts
@@ -14,7 +14,6 @@
TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-
/**
* Types for records stored in the wallet's database.
*
@@ -36,11 +35,7 @@ import {
TipResponse,
} from "./talerTypes";
-import {
- Index,
- Store,
-} from "./query";
-
+import { Index, Store } from "./query";
/**
* Current database version, should be incremented
@@ -50,7 +45,6 @@ import {
*/
export const WALLET_DB_VERSION = 26;
-
/**
* A reserve record as stored in the wallet's database.
*/
@@ -81,12 +75,11 @@ export interface ReserveRecord {
*/
timestamp_depleted: number;
-
/**
* Time when the information about this reserve was posted to the bank.
- *
+ *
* Only applies if bankWithdrawStatusUrl is defined.
- *
+ *
* Set to 0 if that hasn't happened yet.
*/
timestamp_reserve_info_posted: number;
@@ -137,7 +130,6 @@ export interface ReserveRecord {
bankWithdrawStatusUrl?: string;
}
-
/**
* Auditor record as stored with currencies in the exchange database.
*/
@@ -156,7 +148,6 @@ export interface AuditorRecord {
expirationStamp: number;
}
-
/**
* Exchange for currencies as stored in the wallet's currency
* information database.
@@ -172,7 +163,6 @@ export interface ExchangeForCurrencyRecord {
baseUrl: string;
}
-
/**
* Information about a currency as displayed in the wallet's database.
*/
@@ -195,7 +185,6 @@ export interface CurrencyRecord {
exchanges: ExchangeForCurrencyRecord[];
}
-
/**
* Status of a denomination.
*/
@@ -214,7 +203,6 @@ export enum DenominationStatus {
VerifiedBad,
}
-
/**
* Denomination record as stored in the wallet's database.
*/
@@ -321,7 +309,6 @@ export class DenominationRecord {
static checked: (obj: any) => Denomination;
}
-
/**
* Exchange record as stored in the wallet's database.
*/
@@ -362,7 +349,6 @@ export interface ExchangeRecord {
protocolVersion?: string;
}
-
/**
* A coin that isn't yet signed by an exchange.
*/
@@ -385,7 +371,6 @@ export interface PreCoinRecord {
isFromTip: boolean;
}
-
/**
* Planchet for a coin during refrehs.
*/
@@ -408,7 +393,6 @@ export interface RefreshPreCoinRecord {
blindingKey: string;
}
-
/**
* Status of a coin.
*/
@@ -441,13 +425,8 @@ export enum CoinStatus {
* Coin was dirty but can't be refreshed.
*/
Useless,
- /**
- * The coin was withdrawn for a tip that the user hasn't accepted yet.
- */
- TainedByTip,
}
-
/**
* CoinRecord as stored in the "coins" data store
* of the wallet database.
@@ -506,7 +485,7 @@ export interface CoinRecord {
* Reserve public key for the reserve we got this coin from,
* or zero when we got the coin from refresh.
*/
- reservePub: string|undefined;
+ reservePub: string | undefined;
/**
* Status of the coin.
@@ -514,7 +493,6 @@ export interface CoinRecord {
status: CoinStatus;
}
-
/**
* Proposal record, stored in the wallet's database.
*/
@@ -576,7 +554,6 @@ export class ProposalDownloadRecord {
static checked: (obj: any) => ProposalDownloadRecord;
}
-
/**
* Wire fees for an exchange.
*/
@@ -592,7 +569,6 @@ export interface ExchangeWireFeesRecord {
feesForType: { [wireMethod: string]: WireFee[] };
}
-
/**
* Status of a tip we got from a merchant.
*/
@@ -613,12 +589,7 @@ export interface TipRecord {
*/
amount: AmountJson;
- /**
- * Coin public keys from the planchets.
- * This field is redundant and used for indexing the record via
- * a multi-entry index to look up tip records by coin public key.
- */
- coinPubs: string[];
+ totalFees: AmountJson;
/**
* Timestamp, the tip can't be picked up anymore after this deadline.
@@ -641,7 +612,14 @@ export interface TipRecord {
* Planchets, the members included in TipPlanchetDetail will be sent to the
* merchant.
*/
- planchets: TipPlanchet[];
+ planchets?: TipPlanchet[];
+
+ /**
+ * Coin public keys from the planchets.
+ * This field is redundant and used for indexing the record via
+ * a multi-entry index to look up tip records by coin public key.
+ */
+ coinPubs: string[];
/**
* Response if the merchant responded,
@@ -657,11 +635,12 @@ export interface TipRecord {
/**
* URL to go to once the tip has been accepted.
*/
- nextUrl: string;
+ nextUrl?: string;
timestamp: number;
-}
+ pickupUrl: string;
+}
/**
* Ongoing refresh
@@ -740,7 +719,6 @@ export interface RefreshSessionRecord {
id?: number;
}
-
/**
* Tipping planchet stored in the database.
*/
@@ -754,7 +732,6 @@ export interface TipPlanchet {
denomPub: string;
}
-
/**
* Wire fee for one wire method as stored in the
* wallet's database.
@@ -861,7 +838,6 @@ export interface PurchaseRecord {
abortDone: boolean;
}
-
/**
* Information about wire information for bank accounts we withdrew coins from.
*/
@@ -869,7 +845,6 @@ export interface SenderWireRecord {
paytoUri: string;
}
-
/**
* Configuration key/value entries to configure
* the wallet.
@@ -879,7 +854,6 @@ export interface ConfigRecord {
value: any;
}
-
/**
* Coin that we're depositing ourselves.
*/
@@ -893,7 +867,6 @@ export interface DepositCoin {
depositedSig?: string;
}
-
/**
* Record stored in the wallet's database when the user sends coins back to
* their own bank account. Stores the status of coins that are deposited to
@@ -927,7 +900,6 @@ export interface CoinsReturnRecord {
wire: any;
}
-
/* tslint:disable:completed-docs */
/**
@@ -939,7 +911,11 @@ export namespace Stores {
super("exchanges", { keyPath: "baseUrl" });
}
- pubKeyIndex = new Index<string, ExchangeRecord>(this, "pubKeyIndex", "masterPublicKey");
+ pubKeyIndex = new Index<string, ExchangeRecord>(
+ this,
+ "pubKeyIndex",
+ "masterPublicKey",
+ );
}
class CoinsStore extends Store<CoinRecord> {
@@ -947,8 +923,16 @@ export namespace Stores {
super("coins", { keyPath: "coinPub" });
}
- exchangeBaseUrlIndex = new Index<string, CoinRecord>(this, "exchangeBaseUrl", "exchangeBaseUrl");
- denomPubIndex = new Index<string, CoinRecord>(this, "denomPubIndex", "denomPub");
+ exchangeBaseUrlIndex = new Index<string, CoinRecord>(
+ this,
+ "exchangeBaseUrl",
+ "exchangeBaseUrl",
+ );
+ denomPubIndex = new Index<string, CoinRecord>(
+ this,
+ "denomPubIndex",
+ "denomPub",
+ );
}
class ProposalsStore extends Store<ProposalDownloadRecord> {
@@ -958,8 +942,16 @@ export namespace Stores {
keyPath: "id",
});
}
- urlIndex = new Index<string, ProposalDownloadRecord>(this, "urlIndex", "url");
- timestampIndex = new Index<string, ProposalDownloadRecord>(this, "timestampIndex", "timestamp");
+ urlIndex = new Index<string, ProposalDownloadRecord>(
+ this,
+ "urlIndex",
+ "url",
+ );
+ timestampIndex = new Index<string, ProposalDownloadRecord>(
+ this,
+ "timestampIndex",
+ "timestamp",
+ );
}
class PurchasesStore extends Store<PurchaseRecord> {
@@ -967,23 +959,46 @@ export namespace Stores {
super("purchases", { keyPath: "contractTermsHash" });
}
- fulfillmentUrlIndex = new Index<string, PurchaseRecord>(this,
- "fulfillmentUrlIndex",
- "contractTerms.fulfillment_url");
- orderIdIndex = new Index<string, PurchaseRecord>(this, "orderIdIndex", "contractTerms.order_id");
- timestampIndex = new Index<string, PurchaseRecord>(this, "timestampIndex", "timestamp");
+ fulfillmentUrlIndex = new Index<string, PurchaseRecord>(
+ this,
+ "fulfillmentUrlIndex",
+ "contractTerms.fulfillment_url",
+ );
+ orderIdIndex = new Index<string, PurchaseRecord>(
+ this,
+ "orderIdIndex",
+ "contractTerms.order_id",
+ );
+ timestampIndex = new Index<string, PurchaseRecord>(
+ this,
+ "timestampIndex",
+ "timestamp",
+ );
}
class DenominationsStore extends Store<DenominationRecord> {
constructor() {
// cast needed because of bug in type annotations
- super("denominations",
- {keyPath: ["exchangeBaseUrl", "denomPub"] as any as IDBKeyPath});
+ super("denominations", {
+ keyPath: (["exchangeBaseUrl", "denomPub"] as any) as IDBKeyPath,
+ });
}
- denomPubHashIndex = new Index<string, DenominationRecord>(this, "denomPubHashIndex", "denomPubHash");
- exchangeBaseUrlIndex = new Index<string, DenominationRecord>(this, "exchangeBaseUrlIndex", "exchangeBaseUrl");
- denomPubIndex = new Index<string, DenominationRecord>(this, "denomPubIndex", "denomPub");
+ denomPubHashIndex = new Index<string, DenominationRecord>(
+ this,
+ "denomPubHashIndex",
+ "denomPubHash",
+ );
+ exchangeBaseUrlIndex = new Index<string, DenominationRecord>(
+ this,
+ "exchangeBaseUrlIndex",
+ "exchangeBaseUrl",
+ );
+ denomPubIndex = new Index<string, DenominationRecord>(
+ this,
+ "denomPubIndex",
+ "denomPub",
+ );
}
class CurrenciesStore extends Store<CurrencyRecord> {
@@ -1008,16 +1023,35 @@ export namespace Stores {
constructor() {
super("reserves", { keyPath: "reserve_pub" });
}
- timestampCreatedIndex = new Index<string, ReserveRecord>(this, "timestampCreatedIndex", "created");
- timestampConfirmedIndex = new Index<string, ReserveRecord>(this, "timestampConfirmedIndex", "timestamp_confirmed");
- timestampDepletedIndex = new Index<string, ReserveRecord>(this, "timestampDepletedIndex", "timestamp_depleted");
+ timestampCreatedIndex = new Index<string, ReserveRecord>(
+ this,
+ "timestampCreatedIndex",
+ "created",
+ );
+ timestampConfirmedIndex = new Index<string, ReserveRecord>(
+ this,
+ "timestampConfirmedIndex",
+ "timestamp_confirmed",
+ );
+ timestampDepletedIndex = new Index<string, ReserveRecord>(
+ this,
+ "timestampDepletedIndex",
+ "timestamp_depleted",
+ );
}
class TipsStore extends Store<TipRecord> {
constructor() {
- super("tips", { keyPath: ["tipId", "merchantDomain"] as any as IDBKeyPath });
+ super("tips", {
+ keyPath: (["tipId", "merchantDomain"] as any) as IDBKeyPath,
+ });
}
- coinPubIndex = new Index<string, TipRecord>(this, "coinPubIndex", "coinPubs", { multiEntry: true });
+ coinPubIndex = new Index<string, TipRecord>(
+ this,
+ "coinPubIndex",
+ "coinPubs",
+ { multiEntry: true },
+ );
}
class SenderWiresStore extends Store<SenderWireRecord> {
@@ -1027,15 +1061,22 @@ export namespace Stores {
}
export const coins = new CoinsStore();
- export const coinsReturns = new Store<CoinsReturnRecord>("coinsReturns", {keyPath: "contractTermsHash"});
+ export const coinsReturns = new Store<CoinsReturnRecord>("coinsReturns", {
+ keyPath: "contractTermsHash",
+ });
export const config = new ConfigStore();
export const currencies = new CurrenciesStore();
export const denominations = new DenominationsStore();
export const exchangeWireFees = new ExchangeWireFeesStore();
export const exchanges = new ExchangeStore();
- export const precoins = new Store<PreCoinRecord>("precoins", {keyPath: "coinPub"});
+ 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});
+ export const refresh = new Store<RefreshSessionRecord>("refresh", {
+ keyPath: "id",
+ autoIncrement: true,
+ });
export const reserves = new ReservesStore();
export const purchases = new PurchasesStore();
export const tips = new TipsStore();
diff --git a/src/headless/taler-wallet-cli.ts b/src/headless/taler-wallet-cli.ts
index 659cffe67..86eaec64c 100644
--- a/src/headless/taler-wallet-cli.ts
+++ b/src/headless/taler-wallet-cli.ts
@@ -127,7 +127,7 @@ program
});
program
- .command("withdraw-url <withdraw-url>")
+ .command("withdraw-uri <withdraw-uri>")
.action(async (withdrawUrl, cmdObj) => {
applyVerbose(program.verbose);
console.log("withdrawing", withdrawUrl);
@@ -166,7 +166,21 @@ program
});
program
- .command("pay-url <pay-url>")
+ .command("tip-uri <tip-uri>")
+ .action(async (tipUri, cmdObj) => {
+ applyVerbose(program.verbose);
+ console.log("getting tip", tipUri);
+ const wallet = await getDefaultNodeWallet({
+ persistentStoragePath: walletDbPath,
+ });
+ const res = await wallet.getTipStatus(tipUri);
+ console.log("tip status", res);
+ await wallet.acceptTip(tipUri);
+ wallet.stop();
+ });
+
+program
+ .command("pay-uri <pay-uri")
.option("-y, --yes", "automatically answer yes to prompts")
.action(async (payUrl, cmdObj) => {
applyVerbose(program.verbose);
diff --git a/src/talerTypes.ts b/src/talerTypes.ts
index 360be3338..73b97c93d 100644
--- a/src/talerTypes.ts
+++ b/src/talerTypes.ts
@@ -32,7 +32,6 @@ import * as Amounts from "./amounts";
import { timestampCheck } from "./helpers";
-
/**
* Denomination as found in the /keys response from the exchange.
*/
@@ -114,7 +113,6 @@ export class Denomination {
static checked: (obj: any) => Denomination;
}
-
/**
* Signature by the auditor that a particular denomination key is audited.
*/
@@ -133,7 +131,6 @@ export class AuditorDenomSig {
auditor_sig: string;
}
-
/**
* Auditor information as given by the exchange in /keys.
*/
@@ -158,7 +155,6 @@ export class Auditor {
denomination_keys: AuditorDenomSig[];
}
-
/**
* Request that we send to the exchange to get a payback.
*/
@@ -191,7 +187,6 @@ export interface PaybackRequest {
coin_sig: string;
}
-
/**
* Response that we get from the exchange for a payback request.
*/
@@ -242,7 +237,6 @@ export class PaybackConfirmation {
static checked: (obj: any) => PaybackConfirmation;
}
-
/**
* Deposit permission for a single coin.
*/
@@ -274,7 +268,6 @@ export interface CoinPaySig {
exchange_url: string;
}
-
/**
* Information about an exchange as stored inside a
* merchant's contract terms.
@@ -300,11 +293,10 @@ export class ExchangeHandle {
static checked: (obj: any) => ExchangeHandle;
}
-
/**
* Contract terms from a merchant.
*/
-@Checkable.Class({validate: true})
+@Checkable.Class({ validate: true })
export class ContractTerms {
static validate(x: ContractTerms) {
if (x.exchanges.length === 0) {
@@ -447,7 +439,6 @@ export class ContractTerms {
static checked: (obj: any) => ContractTerms;
}
-
/**
* Payment body sent to the merchant's /pay.
*/
@@ -474,7 +465,6 @@ export interface PayReq {
mode: "pay" | "abort-refund";
}
-
/**
* Refund permission in the format that the merchant gives it to us.
*/
@@ -516,7 +506,6 @@ export class MerchantRefundPermission {
static checked: (obj: any) => MerchantRefundPermission;
}
-
/**
* Refund request sent to the exchange.
*/
@@ -560,7 +549,6 @@ export interface RefundRequest {
merchant_sig: string;
}
-
/**
* Response for a refund pickup or a /pay in abort mode.
*/
@@ -591,7 +579,6 @@ export class MerchantRefundResponse {
static checked: (obj: any) => MerchantRefundResponse;
}
-
/**
* Planchet detail sent to the merchant.
*/
@@ -607,7 +594,6 @@ export interface TipPlanchetDetail {
coin_ev: string;
}
-
/**
* Request sent to the merchant to pick up a tip.
*/
@@ -641,7 +627,6 @@ export class ReserveSigSingleton {
static checked: (obj: any) => ReserveSigSingleton;
}
-
/**
* Response to /reserve/status
*/
@@ -690,56 +675,6 @@ export class TipResponse {
}
/**
- * Token containing all the information for the wallet
- * to process a tip. Given by the merchant to the wallet.
- */
-@Checkable.Class()
-export class TipToken {
- /**
- * Expiration for the tip.
- */
- @Checkable.String(timestampCheck)
- expiration: string;
-
- /**
- * URL of the exchange that the tip can be withdrawn from.
- */
- @Checkable.String()
- exchange_url: string;
-
- /**
- * Merchant's URL to pick up the tip.
- */
- @Checkable.String()
- pickup_url: string;
-
- /**
- * Merchant-chosen tip identifier.
- */
- @Checkable.String()
- tip_id: string;
-
- /**
- * Amount of tip.
- */
- @Checkable.String()
- amount: string;
-
- /**
- * URL to navigate after finishing tip processing.
- */
- @Checkable.String()
- next_url: string;
-
- /**
- * Create a TipToken from untyped JSON.
- * Validates the schema and throws on error.
- */
- static checked: (obj: any) => TipToken;
-}
-
-
-/**
* Element of the payback list that the
* exchange gives us in /keys.
*/
@@ -752,11 +687,10 @@ export class Payback {
h_denom_pub: string;
}
-
/**
* Structure that the exchange gives us in /keys.
*/
-@Checkable.Class({extra: true})
+@Checkable.Class({ extra: true })
export class KeysJson {
/**
* List of offered denominations.
@@ -808,7 +742,6 @@ export class KeysJson {
static checked: (obj: any) => KeysJson;
}
-
/**
* Wire fees as anounced by the exchange.
*/
@@ -851,8 +784,7 @@ export class WireFeesJson {
static checked: (obj: any) => WireFeesJson;
}
-
-@Checkable.Class({extra: true})
+@Checkable.Class({ extra: true })
export class AccountInfo {
@Checkable.String()
url: string;
@@ -861,10 +793,12 @@ export class AccountInfo {
master_sig: string;
}
-
-@Checkable.Class({extra: true})
+@Checkable.Class({ extra: true })
export class ExchangeWireJson {
- @Checkable.Map(Checkable.String(), Checkable.List(Checkable.Value(() => WireFeesJson)))
+ @Checkable.Map(
+ Checkable.String(),
+ Checkable.List(Checkable.Value(() => WireFeesJson)),
+ )
fees: { [methodName: string]: WireFeesJson[] };
@Checkable.List(Checkable.Value(() => AccountInfo))
@@ -873,18 +807,16 @@ export class ExchangeWireJson {
static checked: (obj: any) => ExchangeWireJson;
}
-
/**
* Wire detail, arbitrary object that must at least
* contain a "type" key.
*/
export type WireDetail = object & { type: string };
-
/**
* Proposal returned from the contract URL.
*/
-@Checkable.Class({extra: true})
+@Checkable.Class({ extra: true })
export class Proposal {
/**
* Contract terms for the propoal.
@@ -909,7 +841,7 @@ export class Proposal {
/**
* Response from the internal merchant API.
*/
-@Checkable.Class({extra: true})
+@Checkable.Class({ extra: true })
export class CheckPaymentResponse {
@Checkable.Boolean()
paid: boolean;
@@ -939,7 +871,7 @@ export class CheckPaymentResponse {
/**
* Response from the bank.
*/
-@Checkable.Class({extra: true})
+@Checkable.Class({ extra: true })
export class WithdrawOperationStatusResponse {
@Checkable.Boolean()
selection_done: boolean;
@@ -967,4 +899,34 @@ export class WithdrawOperationStatusResponse {
* member.
*/
static checked: (obj: any) => WithdrawOperationStatusResponse;
-} \ No newline at end of file
+}
+
+/**
+ * Response from the merchant.
+ */
+@Checkable.Class({ extra: true })
+export class TipPickupGetResponse {
+ @Checkable.AnyObject()
+ extra: any;
+
+ @Checkable.String()
+ amount: string;
+
+ @Checkable.String()
+ amount_left: string;
+
+ @Checkable.String()
+ exchange_url: string;
+
+ @Checkable.String()
+ stamp_expire: string;
+
+ @Checkable.String()
+ stamp_created: string;
+
+ /**
+ * Verify that a value matches the schema of this class and convert it into a
+ * member.
+ */
+ static checked: (obj: any) => TipPickupGetResponse;
+}
diff --git a/src/taleruri.ts b/src/taleruri.ts
index fa305d1de..f5fc77421 100644
--- a/src/taleruri.ts
+++ b/src/taleruri.ts
@@ -26,6 +26,13 @@ export interface WithdrawUriResult {
statusUrl: string;
}
+export interface TipUriResult {
+ tipPickupUrl: string;
+ tipId: string;
+ merchantInstance: string;
+ merchantOrigin: string;
+}
+
export function parseWithdrawUri(s: string): WithdrawUriResult | undefined {
const parsedUri = new URI(s);
if (parsedUri.scheme() !== "taler") {
@@ -104,3 +111,47 @@ export function parsePayUri(s: string): PayUriResult | undefined {
sessionId: maybeSessionid,
};
}
+
+export function parseTipUri(s: string): TipUriResult | undefined {
+ const parsedUri = new URI(s);
+ if (parsedUri.scheme() != "taler") {
+ return undefined;
+ }
+ if (parsedUri.authority() != "tip") {
+ return undefined;
+ }
+
+ let [_, host, maybePath, maybeInstance, tipId] = parsedUri.path().split("/");
+
+ if (!host) {
+ return undefined;
+ }
+
+ if (!maybePath) {
+ return undefined;
+ }
+
+ if (!tipId) {
+ return undefined;
+ }
+
+ if (maybePath === "-") {
+ maybePath = "public/tip-pickup";
+ } else {
+ maybePath = decodeURIComponent(maybePath);
+ }
+ if (maybeInstance === "-") {
+ maybeInstance = "default";
+ }
+
+ const tipPickupUrl = new URI(
+ "https://" + host + "/" + decodeURIComponent(maybePath),
+ ).href();
+
+ return {
+ tipPickupUrl,
+ tipId: tipId,
+ merchantInstance: maybeInstance,
+ merchantOrigin: new URI(tipPickupUrl).origin(),
+ };
+}
diff --git a/src/wallet.ts b/src/wallet.ts
index e476c94f6..fd1be5293 100644
--- a/src/wallet.ts
+++ b/src/wallet.ts
@@ -80,8 +80,8 @@ import {
ReserveStatus,
TipPlanchetDetail,
TipResponse,
- TipToken,
WithdrawOperationStatusResponse,
+ TipPickupGetResponse,
} from "./talerTypes";
import {
Badge,
@@ -109,7 +109,7 @@ import {
AcceptWithdrawalResponse,
} from "./walletTypes";
import { openPromise } from "./promiseUtils";
-import { parsePayUri, parseWithdrawUri } from "./taleruri";
+import { parsePayUri, parseWithdrawUri, parseTipUri } from "./taleruri";
interface SpeculativePayData {
payCoinInfo: PayCoinInfo;
@@ -345,7 +345,7 @@ export class Wallet {
private timerGroup: TimerGroup;
private speculativePayData: SpeculativePayData | undefined;
private cachedNextUrl: { [fulfillmentUrl: string]: NextUrlResult } = {};
- private activeTipOperations: { [s: string]: Promise<TipRecord> } = {};
+ private activeTipOperations: { [s: string]: Promise<void> } = {};
private activeProcessReserveOperations: {
[reservePub: string]: Promise<void>;
} = {};
@@ -1351,33 +1351,7 @@ export class Wallet {
.add(Stores.coins, coin)
.finish();
- if (coin.status === CoinStatus.TainedByTip) {
- const tip = await this.q().getIndexed(
- Stores.tips.coinPubIndex,
- coin.coinPub,
- );
- if (!tip) {
- throw Error(
- `inconsistent DB: tip for coin pub ${coin.coinPub} not found.`,
- );
- }
-
- if (tip.accepted) {
- console.log("untainting already accepted tip");
- // Transactionally set coin to fresh.
- const mutateCoin = (c: CoinRecord) => {
- if (c.status === CoinStatus.TainedByTip) {
- c.status = CoinStatus.Fresh;
- }
- return c;
- };
- await this.q().mutate(Stores.coins, coin.coinPub, mutateCoin);
- // Show notifications only for accepted tips
- this.badge.showNotification();
- }
- } else {
- this.badge.showNotification();
- }
+ this.badge.showNotification();
this.notifier.notify();
op.resolve();
@@ -1566,7 +1540,7 @@ export class Wallet {
denomSig,
exchangeBaseUrl: pc.exchangeBaseUrl,
reservePub: pc.reservePub,
- status: pc.isFromTip ? CoinStatus.TainedByTip : CoinStatus.Fresh,
+ status: CoinStatus.Fresh,
};
return coin;
}
@@ -1856,14 +1830,14 @@ export class Wallet {
return { isTrusted, isAudited };
}
- async getWithdrawDetails(
- talerPayUri: string,
+ async getWithdrawDetailsForUri(
+ talerWithdrawUri: string,
maybeSelectedExchange?: string,
): Promise<WithdrawDetails> {
- const info = await this.downloadWithdrawInfo(talerPayUri);
+ const info = await this.downloadWithdrawInfo(talerWithdrawUri);
let rci: ReserveCreationInfo | undefined = undefined;
if (maybeSelectedExchange) {
- rci = await this.getReserveCreationInfo(
+ rci = await this.getWithdrawDetailsForAmount(
maybeSelectedExchange,
info.amount,
);
@@ -1874,7 +1848,7 @@ export class Wallet {
};
}
- async getReserveCreationInfo(
+ async getWithdrawDetailsForAmount(
baseUrl: string,
amount: AmountJson,
): Promise<ReserveCreationInfo> {
@@ -3331,14 +3305,13 @@ export class Wallet {
return feeAcc;
}
- async processTip(tipToken: TipToken): Promise<TipRecord> {
- const merchantDomain = new URI(tipToken.pickup_url).origin();
- const key = tipToken.tip_id + merchantDomain;
-
+ async acceptTip(talerTipUri: string): Promise<void> {
+ const { tipId, merchantOrigin } = await this.getTipStatus(talerTipUri);
+ const key = `${tipId}${merchantOrigin}`;
if (this.activeTipOperations[key]) {
return this.activeTipOperations[key];
}
- const p = this.processTipImpl(tipToken);
+ const p = this.acceptTipImpl(tipId, merchantOrigin);
this.activeTipOperations[key] = p;
try {
return await p;
@@ -3347,56 +3320,61 @@ export class Wallet {
}
}
- private async processTipImpl(tipToken: TipToken): Promise<TipRecord> {
- console.log("got tip token", tipToken);
-
- const merchantDomain = new URI(tipToken.pickup_url).origin();
-
- const deadlineSec = getTalerStampSec(tipToken.expiration);
- if (!deadlineSec) {
- throw Error("tipping failed (invalid expiration)");
+ private async acceptTipImpl(
+ tipId: string,
+ merchantOrigin: string,
+ ): Promise<void> {
+ let tipRecord = await this.q().get(Stores.tips, [tipId, merchantOrigin]);
+ if (!tipRecord) {
+ throw Error("tip not in database");
}
- let tipRecord = await this.q().get(Stores.tips, [
- tipToken.tip_id,
- merchantDomain,
- ]);
+ tipRecord.accepted = true;
- if (tipRecord && tipRecord.pickedUp) {
- return tipRecord;
+ // Create one transactional query, within this transaction
+ // both the tip will be marked as accepted and coins
+ // already withdrawn will be untainted.
+ await this.q()
+ .put(Stores.tips, tipRecord)
+ .finish();
+
+ if (tipRecord.pickedUp) {
+ console.log("tip already picked up");
+ return;
}
- const tipAmount = Amounts.parseOrThrow(tipToken.amount);
- await this.updateExchangeFromUrl(tipToken.exchange_url);
+ await this.updateExchangeFromUrl(tipRecord.exchangeUrl);
const denomsForWithdraw = await this.getVerifiedWithdrawDenomList(
- tipToken.exchange_url,
- tipAmount,
+ tipRecord.exchangeUrl,
+ tipRecord.amount,
);
- const planchets = await Promise.all(
- denomsForWithdraw.map(d => this.cryptoApi.createTipPlanchet(d)),
- );
- const coinPubs: string[] = planchets.map(x => x.coinPub);
- const now = new Date().getTime();
- tipRecord = {
- accepted: false,
- amount: Amounts.parseOrThrow(tipToken.amount),
- coinPubs,
- deadline: deadlineSec,
- exchangeUrl: tipToken.exchange_url,
- merchantDomain,
- nextUrl: tipToken.next_url,
- pickedUp: false,
- planchets,
- timestamp: now,
- tipId: tipToken.tip_id,
- };
- let merchantResp;
+ if (!tipRecord.planchets) {
+ const planchets = await Promise.all(
+ denomsForWithdraw.map(d => this.cryptoApi.createTipPlanchet(d)),
+ );
+ const coinPubs: string[] = planchets.map(x => x.coinPub);
- tipRecord = await this.q().putOrGetExisting(Stores.tips, tipRecord, [
- tipRecord.tipId,
- merchantDomain,
- ]);
- this.notifier.notify();
+ await this.q().mutate(Stores.tips, [tipId, merchantOrigin], r => {
+ if (!r.planchets) {
+ r.planchets = planchets;
+ r.coinPubs = coinPubs;
+ }
+ return r;
+ });
+
+ this.notifier.notify();
+ }
+
+ tipRecord = await this.q().get(Stores.tips, [tipId, merchantOrigin]);
+ if (!tipRecord) {
+ throw Error("tip not in database");
+ }
+
+ if (!tipRecord.planchets) {
+ throw Error("invariant violated");
+ }
+
+ console.log("got planchets for tip!");
// Planchets in the form that the merchant expects
const planchetsDetail: TipPlanchetDetail[] = tipRecord.planchets.map(p => ({
@@ -3404,9 +3382,12 @@ export class Wallet {
denom_pub_hash: p.denomPubHash,
}));
+ let merchantResp;
+
try {
- const req = { planchets: planchetsDetail, tip_id: tipToken.tip_id };
- merchantResp = await this.http.postJson(tipToken.pickup_url, req);
+ const req = { planchets: planchetsDetail, tip_id: tipId };
+ merchantResp = await this.http.postJson(tipRecord.pickupUrl, req);
+ console.log("got merchant resp:", merchantResp);
} catch (e) {
console.log("tipping failed", e);
throw e;
@@ -3434,7 +3415,7 @@ export class Wallet {
withdrawSig: response.reserve_sigs[i].reserve_sig,
};
await this.q().put(Stores.precoins, preCoin);
- this.processPreCoin(preCoin.coinPub);
+ await this.processPreCoin(preCoin.coinPub);
}
tipRecord.pickedUp = true;
@@ -3443,61 +3424,75 @@ export class Wallet {
.put(Stores.tips, tipRecord)
.finish();
this.notifier.notify();
-
- return tipRecord;
+ this.badge.showNotification();
+ return;
}
- /**
- * Start using the coins from a tip.
- */
- async acceptTip(tipToken: TipToken): Promise<void> {
- const tipId = tipToken.tip_id;
- const merchantDomain = new URI(tipToken.pickup_url).origin();
- const tipRecord = await this.q().get(Stores.tips, [tipId, merchantDomain]);
- if (!tipRecord) {
- throw Error("tip not found");
+ async getTipStatus(talerTipUri: string): Promise<TipStatus> {
+ const res = parseTipUri(talerTipUri);
+ if (!res) {
+ throw Error("invalid taler://tip URI");
}
- tipRecord.accepted = true;
- // Create one transactional query, within this transaction
- // both the tip will be marked as accepted and coins
- // already withdrawn will be untainted.
- const q = this.q();
+ const tipStatusUrl = new URI(res.tipPickupUrl)
+ .addQuery({
+ instance: res.merchantInstance,
+ tip_id: res.tipId,
+ })
+ .href();
+ console.log("checking tip status from", tipStatusUrl);
+ const merchantResp = await this.http.get(tipStatusUrl);
+ console.log("resp:", merchantResp.responseJson);
+ const tipPickupStatus = TipPickupGetResponse.checked(
+ merchantResp.responseJson,
+ );
- q.put(Stores.tips, tipRecord);
+ console.log("status", tipPickupStatus);
- const updateCoin = (c: CoinRecord) => {
- if (c.status === CoinStatus.TainedByTip) {
- c.status = CoinStatus.Fresh;
- }
- return c;
- };
+ let amount = Amounts.parseOrThrow(tipPickupStatus.amount);
- for (const coinPub of tipRecord.coinPubs) {
- q.mutate(Stores.coins, coinPub, updateCoin);
- }
+ let tipRecord = await this.q().get(Stores.tips, [
+ res.tipId,
+ res.merchantOrigin,
+ ]);
+ if (!tipRecord) {
+ const withdrawDetails = await this.getWithdrawDetailsForAmount(
+ tipPickupStatus.exchange_url,
+ amount,
+ );
- await q.finish();
- this.badge.showNotification();
- this.notifier.notify();
- }
+ tipRecord = {
+ accepted: false,
+ amount,
+ coinPubs: [],
+ deadline: getTalerStampSec(tipPickupStatus.stamp_expire)!,
+ exchangeUrl: tipPickupStatus.exchange_url,
+ merchantDomain: res.merchantOrigin,
+ nextUrl: undefined,
+ pickedUp: false,
+ planchets: undefined,
+ response: undefined,
+ timestamp: new Date().getTime(),
+ tipId: res.tipId,
+ pickupUrl: res.tipPickupUrl,
+ totalFees: Amounts.add(withdrawDetails.overhead, withdrawDetails.withdrawFee).amount,
+ };
+ await this.q().put(Stores.tips, tipRecord);
+ }
- async getTipStatus(tipToken: TipToken): Promise<TipStatus> {
- const tipId = tipToken.tip_id;
- const merchantDomain = new URI(tipToken.pickup_url).origin();
- const tipRecord = await this.q().get(Stores.tips, [tipId, merchantDomain]);
- const amount = Amounts.parseOrThrow(tipToken.amount);
- const exchangeUrl = tipToken.exchange_url;
- this.processTip(tipToken);
- const nextUrl = tipToken.next_url;
const tipStatus: TipStatus = {
accepted: !!tipRecord && tipRecord.accepted,
- amount,
- exchangeUrl,
- merchantDomain,
- nextUrl,
- tipRecord,
+ amount: Amounts.parseOrThrow(tipPickupStatus.amount),
+ amountLeft: Amounts.parseOrThrow(tipPickupStatus.amount_left),
+ exchangeUrl: tipPickupStatus.exchange_url,
+ nextUrl: tipPickupStatus.extra.next_url,
+ merchantOrigin: res.merchantOrigin,
+ tipId: res.tipId,
+ expirationTimestamp: getTalerStampSec(tipPickupStatus.stamp_expire)!,
+ timestamp: getTalerStampSec(tipPickupStatus.stamp_created)!,
+ totalFees: tipRecord.totalFees,
};
+
return tipStatus;
}
@@ -3526,11 +3521,6 @@ export class Wallet {
const abortReq = { ...purchase.payReq, mode: "abort-refund" };
try {
- const config = {
- headers: { "Content-Type": "application/json;charset=UTF-8" },
- timeout: 5000 /* 5 seconds */,
- validateStatus: (s: number) => s === 200,
- };
resp = await this.http.postJson(purchase.contractTerms.pay_url, abortReq);
} catch (e) {
// Gives the user the option to retry / abort and refresh
diff --git a/src/walletTypes.ts b/src/walletTypes.ts
index c657ac02a..aec029371 100644
--- a/src/walletTypes.ts
+++ b/src/walletTypes.ts
@@ -427,10 +427,14 @@ export interface CoinWithDenom {
export interface TipStatus {
accepted: boolean;
amount: AmountJson;
+ amountLeft: AmountJson;
nextUrl: string;
- merchantDomain: string;
exchangeUrl: string;
- tipRecord?: TipRecord;
+ tipId: string;
+ merchantOrigin: string;
+ expirationTimestamp: number;
+ timestamp: number;
+ totalFees: AmountJson;
}
/**
diff --git a/src/webex/messages.ts b/src/webex/messages.ts
index ca0e1c7e1..f1046d5c7 100644
--- a/src/webex/messages.ts
+++ b/src/webex/messages.ts
@@ -174,11 +174,11 @@ export interface MessageMap {
response: AmountJson;
};
"accept-tip": {
- request: { tipToken: talerTypes.TipToken };
- response: walletTypes.TipStatus;
+ request: { talerTipUri: string };
+ response: void;
};
"get-tip-status": {
- request: { tipToken: talerTypes.TipToken };
+ request: { talerTipUri: string };
response: walletTypes.TipStatus;
};
"clear-notification": {
diff --git a/src/webex/pages/tip.tsx b/src/webex/pages/tip.tsx
index c13120c43..a3f5c38c3 100644
--- a/src/webex/pages/tip.tsx
+++ b/src/webex/pages/tip.tsx
@@ -14,7 +14,6 @@
TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-
/**
* Page shown to the user to confirm creation
* of a reserve, usually requested by the bank.
@@ -28,152 +27,114 @@ import URI = require("urijs");
import * as i18n from "../../i18n";
-import {
- acceptTip,
- getReserveCreationInfo,
- getTipStatus,
-} from "../wxApi";
+import { acceptTip, getReserveCreationInfo, getTipStatus } from "../wxApi";
-import {
- WithdrawDetailView,
- renderAmount,
-} from "../renderHtml";
+import { WithdrawDetailView, renderAmount } from "../renderHtml";
import * as Amounts from "../../amounts";
-import { TipToken } from "../../talerTypes";
-import { ReserveCreationInfo, TipStatus } from "../../walletTypes";
+import { useState, useEffect } from "react";
+import { TipStatus } from "../../walletTypes";
-interface TipDisplayProps {
- tipToken: TipToken;
+interface LoadingButtonProps {
+ loading: boolean;
}
-interface TipDisplayState {
- tipStatus?: TipStatus;
- rci?: ReserveCreationInfo;
- working: boolean;
- discarded: boolean;
+function LoadingButton(
+ props:
+ & React.PropsWithChildren<LoadingButtonProps>
+ & React.DetailedHTMLProps<
+ React.ButtonHTMLAttributes<HTMLButtonElement>,
+ HTMLButtonElement
+ >,
+) {
+ return (
+ <button
+ className="pure-button pure-button-primary"
+ type="button"
+ {...props}
+ >
+ {props.loading ? <span><object className="svg-icon svg-baseline" data="/img/spinner-bars.svg" /></span> : null}
+ {props.children}
+ </button>
+ );
}
-class TipDisplay extends React.Component<TipDisplayProps, TipDisplayState> {
- constructor(props: TipDisplayProps) {
- super(props);
- this.state = { working: false, discarded: false };
- }
-
- async update() {
- const tipStatus = await getTipStatus(this.props.tipToken);
- this.setState({ tipStatus });
- const rci = await getReserveCreationInfo(tipStatus.exchangeUrl, tipStatus.amount);
- this.setState({ rci });
- }
-
- componentDidMount() {
- this.update();
- const port = chrome.runtime.connect();
- port.onMessage.addListener((msg: any) => {
- if (msg.notify) {
- console.log("got notified");
- this.update();
- }
- });
- this.update();
- }
-
- renderExchangeInfo() {
- const rci = this.state.rci;
- if (!rci) {
- return <p>Waiting for info about exchange ...</p>;
- }
- const totalCost = Amounts.add(rci.overhead, rci.withdrawFee).amount;
- return (
- <div>
- <p>
- The tip is handled by the exchange <strong>{rci.exchangeInfo.baseUrl}</strong>.{" "}
- The exchange provider will charge
- {" "}
- <strong>{renderAmount(totalCost)}</strong>
- {" "}.
- </p>
- <WithdrawDetailView rci={rci} />
- </div>
- );
+function TipDisplay(props: { talerTipUri: string }) {
+ const [tipStatus, setTipStatus] = useState<TipStatus | undefined>(undefined);
+ const [discarded, setDiscarded] = useState(false);
+ const [loading, setLoading] = useState(false);
+ const [finished, setFinished] = useState(false);
+
+ useEffect(() => {
+ const doFetch = async () => {
+ const ts = await getTipStatus(props.talerTipUri);
+ setTipStatus(ts);
+ };
+ doFetch();
+ }, []);
+
+ if (discarded) {
+ return <span>You've discarded the tip.</span>;
}
- accept() {
- this.setState({ working: true});
- acceptTip(this.props.tipToken);
+ if (finished) {
+ return <span>Tip has been accepted!</span>;
}
- discard() {
- this.setState({ discarded: true });
+ if (!tipStatus) {
+ return <span>Loading ...</span>;
}
- render(): JSX.Element {
- const ts = this.state.tipStatus;
- if (!ts) {
- return <p>Processing ...</p>;
- }
-
- const renderAccepted = () => (
- <>
- <p>You've accepted this tip! <a href={ts.nextUrl}>Go back to merchant</a></p>
- {this.renderExchangeInfo()}
- </>
- );
-
- const renderButtons = () => (
- <>
+ const discard = () => {
+ setDiscarded(true);
+ };
+
+ const accept = async () => {
+ setLoading(true);
+ await acceptTip(props.talerTipUri);
+ setFinished(true);
+ };
+
+ return (
+ <div>
+ <h2>Tip Received!</h2>
+ <p>
+ You received a tip of <strong>{renderAmount(tipStatus.amount)}</strong>{" "}
+ from <span> </span>
+ <strong>{tipStatus.merchantOrigin}</strong>.
+ </p>
+ <p>
+ The tip is handled by the exchange{" "}
+ <strong>{tipStatus.exchangeUrl}</strong>. This exchange will charge fees
+ of <strong>{renderAmount(tipStatus.totalFees)}</strong> for this
+ operation.
+ </p>
<form className="pure-form">
- <button
- className="pure-button pure-button-primary"
- type="button"
- disabled={!(this.state.rci && this.state.tipStatus && this.state.tipStatus.tipRecord)}
- onClick={() => this.accept()}>
- { this.state.working
- ? <span><object className="svg-icon svg-baseline" data="/img/spinner-bars.svg" /> </span>
- : null }
- Accept tip
- </button>
+ <LoadingButton loading={loading} onClick={() => accept()}>
+ AcceptTip
+ </LoadingButton>
{" "}
- <button className="pure-button" type="button" onClick={() => this.discard()}>
+ <button className="pure-button" type="button" onClick={() => discard()}>
Discard tip
</button>
</form>
- { this.renderExchangeInfo() }
- </>
- );
-
- const renderDiscarded = () => (
- <p>You've discarded this tip. <a href={ts.nextUrl}>Go back to merchant.</a></p>
- );
-
- return (
- <div>
- <h2>Tip Received!</h2>
- <p>You received a tip of <strong>{renderAmount(ts.amount)}</strong> from <span> </span>
- <strong>{ts.merchantDomain}</strong>.</p>
- {
- this.state.discarded
- ? renderDiscarded()
- : ts.accepted
- ? renderAccepted()
- : renderButtons()
- }
- </div>
- );
- }
+ </div>
+ );
}
async function main() {
try {
const url = new URI(document.location.href);
const query: any = URI.parseQuery(url.query());
+ const talerTipUri = query.talerTipUri;
+ if (typeof talerTipUri !== "string") {
+ throw Error("talerTipUri must be a string");
+ }
- const tipToken = TipToken.checked(JSON.parse(query.tip_token));
-
- ReactDOM.render(<TipDisplay tipToken={tipToken} />,
- document.getElementById("container")!);
-
+ ReactDOM.render(
+ <TipDisplay talerTipUri={talerTipUri} />,
+ document.getElementById("container")!,
+ );
} catch (e) {
// TODO: provide more context information, maybe factor it out into a
// TODO:generic error reporting function or component.
diff --git a/src/webex/wxApi.ts b/src/webex/wxApi.ts
index feabc7819..fd01aed3f 100644
--- a/src/webex/wxApi.ts
+++ b/src/webex/wxApi.ts
@@ -45,7 +45,6 @@ import {
import {
MerchantRefundPermission,
- TipToken,
} from "../talerTypes";
import { MessageMap, MessageType } from "./messages";
@@ -349,15 +348,15 @@ export function getFullRefundFees(args: { refundPermissions: MerchantRefundPermi
/**
* Get the status of processing a tip.
*/
-export function getTipStatus(tipToken: TipToken): Promise<TipStatus> {
- return callBackend("get-tip-status", { tipToken });
+export function getTipStatus(talerTipUri: string): Promise<TipStatus> {
+ return callBackend("get-tip-status", { talerTipUri });
}
/**
* Mark a tip as accepted by the user.
*/
-export function acceptTip(tipToken: TipToken): Promise<TipStatus> {
- return callBackend("accept-tip", { tipToken });
+export function acceptTip(talerTipUri: string): Promise<void> {
+ return callBackend("accept-tip", { talerTipUri });
}
@@ -423,4 +422,4 @@ export function preparePay(talerPayUri: string) {
*/
export function acceptWithdrawal(talerWithdrawUri: string, selectedExchange: string) {
return callBackend("accept-withdrawal", { talerWithdrawUri, selectedExchange });
-} \ No newline at end of file
+}
diff --git a/src/webex/wxBackend.ts b/src/webex/wxBackend.ts
index d31ea388d..5bff4fe0a 100644
--- a/src/webex/wxBackend.ts
+++ b/src/webex/wxBackend.ts
@@ -50,7 +50,6 @@ import * as wxApi from "./wxApi";
import URI = require("urijs");
import Port = chrome.runtime.Port;
import MessageSender = chrome.runtime.MessageSender;
-import { TipToken } from "../talerTypes";
import { BrowserCryptoWorkerFactory } from "../crypto/cryptoApi";
const NeedsWallet = Symbol("NeedsWallet");
@@ -182,7 +181,7 @@ function handleMessage(
return Promise.resolve({ error: "bad url" });
}
const amount = AmountJson.checked(detail.amount);
- return needsWallet().getReserveCreationInfo(detail.baseUrl, amount);
+ return needsWallet().getWithdrawDetailsForAmount(detail.baseUrl, amount);
}
case "get-history": {
// TODO: limit history length
@@ -295,12 +294,10 @@ function handleMessage(
case "accept-refund":
return needsWallet().acceptRefund(detail.refundUrl);
case "get-tip-status": {
- const tipToken = TipToken.checked(detail.tipToken);
- return needsWallet().getTipStatus(tipToken);
+ return needsWallet().getTipStatus(detail.talerTipUri);
}
case "accept-tip": {
- const tipToken = TipToken.checked(detail.tipToken);
- return needsWallet().acceptTip(tipToken);
+ return needsWallet().acceptTip(detail.talerTipUri);
}
case "clear-notification": {
return needsWallet().clearNotification();
@@ -340,7 +337,7 @@ function handleMessage(
return needsWallet().benchmarkCrypto(detail.repetitions);
}
case "get-withdraw-details": {
- return needsWallet().getWithdrawDetails(
+ return needsWallet().getWithdrawDetailsForUri(
detail.talerWithdrawUri,
detail.maybeSelectedExchange,
);