aboutsummaryrefslogtreecommitdiff
path: root/src/wallet.ts
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 /src/wallet.ts
parentdefbf625bdef0f8a666b72b8ce99de5e01af6b91 (diff)
downloadwallet-core-5ec344290efd937fa82c0704bc7c204a0bf14c78.tar.xz
support for tipping protocol changes
Diffstat (limited to 'src/wallet.ts')
-rw-r--r--src/wallet.ts258
1 files changed, 124 insertions, 134 deletions
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