tipping API and integration test
This commit is contained in:
parent
be77ee284a
commit
b063382d25
@ -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") {
|
||||||
|
@ -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;
|
||||||
|
}
|
106
packages/taler-integrationtests/src/test-tipping.ts
Normal file
106
packages/taler-integrationtests/src/test-tipping.ts
Normal 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);
|
||||||
|
});
|
@ -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:
|
||||||
|
@ -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
|
||||||
|
@ -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,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
|
@ -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" });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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 {
|
||||||
|
@ -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> =>
|
||||||
|
@ -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.
|
||||||
|
* Typically different from the merchant-generated tip ID.
|
||||||
|
*/
|
||||||
|
walletTipId: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Has the tip already been accepted?
|
||||||
*/
|
*/
|
||||||
export interface TipStatus {
|
|
||||||
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");
|
||||||
|
@ -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,
|
||||||
|
@ -22,12 +22,8 @@
|
|||||||
* Imports.
|
* Imports.
|
||||||
*/
|
*/
|
||||||
import {
|
import {
|
||||||
AmountJson,
|
|
||||||
ConfirmPayResult,
|
ConfirmPayResult,
|
||||||
BalancesResponse,
|
BalancesResponse,
|
||||||
PurchaseDetails,
|
|
||||||
TipStatus,
|
|
||||||
BenchmarkResult,
|
|
||||||
PreparePayResult,
|
PreparePayResult,
|
||||||
AcceptWithdrawalResponse,
|
AcceptWithdrawalResponse,
|
||||||
WalletDiagnostics,
|
WalletDiagnostics,
|
||||||
|
Loading…
Reference in New Issue
Block a user