aboutsummaryrefslogtreecommitdiff
path: root/packages/taler-wallet-core
diff options
context:
space:
mode:
authorFlorian Dold <florian.dold@gmail.com>2020-09-08 17:40:47 +0530
committerFlorian Dold <florian.dold@gmail.com>2020-09-08 17:40:47 +0530
commitb063382d25d1ed8572ebe2f52bf54247379300d5 (patch)
treeb60e4abf9b5285ffdf3339639ba8dae30d0bfff1 /packages/taler-wallet-core
parentbe77ee284a819f7932831bd85e88c47c655addb2 (diff)
tipping API and integration test
Diffstat (limited to 'packages/taler-wallet-core')
-rw-r--r--packages/taler-wallet-core/src/db.ts2
-rw-r--r--packages/taler-wallet-core/src/operations/pending.ts2
-rw-r--r--packages/taler-wallet-core/src/operations/tip.ts81
-rw-r--r--packages/taler-wallet-core/src/types/dbTypes.ts4
-rw-r--r--packages/taler-wallet-core/src/types/notifications.ts1
-rw-r--r--packages/taler-wallet-core/src/types/talerTypes.ts17
-rw-r--r--packages/taler-wallet-core/src/types/walletTypes.ts56
-rw-r--r--packages/taler-wallet-core/src/wallet.ts19
8 files changed, 100 insertions, 82 deletions
diff --git a/packages/taler-wallet-core/src/db.ts b/packages/taler-wallet-core/src/db.ts
index b3203935e..c21ff4a43 100644
--- a/packages/taler-wallet-core/src/db.ts
+++ b/packages/taler-wallet-core/src/db.ts
@@ -8,7 +8,7 @@ import { IDBFactory, IDBDatabase } from "idb-bridge";
* with each major change. When incrementing the major version,
* the wallet should import data from the previous version.
*/
-const TALER_DB_NAME = "taler-walletdb-v10";
+const TALER_DB_NAME = "taler-walletdb-v11";
/**
* Current database minor version, should be incremented
diff --git a/packages/taler-wallet-core/src/operations/pending.ts b/packages/taler-wallet-core/src/operations/pending.ts
index 3c631eb77..91a55c705 100644
--- a/packages/taler-wallet-core/src/operations/pending.ts
+++ b/packages/taler-wallet-core/src/operations/pending.ts
@@ -368,7 +368,7 @@ async function gatherTipPending(
type: PendingOperationType.TipPickup,
givesLifeness: true,
merchantBaseUrl: tip.merchantBaseUrl,
- tipId: tip.tipId,
+ tipId: tip.walletTipId,
merchantTipId: tip.merchantTipId,
});
}
diff --git a/packages/taler-wallet-core/src/operations/tip.ts b/packages/taler-wallet-core/src/operations/tip.ts
index 7949648c5..6fe374bf0 100644
--- a/packages/taler-wallet-core/src/operations/tip.ts
+++ b/packages/taler-wallet-core/src/operations/tip.ts
@@ -16,7 +16,7 @@
import { InternalWalletState } from "./state";
import { parseTipUri } from "../util/taleruri";
-import { TipStatus, TalerErrorDetails } from "../types/walletTypes";
+import { PrepareTipResult, TalerErrorDetails } from "../types/walletTypes";
import {
TipPlanchetDetail,
codecForTipPickupGetResponse,
@@ -46,20 +46,23 @@ import { getTimestampNow } from "../util/time";
import { readSuccessResponseJsonOrThrow } from "../util/http";
import { URL } from "../util/url";
import { Logger } from "../util/logging";
+import { checkDbInvariant } from "../util/invariants";
const logger = new Logger("operations/tip.ts");
-export async function getTipStatus(
+export async function prepareTip(
ws: InternalWalletState,
talerTipUri: string,
-): Promise<TipStatus> {
+): Promise<PrepareTipResult> {
const res = parseTipUri(talerTipUri);
if (!res) {
throw Error("invalid taler://tip URI");
}
- const tipStatusUrl = new URL("tip-pickup", res.merchantBaseUrl);
- tipStatusUrl.searchParams.set("tip_id", res.merchantTipId);
+ const tipStatusUrl = new URL(
+ `tips/${res.merchantTipId}`,
+ res.merchantBaseUrl,
+ );
logger.trace("checking tip status from", tipStatusUrl.href);
const merchantResp = await ws.http.get(tipStatusUrl.href);
const tipPickupStatus = await readSuccessResponseJsonOrThrow(
@@ -68,7 +71,7 @@ export async function getTipStatus(
);
logger.trace(`status ${tipPickupStatus}`);
- const amount = Amounts.parseOrThrow(tipPickupStatus.amount);
+ const amount = Amounts.parseOrThrow(tipPickupStatus.tip_amount);
const merchantOrigin = new URL(res.merchantBaseUrl).origin;
@@ -85,7 +88,7 @@ export async function getTipStatus(
amount,
);
- const tipId = encodeCrock(getRandomBytes(32));
+ const walletTipId = encodeCrock(getRandomBytes(32));
const selectedDenoms = await selectWithdrawalDenoms(
ws,
tipPickupStatus.exchange_url,
@@ -93,11 +96,11 @@ export async function getTipStatus(
);
tipRecord = {
- tipId,
+ walletTipId: walletTipId,
acceptedTimestamp: undefined,
rejectedTimestamp: undefined,
amount,
- deadline: tipPickupStatus.stamp_expire,
+ deadline: tipPickupStatus.expiration,
exchangeUrl: tipPickupStatus.exchange_url,
merchantBaseUrl: res.merchantBaseUrl,
nextUrl: undefined,
@@ -117,18 +120,13 @@ export async function getTipStatus(
await ws.db.put(Stores.tips, tipRecord);
}
- const tipStatus: TipStatus = {
+ const tipStatus: PrepareTipResult = {
accepted: !!tipRecord && !!tipRecord.acceptedTimestamp,
- amount: Amounts.parseOrThrow(tipPickupStatus.amount),
- amountLeft: Amounts.parseOrThrow(tipPickupStatus.amount_left),
- exchangeUrl: tipPickupStatus.exchange_url,
- nextUrl: tipPickupStatus.extra.next_url,
- merchantOrigin: merchantOrigin,
- merchantTipId: res.merchantTipId,
- expirationTimestamp: tipPickupStatus.stamp_expire,
- timestamp: tipPickupStatus.stamp_created,
- totalFees: tipRecord.totalFees,
- tipId: tipRecord.tipId,
+ amount: Amounts.stringify(tipPickupStatus.tip_amount),
+ exchangeBaseUrl: tipPickupStatus.exchange_url,
+ expirationTimestamp: tipPickupStatus.expiration,
+ totalFees: Amounts.stringify(tipRecord.totalFees),
+ walletTipId: tipRecord.walletTipId,
};
return tipStatus;
@@ -152,7 +150,9 @@ async function incrementTipRetry(
t.lastError = err;
await tx.put(Stores.tips, t);
});
- ws.notify({ type: NotificationType.TipOperationError });
+ if (err) {
+ ws.notify({ type: NotificationType.TipOperationError, error: err });
+ }
}
export async function processTip(
@@ -225,15 +225,8 @@ async function processTipImpl(
}
tipRecord = await ws.db.get(Stores.tips, tipId);
- if (!tipRecord) {
- throw Error("tip not in database");
- }
-
- if (!tipRecord.planchets) {
- throw Error("invariant violated");
- }
-
- logger.trace("got planchets for tip!");
+ checkDbInvariant(!!tipRecord, "tip record should be in database");
+ checkDbInvariant(!!tipRecord.planchets, "tip record should have planchets");
// Planchets in the form that the merchant expects
const planchetsDetail: TipPlanchetDetail[] = tipRecord.planchets.map((p) => ({
@@ -241,23 +234,17 @@ async function processTipImpl(
denom_pub_hash: p.denomPubHash,
}));
- let merchantResp;
-
- const tipStatusUrl = new URL("tip-pickup", tipRecord.merchantBaseUrl);
-
- try {
- const req = { planchets: planchetsDetail, tip_id: tipRecord.merchantTipId };
- merchantResp = await ws.http.postJson(tipStatusUrl.href, req);
- if (merchantResp.status !== 200) {
- throw Error(`unexpected status ${merchantResp.status} for tip-pickup`);
- }
- logger.trace("got merchant resp:", merchantResp);
- } catch (e) {
- logger.warn("tipping failed", e);
- throw e;
- }
+ const tipStatusUrl = new URL(
+ `/tips/${tipRecord.merchantTipId}/pickup`,
+ tipRecord.merchantBaseUrl,
+ );
- const response = codecForTipResponse().decode(await merchantResp.json());
+ const req = { planchets: planchetsDetail };
+ const merchantResp = await ws.http.postJson(tipStatusUrl.href, req);
+ const response = await readSuccessResponseJsonOrThrow(
+ merchantResp,
+ codecForTipResponse(),
+ );
if (response.reserve_sigs.length !== tipRecord.planchets.length) {
throw Error("number of tip responses does not match requested planchets");
@@ -293,7 +280,7 @@ async function processTipImpl(
exchangeBaseUrl: tipRecord.exchangeUrl,
source: {
type: WithdrawalSourceType.Tip,
- tipId: tipRecord.tipId,
+ tipId: tipRecord.walletTipId,
},
timestampStart: getTimestampNow(),
withdrawalGroupId: withdrawalGroupId,
diff --git a/packages/taler-wallet-core/src/types/dbTypes.ts b/packages/taler-wallet-core/src/types/dbTypes.ts
index 0ee41a6a5..4e2ba1bb4 100644
--- a/packages/taler-wallet-core/src/types/dbTypes.ts
+++ b/packages/taler-wallet-core/src/types/dbTypes.ts
@@ -986,7 +986,7 @@ export interface TipRecord {
/**
* Tip ID chosen by the wallet.
*/
- tipId: string;
+ walletTipId: string;
/**
* The merchant's identifier for this tip.
@@ -1760,7 +1760,7 @@ class ReserveHistoryStore extends Store<ReserveHistoryRecord> {
class TipsStore extends Store<TipRecord> {
constructor() {
- super("tips", { keyPath: "tipId" });
+ super("tips", { keyPath: "walletTipId" });
}
}
diff --git a/packages/taler-wallet-core/src/types/notifications.ts b/packages/taler-wallet-core/src/types/notifications.ts
index 7a51f0d83..e1b9a7aff 100644
--- a/packages/taler-wallet-core/src/types/notifications.ts
+++ b/packages/taler-wallet-core/src/types/notifications.ts
@@ -186,6 +186,7 @@ export interface ProposalOperationErrorNotification {
export interface TipOperationErrorNotification {
type: NotificationType.TipOperationError;
+ error: TalerErrorDetails;
}
export interface WithdrawOperationErrorNotification {
diff --git a/packages/taler-wallet-core/src/types/talerTypes.ts b/packages/taler-wallet-core/src/types/talerTypes.ts
index c944f1561..52dc4cb62 100644
--- a/packages/taler-wallet-core/src/types/talerTypes.ts
+++ b/packages/taler-wallet-core/src/types/talerTypes.ts
@@ -773,17 +773,11 @@ export class WithdrawOperationStatusResponse {
* Response from the merchant.
*/
export class TipPickupGetResponse {
- extra: any;
-
- amount: string;
-
- amount_left: string;
+ tip_amount: string;
exchange_url: string;
- stamp_expire: Timestamp;
-
- stamp_created: Timestamp;
+ expiration: Timestamp;
}
export class WithdrawResponse {
@@ -1261,12 +1255,9 @@ export const codecForWithdrawOperationStatusResponse = (): Codec<
export const codecForTipPickupGetResponse = (): Codec<TipPickupGetResponse> =>
buildCodecForObject<TipPickupGetResponse>()
- .property("extra", codecForAny())
- .property("amount", codecForString())
- .property("amount_left", codecForString())
+ .property("tip_amount", codecForString())
.property("exchange_url", codecForString())
- .property("stamp_expire", codecForTimestamp)
- .property("stamp_created", codecForTimestamp)
+ .property("expiration", codecForTimestamp)
.build("TipPickupGetResponse");
export const codecForRecoupConfirmation = (): Codec<RecoupConfirmation> =>
diff --git a/packages/taler-wallet-core/src/types/walletTypes.ts b/packages/taler-wallet-core/src/types/walletTypes.ts
index 82f29c39d..fb049caf9 100644
--- a/packages/taler-wallet-core/src/types/walletTypes.ts
+++ b/packages/taler-wallet-core/src/types/walletTypes.ts
@@ -38,7 +38,7 @@ import {
ExchangeWireInfo,
DenominationSelectionInfo,
} from "./dbTypes";
-import { Timestamp } from "../util/time";
+import { Timestamp, codecForTimestamp } from "../util/time";
import {
buildCodecForObject,
codecForString,
@@ -348,23 +348,33 @@ export class ReturnCoinsRequest {
static checked: (obj: any) => ReturnCoinsRequest;
}
-/**
- * Status of processing a tip.
- */
-export interface TipStatus {
+export interface PrepareTipResult {
+ /**
+ * Unique ID for the tip assigned by the wallet.
+ * Typically different from the merchant-generated tip ID.
+ */
+ walletTipId: string;
+
+ /**
+ * Has the tip already been accepted?
+ */
accepted: boolean;
- amount: AmountJson;
- amountLeft: AmountJson;
- nextUrl: string;
- exchangeUrl: string;
- tipId: string;
- merchantTipId: string;
- merchantOrigin: string;
+ amount: AmountString;
+ totalFees: AmountString;
+ exchangeBaseUrl: string;
expirationTimestamp: Timestamp;
- timestamp: Timestamp;
- totalFees: AmountJson;
}
+export const codecForPrepareTipResult = (): Codec<PrepareTipResult> =>
+ buildCodecForObject<PrepareTipResult>()
+ .property("accepted", codecForBoolean())
+ .property("amount", codecForAmountString())
+ .property("totalFees", codecForAmountString())
+ .property("exchangeBaseUrl", codecForString())
+ .property("expirationTimestamp", codecForTimestamp)
+ .property("walletTipId", codecForString())
+ .build("PrepareTipResult");
+
export interface BenchmarkResult {
time: { [s: string]: number };
repetitions: number;
@@ -903,3 +913,21 @@ export const codecForForceRefreshRequest = (): Codec<ForceRefreshRequest> =>
buildCodecForObject<ForceRefreshRequest>()
.property("coinPubList", codecForList(codecForString()))
.build("ForceRefreshRequest");
+
+export interface PrepareTipRequest {
+ talerTipUri: string;
+}
+
+export const codecForPrepareTipRequest = (): Codec<PrepareTipRequest> =>
+ buildCodecForObject<PrepareTipRequest>()
+ .property("talerTipUri", codecForString())
+ .build("PrepareTipRequest");
+
+export interface AcceptTipRequest {
+ walletTipId: string;
+}
+
+export const codecForAcceptTipRequest = (): Codec<AcceptTipRequest> =>
+ buildCodecForObject<AcceptTipRequest>()
+ .property("walletTipId", codecForString())
+ .build("AcceptTipRequest");
diff --git a/packages/taler-wallet-core/src/wallet.ts b/packages/taler-wallet-core/src/wallet.ts
index 9666665a4..0507ac8b2 100644
--- a/packages/taler-wallet-core/src/wallet.ts
+++ b/packages/taler-wallet-core/src/wallet.ts
@@ -59,7 +59,6 @@ import {
ConfirmPayResult,
ReturnCoinsRequest,
SenderWireInfos,
- TipStatus,
PreparePayResult,
AcceptWithdrawalResponse,
PurchaseDetails,
@@ -93,6 +92,9 @@ import {
codecForSetCoinSuspendedRequest,
codecForForceExchangeUpdateRequest,
codecForForceRefreshRequest,
+ PrepareTipResult,
+ codecForPrepareTipRequest,
+ codecForAcceptTipRequest,
} from "./types/walletTypes";
import { Logger } from "./util/logging";
@@ -121,7 +123,7 @@ import {
import { processWithdrawGroup } from "./operations/withdraw";
import { getPendingOperations } from "./operations/pending";
import { getBalances } from "./operations/balance";
-import { acceptTip, getTipStatus, processTip } from "./operations/tip";
+import { acceptTip, prepareTip, processTip } from "./operations/tip";
import { TimerGroup } from "./util/timer";
import { AsyncCondition } from "./util/promiseUtils";
import { AsyncOpMemoSingle } from "./util/asyncMemo";
@@ -769,8 +771,8 @@ export class Wallet {
}
}
- async getTipStatus(talerTipUri: string): Promise<TipStatus> {
- return getTipStatus(this.ws, talerTipUri);
+ async prepareTip(talerTipUri: string): Promise<PrepareTipResult> {
+ return prepareTip(this.ws, talerTipUri);
}
async abortFailedPayment(contractTermsHash: string): Promise<void> {
@@ -1096,6 +1098,15 @@ export class Wallet {
refreshGroupId,
};
}
+ case "prepareTip": {
+ const req = codecForPrepareTipRequest().decode(payload);
+ return await this.prepareTip(req.talerTipUri);
+ }
+ case "acceptTip": {
+ const req = codecForAcceptTipRequest().decode(payload);
+ await this.acceptTip(req.walletTipId);
+ return {};
+ }
}
throw OperationFailedError.fromCode(
TalerErrorCode.WALLET_CORE_API_OPERATION_UNKNOWN,