tipping API and integration test

This commit is contained in:
Florian Dold 2020-09-08 17:40:47 +05:30
parent be77ee284a
commit b063382d25
No known key found for this signature in database
GPG Key ID: D2E4F00F29D02A4B
13 changed files with 331 additions and 92 deletions

View File

@ -72,6 +72,10 @@ import {
CoinDumpJson, CoinDumpJson,
ForceExchangeUpdateRequest, ForceExchangeUpdateRequest,
ForceRefreshRequest, ForceRefreshRequest,
PrepareTipResult,
PrepareTipRequest,
codecForPrepareTipResult,
AcceptTipRequest,
} from "taler-wallet-core"; } from "taler-wallet-core";
import { URL } from "url"; import { URL } from "url";
import axios, { AxiosError } from "axios"; import axios, { AxiosError } from "axios";
@ -81,6 +85,9 @@ import {
PostOrderRequest, PostOrderRequest,
PostOrderResponse, PostOrderResponse,
MerchantOrderPrivateStatusResponse, MerchantOrderPrivateStatusResponse,
TippingReserveStatus,
TipCreateConfirmation,
TipCreateRequest,
} from "./merchantApiTypes"; } from "./merchantApiTypes";
import { ApplyRefundResponse } from "taler-wallet-core"; import { ApplyRefundResponse } from "taler-wallet-core";
import { PendingOperationsResponse } from "taler-wallet-core"; import { PendingOperationsResponse } from "taler-wallet-core";
@ -1216,10 +1223,46 @@ export namespace MerchantPrivateApi {
}; };
} }
export async function createTippingReserve(merchantService: MerchantServiceInterface, export async function createTippingReserve(
merchantService: MerchantServiceInterface,
instance: string,
req: CreateMerchantTippingReserveRequest, req: CreateMerchantTippingReserveRequest,
): Promise<CreateMerchantTippingReserveConfirmation> {} ): Promise<CreateMerchantTippingReserveConfirmation> {
const reqUrl = new URL(
`private/reserves`,
merchantService.makeInstanceBaseUrl(instance),
);
const resp = await axios.post(reqUrl.href, req);
// FIXME: validate
return resp.data;
}
export async function queryTippingReserves(
merchantService: MerchantServiceInterface,
instance: string,
): Promise<TippingReserveStatus> {
const reqUrl = new URL(
`private/reserves`,
merchantService.makeInstanceBaseUrl(instance),
);
const resp = await axios.get(reqUrl.href);
// FIXME: validate
return resp.data;
}
export async function giveTip(
merchantService: MerchantServiceInterface,
instance: string,
req: TipCreateRequest,
): Promise<TipCreateConfirmation> {
const reqUrl = new URL(
`private/tips`,
merchantService.makeInstanceBaseUrl(instance),
);
const resp = await axios.post(reqUrl.href, req);
// FIXME: validate
return resp.data;
}
} }
export interface CreateMerchantTippingReserveRequest { export interface CreateMerchantTippingReserveRequest {
@ -1238,7 +1281,7 @@ export interface CreateMerchantTippingReserveConfirmation {
reserve_pub: string; reserve_pub: string;
// Wire account of the exchange where to transfer the funds // Wire account of the exchange where to transfer the funds
payto_url: string; payto_uri: string;
} }
export class MerchantService implements MerchantServiceInterface { export class MerchantService implements MerchantServiceInterface {
@ -1594,6 +1637,22 @@ export class WalletCli {
throw new OperationFailedError(resp.error); throw new OperationFailedError(resp.error);
} }
async prepareTip(req: PrepareTipRequest): Promise<PrepareTipResult> {
const resp = await this.apiRequest("prepareTip", req);
if (resp.type === "response") {
return codecForPrepareTipResult().decode(resp.result);
}
throw new OperationFailedError(resp.error);
}
async acceptTip(req: AcceptTipRequest): Promise<void> {
const resp = await this.apiRequest("acceptTip", req);
if (resp.type === "response") {
return;
}
throw new OperationFailedError(resp.error);
}
async dumpCoins(): Promise<CoinDumpJson> { async dumpCoins(): Promise<CoinDumpJson> {
const resp = await this.apiRequest("dumpCoins", {}); const resp = await this.apiRequest("dumpCoins", {});
if (resp.type === "response") { if (resp.type === "response") {

View File

@ -223,3 +223,63 @@ export interface TransactionWireReport {
// Public key of the coin for which we got the exchange error. // Public key of the coin for which we got the exchange error.
coin_pub: CoinPublicKeyString; coin_pub: CoinPublicKeyString;
} }
export interface TippingReserveStatus {
// Array of all known reserves (possibly empty!)
reserves: ReserveStatusEntry[];
}
export interface ReserveStatusEntry {
// Public key of the reserve
reserve_pub: string;
// Timestamp when it was established
creation_time: Timestamp;
// Timestamp when it expires
expiration_time: Timestamp;
// Initial amount as per reserve creation call
merchant_initial_amount: AmountString;
// Initial amount as per exchange, 0 if exchange did
// not confirm reserve creation yet.
exchange_initial_amount: AmountString;
// Amount picked up so far.
pickup_amount: AmountString;
// Amount approved for tips that exceeds the pickup_amount.
committed_amount: AmountString;
// Is this reserve active (false if it was deleted but not purged)
active: boolean;
}
export interface TipCreateConfirmation {
// Unique tip identifier for the tip that was created.
tip_id: string;
// taler://tip URI for the tip
taler_tip_uri: string;
// URL that will directly trigger processing
// the tip when the browser is redirected to it
tip_status_url: string;
// when does the tip expire
tip_expiration: Timestamp;
}
export interface TipCreateRequest {
// Amount that the customer should be tipped
amount: AmountString;
// Justification for giving the tip
justification: string;
// URL that the user should be directed to after tipping,
// will be included in the tip_token.
next_url: string;
}

View File

@ -0,0 +1,106 @@
/*
This file is part of GNU Taler
(C) 2020 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
Foundation; either version 3, or (at your option) any later version.
GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
/**
* Imports.
*/
import {
runTest,
GlobalTestState,
MerchantPrivateApi,
BankAccessApi,
BankApi,
} from "./harness";
import { createSimpleTestkudosEnvironment, withdrawViaBank } from "./helpers";
/**
* Run test for basic, bank-integrated withdrawal.
*/
runTest(async (t: GlobalTestState) => {
// Set up test environment
const {
wallet,
bank,
exchange,
merchant,
exchangeBankAccount,
} = await createSimpleTestkudosEnvironment(t);
const mbu = await BankApi.createRandomBankUser(bank);
const tipReserveResp = await MerchantPrivateApi.createTippingReserve(
merchant,
"default",
{
exchange_url: exchange.baseUrl,
initial_balance: "TESTKUDOS:10",
wire_method: "x-taler-bank",
},
);
console.log("tipReserveResp:", tipReserveResp);
t.assertDeepEqual(
tipReserveResp.payto_uri,
exchangeBankAccount.accountPaytoUri,
);
await BankApi.adminAddIncoming(bank, {
amount: "TESTKUDOS:10",
debitAccountPayto: mbu.accountPaytoUri,
exchangeBankAccount,
reservePub: tipReserveResp.reserve_pub,
});
await exchange.runWirewatchOnce();
await merchant.stop();
await merchant.start();
await merchant.pingUntilAvailable();
const r = await MerchantPrivateApi.queryTippingReserves(merchant, "default");
console.log("tipping reserves:", JSON.stringify(r, undefined, 2));
t.assertTrue(r.reserves.length === 1);
t.assertDeepEqual(
r.reserves[0].exchange_initial_amount,
r.reserves[0].merchant_initial_amount,
);
const tip = await MerchantPrivateApi.giveTip(merchant, "default", {
amount: "TESTKUDOS:5",
justification: "why not?",
next_url: "https://example.com/after-tip",
});
console.log("created tip", tip);
const ptr = await wallet.prepareTip({
talerTipUri: tip.taler_tip_uri,
});
console.log(ptr);
await wallet.acceptTip({
walletTipId: ptr.walletTipId,
});
await wallet.runUntilDone();
const bal = await wallet.getBalances();
console.log(bal);
});

View File

@ -288,9 +288,9 @@ walletCli
break; break;
case TalerUriType.TalerTip: case TalerUriType.TalerTip:
{ {
const res = await wallet.getTipStatus(uri); const res = await wallet.prepareTip(uri);
console.log("tip status", res); console.log("tip status", res);
await wallet.acceptTip(res.tipId); await wallet.acceptTip(res.walletTipId);
} }
break; break;
case TalerUriType.TalerRefund: case TalerUriType.TalerRefund:

View File

@ -8,7 +8,7 @@ import { IDBFactory, IDBDatabase } from "idb-bridge";
* with each major change. When incrementing the major version, * with each major change. When incrementing the major version,
* the wallet should import data from the previous 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 * Current database minor version, should be incremented

View File

@ -368,7 +368,7 @@ async function gatherTipPending(
type: PendingOperationType.TipPickup, type: PendingOperationType.TipPickup,
givesLifeness: true, givesLifeness: true,
merchantBaseUrl: tip.merchantBaseUrl, merchantBaseUrl: tip.merchantBaseUrl,
tipId: tip.tipId, tipId: tip.walletTipId,
merchantTipId: tip.merchantTipId, merchantTipId: tip.merchantTipId,
}); });
} }

