This commit is contained in:
Florian Dold 2019-12-09 19:59:08 +01:00
parent 99bccae9fe
commit 6415564b92
No known key found for this signature in database
GPG Key ID: D2E4F00F29D02A4B
14 changed files with 241 additions and 78 deletions

View File

@ -26,12 +26,20 @@ import {
} from "../headless/helpers";
import { openPromise, OpenedPromise } from "../util/promiseUtils";
import fs = require("fs");
import { HttpRequestLibrary, HttpResponse, HttpRequestOptions } from "../util/http";
import {
HttpRequestLibrary,
HttpResponse,
HttpRequestOptions,
Headers,
} from "../util/http";
// @ts-ignore: special built-in module
//import akono = require("akono");
export { handleWorkerError, handleWorkerMessage } from "../crypto/workers/nodeThreadWorker";
export {
handleWorkerError,
handleWorkerMessage,
} from "../crypto/workers/nodeThreadWorker";
export class AndroidHttpLib implements HttpRequestLibrary {
useNfcTunnel: boolean = false;
@ -66,7 +74,11 @@ export class AndroidHttpLib implements HttpRequestLibrary {
}
}
postJson(url: string, body: any, opt?: HttpRequestOptions): Promise<import("../util/http").HttpResponse> {
postJson(
url: string,
body: any,
opt?: HttpRequestOptions,
): Promise<import("../util/http").HttpResponse> {
if (this.useNfcTunnel) {
const myId = this.requestId++;
const p = openPromise<HttpResponse>();
@ -89,11 +101,14 @@ export class AndroidHttpLib implements HttpRequestLibrary {
const myId = msg.id;
const p = this.requestMap[myId];
if (!p) {
console.error(`no matching request for tunneled HTTP response, id=${myId}`);
console.error(
`no matching request for tunneled HTTP response, id=${myId}`,
);
}
const headers = new Headers();
if (msg.status != 0) {
const resp: HttpResponse = {
headers: {},
headers,
status: msg.status,
json: async () => JSON.parse(msg.responseText),
text: async () => msg.responseText,
@ -146,7 +161,7 @@ export function installAndroidWalletListener() {
};
const w = await getDefaultNodeWallet(walletArgs);
maybeWallet = w;
w.runRetryLoop().catch((e) => {
w.runRetryLoop().catch(e => {
console.error("Error during wallet retry loop", e);
});
wp.resolve(w);
@ -191,7 +206,10 @@ export function installAndroidWalletListener() {
}
case "confirmPay": {
const wallet = await wp.promise;
result = await wallet.confirmPay(msg.args.proposalId, msg.args.sessionId);
result = await wallet.confirmPay(
msg.args.proposalId,
msg.args.sessionId,
);
break;
}
case "startTunnel": {
@ -206,14 +224,28 @@ export function installAndroidWalletListener() {
httpLib.handleTunnelResponse(msg.args);
break;
}
case "getWithdrawalInfo": {
case "getWithdrawDetailsForUri": {
const wallet = await wp.promise;
result = await wallet.getWithdrawalInfo(msg.args.talerWithdrawUri);
result = await wallet.getWithdrawDetailsForUri(
msg.args.talerWithdrawUri,
msg.args.selectedExchange,
);
break;
}
case "acceptExchangeTermsOfService": {
const wallet = await wp.promise;
result = await wallet.acceptExchangeTermsOfService(
msg.args.exchangeBaseUrl,
msg.args.etag,
);
break;
}
case "acceptWithdrawal": {
const wallet = await wp.promise;
result = await wallet.acceptWithdrawal(msg.args.talerWithdrawUri, msg.args.selectedExchange);
result = await wallet.acceptWithdrawal(
msg.args.talerWithdrawUri,
msg.args.selectedExchange,
);
break;
}
case "reset": {
@ -234,7 +266,7 @@ export function installAndroidWalletListener() {
maybeWallet = undefined;
const w = await getDefaultNodeWallet(walletArgs);
maybeWallet = w;
w.runRetryLoop().catch((e) => {
w.runRetryLoop().catch(e => {
console.error("Error during wallet retry loop", e);
});
wp.resolve(w);

View File

@ -455,9 +455,10 @@ export interface ExchangeDetails {
lastUpdateTime: Timestamp;
}
export enum ExchangeUpdateStatus {
export const enum ExchangeUpdateStatus {
FETCH_KEYS = "fetch_keys",
FETCH_WIRE = "fetch_wire",
FETCH_TERMS = "fetch_terms",
FINISHED = "finished",
}
@ -494,6 +495,26 @@ export interface ExchangeRecord {
*/
timestampAdded: Timestamp;
/**
* Terms of service text or undefined if not downloaded yet.
*/
termsOfServiceText: string | undefined;
/**
* ETag for last terms of service download.
*/
termsOfServiceLastEtag: string | undefined;
/**
* ETag for last terms of service download.
*/
termsOfServiceAcceptedEtag: string | undefined;
/**
* ETag for last terms of service download.
*/
termsOfServiceAcceptedTimestamp: Timestamp | undefined;
/**
* Time when the update to the exchange has been started or
* undefined if no update is in progress.

View File

@ -28,6 +28,7 @@ import Axios, { AxiosPromise, AxiosResponse } from "axios";
import {
HttpRequestLibrary,
HttpRequestOptions,
Headers,
} from "../util/http";
import * as amounts from "../util/amounts";
import { Bank } from "./bank";
@ -83,8 +84,12 @@ export class NodeHttpLib implements HttpRequestLibrary {
}
return responseJson;
};
const headers = new Headers();
for (const hn of Object.keys(resp.headers)) {
headers.set(hn, resp.headers[hn]);
}
return {
headers: resp.headers,
headers,
status: resp.status,
text: async () => resp.data,
json: makeJson,

View File

@ -230,8 +230,8 @@ walletCli
break;
case TalerUriType.TalerWithdraw:
{
const withdrawInfo = await wallet.getWithdrawalInfo(uri);
const selectedExchange = withdrawInfo.suggestedExchange;
const withdrawInfo = await wallet.getWithdrawDetailsForUri(uri);
const selectedExchange = withdrawInfo.bankWithdrawDetails.suggestedExchange;
if (!selectedExchange) {
console.error("no suggested exchange!");
process.exit(1);

View File

@ -24,7 +24,7 @@
*/
export interface HttpResponse {
status: number;
headers: { [name: string]: string };
headers: Headers;
json(): Promise<any>;
text(): Promise<string>;
}
@ -33,6 +33,31 @@ export interface HttpRequestOptions {
headers?: { [name: string]: string };
}
/**
* Headers, roughly modeled after the fetch API's headers object.
*/
export class Headers {
private headerMap = new Map<string, string>();
get(name: string): string | null {
const r = this.headerMap.get(name.toLowerCase());
if (r) {
return r;
}
return null;
}
set(name: string, value: string): void {
const normalizedName = name.toLowerCase();
const existing = this.headerMap.get(normalizedName);
if (existing !== undefined) {
this.headerMap.set(normalizedName, existing + "," + value);
} else {
this.headerMap.set(normalizedName, value);
}
}
}
/**
* The request library is bundled into an interface to m responseJson: object & any;ake mocking easy.
*/
@ -103,12 +128,12 @@ export class BrowserHttpLib implements HttpRequestLibrary {
const arr = headers.trim().split(/[\r\n]+/);
// Create a map of header names to values
const headerMap: { [name: string]: string } = {};
const headerMap = new Headers();
arr.forEach(function(line) {
const parts = line.split(": ");
const header = parts.shift();
const value = parts.join(": ");
headerMap[header!] = value;
headerMap.set(header!, value);
});
const resp: HttpResponse = {
status: myRequest.status,

View File

@ -16,11 +16,7 @@
import { InternalWalletState } from "./state";
import { WALLET_CACHE_BREAKER_CLIENT_VERSION } from "../wallet";
import {
KeysJson,
Denomination,
ExchangeWireJson,
} from "../talerTypes";
import { KeysJson, Denomination, ExchangeWireJson } from "../talerTypes";
import { getTimestampNow, OperationError } from "../walletTypes";
import {
ExchangeRecord,
@ -222,6 +218,62 @@ async function updateExchangeWithKeys(
);
}
async function updateExchangeWithTermsOfService(
ws: InternalWalletState,
exchangeBaseUrl: string,
) {
const exchange = await oneShotGet(ws.db, Stores.exchanges, exchangeBaseUrl);
if (!exchange) {
return;
}
if (exchange.updateStatus != ExchangeUpdateStatus.FETCH_TERMS) {
return;
}
const reqUrl = new URL("terms", exchangeBaseUrl);
reqUrl.searchParams.set("cacheBreaker", WALLET_CACHE_BREAKER_CLIENT_VERSION);
const headers = {
Accept: "text/plain",
};
const resp = await ws.http.get(reqUrl.href, { headers });
if (resp.status !== 200) {
throw Error(`/terms response has unexpected status code (${resp.status})`);
}
const tosText = await resp.text();
const tosEtag = resp.headers.get("etag") || undefined;
await runWithWriteTransaction(ws.db, [Stores.exchanges], async tx => {
const r = await tx.get(Stores.exchanges, exchangeBaseUrl);
if (!r) {
return;
}
if (r.updateStatus != ExchangeUpdateStatus.FETCH_TERMS) {
return;
}
r.termsOfServiceText = tosText;
r.termsOfServiceLastEtag = tosEtag;
r.updateStatus = ExchangeUpdateStatus.FINISHED;
await tx.put(Stores.exchanges, r);
});
}
export async function acceptExchangeTermsOfService(
ws: InternalWalletState,
exchangeBaseUrl: string,
etag: string | undefined,
) {
await runWithWriteTransaction(ws.db, [Stores.exchanges], async tx => {
const r = await tx.get(Stores.exchanges, exchangeBaseUrl);
if (!r) {
return;
}
r.termsOfServiceAcceptedEtag = etag;
r.termsOfServiceAcceptedTimestamp = getTimestampNow();
await tx.put(Stores.exchanges, r);
});
}
/**
* Fetch wire information for an exchange and store it in the database.
*
@ -309,7 +361,7 @@ async function updateExchangeWithWireInfo(
accounts: wireInfo.accounts,
feesForType: feesForType,
};
r.updateStatus = ExchangeUpdateStatus.FINISHED;
r.updateStatus = ExchangeUpdateStatus.FETCH_TERMS;
r.lastError = undefined;
await tx.put(Stores.exchanges, r);
});
@ -350,6 +402,10 @@ async function updateExchangeFromUrlImpl(
updateStarted: now,
updateReason: "initial",
timestampAdded: getTimestampNow(),
termsOfServiceAcceptedEtag: undefined,
termsOfServiceAcceptedTimestamp: undefined,
termsOfServiceLastEtag: undefined,
termsOfServiceText: undefined,
};
await oneShotPut(ws.db, Stores.exchanges, newExchangeRecord);
} else {
@ -373,6 +429,7 @@ async function updateExchangeFromUrlImpl(
await updateExchangeWithKeys(ws, baseUrl);
await updateExchangeWithWireInfo(ws, baseUrl);
await updateExchangeWithTermsOfService(ws, baseUrl);
const updatedExchange = await oneShotGet(ws.db, Stores.exchanges, baseUrl);

View File

@ -22,7 +22,7 @@ import { TipStatus, getTimestampNow, OperationError, NotificationType } from "..
import { TipPickupGetResponse, TipPlanchetDetail, TipResponse } from "../talerTypes";
import * as Amounts from "../util/amounts";
import { Stores, PlanchetRecord, WithdrawalSessionRecord, initRetryInfo, updateRetryInfoTimeout } from "../dbTypes";
import { getWithdrawDetailsForAmount, getVerifiedWithdrawDenomList, processWithdrawSession } from "./withdraw";
import { getExchangeWithdrawalInfo, getVerifiedWithdrawDenomList, processWithdrawSession } from "./withdraw";
import { getTalerStampSec, extractTalerStampOrThrow } from "../util/helpers";
import { updateExchangeFromUrl } from "./exchanges";
import { getRandomBytes, encodeCrock } from "../crypto/talerCrypto";
@ -58,7 +58,7 @@ export async function getTipStatus(
]);
if (!tipRecord) {
const withdrawDetails = await getWithdrawDetailsForAmount(
const withdrawDetails = await getExchangeWithdrawalInfo(
ws,
tipPickupStatus.exchange_url,
amount,

View File

@ -29,8 +29,8 @@ import * as Amounts from "../util/amounts";
import {
getTimestampNow,
AcceptWithdrawalResponse,
DownloadedWithdrawInfo,
ReserveCreationInfo,
BankWithdrawDetails,
ExchangeWithdrawDetails,
WithdrawDetails,
OperationError,
NotificationType,
@ -106,12 +106,12 @@ export function getWithdrawDenomList(
/**
* Get information about a withdrawal from
* a taler://withdraw URI.
* a taler://withdraw URI by asking the bank.
*/
export async function getWithdrawalInfo(
async function getBankWithdrawalInfo(
ws: InternalWalletState,
talerWithdrawUri: string,
): Promise<DownloadedWithdrawInfo> {
): Promise<BankWithdrawDetails> {
const uriResult = parseWithdrawUri(talerWithdrawUri);
if (!uriResult) {
throw Error("can't parse URL");
@ -140,7 +140,7 @@ export async function acceptWithdrawal(
talerWithdrawUri: string,
selectedExchange: string,
): Promise<AcceptWithdrawalResponse> {
const withdrawInfo = await getWithdrawalInfo(ws, talerWithdrawUri);
const withdrawInfo = await getBankWithdrawalInfo(ws, talerWithdrawUri);
const exchangeWire = await getExchangePaytoUri(
ws,
selectedExchange,
@ -572,11 +572,11 @@ async function processWithdrawSessionImpl(
return;
}
export async function getWithdrawDetailsForAmount(
export async function getExchangeWithdrawalInfo(
ws: InternalWalletState,
baseUrl: string,
amount: AmountJson,
): Promise<ReserveCreationInfo> {
): Promise<ExchangeWithdrawDetails> {
const exchangeInfo = await updateExchangeFromUrl(ws, baseUrl);
const exchangeDetails = exchangeInfo.details;
if (!exchangeDetails) {
@ -650,7 +650,15 @@ export async function getWithdrawDetailsForAmount(
}
}
const ret: ReserveCreationInfo = {
let tosAccepted = false;
if (exchangeInfo.termsOfServiceAcceptedTimestamp) {
if (exchangeInfo.termsOfServiceAcceptedEtag == exchangeInfo.termsOfServiceLastEtag) {
tosAccepted = true;
}
}
const ret: ExchangeWithdrawDetails = {
earliestDepositExpiration,
exchangeInfo,
exchangeWireAccounts,
@ -665,6 +673,7 @@ export async function getWithdrawDetailsForAmount(
walletVersion: WALLET_PROTOCOL_VERSION,
wireFees: exchangeWireInfo,
withdrawFee: acc,
termsOfServiceAccepted: tosAccepted,
};
return ret;
}
@ -674,17 +683,17 @@ export async function getWithdrawDetailsForUri(
talerWithdrawUri: string,
maybeSelectedExchange?: string,
): Promise<WithdrawDetails> {
const info = await getWithdrawalInfo(ws, talerWithdrawUri);
let rci: ReserveCreationInfo | undefined = undefined;
const info = await getBankWithdrawalInfo(ws, talerWithdrawUri);
let rci: ExchangeWithdrawDetails | undefined = undefined;
if (maybeSelectedExchange) {
rci = await getWithdrawDetailsForAmount(
rci = await getExchangeWithdrawalInfo(
ws,
maybeSelectedExchange,
info.amount,
);
}
return {
withdrawInfo: info,
reserveCreationInfo: rci,
bankWithdrawDetails: info,
exchangeWithdrawDetails: rci,
};
}

View File

@ -37,9 +37,8 @@ import * as Amounts from "./util/amounts";
import {
acceptWithdrawal,
getWithdrawalInfo,
getWithdrawDetailsForUri,
getWithdrawDetailsForAmount,
getExchangeWithdrawalInfo,
} from "./wallet-impl/withdraw";
import {
@ -79,7 +78,7 @@ import {
TipStatus,
WalletBalance,
PreparePayResult,
DownloadedWithdrawInfo,
BankWithdrawDetails,
WithdrawDetails,
AcceptWithdrawalResponse,
PurchaseDetails,
@ -88,6 +87,7 @@ import {
HistoryQuery,
WalletNotification,
NotificationType,
ExchangeWithdrawDetails,
} from "./walletTypes";
import { Logger } from "./util/logging";
@ -97,6 +97,7 @@ import {
updateExchangeFromUrl,
getExchangeTrust,
getExchangePaytoUri,
acceptExchangeTermsOfService,
} from "./wallet-impl/exchanges";
import { processReserve } from "./wallet-impl/reserves";
@ -167,8 +168,11 @@ export class Wallet {
return getExchangePaytoUri(this.ws, exchangeBaseUrl, supportedTargetTypes);
}
getWithdrawDetailsForAmount(baseUrl: any, amount: AmountJson): any {
return getWithdrawDetailsForAmount(this.ws, baseUrl, amount);
getWithdrawDetailsForAmount(
exchangeBaseUrl: string,
amount: AmountJson,
): Promise<ExchangeWithdrawDetails> {
return getExchangeWithdrawalInfo(this.ws, exchangeBaseUrl, amount);
}
addNotificationListener(f: (n: WalletNotification) => void): void {
@ -194,13 +198,21 @@ export class Wallet {
await updateExchangeFromUrl(this.ws, pending.exchangeBaseUrl, forceNow);
break;
case "refresh":
await processRefreshSession(this.ws, pending.refreshSessionId, forceNow);
await processRefreshSession(
this.ws,
pending.refreshSessionId,
forceNow,
);
break;
case "reserve":
await processReserve(this.ws, pending.reservePub, forceNow);
break;
case "withdraw":
await processWithdrawSession(this.ws, pending.withdrawSessionId, forceNow);
await processWithdrawSession(
this.ws,
pending.withdrawSessionId,
forceNow,
);
break;
case "proposal-choice":
// Nothing to do, user needs to accept/reject
@ -524,6 +536,13 @@ export class Wallet {
);
}
async acceptExchangeTermsOfService(
exchangeBaseUrl: string,
etag: string | undefined,
) {
return acceptExchangeTermsOfService(this.ws, exchangeBaseUrl, etag);
}
async getDenoms(exchangeUrl: string): Promise<DenominationRecord[]> {
const denoms = await oneShotIterIndex(
this.db,
@ -663,6 +682,10 @@ export class Wallet {
}
}
/**
* Inform the wallet that the status of a reserve has changed (e.g. due to a
* confirmation from the bank.).
*/
public async handleNotifyReserve() {
const reserves = await oneShotIter(this.db, Stores.reserves).toArray();
for (const r of reserves) {
@ -687,20 +710,6 @@ export class Wallet {
// strategy to test it.
}
/**
* Get information about a withdrawal from
* a taler://withdraw URI.
*/
async getWithdrawalInfo(
talerWithdrawUri: string,
): Promise<DownloadedWithdrawInfo> {
try {
return getWithdrawalInfo(this.ws, talerWithdrawUri);
} finally {
this.latch.trigger();
}
}
async acceptWithdrawal(
talerWithdrawUri: string,
selectedExchange: string,

View File

@ -70,7 +70,7 @@ export class CreateReserveResponse {
*
* Sent to the wallet frontend to be rendered and shown to the user.
*/
export interface ReserveCreationInfo {
export interface ExchangeWithdrawDetails {
/**
* Exchange that the reserve will be created at.
*/
@ -107,6 +107,11 @@ export interface ReserveCreationInfo {
*/
isAudited: boolean;
/**
* Did the user already accept the current terms of service for the exchange?
*/
termsOfServiceAccepted: boolean;
/**
* The exchange is trusted directly.
*/
@ -148,8 +153,8 @@ export interface ReserveCreationInfo {
}
export interface WithdrawDetails {
withdrawInfo: DownloadedWithdrawInfo;
reserveCreationInfo: ReserveCreationInfo | undefined;
bankWithdrawDetails: BankWithdrawDetails;
exchangeWithdrawDetails: ExchangeWithdrawDetails | undefined;
}
/**
@ -449,7 +454,7 @@ export interface PreparePayResultPaid {
nextUrl: string;
}
export interface DownloadedWithdrawInfo {
export interface BankWithdrawDetails {
selectionDone: boolean;
transferDone: boolean;
amount: AmountJson;

View File

@ -75,7 +75,7 @@ export interface MessageMap {
};
"reserve-creation-info": {
request: { baseUrl: string; amount: AmountJson };
response: walletTypes.ReserveCreationInfo;
response: walletTypes.ExchangeWithdrawDetails;
};
"get-history": {
request: {};

View File

@ -57,9 +57,9 @@ function NewExchangeSelection(props: { talerWithdrawUri: string }) {
return;
}
console.log("got withdrawDetails", d);
if (!selectedExchange && d.withdrawInfo.suggestedExchange) {
if (!selectedExchange && d.bankWithdrawDetails.suggestedExchange) {
console.log("setting selected exchange");
setSelectedExchange(d.withdrawInfo.suggestedExchange);
setSelectedExchange(d.bankWithdrawDetails.suggestedExchange);
}
setDetails(d);
};
@ -101,7 +101,7 @@ function NewExchangeSelection(props: { talerWithdrawUri: string }) {
}
if (selecting) {
const bankSuggestion = details && details.withdrawInfo.suggestedExchange;
const bankSuggestion = details && details.bankWithdrawDetails.suggestedExchange;
return (
<div>
{i18n.str`Please select an exchange. You can review the details before after your selection.`}
@ -157,7 +157,7 @@ function NewExchangeSelection(props: { talerWithdrawUri: string }) {
<div>
<i18n.Translate wrap="p">
You are about to withdraw{" "}
<strong>{renderAmount(details.withdrawInfo.amount)}</strong> from your
<strong>{renderAmount(details.bankWithdrawDetails.amount)}</strong> from your
bank account into your wallet.
</i18n.Translate>
<div>
@ -188,8 +188,8 @@ function NewExchangeSelection(props: { talerWithdrawUri: string }) {
</span>
</p>
{details.reserveCreationInfo ? (
<WithdrawDetailView rci={details.reserveCreationInfo} />
{details.exchangeWithdrawDetails ? (
<WithdrawDetailView rci={details.exchangeWithdrawDetails} />
) : null}
</div>
</div>

View File

@ -26,7 +26,7 @@
import { AmountJson } from "../util/amounts";
import * as Amounts from "../util/amounts";
import { DenominationRecord } from "../dbTypes";
import { ReserveCreationInfo } from "../walletTypes";
import { ExchangeWithdrawDetails } from "../walletTypes";
import * as moment from "moment";
import * as i18n from "../i18n";
import React from "react";
@ -126,7 +126,7 @@ export class Collapsible extends React.Component<
}
function AuditorDetailsView(props: {
rci: ReserveCreationInfo | null;
rci: ExchangeWithdrawDetails | null;
}): JSX.Element {
const rci = props.rci;
console.log("rci", rci);
@ -163,7 +163,7 @@ function AuditorDetailsView(props: {
}
function FeeDetailsView(props: {
rci: ReserveCreationInfo | null;
rci: ExchangeWithdrawDetails | null;
}): JSX.Element {
const rci = props.rci;
if (!rci) {
@ -271,7 +271,7 @@ function FeeDetailsView(props: {
* Shows details about a withdraw request.
*/
export function WithdrawDetailView(props: {
rci: ReserveCreationInfo | null;
rci: ExchangeWithdrawDetails | null;
}): JSX.Element {
const rci = props.rci;
return (

View File

@ -34,7 +34,7 @@ import {
import {
BenchmarkResult,
ConfirmPayResult,
ReserveCreationInfo,
ExchangeWithdrawDetails,
SenderWireInfos,
TipStatus,
WalletBalance,
@ -102,7 +102,7 @@ async function callBackend<T extends MessageType>(
* from a given reserve.
*/
export function getReserveCreationInfo(baseUrl: string,
amount: AmountJson): Promise<ReserveCreationInfo> {
amount: AmountJson): Promise<ExchangeWithdrawDetails> {
return callBackend("reserve-creation-info", { baseUrl, amount });
}