View File

@ -16,7 +16,7 @@
import { InternalWalletState } from "./state"; import { InternalWalletState } from "./state";
import { parseTipUri } from "../util/taleruri"; import { parseTipUri } from "../util/taleruri";
import { TipStatus, TalerErrorDetails } from "../types/walletTypes"; import { PrepareTipResult, TalerErrorDetails } from "../types/walletTypes";
import { import {
TipPlanchetDetail, TipPlanchetDetail,
codecForTipPickupGetResponse, codecForTipPickupGetResponse,
@ -46,20 +46,23 @@ import { getTimestampNow } from "../util/time";
import { readSuccessResponseJsonOrThrow } from "../util/http"; import { readSuccessResponseJsonOrThrow } from "../util/http";
import { URL } from "../util/url"; import { URL } from "../util/url";
import { Logger } from "../util/logging"; import { Logger } from "../util/logging";
import { checkDbInvariant } from "../util/invariants";
const logger = new Logger("operations/tip.ts"); const logger = new Logger("operations/tip.ts");
export async function getTipStatus( export async function prepareTip(
ws: InternalWalletState, ws: InternalWalletState,
talerTipUri: string, talerTipUri: string,
): Promise<TipStatus> { ): Promise<PrepareTipResult> {
const res = parseTipUri(talerTipUri); const res = parseTipUri(talerTipUri);
if (!res) { if (!res) {
throw Error("invalid taler://tip URI"); throw Error("invalid taler://tip URI");
} }
const tipStatusUrl = new URL("tip-pickup", res.merchantBaseUrl); const tipStatusUrl = new URL(
tipStatusUrl.searchParams.set("tip_id", res.merchantTipId); `tips/${res.merchantTipId}`,
res.merchantBaseUrl,
);
logger.trace("checking tip status from", tipStatusUrl.href); logger.trace("checking tip status from", tipStatusUrl.href);
const merchantResp = await ws.http.get(tipStatusUrl.href); const merchantResp = await ws.http.get(tipStatusUrl.href);
const tipPickupStatus = await readSuccessResponseJsonOrThrow( const tipPickupStatus = await readSuccessResponseJsonOrThrow(
@ -68,7 +71,7 @@ export async function getTipStatus(
); );
logger.trace(`status ${tipPickupStatus}`); logger.trace(`status ${tipPickupStatus}`);
const amount = Amounts.parseOrThrow(tipPickupStatus.amount); const amount = Amounts.parseOrThrow(tipPickupStatus.tip_amount);
const merchantOrigin = new URL(res.merchantBaseUrl).origin; const merchantOrigin = new URL(res.merchantBaseUrl).origin;
@ -85,7 +88,7 @@ export async function getTipStatus(
amount, amount,
); );
const tipId = encodeCrock(getRandomBytes(32)); const walletTipId = encodeCrock(getRandomBytes(32));
const selectedDenoms = await selectWithdrawalDenoms( const selectedDenoms = await selectWithdrawalDenoms(
ws, ws,
tipPickupStatus.exchange_url, tipPickupStatus.exchange_url,
@ -93,11 +96,11 @@ export async function getTipStatus(
); );
tipRecord = { tipRecord = {
tipId, walletTipId: walletTipId,
acceptedTimestamp: undefined, acceptedTimestamp: undefined,
rejectedTimestamp: undefined, rejectedTimestamp: undefined,
amount, amount,
deadline: tipPickupStatus.stamp_expire, deadline: tipPickupStatus.expiration,
exchangeUrl: tipPickupStatus.exchange_url, exchangeUrl: tipPickupStatus.exchange_url,
merchantBaseUrl: res.merchantBaseUrl, merchantBaseUrl: res.merchantBaseUrl,
nextUrl: undefined, nextUrl: undefined,
@ -117,18 +120,13 @@ export async function getTipStatus(
await ws.db.put(Stores.tips, tipRecord); await ws.db.put(Stores.tips, tipRecord);
} }
const tipStatus: TipStatus = { const tipStatus: PrepareTipResult = {
accepted: !!tipRecord && !!tipRecord.acceptedTimestamp, accepted: !!tipRecord && !!tipRecord.acceptedTimestamp,
amount: Amounts.parseOrThrow(tipPickupStatus.amount), amount: Amounts.stringify(tipPickupStatus.tip_amount),
amountLeft: Amounts.parseOrThrow(tipPickupStatus.amount_left), exchangeBaseUrl: tipPickupStatus.exchange_url,
exchangeUrl: tipPickupStatus.exchange_url, expirationTimestamp: tipPickupStatus.expiration,
nextUrl: tipPickupStatus.extra.next_url, totalFees: Amounts.stringify(tipRecord.totalFees),
merchantOrigin: merchantOrigin, walletTipId: tipRecord.walletTipId,
merchantTipId: res.merchantTipId,
expirationTimestamp: tipPickupStatus.stamp_expire,
timestamp: tipPickupStatus.stamp_created,
totalFees: tipRecord.totalFees,
tipId: tipRecord.tipId,
}; };
return tipStatus; return tipStatus;
@ -152,7 +150,9 @@ async function incrementTipRetry(
t.lastError = err; t.lastError = err;
await tx.put(Stores.tips, t); await tx.put(Stores.tips, t);
}); });
ws.notify({ type: NotificationType.TipOperationError }); if (err) {
ws.notify({ type: NotificationType.TipOperationError, error: err });
}
} }
export async function processTip( export async function processTip(
@ -225,15 +225,8 @@ async function processTipImpl(
} }
tipRecord = await ws.db.get(Stores.tips, tipId); tipRecord = await ws.db.get(Stores.tips, tipId);
if (!tipRecord) { checkDbInvariant(!!tipRecord, "tip record should be in database");
throw Error("tip not in database"); checkDbInvariant(!!tipRecord.planchets, "tip record should have planchets");
}
if (!tipRecord.planchets) {
throw Error("invariant violated");
}
logger.trace("got planchets for tip!");
// Planchets in the form that the merchant expects // Planchets in the form that the merchant expects
const planchetsDetail: TipPlanchetDetail[] = tipRecord.planchets.map((p) => ({ const planchetsDetail: TipPlanchetDetail[] = tipRecord.planchets.map((p) => ({
@ -241,23 +234,17 @@ async function processTipImpl(
denom_pub_hash: p.denomPubHash, denom_pub_hash: p.denomPubHash,
})); }));
let merchantResp; const tipStatusUrl = new URL(
`/tips/${tipRecord.merchantTipId}/pickup`,
tipRecord.merchantBaseUrl,
);
const tipStatusUrl = new URL("tip-pickup", tipRecord.merchantBaseUrl); const req = { planchets: planchetsDetail };
const merchantResp = await ws.http.postJson(tipStatusUrl.href, req);
try { const response = await readSuccessResponseJsonOrThrow(
const req = { planchets: planchetsDetail, tip_id: tipRecord.merchantTipId }; merchantResp,
merchantResp = await ws.http.postJson(tipStatusUrl.href, req); codecForTipResponse(),
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 response = codecForTipResponse().decode(await merchantResp.json());
if (response.reserve_sigs.length !== tipRecord.planchets.length) { if (response.reserve_sigs.length !== tipRecord.planchets.length) {
throw Error("number of tip responses does not match requested planchets"); throw Error("number of tip responses does not match requested planchets");
@ -293,7 +280,7 @@ async function processTipImpl(
exchangeBaseUrl: tipRecord.exchangeUrl, exchangeBaseUrl: tipRecord.exchangeUrl,
source: { source: {
type: WithdrawalSourceType.Tip, type: WithdrawalSourceType.Tip,
tipId: tipRecord.tipId, tipId: tipRecord.walletTipId,
}, },
timestampStart: getTimestampNow(), timestampStart: getTimestampNow(),
withdrawalGroupId: withdrawalGroupId, withdrawalGroupId: withdrawalGroupId,

View File

@ -986,7 +986,7 @@ export interface TipRecord {
/** /**
* Tip ID chosen by the wallet. * Tip ID chosen by the wallet.
*/ */
tipId: string; walletTipId: string;
/** /**
* The merchant's identifier for this tip. * The merchant's identifier for this tip.
@ -1760,7 +1760,7 @@ class ReserveHistoryStore extends Store<ReserveHistoryRecord> {
class TipsStore extends Store<TipRecord> { class TipsStore extends Store<TipRecord> {
constructor() { constructor() {
super("tips", { keyPath: "tipId" }); super("tips", { keyPath: "walletTipId" });
} }
} }

View File

@ -186,6 +186,7 @@ export interface ProposalOperationErrorNotification {
export interface TipOperationErrorNotification { export interface TipOperationErrorNotification {
type: NotificationType.TipOperationError; type: NotificationType.TipOperationError;
error: TalerErrorDetails;
} }
export interface WithdrawOperationErrorNotification { export interface WithdrawOperationErrorNotification {

View File

@ -773,17 +773,11 @@ export class WithdrawOperationStatusResponse {
* Response from the merchant. * Response from the merchant.
*/ */
export class TipPickupGetResponse { export class TipPickupGetResponse {
extra: any; tip_amount: string;
amount: string;
amount_left: string;
exchange_url: string; exchange_url: string;
stamp_expire: Timestamp; expiration: Timestamp;
stamp_created: Timestamp;
} }
export class WithdrawResponse { export class WithdrawResponse {
@ -1261,12 +1255,9 @@ export const codecForWithdrawOperationStatusResponse = (): Codec<
export const codecForTipPickupGetResponse = (): Codec<TipPickupGetResponse> => export const codecForTipPickupGetResponse = (): Codec<TipPickupGetResponse> =>
buildCodecForObject<TipPickupGetResponse>() buildCodecForObject<TipPickupGetResponse>()
.property("extra", codecForAny()) .property("tip_amount", codecForString())
.property("amount", codecForString())
.property("amount_left", codecForString())
.property("exchange_url", codecForString()) .property("exchange_url", codecForString())
.property("stamp_expire", codecForTimestamp) .property("expiration", codecForTimestamp)
.property("stamp_created", codecForTimestamp)
.build("TipPickupGetResponse"); .build("TipPickupGetResponse");
export const codecForRecoupConfirmation = (): Codec<RecoupConfirmation> => export const codecForRecoupConfirmation = (): Codec<RecoupConfirmation> =>

View File

@ -38,7 +38,7 @@ import {
ExchangeWireInfo, ExchangeWireInfo,
DenominationSelectionInfo, DenominationSelectionInfo,
} from "./dbTypes"; } from "./dbTypes";
import { Timestamp } from "../util/time"; import { Timestamp, codecForTimestamp } from "../util/time";
import { import {
buildCodecForObject, buildCodecForObject,
codecForString, codecForString,
@ -348,23 +348,33 @@ export class ReturnCoinsRequest {
static checked: (obj: any) => ReturnCoinsRequest; static checked: (obj: any) => ReturnCoinsRequest;
} }
/** export interface PrepareTipResult {
* Status of processing a tip. /**
*/ * Unique ID for the tip assigned by the wallet.
export interface TipStatus { * Typically different from the merchant-generated tip ID.
*/
walletTipId: string;
/**
* Has the tip already been accepted?
*/
accepted: boolean; accepted: boolean;
amount: AmountJson; amount: AmountString;
amountLeft: AmountJson; totalFees: AmountString;
nextUrl: string; exchangeBaseUrl: string;
exchangeUrl: string;
tipId: string;
merchantTipId: string;
merchantOrigin: string;
expirationTimestamp: Timestamp; 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 { export interface BenchmarkResult {
time: { [s: string]: number }; time: { [s: string]: number };
repetitions: number; repetitions: number;
@ -903,3 +913,21 @@ export const codecForForceRefreshRequest = (): Codec<ForceRefreshRequest> =>
buildCodecForObject<ForceRefreshRequest>() buildCodecForObject<ForceRefreshRequest>()
.property("coinPubList", codecForList(codecForString())) .property("coinPubList", codecForList(codecForString()))
.build("ForceRefreshRequest"); .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");

View File

@ -59,7 +59,6 @@ import {
ConfirmPayResult, ConfirmPayResult,
ReturnCoinsRequest, ReturnCoinsRequest,
SenderWireInfos, SenderWireInfos,
TipStatus,
PreparePayResult, PreparePayResult,
AcceptWithdrawalResponse, AcceptWithdrawalResponse,
PurchaseDetails, PurchaseDetails,
@ -93,6 +92,9 @@ import {
codecForSetCoinSuspendedRequest, codecForSetCoinSuspendedRequest,
codecForForceExchangeUpdateRequest, codecForForceExchangeUpdateRequest,
codecForForceRefreshRequest, codecForForceRefreshRequest,
PrepareTipResult,
codecForPrepareTipRequest,
codecForAcceptTipRequest,
} from "./types/walletTypes"; } from "./types/walletTypes";
import { Logger } from "./util/logging"; import { Logger } from "./util/logging";
@ -121,7 +123,7 @@ import {
import { processWithdrawGroup } from "./operations/withdraw"; import { processWithdrawGroup } from "./operations/withdraw";
import { getPendingOperations } from "./operations/pending"; import { getPendingOperations } from "./operations/pending";
import { getBalances } from "./operations/balance"; 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 { TimerGroup } from "./util/timer";
import { AsyncCondition } from "./util/promiseUtils"; import { AsyncCondition } from "./util/promiseUtils";
import { AsyncOpMemoSingle } from "./util/asyncMemo"; import { AsyncOpMemoSingle } from "./util/asyncMemo";
@ -769,8 +771,8 @@ export class Wallet {
} }
} }
async getTipStatus(talerTipUri: string): Promise<TipStatus> { async prepareTip(talerTipUri: string): Promise<PrepareTipResult> {
return getTipStatus(this.ws, talerTipUri); return prepareTip(this.ws, talerTipUri);
} }
async abortFailedPayment(contractTermsHash: string): Promise<void> { async abortFailedPayment(contractTermsHash: string): Promise<void> {
@ -1096,6 +1098,15 @@ export class Wallet {
refreshGroupId, 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( throw OperationFailedError.fromCode(
TalerErrorCode.WALLET_CORE_API_OPERATION_UNKNOWN, TalerErrorCode.WALLET_CORE_API_OPERATION_UNKNOWN,

View File

@ -22,12 +22,8 @@
* Imports. * Imports.
*/ */
import { import {
AmountJson,
ConfirmPayResult, ConfirmPayResult,
BalancesResponse, BalancesResponse,
PurchaseDetails,
TipStatus,
BenchmarkResult,
PreparePayResult, PreparePayResult,
AcceptWithdrawalResponse, AcceptWithdrawalResponse,
WalletDiagnostics, WalletDiagnostics,