consistent error handling for HTTP request (and some other things)
This commit is contained in:
parent
f4a8702b3c
commit
e60563fb54
@ -114,6 +114,8 @@ export class AndroidHttpLib implements HttpRequestLibrary {
|
|||||||
const headers = new Headers();
|
const headers = new Headers();
|
||||||
if (msg.status != 0) {
|
if (msg.status != 0) {
|
||||||
const resp: HttpResponse = {
|
const resp: HttpResponse = {
|
||||||
|
// FIXME: pass through this URL
|
||||||
|
requestUrl: "",
|
||||||
headers,
|
headers,
|
||||||
status: msg.status,
|
status: msg.status,
|
||||||
json: async () => JSON.parse(msg.responseText),
|
json: async () => JSON.parse(msg.responseText),
|
||||||
@ -196,7 +198,10 @@ class AndroidWalletMessageHandler {
|
|||||||
}
|
}
|
||||||
case "getWithdrawalDetailsForAmount": {
|
case "getWithdrawalDetailsForAmount": {
|
||||||
const wallet = await this.wp.promise;
|
const wallet = await this.wp.promise;
|
||||||
return await wallet.getWithdrawalDetailsForAmount(args.exchangeBaseUrl, args.amount);
|
return await wallet.getWithdrawalDetailsForAmount(
|
||||||
|
args.exchangeBaseUrl,
|
||||||
|
args.amount,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
case "withdrawTestkudos": {
|
case "withdrawTestkudos": {
|
||||||
const wallet = await this.wp.promise;
|
const wallet = await this.wp.promise;
|
||||||
@ -218,7 +223,10 @@ class AndroidWalletMessageHandler {
|
|||||||
}
|
}
|
||||||
case "setExchangeTosAccepted": {
|
case "setExchangeTosAccepted": {
|
||||||
const wallet = await this.wp.promise;
|
const wallet = await this.wp.promise;
|
||||||
await wallet.acceptExchangeTermsOfService(args.exchangeBaseUrl, args.acceptedEtag);
|
await wallet.acceptExchangeTermsOfService(
|
||||||
|
args.exchangeBaseUrl,
|
||||||
|
args.acceptedEtag,
|
||||||
|
);
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
case "retryPendingNow": {
|
case "retryPendingNow": {
|
||||||
@ -237,7 +245,10 @@ class AndroidWalletMessageHandler {
|
|||||||
}
|
}
|
||||||
case "acceptManualWithdrawal": {
|
case "acceptManualWithdrawal": {
|
||||||
const wallet = await this.wp.promise;
|
const wallet = await this.wp.promise;
|
||||||
const res = await wallet.acceptManualWithdrawal(args.exchangeBaseUrl, Amounts.parseOrThrow(args.amount));
|
const res = await wallet.acceptManualWithdrawal(
|
||||||
|
args.exchangeBaseUrl,
|
||||||
|
Amounts.parseOrThrow(args.amount),
|
||||||
|
);
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
case "startTunnel": {
|
case "startTunnel": {
|
||||||
|
@ -27,6 +27,8 @@ import {
|
|||||||
} from "../util/http";
|
} from "../util/http";
|
||||||
import { RequestThrottler } from "../util/RequestThrottler";
|
import { RequestThrottler } from "../util/RequestThrottler";
|
||||||
import Axios from "axios";
|
import Axios from "axios";
|
||||||
|
import { OperationFailedError, makeErrorDetails } from "../operations/errors";
|
||||||
|
import { TalerErrorCode } from "../TalerErrorCode";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Implementation of the HTTP request library interface for node.
|
* Implementation of the HTTP request library interface for node.
|
||||||
@ -63,17 +65,44 @@ export class NodeHttpLib implements HttpRequestLibrary {
|
|||||||
|
|
||||||
const respText = resp.data;
|
const respText = resp.data;
|
||||||
if (typeof respText !== "string") {
|
if (typeof respText !== "string") {
|
||||||
throw Error("unexpected response type");
|
throw new OperationFailedError(
|
||||||
|
makeErrorDetails(
|
||||||
|
TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE,
|
||||||
|
"unexpected response type",
|
||||||
|
{
|
||||||
|
httpStatusCode: resp.status,
|
||||||
|
requestUrl: url,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
const makeJson = async (): Promise<any> => {
|
const makeJson = async (): Promise<any> => {
|
||||||
let responseJson;
|
let responseJson;
|
||||||
try {
|
try {
|
||||||
responseJson = JSON.parse(respText);
|
responseJson = JSON.parse(respText);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw Error("Invalid JSON from HTTP response");
|
throw new OperationFailedError(
|
||||||
|
makeErrorDetails(
|
||||||
|
TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE,
|
||||||
|
"invalid JSON",
|
||||||
|
{
|
||||||
|
httpStatusCode: resp.status,
|
||||||
|
requestUrl: url,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if (responseJson === null || typeof responseJson !== "object") {
|
if (responseJson === null || typeof responseJson !== "object") {
|
||||||
throw Error("Invalid JSON from HTTP response");
|
throw new OperationFailedError(
|
||||||
|
makeErrorDetails(
|
||||||
|
TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE,
|
||||||
|
"invalid JSON",
|
||||||
|
{
|
||||||
|
httpStatusCode: resp.status,
|
||||||
|
requestUrl: url,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return responseJson;
|
return responseJson;
|
||||||
};
|
};
|
||||||
@ -82,6 +111,7 @@ export class NodeHttpLib implements HttpRequestLibrary {
|
|||||||
headers.set(hn, resp.headers[hn]);
|
headers.set(hn, resp.headers[hn]);
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
|
requestUrl: url,
|
||||||
headers,
|
headers,
|
||||||
status: resp.status,
|
status: resp.status,
|
||||||
text: async () => resp.data,
|
text: async () => resp.data,
|
||||||
|
@ -143,7 +143,10 @@ export async function withdrawTestBalance(
|
|||||||
exchangeBaseUrl = "https://exchange.test.taler.net/",
|
exchangeBaseUrl = "https://exchange.test.taler.net/",
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
await myWallet.updateExchangeFromUrl(exchangeBaseUrl, true);
|
await myWallet.updateExchangeFromUrl(exchangeBaseUrl, true);
|
||||||
const reserveResponse = await myWallet.acceptManualWithdrawal(exchangeBaseUrl, Amounts.parseOrThrow(amount));
|
const reserveResponse = await myWallet.acceptManualWithdrawal(
|
||||||
|
exchangeBaseUrl,
|
||||||
|
Amounts.parseOrThrow(amount),
|
||||||
|
);
|
||||||
|
|
||||||
const reservePub = reserveResponse.reservePub;
|
const reservePub = reserveResponse.reservePub;
|
||||||
|
|
||||||
|
@ -25,7 +25,6 @@ import { NodeHttpLib } from "./NodeHttpLib";
|
|||||||
import { Wallet } from "../wallet";
|
import { Wallet } from "../wallet";
|
||||||
import { Configuration } from "../util/talerconfig";
|
import { Configuration } from "../util/talerconfig";
|
||||||
import { Amounts, AmountJson } from "../util/amounts";
|
import { Amounts, AmountJson } from "../util/amounts";
|
||||||
import { OperationFailedAndReportedError, OperationFailedError } from "../operations/errors";
|
|
||||||
|
|
||||||
const logger = new Logger("integrationtest.ts");
|
const logger = new Logger("integrationtest.ts");
|
||||||
|
|
||||||
@ -70,9 +69,9 @@ async function makePayment(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const confirmPayResult = await wallet.confirmPay(
|
const confirmPayResult = await wallet.confirmPay(
|
||||||
preparePayResult.proposalId,
|
preparePayResult.proposalId,
|
||||||
undefined,
|
undefined,
|
||||||
);
|
);
|
||||||
|
|
||||||
console.log("confirmPayResult", confirmPayResult);
|
console.log("confirmPayResult", confirmPayResult);
|
||||||
|
|
||||||
|
@ -37,7 +37,10 @@ export class MerchantBackendConnection {
|
|||||||
reason: string,
|
reason: string,
|
||||||
refundAmount: string,
|
refundAmount: string,
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
const reqUrl = new URL(`private/orders/${orderId}/refund`, this.merchantBaseUrl);
|
const reqUrl = new URL(
|
||||||
|
`private/orders/${orderId}/refund`,
|
||||||
|
this.merchantBaseUrl,
|
||||||
|
);
|
||||||
const refundReq = {
|
const refundReq = {
|
||||||
reason,
|
reason,
|
||||||
refund: refundAmount,
|
refund: refundAmount,
|
||||||
@ -123,7 +126,8 @@ export class MerchantBackendConnection {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async checkPayment(orderId: string): Promise<CheckPaymentResponse> {
|
async checkPayment(orderId: string): Promise<CheckPaymentResponse> {
|
||||||
const reqUrl = new URL(`private/orders/${orderId}`, this.merchantBaseUrl).href;
|
const reqUrl = new URL(`private/orders/${orderId}`, this.merchantBaseUrl)
|
||||||
|
.href;
|
||||||
const resp = await axios({
|
const resp = await axios({
|
||||||
method: "get",
|
method: "get",
|
||||||
url: reqUrl,
|
url: reqUrl,
|
||||||
|
@ -30,7 +30,7 @@ import {
|
|||||||
setupRefreshPlanchet,
|
setupRefreshPlanchet,
|
||||||
encodeCrock,
|
encodeCrock,
|
||||||
} from "../crypto/talerCrypto";
|
} from "../crypto/talerCrypto";
|
||||||
import { OperationFailedAndReportedError, OperationFailedError } from "../operations/errors";
|
import { OperationFailedAndReportedError } from "../operations/errors";
|
||||||
import { Bank } from "./bank";
|
import { Bank } from "./bank";
|
||||||
import { classifyTalerUri, TalerUriType } from "../util/taleruri";
|
import { classifyTalerUri, TalerUriType } from "../util/taleruri";
|
||||||
import { Configuration } from "../util/talerconfig";
|
import { Configuration } from "../util/talerconfig";
|
||||||
@ -527,7 +527,10 @@ advancedCli
|
|||||||
.maybeOption("sessionIdOverride", ["--session-id"], clk.STRING)
|
.maybeOption("sessionIdOverride", ["--session-id"], clk.STRING)
|
||||||
.action(async (args) => {
|
.action(async (args) => {
|
||||||
await withWallet(args, async (wallet) => {
|
await withWallet(args, async (wallet) => {
|
||||||
wallet.confirmPay(args.payConfirm.proposalId, args.payConfirm.sessionIdOverride);
|
wallet.confirmPay(
|
||||||
|
args.payConfirm.proposalId,
|
||||||
|
args.payConfirm.sessionIdOverride,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -23,14 +23,15 @@
|
|||||||
/**
|
/**
|
||||||
* Imports.
|
* Imports.
|
||||||
*/
|
*/
|
||||||
import { OperationError } from "../types/walletTypes";
|
import { OperationErrorDetails } from "../types/walletTypes";
|
||||||
|
import { TalerErrorCode } from "../TalerErrorCode";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This exception is there to let the caller know that an error happened,
|
* This exception is there to let the caller know that an error happened,
|
||||||
* but the error has already been reported by writing it to the database.
|
* but the error has already been reported by writing it to the database.
|
||||||
*/
|
*/
|
||||||
export class OperationFailedAndReportedError extends Error {
|
export class OperationFailedAndReportedError extends Error {
|
||||||
constructor(public operationError: OperationError) {
|
constructor(public operationError: OperationErrorDetails) {
|
||||||
super(operationError.message);
|
super(operationError.message);
|
||||||
|
|
||||||
// Set the prototype explicitly.
|
// Set the prototype explicitly.
|
||||||
@ -43,7 +44,15 @@ export class OperationFailedAndReportedError extends Error {
|
|||||||
* responsible for recording the failure in the database.
|
* responsible for recording the failure in the database.
|
||||||
*/
|
*/
|
||||||
export class OperationFailedError extends Error {
|
export class OperationFailedError extends Error {
|
||||||
constructor(public operationError: OperationError) {
|
static fromCode(
|
||||||
|
ec: TalerErrorCode,
|
||||||
|
message: string,
|
||||||
|
details: Record<string, unknown>,
|
||||||
|
): OperationFailedError {
|
||||||
|
return new OperationFailedError(makeErrorDetails(ec, message, details));
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(public operationError: OperationErrorDetails) {
|
||||||
super(operationError.message);
|
super(operationError.message);
|
||||||
|
|
||||||
// Set the prototype explicitly.
|
// Set the prototype explicitly.
|
||||||
@ -51,6 +60,19 @@ export class OperationFailedError extends Error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function makeErrorDetails(
|
||||||
|
ec: TalerErrorCode,
|
||||||
|
message: string,
|
||||||
|
details: Record<string, unknown>,
|
||||||
|
): OperationErrorDetails {
|
||||||
|
return {
|
||||||
|
talerErrorCode: ec,
|
||||||
|
talerErrorHint: `Error: ${TalerErrorCode[ec]}`,
|
||||||
|
details: details,
|
||||||
|
message,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Run an operation and call the onOpError callback
|
* Run an operation and call the onOpError callback
|
||||||
* when there was an exception or operation error that must be reported.
|
* when there was an exception or operation error that must be reported.
|
||||||
@ -58,7 +80,7 @@ export class OperationFailedError extends Error {
|
|||||||
*/
|
*/
|
||||||
export async function guardOperationException<T>(
|
export async function guardOperationException<T>(
|
||||||
op: () => Promise<T>,
|
op: () => Promise<T>,
|
||||||
onOpError: (e: OperationError) => Promise<void>,
|
onOpError: (e: OperationErrorDetails) => Promise<void>,
|
||||||
): Promise<T> {
|
): Promise<T> {
|
||||||
try {
|
try {
|
||||||
return await op();
|
return await op();
|
||||||
@ -71,21 +93,28 @@ export async function guardOperationException<T>(
|
|||||||
throw new OperationFailedAndReportedError(e.operationError);
|
throw new OperationFailedAndReportedError(e.operationError);
|
||||||
}
|
}
|
||||||
if (e instanceof Error) {
|
if (e instanceof Error) {
|
||||||
const opErr = {
|
const opErr = makeErrorDetails(
|
||||||
type: "exception",
|
TalerErrorCode.WALLET_UNEXPECTED_EXCEPTION,
|
||||||
message: e.message,
|
`unexpected exception (message: ${e.message})`,
|
||||||
details: {},
|
{},
|
||||||
};
|
);
|
||||||
await onOpError(opErr);
|
await onOpError(opErr);
|
||||||
throw new OperationFailedAndReportedError(opErr);
|
throw new OperationFailedAndReportedError(opErr);
|
||||||
}
|
}
|
||||||
const opErr = {
|
// Something was thrown that is not even an exception!
|
||||||
type: "exception",
|
// Try to stringify it.
|
||||||
message: "unexpected exception thrown",
|
let excString: string;
|
||||||
details: {
|
try {
|
||||||
value: e.toString(),
|
excString = e.toString();
|
||||||
},
|
} catch (e) {
|
||||||
};
|
// Something went horribly wrong.
|
||||||
|
excString = "can't stringify exception";
|
||||||
|
}
|
||||||
|
const opErr = makeErrorDetails(
|
||||||
|
TalerErrorCode.WALLET_UNEXPECTED_EXCEPTION,
|
||||||
|
`unexpected exception (not an exception, ${excString})`,
|
||||||
|
{},
|
||||||
|
);
|
||||||
await onOpError(opErr);
|
await onOpError(opErr);
|
||||||
throw new OperationFailedAndReportedError(opErr);
|
throw new OperationFailedAndReportedError(opErr);
|
||||||
}
|
}
|
||||||
|
@ -16,12 +16,11 @@
|
|||||||
|
|
||||||
import { InternalWalletState } from "./state";
|
import { InternalWalletState } from "./state";
|
||||||
import {
|
import {
|
||||||
ExchangeKeysJson,
|
|
||||||
Denomination,
|
Denomination,
|
||||||
codecForExchangeKeysJson,
|
codecForExchangeKeysJson,
|
||||||
codecForExchangeWireJson,
|
codecForExchangeWireJson,
|
||||||
} from "../types/talerTypes";
|
} from "../types/talerTypes";
|
||||||
import { OperationError } from "../types/walletTypes";
|
import { OperationErrorDetails } from "../types/walletTypes";
|
||||||
import {
|
import {
|
||||||
ExchangeRecord,
|
ExchangeRecord,
|
||||||
ExchangeUpdateStatus,
|
ExchangeUpdateStatus,
|
||||||
@ -38,6 +37,7 @@ import { parsePaytoUri } from "../util/payto";
|
|||||||
import {
|
import {
|
||||||
OperationFailedAndReportedError,
|
OperationFailedAndReportedError,
|
||||||
guardOperationException,
|
guardOperationException,
|
||||||
|
makeErrorDetails,
|
||||||
} from "./errors";
|
} from "./errors";
|
||||||
import {
|
import {
|
||||||
WALLET_CACHE_BREAKER_CLIENT_VERSION,
|
WALLET_CACHE_BREAKER_CLIENT_VERSION,
|
||||||
@ -46,6 +46,11 @@ import {
|
|||||||
import { getTimestampNow } from "../util/time";
|
import { getTimestampNow } from "../util/time";
|
||||||
import { compare } from "../util/libtoolVersion";
|
import { compare } from "../util/libtoolVersion";
|
||||||
import { createRecoupGroup, processRecoupGroup } from "./recoup";
|
import { createRecoupGroup, processRecoupGroup } from "./recoup";
|
||||||
|
import { TalerErrorCode } from "../TalerErrorCode";
|
||||||
|
import {
|
||||||
|
readSuccessResponseJsonOrThrow,
|
||||||
|
readSuccessResponseTextOrThrow,
|
||||||
|
} from "../util/http";
|
||||||
|
|
||||||
async function denominationRecordFromKeys(
|
async function denominationRecordFromKeys(
|
||||||
ws: InternalWalletState,
|
ws: InternalWalletState,
|
||||||
@ -77,7 +82,7 @@ async function denominationRecordFromKeys(
|
|||||||
async function setExchangeError(
|
async function setExchangeError(
|
||||||
ws: InternalWalletState,
|
ws: InternalWalletState,
|
||||||
baseUrl: string,
|
baseUrl: string,
|
||||||
err: OperationError,
|
err: OperationErrorDetails,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
console.log(`last error for exchange ${baseUrl}:`, err);
|
console.log(`last error for exchange ${baseUrl}:`, err);
|
||||||
const mut = (exchange: ExchangeRecord): ExchangeRecord => {
|
const mut = (exchange: ExchangeRecord): ExchangeRecord => {
|
||||||
@ -102,88 +107,40 @@ async function updateExchangeWithKeys(
|
|||||||
if (existingExchangeRecord?.updateStatus != ExchangeUpdateStatus.FetchKeys) {
|
if (existingExchangeRecord?.updateStatus != ExchangeUpdateStatus.FetchKeys) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const keysUrl = new URL("keys", baseUrl);
|
const keysUrl = new URL("keys", baseUrl);
|
||||||
keysUrl.searchParams.set("cacheBreaker", WALLET_CACHE_BREAKER_CLIENT_VERSION);
|
keysUrl.searchParams.set("cacheBreaker", WALLET_CACHE_BREAKER_CLIENT_VERSION);
|
||||||
|
|
||||||
let keysResp;
|
const resp = await ws.http.get(keysUrl.href);
|
||||||
try {
|
const exchangeKeysJson = await readSuccessResponseJsonOrThrow(
|
||||||
const r = await ws.http.get(keysUrl.href);
|
resp,
|
||||||
if (r.status !== 200) {
|
codecForExchangeKeysJson(),
|
||||||
throw Error(`unexpected status for keys: ${r.status}`);
|
);
|
||||||
}
|
|
||||||
keysResp = await r.json();
|
|
||||||
} catch (e) {
|
|
||||||
const m = `Fetching keys failed: ${e.message}`;
|
|
||||||
const opErr = {
|
|
||||||
type: "network",
|
|
||||||
details: {
|
|
||||||
requestUrl: e.config?.url,
|
|
||||||
},
|
|
||||||
message: m,
|
|
||||||
};
|
|
||||||
await setExchangeError(ws, baseUrl, opErr);
|
|
||||||
throw new OperationFailedAndReportedError(opErr);
|
|
||||||
}
|
|
||||||
let exchangeKeysJson: ExchangeKeysJson;
|
|
||||||
try {
|
|
||||||
exchangeKeysJson = codecForExchangeKeysJson().decode(keysResp);
|
|
||||||
} catch (e) {
|
|
||||||
const m = `Parsing /keys response failed: ${e.message}`;
|
|
||||||
const opErr = {
|
|
||||||
type: "protocol-violation",
|
|
||||||
details: {},
|
|
||||||
message: m,
|
|
||||||
};
|
|
||||||
await setExchangeError(ws, baseUrl, opErr);
|
|
||||||
throw new OperationFailedAndReportedError(opErr);
|
|
||||||
}
|
|
||||||
|
|
||||||
const lastUpdateTimestamp = exchangeKeysJson.list_issue_date;
|
|
||||||
if (!lastUpdateTimestamp) {
|
|
||||||
const m = `Parsing /keys response failed: invalid list_issue_date.`;
|
|
||||||
const opErr = {
|
|
||||||
type: "protocol-violation",
|
|
||||||
details: {},
|
|
||||||
message: m,
|
|
||||||
};
|
|
||||||
await setExchangeError(ws, baseUrl, opErr);
|
|
||||||
throw new OperationFailedAndReportedError(opErr);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (exchangeKeysJson.denoms.length === 0) {
|
if (exchangeKeysJson.denoms.length === 0) {
|
||||||
const m = "exchange doesn't offer any denominations";
|
const opErr = makeErrorDetails(
|
||||||
const opErr = {
|
TalerErrorCode.WALLET_EXCHANGE_DENOMINATIONS_INSUFFICIENT,
|
||||||
type: "protocol-violation",
|
"exchange doesn't offer any denominations",
|
||||||
details: {},
|
{
|
||||||
message: m,
|
exchangeBaseUrl: baseUrl,
|
||||||
};
|
},
|
||||||
|
);
|
||||||
await setExchangeError(ws, baseUrl, opErr);
|
await setExchangeError(ws, baseUrl, opErr);
|
||||||
throw new OperationFailedAndReportedError(opErr);
|
throw new OperationFailedAndReportedError(opErr);
|
||||||
}
|
}
|
||||||
|
|
||||||
const protocolVersion = exchangeKeysJson.version;
|
const protocolVersion = exchangeKeysJson.version;
|
||||||
if (!protocolVersion) {
|
|
||||||
const m = "outdate exchange, no version in /keys response";
|
|
||||||
const opErr = {
|
|
||||||
type: "protocol-violation",
|
|
||||||
details: {},
|
|
||||||
message: m,
|
|
||||||
};
|
|
||||||
await setExchangeError(ws, baseUrl, opErr);
|
|
||||||
throw new OperationFailedAndReportedError(opErr);
|
|
||||||
}
|
|
||||||
|
|
||||||
const versionRes = compare(WALLET_EXCHANGE_PROTOCOL_VERSION, protocolVersion);
|
const versionRes = compare(WALLET_EXCHANGE_PROTOCOL_VERSION, protocolVersion);
|
||||||
if (versionRes?.compatible != true) {
|
if (versionRes?.compatible != true) {
|
||||||
const m = "exchange protocol version not compatible with wallet";
|
const opErr = makeErrorDetails(
|
||||||
const opErr = {
|
TalerErrorCode.WALLET_EXCHANGE_PROTOCOL_VERSION_INCOMPATIBLE,
|
||||||
type: "protocol-incompatible",
|
"exchange protocol version not compatible with wallet",
|
||||||
details: {
|
{
|
||||||
exchangeProtocolVersion: protocolVersion,
|
exchangeProtocolVersion: protocolVersion,
|
||||||
walletProtocolVersion: WALLET_EXCHANGE_PROTOCOL_VERSION,
|
walletProtocolVersion: WALLET_EXCHANGE_PROTOCOL_VERSION,
|
||||||
},
|
},
|
||||||
message: m,
|
);
|
||||||
};
|
|
||||||
await setExchangeError(ws, baseUrl, opErr);
|
await setExchangeError(ws, baseUrl, opErr);
|
||||||
throw new OperationFailedAndReportedError(opErr);
|
throw new OperationFailedAndReportedError(opErr);
|
||||||
}
|
}
|
||||||
@ -197,6 +154,8 @@ async function updateExchangeWithKeys(
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const lastUpdateTimestamp = getTimestampNow();
|
||||||
|
|
||||||
const recoupGroupId: string | undefined = undefined;
|
const recoupGroupId: string | undefined = undefined;
|
||||||
|
|
||||||
await ws.db.runWithWriteTransaction(
|
await ws.db.runWithWriteTransaction(
|
||||||
@ -331,11 +290,7 @@ async function updateExchangeWithTermsOfService(
|
|||||||
};
|
};
|
||||||
|
|
||||||
const resp = await ws.http.get(reqUrl.href, { headers });
|
const resp = await ws.http.get(reqUrl.href, { headers });
|
||||||
if (resp.status !== 200) {
|
const tosText = await readSuccessResponseTextOrThrow(resp);
|
||||||
throw Error(`/terms response has unexpected status code (${resp.status})`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const tosText = await resp.text();
|
|
||||||
const tosEtag = resp.headers.get("etag") || undefined;
|
const tosEtag = resp.headers.get("etag") || undefined;
|
||||||
|
|
||||||
await ws.db.runWithWriteTransaction([Stores.exchanges], async (tx) => {
|
await ws.db.runWithWriteTransaction([Stores.exchanges], async (tx) => {
|
||||||
@ -393,14 +348,11 @@ async function updateExchangeWithWireInfo(
|
|||||||
reqUrl.searchParams.set("cacheBreaker", WALLET_CACHE_BREAKER_CLIENT_VERSION);
|
reqUrl.searchParams.set("cacheBreaker", WALLET_CACHE_BREAKER_CLIENT_VERSION);
|
||||||
|
|
||||||
const resp = await ws.http.get(reqUrl.href);
|
const resp = await ws.http.get(reqUrl.href);
|
||||||
if (resp.status !== 200) {
|
const wireInfo = await readSuccessResponseJsonOrThrow(
|
||||||
throw Error(`/wire response has unexpected status code (${resp.status})`);
|
resp,
|
||||||
}
|
codecForExchangeWireJson(),
|
||||||
const wiJson = await resp.json();
|
);
|
||||||
if (!wiJson) {
|
|
||||||
throw Error("/wire response malformed");
|
|
||||||
}
|
|
||||||
const wireInfo = codecForExchangeWireJson().decode(wiJson);
|
|
||||||
for (const a of wireInfo.accounts) {
|
for (const a of wireInfo.accounts) {
|
||||||
console.log("validating exchange acct");
|
console.log("validating exchange acct");
|
||||||
const isValid = await ws.cryptoApi.isValidWireAccount(
|
const isValid = await ws.cryptoApi.isValidWireAccount(
|
||||||
@ -461,7 +413,7 @@ export async function updateExchangeFromUrl(
|
|||||||
baseUrl: string,
|
baseUrl: string,
|
||||||
forceNow = false,
|
forceNow = false,
|
||||||
): Promise<ExchangeRecord> {
|
): Promise<ExchangeRecord> {
|
||||||
const onOpErr = (e: OperationError): Promise<void> =>
|
const onOpErr = (e: OperationErrorDetails): Promise<void> =>
|
||||||
setExchangeError(ws, baseUrl, e);
|
setExchangeError(ws, baseUrl, e);
|
||||||
return await guardOperationException(
|
return await guardOperationException(
|
||||||
() => updateExchangeFromUrlImpl(ws, baseUrl, forceNow),
|
() => updateExchangeFromUrlImpl(ws, baseUrl, forceNow),
|
||||||
|
@ -38,7 +38,6 @@ import {
|
|||||||
} from "../types/dbTypes";
|
} from "../types/dbTypes";
|
||||||
import { NotificationType } from "../types/notifications";
|
import { NotificationType } from "../types/notifications";
|
||||||
import {
|
import {
|
||||||
PayReq,
|
|
||||||
codecForProposal,
|
codecForProposal,
|
||||||
codecForContractTerms,
|
codecForContractTerms,
|
||||||
CoinDepositPermission,
|
CoinDepositPermission,
|
||||||
@ -46,7 +45,7 @@ import {
|
|||||||
} from "../types/talerTypes";
|
} from "../types/talerTypes";
|
||||||
import {
|
import {
|
||||||
ConfirmPayResult,
|
ConfirmPayResult,
|
||||||
OperationError,
|
OperationErrorDetails,
|
||||||
PreparePayResult,
|
PreparePayResult,
|
||||||
RefreshReason,
|
RefreshReason,
|
||||||
} from "../types/walletTypes";
|
} from "../types/walletTypes";
|
||||||
@ -59,7 +58,10 @@ import { createRefreshGroup, getTotalRefreshCost } from "./refresh";
|
|||||||
import { InternalWalletState } from "./state";
|
import { InternalWalletState } from "./state";
|
||||||
import { getTimestampNow, timestampAddDuration } from "../util/time";
|
import { getTimestampNow, timestampAddDuration } from "../util/time";
|
||||||
import { strcmp, canonicalJson } from "../util/helpers";
|
import { strcmp, canonicalJson } from "../util/helpers";
|
||||||
import { httpPostTalerJson } from "../util/http";
|
import {
|
||||||
|
readSuccessResponseJsonOrErrorCode,
|
||||||
|
readSuccessResponseJsonOrThrow,
|
||||||
|
} from "../util/http";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Logger.
|
* Logger.
|
||||||
@ -515,7 +517,7 @@ function getNextUrl(contractData: WalletContractData): string {
|
|||||||
async function incrementProposalRetry(
|
async function incrementProposalRetry(
|
||||||
ws: InternalWalletState,
|
ws: InternalWalletState,
|
||||||
proposalId: string,
|
proposalId: string,
|
||||||
err: OperationError | undefined,
|
err: OperationErrorDetails | undefined,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
await ws.db.runWithWriteTransaction([Stores.proposals], async (tx) => {
|
await ws.db.runWithWriteTransaction([Stores.proposals], async (tx) => {
|
||||||
const pr = await tx.get(Stores.proposals, proposalId);
|
const pr = await tx.get(Stores.proposals, proposalId);
|
||||||
@ -538,7 +540,7 @@ async function incrementProposalRetry(
|
|||||||
async function incrementPurchasePayRetry(
|
async function incrementPurchasePayRetry(
|
||||||
ws: InternalWalletState,
|
ws: InternalWalletState,
|
||||||
proposalId: string,
|
proposalId: string,
|
||||||
err: OperationError | undefined,
|
err: OperationErrorDetails | undefined,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
console.log("incrementing purchase pay retry with error", err);
|
console.log("incrementing purchase pay retry with error", err);
|
||||||
await ws.db.runWithWriteTransaction([Stores.purchases], async (tx) => {
|
await ws.db.runWithWriteTransaction([Stores.purchases], async (tx) => {
|
||||||
@ -554,7 +556,9 @@ async function incrementPurchasePayRetry(
|
|||||||
pr.lastPayError = err;
|
pr.lastPayError = err;
|
||||||
await tx.put(Stores.purchases, pr);
|
await tx.put(Stores.purchases, pr);
|
||||||
});
|
});
|
||||||
ws.notify({ type: NotificationType.PayOperationError });
|
if (err) {
|
||||||
|
ws.notify({ type: NotificationType.PayOperationError, error: err });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function processDownloadProposal(
|
export async function processDownloadProposal(
|
||||||
@ -562,7 +566,7 @@ export async function processDownloadProposal(
|
|||||||
proposalId: string,
|
proposalId: string,
|
||||||
forceNow = false,
|
forceNow = false,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const onOpErr = (err: OperationError): Promise<void> =>
|
const onOpErr = (err: OperationErrorDetails): Promise<void> =>
|
||||||
incrementProposalRetry(ws, proposalId, err);
|
incrementProposalRetry(ws, proposalId, err);
|
||||||
await guardOperationException(
|
await guardOperationException(
|
||||||
() => processDownloadProposalImpl(ws, proposalId, forceNow),
|
() => processDownloadProposalImpl(ws, proposalId, forceNow),
|
||||||
@ -604,14 +608,15 @@ async function processDownloadProposalImpl(
|
|||||||
).href;
|
).href;
|
||||||
logger.trace("downloading contract from '" + orderClaimUrl + "'");
|
logger.trace("downloading contract from '" + orderClaimUrl + "'");
|
||||||
|
|
||||||
const proposalResp = await httpPostTalerJson({
|
const reqestBody = {
|
||||||
url: orderClaimUrl,
|
nonce: proposal.noncePub,
|
||||||
body: {
|
};
|
||||||
nonce: proposal.noncePub,
|
|
||||||
},
|
const resp = await ws.http.postJson(orderClaimUrl, reqestBody);
|
||||||
codec: codecForProposal(),
|
const proposalResp = await readSuccessResponseJsonOrThrow(
|
||||||
http: ws.http,
|
resp,
|
||||||
});
|
codecForProposal(),
|
||||||
|
);
|
||||||
|
|
||||||
// The proposalResp contains the contract terms as raw JSON,
|
// The proposalResp contains the contract terms as raw JSON,
|
||||||
// as the coded to parse them doesn't necessarily round-trip.
|
// as the coded to parse them doesn't necessarily round-trip.
|
||||||
@ -779,15 +784,17 @@ export async function submitPay(
|
|||||||
purchase.contractData.merchantBaseUrl,
|
purchase.contractData.merchantBaseUrl,
|
||||||
).href;
|
).href;
|
||||||
|
|
||||||
const merchantResp = await httpPostTalerJson({
|
const reqBody = {
|
||||||
url: payUrl,
|
coins: purchase.coinDepositPermissions,
|
||||||
body: {
|
session_id: purchase.lastSessionId,
|
||||||
coins: purchase.coinDepositPermissions,
|
};
|
||||||
session_id: purchase.lastSessionId,
|
|
||||||
},
|
const resp = await ws.http.postJson(payUrl, reqBody);
|
||||||
codec: codecForMerchantPayResponse(),
|
|
||||||
http: ws.http,
|
const merchantResp = await readSuccessResponseJsonOrThrow(
|
||||||
});
|
resp,
|
||||||
|
codecForMerchantPayResponse(),
|
||||||
|
);
|
||||||
|
|
||||||
console.log("got success from pay URL", merchantResp);
|
console.log("got success from pay URL", merchantResp);
|
||||||
|
|
||||||
@ -1050,7 +1057,7 @@ export async function processPurchasePay(
|
|||||||
proposalId: string,
|
proposalId: string,
|
||||||
forceNow = false,
|
forceNow = false,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const onOpErr = (e: OperationError): Promise<void> =>
|
const onOpErr = (e: OperationErrorDetails): Promise<void> =>
|
||||||
incrementPurchasePayRetry(ws, proposalId, e);
|
incrementPurchasePayRetry(ws, proposalId, e);
|
||||||
await guardOperationException(
|
await guardOperationException(
|
||||||
() => processPurchasePayImpl(ws, proposalId, forceNow),
|
() => processPurchasePayImpl(ws, proposalId, forceNow),
|
||||||
|
@ -44,17 +44,17 @@ import { forceQueryReserve } from "./reserves";
|
|||||||
|
|
||||||
import { Amounts } from "../util/amounts";
|
import { Amounts } from "../util/amounts";
|
||||||
import { createRefreshGroup, processRefreshGroup } from "./refresh";
|
import { createRefreshGroup, processRefreshGroup } from "./refresh";
|
||||||
import { RefreshReason, OperationError } from "../types/walletTypes";
|
import { RefreshReason, OperationErrorDetails } from "../types/walletTypes";
|
||||||
import { TransactionHandle } from "../util/query";
|
import { TransactionHandle } from "../util/query";
|
||||||
import { encodeCrock, getRandomBytes } from "../crypto/talerCrypto";
|
import { encodeCrock, getRandomBytes } from "../crypto/talerCrypto";
|
||||||
import { getTimestampNow } from "../util/time";
|
import { getTimestampNow } from "../util/time";
|
||||||
import { guardOperationException } from "./errors";
|
import { guardOperationException } from "./errors";
|
||||||
import { httpPostTalerJson } from "../util/http";
|
import { readSuccessResponseJsonOrThrow } from "../util/http";
|
||||||
|
|
||||||
async function incrementRecoupRetry(
|
async function incrementRecoupRetry(
|
||||||
ws: InternalWalletState,
|
ws: InternalWalletState,
|
||||||
recoupGroupId: string,
|
recoupGroupId: string,
|
||||||
err: OperationError | undefined,
|
err: OperationErrorDetails | undefined,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
await ws.db.runWithWriteTransaction([Stores.recoupGroups], async (tx) => {
|
await ws.db.runWithWriteTransaction([Stores.recoupGroups], async (tx) => {
|
||||||
const r = await tx.get(Stores.recoupGroups, recoupGroupId);
|
const r = await tx.get(Stores.recoupGroups, recoupGroupId);
|
||||||
@ -69,7 +69,9 @@ async function incrementRecoupRetry(
|
|||||||
r.lastError = err;
|
r.lastError = err;
|
||||||
await tx.put(Stores.recoupGroups, r);
|
await tx.put(Stores.recoupGroups, r);
|
||||||
});
|
});
|
||||||
ws.notify({ type: NotificationType.RecoupOperationError });
|
if (err) {
|
||||||
|
ws.notify({ type: NotificationType.RecoupOperationError, error: err });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function putGroupAsFinished(
|
async function putGroupAsFinished(
|
||||||
@ -147,12 +149,11 @@ async function recoupWithdrawCoin(
|
|||||||
|
|
||||||
const recoupRequest = await ws.cryptoApi.createRecoupRequest(coin);
|
const recoupRequest = await ws.cryptoApi.createRecoupRequest(coin);
|
||||||
const reqUrl = new URL(`/coins/${coin.coinPub}/recoup`, coin.exchangeBaseUrl);
|
const reqUrl = new URL(`/coins/${coin.coinPub}/recoup`, coin.exchangeBaseUrl);
|
||||||
const recoupConfirmation = await httpPostTalerJson({
|
const resp = await ws.http.postJson(reqUrl.href, recoupRequest);
|
||||||
url: reqUrl.href,
|
const recoupConfirmation = await readSuccessResponseJsonOrThrow(
|
||||||
body: recoupRequest,
|
resp,
|
||||||
codec: codecForRecoupConfirmation(),
|
codecForRecoupConfirmation(),
|
||||||
http: ws.http,
|
);
|
||||||
});
|
|
||||||
|
|
||||||
if (recoupConfirmation.reserve_pub !== reservePub) {
|
if (recoupConfirmation.reserve_pub !== reservePub) {
|
||||||
throw Error(`Coin's reserve doesn't match reserve on recoup`);
|
throw Error(`Coin's reserve doesn't match reserve on recoup`);
|
||||||
@ -223,12 +224,11 @@ async function recoupRefreshCoin(
|
|||||||
const reqUrl = new URL(`/coins/${coin.coinPub}/recoup`, coin.exchangeBaseUrl);
|
const reqUrl = new URL(`/coins/${coin.coinPub}/recoup`, coin.exchangeBaseUrl);
|
||||||
console.log("making recoup request");
|
console.log("making recoup request");
|
||||||
|
|
||||||
const recoupConfirmation = await httpPostTalerJson({
|
const resp = await ws.http.postJson(reqUrl.href, recoupRequest);
|
||||||
url: reqUrl.href,
|
const recoupConfirmation = await readSuccessResponseJsonOrThrow(
|
||||||
body: recoupRequest,
|
resp,
|
||||||
codec: codecForRecoupConfirmation(),
|
codecForRecoupConfirmation(),
|
||||||
http: ws.http,
|
);
|
||||||
});
|
|
||||||
|
|
||||||
if (recoupConfirmation.old_coin_pub != cs.oldCoinPub) {
|
if (recoupConfirmation.old_coin_pub != cs.oldCoinPub) {
|
||||||
throw Error(`Coin's oldCoinPub doesn't match reserve on recoup`);
|
throw Error(`Coin's oldCoinPub doesn't match reserve on recoup`);
|
||||||
@ -298,7 +298,7 @@ export async function processRecoupGroup(
|
|||||||
forceNow = false,
|
forceNow = false,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
await ws.memoProcessRecoup.memo(recoupGroupId, async () => {
|
await ws.memoProcessRecoup.memo(recoupGroupId, async () => {
|
||||||
const onOpErr = (e: OperationError): Promise<void> =>
|
const onOpErr = (e: OperationErrorDetails): Promise<void> =>
|
||||||
incrementRecoupRetry(ws, recoupGroupId, e);
|
incrementRecoupRetry(ws, recoupGroupId, e);
|
||||||
return await guardOperationException(
|
return await guardOperationException(
|
||||||
async () => await processRecoupGroupImpl(ws, recoupGroupId, forceNow),
|
async () => await processRecoupGroupImpl(ws, recoupGroupId, forceNow),
|
||||||
|
@ -34,7 +34,7 @@ import { Logger } from "../util/logging";
|
|||||||
import { getWithdrawDenomList } from "./withdraw";
|
import { getWithdrawDenomList } from "./withdraw";
|
||||||
import { updateExchangeFromUrl } from "./exchanges";
|
import { updateExchangeFromUrl } from "./exchanges";
|
||||||
import {
|
import {
|
||||||
OperationError,
|
OperationErrorDetails,
|
||||||
CoinPublicKey,
|
CoinPublicKey,
|
||||||
RefreshReason,
|
RefreshReason,
|
||||||
RefreshGroupId,
|
RefreshGroupId,
|
||||||
@ -43,6 +43,11 @@ import { guardOperationException } from "./errors";
|
|||||||
import { NotificationType } from "../types/notifications";
|
import { NotificationType } from "../types/notifications";
|
||||||
import { getRandomBytes, encodeCrock } from "../crypto/talerCrypto";
|
import { getRandomBytes, encodeCrock } from "../crypto/talerCrypto";
|
||||||
import { getTimestampNow } from "../util/time";
|
import { getTimestampNow } from "../util/time";
|
||||||
|
import { readSuccessResponseJsonOrThrow } from "../util/http";
|
||||||
|
import {
|
||||||
|
codecForExchangeMeltResponse,
|
||||||
|
codecForExchangeRevealResponse,
|
||||||
|
} from "../types/talerTypes";
|
||||||
|
|
||||||
const logger = new Logger("refresh.ts");
|
const logger = new Logger("refresh.ts");
|
||||||
|
|
||||||
@ -243,34 +248,12 @@ async function refreshMelt(
|
|||||||
};
|
};
|
||||||
logger.trace(`melt request for coin:`, meltReq);
|
logger.trace(`melt request for coin:`, meltReq);
|
||||||
const resp = await ws.http.postJson(reqUrl.href, meltReq);
|
const resp = await ws.http.postJson(reqUrl.href, meltReq);
|
||||||
if (resp.status !== 200) {
|
const meltResponse = await readSuccessResponseJsonOrThrow(
|
||||||
console.log(`got status ${resp.status} for refresh/melt`);
|
resp,
|
||||||
try {
|
codecForExchangeMeltResponse(),
|
||||||
const respJson = await resp.json();
|
);
|
||||||
console.log(
|
|
||||||
`body of refresh/melt error response:`,
|
|
||||||
JSON.stringify(respJson, undefined, 2),
|
|
||||||
);
|
|
||||||
} catch (e) {
|
|
||||||
console.log(`body of refresh/melt error response is not JSON`);
|
|
||||||
}
|
|
||||||
throw Error(`unexpected status code ${resp.status} for refresh/melt`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const respJson = await resp.json();
|
const norevealIndex = meltResponse.noreveal_index;
|
||||||
|
|
||||||
logger.trace("melt response:", respJson);
|
|
||||||
|
|
||||||
if (resp.status !== 200) {
|
|
||||||
console.error(respJson);
|
|
||||||
throw Error("refresh failed");
|
|
||||||
}
|
|
||||||
|
|
||||||
const norevealIndex = respJson.noreveal_index;
|
|
||||||
|
|
||||||
if (typeof norevealIndex !== "number") {
|
|
||||||
throw Error("invalid response");
|
|
||||||
}
|
|
||||||
|
|
||||||
refreshSession.norevealIndex = norevealIndex;
|
refreshSession.norevealIndex = norevealIndex;
|
||||||
|
|
||||||
@ -355,30 +338,15 @@ async function refreshReveal(
|
|||||||
refreshSession.exchangeBaseUrl,
|
refreshSession.exchangeBaseUrl,
|
||||||
);
|
);
|
||||||
|
|
||||||
let resp;
|
const resp = await ws.http.postJson(reqUrl.href, req);
|
||||||
try {
|
const reveal = await readSuccessResponseJsonOrThrow(
|
||||||
resp = await ws.http.postJson(reqUrl.href, req);
|
resp,
|
||||||
} catch (e) {
|
codecForExchangeRevealResponse(),
|
||||||
console.error("got error during /refresh/reveal request");
|
);
|
||||||
console.error(e);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (resp.status !== 200) {
|
|
||||||
console.error("error: /refresh/reveal returned status " + resp.status);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const respJson = await resp.json();
|
|
||||||
|
|
||||||
if (!respJson.ev_sigs || !Array.isArray(respJson.ev_sigs)) {
|
|
||||||
console.error("/refresh/reveal did not contain ev_sigs");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const coins: CoinRecord[] = [];
|
const coins: CoinRecord[] = [];
|
||||||
|
|
||||||
for (let i = 0; i < respJson.ev_sigs.length; i++) {
|
for (let i = 0; i < reveal.ev_sigs.length; i++) {
|
||||||
const denom = await ws.db.get(Stores.denominations, [
|
const denom = await ws.db.get(Stores.denominations, [
|
||||||
refreshSession.exchangeBaseUrl,
|
refreshSession.exchangeBaseUrl,
|
||||||
refreshSession.newDenoms[i],
|
refreshSession.newDenoms[i],
|
||||||
@ -389,7 +357,7 @@ async function refreshReveal(
|
|||||||
}
|
}
|
||||||
const pc = refreshSession.planchetsForGammas[norevealIndex][i];
|
const pc = refreshSession.planchetsForGammas[norevealIndex][i];
|
||||||
const denomSig = await ws.cryptoApi.rsaUnblind(
|
const denomSig = await ws.cryptoApi.rsaUnblind(
|
||||||
respJson.ev_sigs[i].ev_sig,
|
reveal.ev_sigs[i].ev_sig,
|
||||||
pc.blindingKey,
|
pc.blindingKey,
|
||||||
denom.denomPub,
|
denom.denomPub,
|
||||||
);
|
);
|
||||||
@ -457,7 +425,7 @@ async function refreshReveal(
|
|||||||
async function incrementRefreshRetry(
|
async function incrementRefreshRetry(
|
||||||
ws: InternalWalletState,
|
ws: InternalWalletState,
|
||||||
refreshGroupId: string,
|
refreshGroupId: string,
|
||||||
err: OperationError | undefined,
|
err: OperationErrorDetails | undefined,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
await ws.db.runWithWriteTransaction([Stores.refreshGroups], async (tx) => {
|
await ws.db.runWithWriteTransaction([Stores.refreshGroups], async (tx) => {
|
||||||
const r = await tx.get(Stores.refreshGroups, refreshGroupId);
|
const r = await tx.get(Stores.refreshGroups, refreshGroupId);
|
||||||
@ -472,7 +440,9 @@ async function incrementRefreshRetry(
|
|||||||
r.lastError = err;
|
r.lastError = err;
|
||||||
await tx.put(Stores.refreshGroups, r);
|
await tx.put(Stores.refreshGroups, r);
|
||||||
});
|
});
|
||||||
ws.notify({ type: NotificationType.RefreshOperationError });
|
if (err) {
|
||||||
|
ws.notify({ type: NotificationType.RefreshOperationError, error: err });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function processRefreshGroup(
|
export async function processRefreshGroup(
|
||||||
@ -481,7 +451,7 @@ export async function processRefreshGroup(
|
|||||||
forceNow = false,
|
forceNow = false,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
await ws.memoProcessRefresh.memo(refreshGroupId, async () => {
|
await ws.memoProcessRefresh.memo(refreshGroupId, async () => {
|
||||||
const onOpErr = (e: OperationError): Promise<void> =>
|
const onOpErr = (e: OperationErrorDetails): Promise<void> =>
|
||||||
incrementRefreshRetry(ws, refreshGroupId, e);
|
incrementRefreshRetry(ws, refreshGroupId, e);
|
||||||
return await guardOperationException(
|
return await guardOperationException(
|
||||||
async () => await processRefreshGroupImpl(ws, refreshGroupId, forceNow),
|
async () => await processRefreshGroupImpl(ws, refreshGroupId, forceNow),
|
||||||
|
@ -25,7 +25,7 @@
|
|||||||
*/
|
*/
|
||||||
import { InternalWalletState } from "./state";
|
import { InternalWalletState } from "./state";
|
||||||
import {
|
import {
|
||||||
OperationError,
|
OperationErrorDetails,
|
||||||
RefreshReason,
|
RefreshReason,
|
||||||
CoinPublicKey,
|
CoinPublicKey,
|
||||||
} from "../types/walletTypes";
|
} from "../types/walletTypes";
|
||||||
@ -52,15 +52,18 @@ import { randomBytes } from "../crypto/primitives/nacl-fast";
|
|||||||
import { encodeCrock } from "../crypto/talerCrypto";
|
import { encodeCrock } from "../crypto/talerCrypto";
|
||||||
import { getTimestampNow } from "../util/time";
|
import { getTimestampNow } from "../util/time";
|
||||||
import { Logger } from "../util/logging";
|
import { Logger } from "../util/logging";
|
||||||
|
import { readSuccessResponseJsonOrThrow } from "../util/http";
|
||||||
|
|
||||||
const logger = new Logger("refund.ts");
|
const logger = new Logger("refund.ts");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retry querying and applying refunds for an order later.
|
||||||
|
*/
|
||||||
async function incrementPurchaseQueryRefundRetry(
|
async function incrementPurchaseQueryRefundRetry(
|
||||||
ws: InternalWalletState,
|
ws: InternalWalletState,
|
||||||
proposalId: string,
|
proposalId: string,
|
||||||
err: OperationError | undefined,
|
err: OperationErrorDetails | undefined,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
console.log("incrementing purchase refund query retry with error", err);
|
|
||||||
await ws.db.runWithWriteTransaction([Stores.purchases], async (tx) => {
|
await ws.db.runWithWriteTransaction([Stores.purchases], async (tx) => {
|
||||||
const pr = await tx.get(Stores.purchases, proposalId);
|
const pr = await tx.get(Stores.purchases, proposalId);
|
||||||
if (!pr) {
|
if (!pr) {
|
||||||
@ -74,54 +77,12 @@ async function incrementPurchaseQueryRefundRetry(
|
|||||||
pr.lastRefundStatusError = err;
|
pr.lastRefundStatusError = err;
|
||||||
await tx.put(Stores.purchases, pr);
|
await tx.put(Stores.purchases, pr);
|
||||||
});
|
});
|
||||||
ws.notify({ type: NotificationType.RefundStatusOperationError });
|
if (err) {
|
||||||
}
|
ws.notify({
|
||||||
|
type: NotificationType.RefundStatusOperationError,
|
||||||
export async function getFullRefundFees(
|
error: err,
|
||||||
ws: InternalWalletState,
|
});
|
||||||
refundPermissions: MerchantRefundDetails[],
|
|
||||||
): Promise<AmountJson> {
|
|
||||||
if (refundPermissions.length === 0) {
|
|
||||||
throw Error("no refunds given");
|
|
||||||
}
|
}
|
||||||
const coin0 = await ws.db.get(Stores.coins, refundPermissions[0].coin_pub);
|
|
||||||
if (!coin0) {
|
|
||||||
throw Error("coin not found");
|
|
||||||
}
|
|
||||||
let feeAcc = Amounts.getZero(
|
|
||||||
Amounts.parseOrThrow(refundPermissions[0].refund_amount).currency,
|
|
||||||
);
|
|
||||||
|
|
||||||
const denoms = await ws.db
|
|
||||||
.iterIndex(Stores.denominations.exchangeBaseUrlIndex, coin0.exchangeBaseUrl)
|
|
||||||
.toArray();
|
|
||||||
|
|
||||||
for (const rp of refundPermissions) {
|
|
||||||
const coin = await ws.db.get(Stores.coins, rp.coin_pub);
|
|
||||||
if (!coin) {
|
|
||||||
throw Error("coin not found");
|
|
||||||
}
|
|
||||||
const denom = await ws.db.get(Stores.denominations, [
|
|
||||||
coin0.exchangeBaseUrl,
|
|
||||||
coin.denomPub,
|
|
||||||
]);
|
|
||||||
if (!denom) {
|
|
||||||
throw Error(`denom not found (${coin.denomPub})`);
|
|
||||||
}
|
|
||||||
// FIXME: this assumes that the refund already happened.
|
|
||||||
// When it hasn't, the refresh cost is inaccurate. To fix this,
|
|
||||||
// we need introduce a flag to tell if a coin was refunded or
|
|
||||||
// refreshed normally (and what about incremental refunds?)
|
|
||||||
const refundAmount = Amounts.parseOrThrow(rp.refund_amount);
|
|
||||||
const refundFee = Amounts.parseOrThrow(rp.refund_fee);
|
|
||||||
const refreshCost = getTotalRefreshCost(
|
|
||||||
denoms,
|
|
||||||
denom,
|
|
||||||
Amounts.sub(refundAmount, refundFee).amount,
|
|
||||||
);
|
|
||||||
feeAcc = Amounts.add(feeAcc, refreshCost, refundFee).amount;
|
|
||||||
}
|
|
||||||
return feeAcc;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getRefundKey(d: MerchantRefundDetails): string {
|
function getRefundKey(d: MerchantRefundDetails): string {
|
||||||
@ -310,14 +271,14 @@ async function acceptRefundResponse(
|
|||||||
p.lastRefundStatusError = undefined;
|
p.lastRefundStatusError = undefined;
|
||||||
p.refundStatusRetryInfo = initRetryInfo(false);
|
p.refundStatusRetryInfo = initRetryInfo(false);
|
||||||
p.refundStatusRequested = false;
|
p.refundStatusRequested = false;
|
||||||
console.log("refund query done");
|
logger.trace("refund query done");
|
||||||
} else {
|
} else {
|
||||||
// No error, but we need to try again!
|
// No error, but we need to try again!
|
||||||
p.timestampLastRefundStatus = now;
|
p.timestampLastRefundStatus = now;
|
||||||
p.refundStatusRetryInfo.retryCounter++;
|
p.refundStatusRetryInfo.retryCounter++;
|
||||||
updateRetryInfoTimeout(p.refundStatusRetryInfo);
|
updateRetryInfoTimeout(p.refundStatusRetryInfo);
|
||||||
p.lastRefundStatusError = undefined;
|
p.lastRefundStatusError = undefined;
|
||||||
console.log("refund query not done");
|
logger.trace("refund query not done");
|
||||||
}
|
}
|
||||||
|
|
||||||
p.refundsRefreshCost = { ...p.refundsRefreshCost, ...refundsRefreshCost };
|
p.refundsRefreshCost = { ...p.refundsRefreshCost, ...refundsRefreshCost };
|
||||||
@ -369,7 +330,7 @@ async function startRefundQuery(
|
|||||||
async (tx) => {
|
async (tx) => {
|
||||||
const p = await tx.get(Stores.purchases, proposalId);
|
const p = await tx.get(Stores.purchases, proposalId);
|
||||||
if (!p) {
|
if (!p) {
|
||||||
console.log("no purchase found for refund URL");
|
logger.error("no purchase found for refund URL");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
p.refundStatusRequested = true;
|
p.refundStatusRequested = true;
|
||||||
@ -401,7 +362,7 @@ export async function applyRefund(
|
|||||||
): Promise<{ contractTermsHash: string; proposalId: string }> {
|
): Promise<{ contractTermsHash: string; proposalId: string }> {
|
||||||
const parseResult = parseRefundUri(talerRefundUri);
|
const parseResult = parseRefundUri(talerRefundUri);
|
||||||
|
|
||||||
console.log("applying refund", parseResult);
|
logger.trace("applying refund", parseResult);
|
||||||
|
|
||||||
if (!parseResult) {
|
if (!parseResult) {
|
||||||
throw Error("invalid refund URI");
|
throw Error("invalid refund URI");
|
||||||
@ -432,7 +393,7 @@ export async function processPurchaseQueryRefund(
|
|||||||
proposalId: string,
|
proposalId: string,
|
||||||
forceNow = false,
|
forceNow = false,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const onOpErr = (e: OperationError): Promise<void> =>
|
const onOpErr = (e: OperationErrorDetails): Promise<void> =>
|
||||||
incrementPurchaseQueryRefundRetry(ws, proposalId, e);
|
incrementPurchaseQueryRefundRetry(ws, proposalId, e);
|
||||||
await guardOperationException(
|
await guardOperationException(
|
||||||
() => processPurchaseQueryRefundImpl(ws, proposalId, forceNow),
|
() => processPurchaseQueryRefundImpl(ws, proposalId, forceNow),
|
||||||
@ -464,27 +425,23 @@ async function processPurchaseQueryRefundImpl(
|
|||||||
if (!purchase) {
|
if (!purchase) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!purchase.refundStatusRequested) {
|
if (!purchase.refundStatusRequested) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const refundUrlObj = new URL("refund", purchase.contractData.merchantBaseUrl);
|
const request = await ws.http.get(
|
||||||
refundUrlObj.searchParams.set("order_id", purchase.contractData.orderId);
|
new URL(
|
||||||
const refundUrl = refundUrlObj.href;
|
`orders/${purchase.contractData.orderId}`,
|
||||||
let resp;
|
purchase.contractData.merchantBaseUrl,
|
||||||
try {
|
).href,
|
||||||
resp = await ws.http.get(refundUrl);
|
|
||||||
} catch (e) {
|
|
||||||
console.error("error downloading refund permission", e);
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
if (resp.status !== 200) {
|
|
||||||
throw Error(`unexpected status code (${resp.status}) for /refund`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const refundResponse = codecForMerchantRefundResponse().decode(
|
|
||||||
await resp.json(),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const refundResponse = await readSuccessResponseJsonOrThrow(
|
||||||
|
request,
|
||||||
|
codecForMerchantRefundResponse(),
|
||||||
|
);
|
||||||
|
|
||||||
await acceptRefundResponse(
|
await acceptRefundResponse(
|
||||||
ws,
|
ws,
|
||||||
proposalId,
|
proposalId,
|
||||||
|
@ -17,7 +17,7 @@
|
|||||||
import {
|
import {
|
||||||
CreateReserveRequest,
|
CreateReserveRequest,
|
||||||
CreateReserveResponse,
|
CreateReserveResponse,
|
||||||
OperationError,
|
OperationErrorDetails,
|
||||||
AcceptWithdrawalResponse,
|
AcceptWithdrawalResponse,
|
||||||
} from "../types/walletTypes";
|
} from "../types/walletTypes";
|
||||||
import { canonicalizeBaseUrl } from "../util/helpers";
|
import { canonicalizeBaseUrl } from "../util/helpers";
|
||||||
@ -56,7 +56,7 @@ import {
|
|||||||
import {
|
import {
|
||||||
guardOperationException,
|
guardOperationException,
|
||||||
OperationFailedAndReportedError,
|
OperationFailedAndReportedError,
|
||||||
OperationFailedError,
|
makeErrorDetails,
|
||||||
} from "./errors";
|
} from "./errors";
|
||||||
import { NotificationType } from "../types/notifications";
|
import { NotificationType } from "../types/notifications";
|
||||||
import { codecForReserveStatus } from "../types/ReserveStatus";
|
import { codecForReserveStatus } from "../types/ReserveStatus";
|
||||||
@ -67,6 +67,11 @@ import {
|
|||||||
} from "../util/reserveHistoryUtil";
|
} from "../util/reserveHistoryUtil";
|
||||||
import { TransactionHandle } from "../util/query";
|
import { TransactionHandle } from "../util/query";
|
||||||
import { addPaytoQueryParams } from "../util/payto";
|
import { addPaytoQueryParams } from "../util/payto";
|
||||||
|
import { TalerErrorCode } from "../TalerErrorCode";
|
||||||
|
import {
|
||||||
|
readSuccessResponseJsonOrErrorCode,
|
||||||
|
throwUnexpectedRequestError,
|
||||||
|
} from "../util/http";
|
||||||
|
|
||||||
const logger = new Logger("reserves.ts");
|
const logger = new Logger("reserves.ts");
|
||||||
|
|
||||||
@ -107,7 +112,9 @@ export async function createReserve(
|
|||||||
|
|
||||||
if (req.bankWithdrawStatusUrl) {
|
if (req.bankWithdrawStatusUrl) {
|
||||||
if (!req.exchangePaytoUri) {
|
if (!req.exchangePaytoUri) {
|
||||||
throw Error("Exchange payto URI must be specified for a bank-integrated withdrawal");
|
throw Error(
|
||||||
|
"Exchange payto URI must be specified for a bank-integrated withdrawal",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
bankInfo = {
|
bankInfo = {
|
||||||
statusUrl: req.bankWithdrawStatusUrl,
|
statusUrl: req.bankWithdrawStatusUrl,
|
||||||
@ -285,7 +292,7 @@ export async function processReserve(
|
|||||||
forceNow = false,
|
forceNow = false,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
return ws.memoProcessReserve.memo(reservePub, async () => {
|
return ws.memoProcessReserve.memo(reservePub, async () => {
|
||||||
const onOpError = (err: OperationError): Promise<void> =>
|
const onOpError = (err: OperationErrorDetails): Promise<void> =>
|
||||||
incrementReserveRetry(ws, reservePub, err);
|
incrementReserveRetry(ws, reservePub, err);
|
||||||
await guardOperationException(
|
await guardOperationException(
|
||||||
() => processReserveImpl(ws, reservePub, forceNow),
|
() => processReserveImpl(ws, reservePub, forceNow),
|
||||||
@ -344,7 +351,7 @@ export async function processReserveBankStatus(
|
|||||||
ws: InternalWalletState,
|
ws: InternalWalletState,
|
||||||
reservePub: string,
|
reservePub: string,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const onOpError = (err: OperationError): Promise<void> =>
|
const onOpError = (err: OperationErrorDetails): Promise<void> =>
|
||||||
incrementReserveRetry(ws, reservePub, err);
|
incrementReserveRetry(ws, reservePub, err);
|
||||||
await guardOperationException(
|
await guardOperationException(
|
||||||
() => processReserveBankStatusImpl(ws, reservePub),
|
() => processReserveBankStatusImpl(ws, reservePub),
|
||||||
@ -423,7 +430,7 @@ async function processReserveBankStatusImpl(
|
|||||||
async function incrementReserveRetry(
|
async function incrementReserveRetry(
|
||||||
ws: InternalWalletState,
|
ws: InternalWalletState,
|
||||||
reservePub: string,
|
reservePub: string,
|
||||||
err: OperationError | undefined,
|
err: OperationErrorDetails | undefined,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
await ws.db.runWithWriteTransaction([Stores.reserves], async (tx) => {
|
await ws.db.runWithWriteTransaction([Stores.reserves], async (tx) => {
|
||||||
const r = await tx.get(Stores.reserves, reservePub);
|
const r = await tx.get(Stores.reserves, reservePub);
|
||||||
@ -444,7 +451,7 @@ async function incrementReserveRetry(
|
|||||||
if (err) {
|
if (err) {
|
||||||
ws.notify({
|
ws.notify({
|
||||||
type: NotificationType.ReserveOperationError,
|
type: NotificationType.ReserveOperationError,
|
||||||
operationError: err,
|
error: err,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -466,35 +473,32 @@ async function updateReserve(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const reqUrl = new URL(`reserves/${reservePub}`, reserve.exchangeBaseUrl);
|
const resp = await ws.http.get(
|
||||||
let resp;
|
new URL(`reserves/${reservePub}`, reserve.exchangeBaseUrl).href,
|
||||||
try {
|
);
|
||||||
resp = await ws.http.get(reqUrl.href);
|
|
||||||
console.log("got reserves/${RESERVE_PUB} response", await resp.json());
|
const result = await readSuccessResponseJsonOrErrorCode(
|
||||||
if (resp.status === 404) {
|
resp,
|
||||||
const m = "reserve not known to the exchange yet";
|
codecForReserveStatus(),
|
||||||
throw new OperationFailedError({
|
);
|
||||||
type: "waiting",
|
if (result.isError) {
|
||||||
message: m,
|
if (
|
||||||
details: {},
|
resp.status === 404 &&
|
||||||
|
result.talerErrorResponse.code === TalerErrorCode.RESERVE_STATUS_UNKNOWN
|
||||||
|
) {
|
||||||
|
ws.notify({
|
||||||
|
type: NotificationType.ReserveNotYetFound,
|
||||||
|
reservePub,
|
||||||
});
|
});
|
||||||
|
await incrementReserveRetry(ws, reservePub, undefined);
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
throwUnexpectedRequestError(resp, result.talerErrorResponse);
|
||||||
}
|
}
|
||||||
if (resp.status !== 200) {
|
|
||||||
throw Error(`unexpected status code ${resp.status} for reserve/status`);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
logger.trace("caught exception for reserve/status");
|
|
||||||
const m = e.message;
|
|
||||||
const opErr = {
|
|
||||||
type: "network",
|
|
||||||
details: {},
|
|
||||||
message: m,
|
|
||||||
};
|
|
||||||
await incrementReserveRetry(ws, reservePub, opErr);
|
|
||||||
throw new OperationFailedAndReportedError(opErr);
|
|
||||||
}
|
}
|
||||||
const respJson = await resp.json();
|
|
||||||
const reserveInfo = codecForReserveStatus().decode(respJson);
|
const reserveInfo = result.response;
|
||||||
|
|
||||||
const balance = Amounts.parseOrThrow(reserveInfo.balance);
|
const balance = Amounts.parseOrThrow(reserveInfo.balance);
|
||||||
const currency = balance.currency;
|
const currency = balance.currency;
|
||||||
await ws.db.runWithWriteTransaction(
|
await ws.db.runWithWriteTransaction(
|
||||||
@ -656,14 +660,12 @@ async function depleteReserve(
|
|||||||
// Only complain about inability to withdraw if we
|
// Only complain about inability to withdraw if we
|
||||||
// didn't withdraw before.
|
// didn't withdraw before.
|
||||||
if (Amounts.isZero(summary.withdrawnAmount)) {
|
if (Amounts.isZero(summary.withdrawnAmount)) {
|
||||||
const m = `Unable to withdraw from reserve, no denominations are available to withdraw.`;
|
const opErr = makeErrorDetails(
|
||||||
const opErr = {
|
TalerErrorCode.WALLET_EXCHANGE_DENOMINATIONS_INSUFFICIENT,
|
||||||
type: "internal",
|
`Unable to withdraw from reserve, no denominations are available to withdraw.`,
|
||||||
message: m,
|
{},
|
||||||
details: {},
|
);
|
||||||
};
|
|
||||||
await incrementReserveRetry(ws, reserve.reservePub, opErr);
|
await incrementReserveRetry(ws, reserve.reservePub, opErr);
|
||||||
console.log(m);
|
|
||||||
throw new OperationFailedAndReportedError(opErr);
|
throw new OperationFailedAndReportedError(opErr);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
|
@ -16,7 +16,7 @@
|
|||||||
|
|
||||||
import { InternalWalletState } from "./state";
|
import { InternalWalletState } from "./state";
|
||||||
import { parseTipUri } from "../util/taleruri";
|
import { parseTipUri } from "../util/taleruri";
|
||||||
import { TipStatus, OperationError } from "../types/walletTypes";
|
import { TipStatus, OperationErrorDetails } from "../types/walletTypes";
|
||||||
import {
|
import {
|
||||||
TipPlanchetDetail,
|
TipPlanchetDetail,
|
||||||
codecForTipPickupGetResponse,
|
codecForTipPickupGetResponse,
|
||||||
@ -43,6 +43,7 @@ import { getRandomBytes, encodeCrock } from "../crypto/talerCrypto";
|
|||||||
import { guardOperationException } from "./errors";
|
import { guardOperationException } from "./errors";
|
||||||
import { NotificationType } from "../types/notifications";
|
import { NotificationType } from "../types/notifications";
|
||||||
import { getTimestampNow } from "../util/time";
|
import { getTimestampNow } from "../util/time";
|
||||||
|
import { readSuccessResponseJsonOrThrow } from "../util/http";
|
||||||
|
|
||||||
export async function getTipStatus(
|
export async function getTipStatus(
|
||||||
ws: InternalWalletState,
|
ws: InternalWalletState,
|
||||||
@ -57,13 +58,10 @@ export async function getTipStatus(
|
|||||||
tipStatusUrl.searchParams.set("tip_id", res.merchantTipId);
|
tipStatusUrl.searchParams.set("tip_id", res.merchantTipId);
|
||||||
console.log("checking tip status from", tipStatusUrl.href);
|
console.log("checking tip status from", tipStatusUrl.href);
|
||||||
const merchantResp = await ws.http.get(tipStatusUrl.href);
|
const merchantResp = await ws.http.get(tipStatusUrl.href);
|
||||||
if (merchantResp.status !== 200) {
|
const tipPickupStatus = await readSuccessResponseJsonOrThrow(
|
||||||
throw Error(`unexpected status ${merchantResp.status} for tip-pickup`);
|
merchantResp,
|
||||||
}
|
codecForTipPickupGetResponse(),
|
||||||
const respJson = await merchantResp.json();
|
);
|
||||||
console.log("resp:", respJson);
|
|
||||||
const tipPickupStatus = codecForTipPickupGetResponse().decode(respJson);
|
|
||||||
|
|
||||||
console.log("status", tipPickupStatus);
|
console.log("status", tipPickupStatus);
|
||||||
|
|
||||||
const amount = Amounts.parseOrThrow(tipPickupStatus.amount);
|
const amount = Amounts.parseOrThrow(tipPickupStatus.amount);
|
||||||
@ -133,7 +131,7 @@ export async function getTipStatus(
|
|||||||
async function incrementTipRetry(
|
async function incrementTipRetry(
|
||||||
ws: InternalWalletState,
|
ws: InternalWalletState,
|
||||||
refreshSessionId: string,
|
refreshSessionId: string,
|
||||||
err: OperationError | undefined,
|
err: OperationErrorDetails | undefined,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
await ws.db.runWithWriteTransaction([Stores.tips], async (tx) => {
|
await ws.db.runWithWriteTransaction([Stores.tips], async (tx) => {
|
||||||
const t = await tx.get(Stores.tips, refreshSessionId);
|
const t = await tx.get(Stores.tips, refreshSessionId);
|
||||||
@ -156,7 +154,7 @@ export async function processTip(
|
|||||||
tipId: string,
|
tipId: string,
|
||||||
forceNow = false,
|
forceNow = false,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const onOpErr = (e: OperationError): Promise<void> =>
|
const onOpErr = (e: OperationErrorDetails): Promise<void> =>
|
||||||
incrementTipRetry(ws, tipId, e);
|
incrementTipRetry(ws, tipId, e);
|
||||||
await guardOperationException(
|
await guardOperationException(
|
||||||
() => processTipImpl(ws, tipId, forceNow),
|
() => processTipImpl(ws, tipId, forceNow),
|
||||||
|
@ -177,50 +177,57 @@ export async function getTransactions(
|
|||||||
}
|
}
|
||||||
|
|
||||||
switch (wsr.source.type) {
|
switch (wsr.source.type) {
|
||||||
case WithdrawalSourceType.Reserve: {
|
case WithdrawalSourceType.Reserve:
|
||||||
const r = await tx.get(Stores.reserves, wsr.source.reservePub);
|
{
|
||||||
if (!r) {
|
const r = await tx.get(Stores.reserves, wsr.source.reservePub);
|
||||||
break;
|
if (!r) {
|
||||||
}
|
break;
|
||||||
let amountRaw: AmountJson | undefined = undefined;
|
}
|
||||||
if (wsr.withdrawalGroupId === r.initialWithdrawalGroupId) {
|
let amountRaw: AmountJson | undefined = undefined;
|
||||||
amountRaw = r.instructedAmount;
|
if (wsr.withdrawalGroupId === r.initialWithdrawalGroupId) {
|
||||||
} else {
|
amountRaw = r.instructedAmount;
|
||||||
amountRaw = wsr.denomsSel.totalWithdrawCost;
|
} else {
|
||||||
}
|
amountRaw = wsr.denomsSel.totalWithdrawCost;
|
||||||
let withdrawalDetails: WithdrawalDetails;
|
}
|
||||||
if (r.bankInfo) {
|
let withdrawalDetails: WithdrawalDetails;
|
||||||
|
if (r.bankInfo) {
|
||||||
withdrawalDetails = {
|
withdrawalDetails = {
|
||||||
type: WithdrawalType.TalerBankIntegrationApi,
|
type: WithdrawalType.TalerBankIntegrationApi,
|
||||||
confirmed: true,
|
confirmed: true,
|
||||||
bankConfirmationUrl: r.bankInfo.confirmUrl,
|
bankConfirmationUrl: r.bankInfo.confirmUrl,
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
const exchange = await tx.get(Stores.exchanges, r.exchangeBaseUrl);
|
const exchange = await tx.get(
|
||||||
if (!exchange) {
|
Stores.exchanges,
|
||||||
// FIXME: report somehow
|
r.exchangeBaseUrl,
|
||||||
break;
|
);
|
||||||
|
if (!exchange) {
|
||||||
|
// FIXME: report somehow
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
withdrawalDetails = {
|
||||||
|
type: WithdrawalType.ManualTransfer,
|
||||||
|
exchangePaytoUris:
|
||||||
|
exchange.wireInfo?.accounts.map((x) => x.payto_uri) ?? [],
|
||||||
|
};
|
||||||
}
|
}
|
||||||
withdrawalDetails = {
|
transactions.push({
|
||||||
type: WithdrawalType.ManualTransfer,
|
type: TransactionType.Withdrawal,
|
||||||
exchangePaytoUris: exchange.wireInfo?.accounts.map((x) => x.payto_uri) ?? [],
|
amountEffective: Amounts.stringify(
|
||||||
};
|
wsr.denomsSel.totalCoinValue,
|
||||||
|
),
|
||||||
|
amountRaw: Amounts.stringify(amountRaw),
|
||||||
|
withdrawalDetails,
|
||||||
|
exchangeBaseUrl: wsr.exchangeBaseUrl,
|
||||||
|
pending: !wsr.timestampFinish,
|
||||||
|
timestamp: wsr.timestampStart,
|
||||||
|
transactionId: makeEventId(
|
||||||
|
TransactionType.Withdrawal,
|
||||||
|
wsr.withdrawalGroupId,
|
||||||
|
),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
transactions.push({
|
break;
|
||||||
type: TransactionType.Withdrawal,
|
|
||||||
amountEffective: Amounts.stringify(wsr.denomsSel.totalCoinValue),
|
|
||||||
amountRaw: Amounts.stringify(amountRaw),
|
|
||||||
withdrawalDetails,
|
|
||||||
exchangeBaseUrl: wsr.exchangeBaseUrl,
|
|
||||||
pending: !wsr.timestampFinish,
|
|
||||||
timestamp: wsr.timestampStart,
|
|
||||||
transactionId: makeEventId(
|
|
||||||
TransactionType.Withdrawal,
|
|
||||||
wsr.withdrawalGroupId,
|
|
||||||
),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
default:
|
default:
|
||||||
// Tips are reported via their own event
|
// Tips are reported via their own event
|
||||||
break;
|
break;
|
||||||
@ -254,7 +261,7 @@ export async function getTransactions(
|
|||||||
type: WithdrawalType.TalerBankIntegrationApi,
|
type: WithdrawalType.TalerBankIntegrationApi,
|
||||||
confirmed: false,
|
confirmed: false,
|
||||||
bankConfirmationUrl: r.bankInfo.confirmUrl,
|
bankConfirmationUrl: r.bankInfo.confirmUrl,
|
||||||
}
|
};
|
||||||
} else {
|
} else {
|
||||||
withdrawalDetails = {
|
withdrawalDetails = {
|
||||||
type: WithdrawalType.ManualTransfer,
|
type: WithdrawalType.ManualTransfer,
|
||||||
@ -264,9 +271,7 @@ export async function getTransactions(
|
|||||||
transactions.push({
|
transactions.push({
|
||||||
type: TransactionType.Withdrawal,
|
type: TransactionType.Withdrawal,
|
||||||
amountRaw: Amounts.stringify(r.instructedAmount),
|
amountRaw: Amounts.stringify(r.instructedAmount),
|
||||||
amountEffective: Amounts.stringify(
|
amountEffective: Amounts.stringify(r.initialDenomSel.totalCoinValue),
|
||||||
r.initialDenomSel.totalCoinValue,
|
|
||||||
),
|
|
||||||
exchangeBaseUrl: r.exchangeBaseUrl,
|
exchangeBaseUrl: r.exchangeBaseUrl,
|
||||||
pending: true,
|
pending: true,
|
||||||
timestamp: r.timestampCreated,
|
timestamp: r.timestampCreated,
|
||||||
|
@ -33,7 +33,7 @@ import {
|
|||||||
BankWithdrawDetails,
|
BankWithdrawDetails,
|
||||||
ExchangeWithdrawDetails,
|
ExchangeWithdrawDetails,
|
||||||
WithdrawalDetailsResponse,
|
WithdrawalDetailsResponse,
|
||||||
OperationError,
|
OperationErrorDetails,
|
||||||
} from "../types/walletTypes";
|
} from "../types/walletTypes";
|
||||||
import {
|
import {
|
||||||
codecForWithdrawOperationStatusResponse,
|
codecForWithdrawOperationStatusResponse,
|
||||||
@ -54,7 +54,7 @@ import {
|
|||||||
timestampCmp,
|
timestampCmp,
|
||||||
timestampSubtractDuraction,
|
timestampSubtractDuraction,
|
||||||
} from "../util/time";
|
} from "../util/time";
|
||||||
import { httpPostTalerJson } from "../util/http";
|
import { readSuccessResponseJsonOrThrow } from "../util/http";
|
||||||
|
|
||||||
const logger = new Logger("withdraw.ts");
|
const logger = new Logger("withdraw.ts");
|
||||||
|
|
||||||
@ -142,14 +142,11 @@ export async function getBankWithdrawalInfo(
|
|||||||
throw Error(`can't parse URL ${talerWithdrawUri}`);
|
throw Error(`can't parse URL ${talerWithdrawUri}`);
|
||||||
}
|
}
|
||||||
const resp = await ws.http.get(uriResult.statusUrl);
|
const resp = await ws.http.get(uriResult.statusUrl);
|
||||||
if (resp.status !== 200) {
|
const status = await readSuccessResponseJsonOrThrow(
|
||||||
throw Error(
|
resp,
|
||||||
`unexpected status (${resp.status}) from bank for ${uriResult.statusUrl}`,
|
codecForWithdrawOperationStatusResponse(),
|
||||||
);
|
);
|
||||||
}
|
|
||||||
const respJson = await resp.json();
|
|
||||||
|
|
||||||
const status = codecForWithdrawOperationStatusResponse().decode(respJson);
|
|
||||||
return {
|
return {
|
||||||
amount: Amounts.parseOrThrow(status.amount),
|
amount: Amounts.parseOrThrow(status.amount),
|
||||||
confirmTransferUrl: status.confirm_transfer_url,
|
confirmTransferUrl: status.confirm_transfer_url,
|
||||||
@ -310,12 +307,11 @@ async function processPlanchet(
|
|||||||
exchange.baseUrl,
|
exchange.baseUrl,
|
||||||
).href;
|
).href;
|
||||||
|
|
||||||
const r = await httpPostTalerJson({
|
const resp = await ws.http.postJson(reqUrl, wd);
|
||||||
url: reqUrl,
|
const r = await readSuccessResponseJsonOrThrow(
|
||||||
body: wd,
|
resp,
|
||||||
codec: codecForWithdrawResponse(),
|
codecForWithdrawResponse(),
|
||||||
http: ws.http,
|
);
|
||||||
});
|
|
||||||
|
|
||||||
logger.trace(`got response for /withdraw`);
|
logger.trace(`got response for /withdraw`);
|
||||||
|
|
||||||
@ -505,7 +501,7 @@ export async function selectWithdrawalDenoms(
|
|||||||
async function incrementWithdrawalRetry(
|
async function incrementWithdrawalRetry(
|
||||||
ws: InternalWalletState,
|
ws: InternalWalletState,
|
||||||
withdrawalGroupId: string,
|
withdrawalGroupId: string,
|
||||||
err: OperationError | undefined,
|
err: OperationErrorDetails | undefined,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
await ws.db.runWithWriteTransaction([Stores.withdrawalGroups], async (tx) => {
|
await ws.db.runWithWriteTransaction([Stores.withdrawalGroups], async (tx) => {
|
||||||
const wsr = await tx.get(Stores.withdrawalGroups, withdrawalGroupId);
|
const wsr = await tx.get(Stores.withdrawalGroups, withdrawalGroupId);
|
||||||
@ -530,7 +526,7 @@ export async function processWithdrawGroup(
|
|||||||
withdrawalGroupId: string,
|
withdrawalGroupId: string,
|
||||||
forceNow = false,
|
forceNow = false,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const onOpErr = (e: OperationError): Promise<void> =>
|
const onOpErr = (e: OperationErrorDetails): Promise<void> =>
|
||||||
incrementWithdrawalRetry(ws, withdrawalGroupId, e);
|
incrementWithdrawalRetry(ws, withdrawalGroupId, e);
|
||||||
await guardOperationException(
|
await guardOperationException(
|
||||||
() => processWithdrawGroupImpl(ws, withdrawalGroupId, forceNow),
|
() => processWithdrawGroupImpl(ws, withdrawalGroupId, forceNow),
|
||||||
|
@ -28,7 +28,6 @@ import {
|
|||||||
Auditor,
|
Auditor,
|
||||||
CoinDepositPermission,
|
CoinDepositPermission,
|
||||||
MerchantRefundDetails,
|
MerchantRefundDetails,
|
||||||
PayReq,
|
|
||||||
TipResponse,
|
TipResponse,
|
||||||
ExchangeSignKeyJson,
|
ExchangeSignKeyJson,
|
||||||
MerchantInfo,
|
MerchantInfo,
|
||||||
@ -36,7 +35,7 @@ import {
|
|||||||
} from "./talerTypes";
|
} from "./talerTypes";
|
||||||
|
|
||||||
import { Index, Store } from "../util/query";
|
import { Index, Store } from "../util/query";
|
||||||
import { OperationError, RefreshReason } from "./walletTypes";
|
import { OperationErrorDetails, RefreshReason } from "./walletTypes";
|
||||||
import {
|
import {
|
||||||
ReserveTransaction,
|
ReserveTransaction,
|
||||||
ReserveCreditTransaction,
|
ReserveCreditTransaction,
|
||||||
@ -319,7 +318,7 @@ export interface ReserveRecord {
|
|||||||
* Last error that happened in a reserve operation
|
* Last error that happened in a reserve operation
|
||||||
* (either talking to the bank or the exchange).
|
* (either talking to the bank or the exchange).
|
||||||
*/
|
*/
|
||||||
lastError: OperationError | undefined;
|
lastError: OperationErrorDetails | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -633,7 +632,7 @@ export interface ExchangeRecord {
|
|||||||
*/
|
*/
|
||||||
updateDiff: ExchangeUpdateDiff | undefined;
|
updateDiff: ExchangeUpdateDiff | undefined;
|
||||||
|
|
||||||
lastError?: OperationError;
|
lastError?: OperationErrorDetails;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -890,14 +889,14 @@ export interface ProposalRecord {
|
|||||||
*/
|
*/
|
||||||
retryInfo: RetryInfo;
|
retryInfo: RetryInfo;
|
||||||
|
|
||||||
lastError: OperationError | undefined;
|
lastError: OperationErrorDetails | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Status of a tip we got from a merchant.
|
* Status of a tip we got from a merchant.
|
||||||
*/
|
*/
|
||||||
export interface TipRecord {
|
export interface TipRecord {
|
||||||
lastError: OperationError | undefined;
|
lastError: OperationErrorDetails | undefined;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Has the user accepted the tip? Only after the tip has been accepted coins
|
* Has the user accepted the tip? Only after the tip has been accepted coins
|
||||||
@ -982,9 +981,9 @@ export interface RefreshGroupRecord {
|
|||||||
*/
|
*/
|
||||||
retryInfo: RetryInfo;
|
retryInfo: RetryInfo;
|
||||||
|
|
||||||
lastError: OperationError | undefined;
|
lastError: OperationErrorDetails | undefined;
|
||||||
|
|
||||||
lastErrorPerCoin: { [coinIndex: number]: OperationError };
|
lastErrorPerCoin: { [coinIndex: number]: OperationErrorDetails };
|
||||||
|
|
||||||
refreshGroupId: string;
|
refreshGroupId: string;
|
||||||
|
|
||||||
@ -1012,7 +1011,7 @@ export interface RefreshGroupRecord {
|
|||||||
* Ongoing refresh
|
* Ongoing refresh
|
||||||
*/
|
*/
|
||||||
export interface RefreshSessionRecord {
|
export interface RefreshSessionRecord {
|
||||||
lastError: OperationError | undefined;
|
lastError: OperationErrorDetails | undefined;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Public key that's being melted in this session.
|
* Public key that's being melted in this session.
|
||||||
@ -1330,7 +1329,7 @@ export interface PurchaseRecord {
|
|||||||
|
|
||||||
payRetryInfo: RetryInfo;
|
payRetryInfo: RetryInfo;
|
||||||
|
|
||||||
lastPayError: OperationError | undefined;
|
lastPayError: OperationErrorDetails | undefined;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retry information for querying the refund status with the merchant.
|
* Retry information for querying the refund status with the merchant.
|
||||||
@ -1340,7 +1339,7 @@ export interface PurchaseRecord {
|
|||||||
/**
|
/**
|
||||||
* Last error (or undefined) for querying the refund status with the merchant.
|
* Last error (or undefined) for querying the refund status with the merchant.
|
||||||
*/
|
*/
|
||||||
lastRefundStatusError: OperationError | undefined;
|
lastRefundStatusError: OperationErrorDetails | undefined;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Continue querying the refund status until this deadline has expired.
|
* Continue querying the refund status until this deadline has expired.
|
||||||
@ -1492,9 +1491,9 @@ export interface WithdrawalGroupRecord {
|
|||||||
* Last error per coin/planchet, or undefined if no error occured for
|
* Last error per coin/planchet, or undefined if no error occured for
|
||||||
* the coin/planchet.
|
* the coin/planchet.
|
||||||
*/
|
*/
|
||||||
lastErrorPerCoin: { [coinIndex: number]: OperationError };
|
lastErrorPerCoin: { [coinIndex: number]: OperationErrorDetails };
|
||||||
|
|
||||||
lastError: OperationError | undefined;
|
lastError: OperationErrorDetails | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface BankWithdrawUriRecord {
|
export interface BankWithdrawUriRecord {
|
||||||
@ -1559,7 +1558,7 @@ export interface RecoupGroupRecord {
|
|||||||
/**
|
/**
|
||||||
* Last error that occured, if any.
|
* Last error that occured, if any.
|
||||||
*/
|
*/
|
||||||
lastError: OperationError | undefined;
|
lastError: OperationErrorDetails | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const enum ImportPayloadType {
|
export const enum ImportPayloadType {
|
||||||
|
@ -22,7 +22,7 @@
|
|||||||
/**
|
/**
|
||||||
* Imports.
|
* Imports.
|
||||||
*/
|
*/
|
||||||
import { OperationError } from "./walletTypes";
|
import { OperationErrorDetails } from "./walletTypes";
|
||||||
import { WithdrawalSource } from "./dbTypes";
|
import { WithdrawalSource } from "./dbTypes";
|
||||||
|
|
||||||
export const enum NotificationType {
|
export const enum NotificationType {
|
||||||
@ -54,6 +54,7 @@ export const enum NotificationType {
|
|||||||
TipOperationError = "tip-error",
|
TipOperationError = "tip-error",
|
||||||
PayOperationError = "pay-error",
|
PayOperationError = "pay-error",
|
||||||
WithdrawOperationError = "withdraw-error",
|
WithdrawOperationError = "withdraw-error",
|
||||||
|
ReserveNotYetFound = "reserve-not-yet-found",
|
||||||
ReserveOperationError = "reserve-error",
|
ReserveOperationError = "reserve-error",
|
||||||
InternalError = "internal-error",
|
InternalError = "internal-error",
|
||||||
PendingOperationProcessed = "pending-operation-processed",
|
PendingOperationProcessed = "pending-operation-processed",
|
||||||
@ -72,6 +73,11 @@ export interface InternalErrorNotification {
|
|||||||
exception: any;
|
exception: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ReserveNotYetFoundNotification {
|
||||||
|
type: NotificationType.ReserveNotYetFound;
|
||||||
|
reservePub: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface CoinWithdrawnNotification {
|
export interface CoinWithdrawnNotification {
|
||||||
type: NotificationType.CoinWithdrawn;
|
type: NotificationType.CoinWithdrawn;
|
||||||
}
|
}
|
||||||
@ -148,27 +154,32 @@ export interface RefundFinishedNotification {
|
|||||||
|
|
||||||
export interface ExchangeOperationErrorNotification {
|
export interface ExchangeOperationErrorNotification {
|
||||||
type: NotificationType.ExchangeOperationError;
|
type: NotificationType.ExchangeOperationError;
|
||||||
|
error: OperationErrorDetails;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RefreshOperationErrorNotification {
|
export interface RefreshOperationErrorNotification {
|
||||||
type: NotificationType.RefreshOperationError;
|
type: NotificationType.RefreshOperationError;
|
||||||
|
error: OperationErrorDetails;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RefundStatusOperationErrorNotification {
|
export interface RefundStatusOperationErrorNotification {
|
||||||
type: NotificationType.RefundStatusOperationError;
|
type: NotificationType.RefundStatusOperationError;
|
||||||
|
error: OperationErrorDetails;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RefundApplyOperationErrorNotification {
|
export interface RefundApplyOperationErrorNotification {
|
||||||
type: NotificationType.RefundApplyOperationError;
|
type: NotificationType.RefundApplyOperationError;
|
||||||
|
error: OperationErrorDetails;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PayOperationErrorNotification {
|
export interface PayOperationErrorNotification {
|
||||||
type: NotificationType.PayOperationError;
|
type: NotificationType.PayOperationError;
|
||||||
|
error: OperationErrorDetails;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ProposalOperationErrorNotification {
|
export interface ProposalOperationErrorNotification {
|
||||||
type: NotificationType.ProposalOperationError;
|
type: NotificationType.ProposalOperationError;
|
||||||
error: OperationError;
|
error: OperationErrorDetails;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TipOperationErrorNotification {
|
export interface TipOperationErrorNotification {
|
||||||
@ -177,16 +188,17 @@ export interface TipOperationErrorNotification {
|
|||||||
|
|
||||||
export interface WithdrawOperationErrorNotification {
|
export interface WithdrawOperationErrorNotification {
|
||||||
type: NotificationType.WithdrawOperationError;
|
type: NotificationType.WithdrawOperationError;
|
||||||
error: OperationError,
|
error: OperationErrorDetails;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RecoupOperationErrorNotification {
|
export interface RecoupOperationErrorNotification {
|
||||||
type: NotificationType.RecoupOperationError;
|
type: NotificationType.RecoupOperationError;
|
||||||
|
error: OperationErrorDetails;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ReserveOperationErrorNotification {
|
export interface ReserveOperationErrorNotification {
|
||||||
type: NotificationType.ReserveOperationError;
|
type: NotificationType.ReserveOperationError;
|
||||||
operationError: OperationError;
|
error: OperationErrorDetails;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ReserveCreatedNotification {
|
export interface ReserveCreatedNotification {
|
||||||
@ -238,4 +250,5 @@ export type WalletNotification =
|
|||||||
| InternalErrorNotification
|
| InternalErrorNotification
|
||||||
| PendingOperationProcessedNotification
|
| PendingOperationProcessedNotification
|
||||||
| ProposalRefusedNotification
|
| ProposalRefusedNotification
|
||||||
| ReserveRegisteredWithBankNotification;
|
| ReserveRegisteredWithBankNotification
|
||||||
|
| ReserveNotYetFoundNotification;
|
||||||
|
@ -21,7 +21,7 @@
|
|||||||
/**
|
/**
|
||||||
* Imports.
|
* Imports.
|
||||||
*/
|
*/
|
||||||
import { OperationError, WalletBalance } from "./walletTypes";
|
import { OperationErrorDetails, WalletBalance } from "./walletTypes";
|
||||||
import { WithdrawalSource, RetryInfo, ReserveRecordStatus } from "./dbTypes";
|
import { WithdrawalSource, RetryInfo, ReserveRecordStatus } from "./dbTypes";
|
||||||
import { Timestamp, Duration } from "../util/time";
|
import { Timestamp, Duration } from "../util/time";
|
||||||
import { ReserveType } from "./history";
|
import { ReserveType } from "./history";
|
||||||
@ -68,7 +68,7 @@ export interface PendingExchangeUpdateOperation {
|
|||||||
stage: ExchangeUpdateOperationStage;
|
stage: ExchangeUpdateOperationStage;
|
||||||
reason: string;
|
reason: string;
|
||||||
exchangeBaseUrl: string;
|
exchangeBaseUrl: string;
|
||||||
lastError: OperationError | undefined;
|
lastError: OperationErrorDetails | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -112,7 +112,7 @@ export interface PendingReserveOperation {
|
|||||||
*/
|
*/
|
||||||
export interface PendingRefreshOperation {
|
export interface PendingRefreshOperation {
|
||||||
type: PendingOperationType.Refresh;
|
type: PendingOperationType.Refresh;
|
||||||
lastError?: OperationError;
|
lastError?: OperationErrorDetails;
|
||||||
refreshGroupId: string;
|
refreshGroupId: string;
|
||||||
finishedPerCoin: boolean[];
|
finishedPerCoin: boolean[];
|
||||||
retryInfo: RetryInfo;
|
retryInfo: RetryInfo;
|
||||||
@ -127,7 +127,7 @@ export interface PendingProposalDownloadOperation {
|
|||||||
proposalTimestamp: Timestamp;
|
proposalTimestamp: Timestamp;
|
||||||
proposalId: string;
|
proposalId: string;
|
||||||
orderId: string;
|
orderId: string;
|
||||||
lastError?: OperationError;
|
lastError?: OperationErrorDetails;
|
||||||
retryInfo: RetryInfo;
|
retryInfo: RetryInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -172,7 +172,7 @@ export interface PendingPayOperation {
|
|||||||
proposalId: string;
|
proposalId: string;
|
||||||
isReplay: boolean;
|
isReplay: boolean;
|
||||||
retryInfo: RetryInfo;
|
retryInfo: RetryInfo;
|
||||||
lastError: OperationError | undefined;
|
lastError: OperationErrorDetails | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -183,14 +183,14 @@ export interface PendingRefundQueryOperation {
|
|||||||
type: PendingOperationType.RefundQuery;
|
type: PendingOperationType.RefundQuery;
|
||||||
proposalId: string;
|
proposalId: string;
|
||||||
retryInfo: RetryInfo;
|
retryInfo: RetryInfo;
|
||||||
lastError: OperationError | undefined;
|
lastError: OperationErrorDetails | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PendingRecoupOperation {
|
export interface PendingRecoupOperation {
|
||||||
type: PendingOperationType.Recoup;
|
type: PendingOperationType.Recoup;
|
||||||
recoupGroupId: string;
|
recoupGroupId: string;
|
||||||
retryInfo: RetryInfo;
|
retryInfo: RetryInfo;
|
||||||
lastError: OperationError | undefined;
|
lastError: OperationErrorDetails | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -199,7 +199,7 @@ export interface PendingRecoupOperation {
|
|||||||
export interface PendingWithdrawOperation {
|
export interface PendingWithdrawOperation {
|
||||||
type: PendingOperationType.Withdraw;
|
type: PendingOperationType.Withdraw;
|
||||||
source: WithdrawalSource;
|
source: WithdrawalSource;
|
||||||
lastError: OperationError | undefined;
|
lastError: OperationErrorDetails | undefined;
|
||||||
withdrawalGroupId: string;
|
withdrawalGroupId: string;
|
||||||
numCoinsWithdrawn: number;
|
numCoinsWithdrawn: number;
|
||||||
numCoinsTotal: number;
|
numCoinsTotal: number;
|
||||||
|
@ -433,7 +433,6 @@ export class ContractTerms {
|
|||||||
extra: any;
|
extra: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Refund permission in the format that the merchant gives it to us.
|
* Refund permission in the format that the merchant gives it to us.
|
||||||
*/
|
*/
|
||||||
@ -788,6 +787,53 @@ export interface MerchantPayResponse {
|
|||||||
sig: string;
|
sig: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ExchangeMeltResponse {
|
||||||
|
/**
|
||||||
|
* Which of the kappa indices does the client not have to reveal.
|
||||||
|
*/
|
||||||
|
noreveal_index: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Signature of TALER_RefreshMeltConfirmationPS whereby the exchange
|
||||||
|
* affirms the successful melt and confirming the noreveal_index
|
||||||
|
*/
|
||||||
|
exchange_sig: EddsaSignatureString;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* public EdDSA key of the exchange that was used to generate the signature.
|
||||||
|
* Should match one of the exchange's signing keys from /keys. Again given
|
||||||
|
* explicitly as the client might otherwise be confused by clock skew as to
|
||||||
|
* which signing key was used.
|
||||||
|
*/
|
||||||
|
exchange_pub: EddsaPublicKeyString;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Base URL to use for operations on the refresh context
|
||||||
|
* (so the reveal operation). If not given,
|
||||||
|
* the base URL is the same as the one used for this request.
|
||||||
|
* Can be used if the base URL for /refreshes/ differs from that
|
||||||
|
* for /coins/, i.e. for load balancing. Clients SHOULD
|
||||||
|
* respect the refresh_base_url if provided. Any HTTP server
|
||||||
|
* belonging to an exchange MUST generate a 307 or 308 redirection
|
||||||
|
* to the correct base URL should a client uses the wrong base
|
||||||
|
* URL, or if the base URL has changed since the melt.
|
||||||
|
*
|
||||||
|
* When melting the same coin twice (technically allowed
|
||||||
|
* as the response might have been lost on the network),
|
||||||
|
* the exchange may return different values for the refresh_base_url.
|
||||||
|
*/
|
||||||
|
refresh_base_url?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ExchangeRevealItem {
|
||||||
|
ev_sig: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ExchangeRevealResponse {
|
||||||
|
// List of the exchange's blinded RSA signatures on the new coins.
|
||||||
|
ev_sigs: ExchangeRevealItem[];
|
||||||
|
}
|
||||||
|
|
||||||
export type AmountString = string;
|
export type AmountString = string;
|
||||||
export type Base32String = string;
|
export type Base32String = string;
|
||||||
export type EddsaSignatureString = string;
|
export type EddsaSignatureString = string;
|
||||||
@ -1028,3 +1074,23 @@ export const codecForMerchantPayResponse = (): Codec<MerchantPayResponse> =>
|
|||||||
makeCodecForObject<MerchantPayResponse>()
|
makeCodecForObject<MerchantPayResponse>()
|
||||||
.property("sig", codecForString)
|
.property("sig", codecForString)
|
||||||
.build("MerchantPayResponse");
|
.build("MerchantPayResponse");
|
||||||
|
|
||||||
|
export const codecForExchangeMeltResponse = (): Codec<ExchangeMeltResponse> =>
|
||||||
|
makeCodecForObject<ExchangeMeltResponse>()
|
||||||
|
.property("exchange_pub", codecForString)
|
||||||
|
.property("exchange_sig", codecForString)
|
||||||
|
.property("noreveal_index", codecForNumber)
|
||||||
|
.property("refresh_base_url", makeCodecOptional(codecForString))
|
||||||
|
.build("ExchangeMeltResponse");
|
||||||
|
|
||||||
|
export const codecForExchangeRevealItem = (): Codec<ExchangeRevealItem> =>
|
||||||
|
makeCodecForObject<ExchangeRevealItem>()
|
||||||
|
.property("ev_sig", codecForString)
|
||||||
|
.build("ExchangeRevealItem");
|
||||||
|
|
||||||
|
export const codecForExchangeRevealResponse = (): Codec<
|
||||||
|
ExchangeRevealResponse
|
||||||
|
> =>
|
||||||
|
makeCodecForObject<ExchangeRevealResponse>()
|
||||||
|
.property("ev_sigs", makeCodecForList(codecForExchangeRevealItem()))
|
||||||
|
.build("ExchangeRevealResponse");
|
||||||
|
@ -303,7 +303,7 @@ export class ReturnCoinsRequest {
|
|||||||
* Wire details for the bank account of the customer that will
|
* Wire details for the bank account of the customer that will
|
||||||
* receive the funds.
|
* receive the funds.
|
||||||
*/
|
*/
|
||||||
senderWire?: object;
|
senderWire?: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Verify that a value matches the schema of this class and convert it into a
|
* Verify that a value matches the schema of this class and convert it into a
|
||||||
@ -406,10 +406,11 @@ export interface WalletDiagnostics {
|
|||||||
dbOutdated: boolean;
|
dbOutdated: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface OperationError {
|
export interface OperationErrorDetails {
|
||||||
type: string;
|
talerErrorCode: number;
|
||||||
|
talerErrorHint: string;
|
||||||
message: string;
|
message: string;
|
||||||
details: any;
|
details: unknown;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PlanchetCreationResult {
|
export interface PlanchetCreationResult {
|
||||||
|
@ -24,9 +24,7 @@ const jAmt = (
|
|||||||
currency: string,
|
currency: string,
|
||||||
): AmountJson => ({ value, fraction, currency });
|
): AmountJson => ({ value, fraction, currency });
|
||||||
|
|
||||||
const sAmt = (
|
const sAmt = (s: string): AmountJson => Amounts.parseOrThrow(s);
|
||||||
s: string
|
|
||||||
): AmountJson => Amounts.parseOrThrow(s);
|
|
||||||
|
|
||||||
test("amount addition (simple)", (t) => {
|
test("amount addition (simple)", (t) => {
|
||||||
const a1 = jAmt(1, 0, "EUR");
|
const a1 = jAmt(1, 0, "EUR");
|
||||||
|
@ -349,7 +349,7 @@ function mult(a: AmountJson, n: number): Result {
|
|||||||
n = n / 2;
|
n = n / 2;
|
||||||
} else {
|
} else {
|
||||||
n = (n - 1) / 2;
|
n = (n - 1) / 2;
|
||||||
const r2 = add(acc, x)
|
const r2 = add(acc, x);
|
||||||
if (r2.saturated) {
|
if (r2.saturated) {
|
||||||
return r2;
|
return r2;
|
||||||
}
|
}
|
||||||
|
269
src/util/http.ts
269
src/util/http.ts
@ -14,18 +14,26 @@
|
|||||||
TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
|
TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Codec } from "./codec";
|
|
||||||
import { OperationFailedError } from "../operations/errors";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Helpers for doing XMLHttpRequest-s that are based on ES6 promises.
|
* Helpers for doing XMLHttpRequest-s that are based on ES6 promises.
|
||||||
* Allows for easy mocking for test cases.
|
* Allows for easy mocking for test cases.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Imports
|
||||||
|
*/
|
||||||
|
import { Codec } from "./codec";
|
||||||
|
import { OperationFailedError, makeErrorDetails } from "../operations/errors";
|
||||||
|
import { TalerErrorCode } from "../TalerErrorCode";
|
||||||
|
import { Logger } from "./logging";
|
||||||
|
|
||||||
|
const logger = new Logger("http.ts");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An HTTP response that is returned by all request methods of this library.
|
* An HTTP response that is returned by all request methods of this library.
|
||||||
*/
|
*/
|
||||||
export interface HttpResponse {
|
export interface HttpResponse {
|
||||||
|
requestUrl: string;
|
||||||
status: number;
|
status: number;
|
||||||
headers: Headers;
|
headers: Headers;
|
||||||
json(): Promise<any>;
|
json(): Promise<any>;
|
||||||
@ -67,10 +75,20 @@ export class Headers {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The request library is bundled into an interface to m responseJson: object & any;ake mocking easy.
|
* Interface for the HTTP request library used by the wallet.
|
||||||
|
*
|
||||||
|
* The request library is bundled into an interface to make mocking and
|
||||||
|
* request tunneling easy.
|
||||||
*/
|
*/
|
||||||
export interface HttpRequestLibrary {
|
export interface HttpRequestLibrary {
|
||||||
|
/**
|
||||||
|
* Make an HTTP GET request.
|
||||||
|
*/
|
||||||
get(url: string, opt?: HttpRequestOptions): Promise<HttpResponse>;
|
get(url: string, opt?: HttpRequestOptions): Promise<HttpResponse>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Make an HTTP POST request with a JSON body.
|
||||||
|
*/
|
||||||
postJson(
|
postJson(
|
||||||
url: string,
|
url: string,
|
||||||
body: any,
|
body: any,
|
||||||
@ -105,18 +123,29 @@ export class BrowserHttpLib implements HttpRequestLibrary {
|
|||||||
}
|
}
|
||||||
|
|
||||||
myRequest.onerror = (e) => {
|
myRequest.onerror = (e) => {
|
||||||
console.error("http request error");
|
logger.error("http request error");
|
||||||
reject(Error("could not make XMLHttpRequest"));
|
reject(
|
||||||
|
OperationFailedError.fromCode(
|
||||||
|
TalerErrorCode.WALLET_NETWORK_ERROR,
|
||||||
|
"Could not make request",
|
||||||
|
{
|
||||||
|
requestUrl: url,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
myRequest.addEventListener("readystatechange", (e) => {
|
myRequest.addEventListener("readystatechange", (e) => {
|
||||||
if (myRequest.readyState === XMLHttpRequest.DONE) {
|
if (myRequest.readyState === XMLHttpRequest.DONE) {
|
||||||
if (myRequest.status === 0) {
|
if (myRequest.status === 0) {
|
||||||
reject(
|
const exc = OperationFailedError.fromCode(
|
||||||
Error(
|
TalerErrorCode.WALLET_NETWORK_ERROR,
|
||||||
"HTTP Request failed (status code 0, maybe URI scheme is wrong?)",
|
"HTTP request failed (status 0, maybe URI scheme was wrong?)",
|
||||||
),
|
{
|
||||||
|
requestUrl: url,
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
reject(exc);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const makeJson = async (): Promise<any> => {
|
const makeJson = async (): Promise<any> => {
|
||||||
@ -124,10 +153,24 @@ export class BrowserHttpLib implements HttpRequestLibrary {
|
|||||||
try {
|
try {
|
||||||
responseJson = JSON.parse(myRequest.responseText);
|
responseJson = JSON.parse(myRequest.responseText);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw Error("Invalid JSON from HTTP response");
|
throw OperationFailedError.fromCode(
|
||||||
|
TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE,
|
||||||
|
"Invalid JSON from HTTP response",
|
||||||
|
{
|
||||||
|
requestUrl: url,
|
||||||
|
httpStatusCode: myRequest.status,
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if (responseJson === null || typeof responseJson !== "object") {
|
if (responseJson === null || typeof responseJson !== "object") {
|
||||||
throw Error("Invalid JSON from HTTP response");
|
throw OperationFailedError.fromCode(
|
||||||
|
TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE,
|
||||||
|
"Invalid JSON from HTTP response",
|
||||||
|
{
|
||||||
|
requestUrl: url,
|
||||||
|
httpStatusCode: myRequest.status,
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return responseJson;
|
return responseJson;
|
||||||
};
|
};
|
||||||
@ -141,13 +184,14 @@ export class BrowserHttpLib implements HttpRequestLibrary {
|
|||||||
const parts = line.split(": ");
|
const parts = line.split(": ");
|
||||||
const headerName = parts.shift();
|
const headerName = parts.shift();
|
||||||
if (!headerName) {
|
if (!headerName) {
|
||||||
console.error("invalid header");
|
logger.warn("skipping invalid header");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const value = parts.join(": ");
|
const value = parts.join(": ");
|
||||||
headerMap.set(headerName, value);
|
headerMap.set(headerName, value);
|
||||||
});
|
});
|
||||||
const resp: HttpResponse = {
|
const resp: HttpResponse = {
|
||||||
|
requestUrl: url,
|
||||||
status: myRequest.status,
|
status: myRequest.status,
|
||||||
headers: headerMap,
|
headers: headerMap,
|
||||||
json: makeJson,
|
json: makeJson,
|
||||||
@ -165,7 +209,7 @@ export class BrowserHttpLib implements HttpRequestLibrary {
|
|||||||
|
|
||||||
postJson(
|
postJson(
|
||||||
url: string,
|
url: string,
|
||||||
body: any,
|
body: unknown,
|
||||||
opt?: HttpRequestOptions,
|
opt?: HttpRequestOptions,
|
||||||
): Promise<HttpResponse> {
|
): Promise<HttpResponse> {
|
||||||
return this.req("post", url, JSON.stringify(body), opt);
|
return this.req("post", url, JSON.stringify(body), opt);
|
||||||
@ -176,114 +220,121 @@ export class BrowserHttpLib implements HttpRequestLibrary {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PostJsonRequest<RespType> {
|
type TalerErrorResponse = {
|
||||||
http: HttpRequestLibrary;
|
code: number;
|
||||||
url: string;
|
} & unknown;
|
||||||
body: any;
|
|
||||||
codec: Codec<RespType>;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
type ResponseOrError<T> =
|
||||||
* Helper for making Taler-style HTTP POST requests with a JSON payload and response.
|
| { isError: false; response: T }
|
||||||
*/
|
| { isError: true; talerErrorResponse: TalerErrorResponse };
|
||||||
export async function httpPostTalerJson<RespType>(
|
|
||||||
req: PostJsonRequest<RespType>,
|
|
||||||
): Promise<RespType> {
|
|
||||||
const resp = await req.http.postJson(req.url, req.body);
|
|
||||||
|
|
||||||
if (resp.status !== 200) {
|
export async function readSuccessResponseJsonOrErrorCode<T>(
|
||||||
let exc: OperationFailedError | undefined = undefined;
|
httpResponse: HttpResponse,
|
||||||
try {
|
codec: Codec<T>,
|
||||||
const errorJson = await resp.json();
|
): Promise<ResponseOrError<T>> {
|
||||||
const m = `received error response (status ${resp.status})`;
|
if (!(httpResponse.status >= 200 && httpResponse.status < 300)) {
|
||||||
exc = new OperationFailedError({
|
const errJson = await httpResponse.json();
|
||||||
type: "protocol",
|
const talerErrorCode = errJson.code;
|
||||||
message: m,
|
if (typeof talerErrorCode !== "number") {
|
||||||
details: {
|
throw new OperationFailedError(
|
||||||
httpStatusCode: resp.status,
|
makeErrorDetails(
|
||||||
errorResponse: errorJson,
|
TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE,
|
||||||
},
|
"Error response did not contain error code",
|
||||||
});
|
{
|
||||||
} catch (e) {
|
requestUrl: httpResponse.requestUrl,
|
||||||
const m = "could not parse response JSON";
|
},
|
||||||
exc = new OperationFailedError({
|
),
|
||||||
type: "network",
|
);
|
||||||
message: m,
|
|
||||||
details: {
|
|
||||||
status: resp.status,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
throw exc;
|
return {
|
||||||
|
isError: true,
|
||||||
|
talerErrorResponse: errJson,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
let json: any;
|
const respJson = await httpResponse.json();
|
||||||
|
let parsedResponse: T;
|
||||||
try {
|
try {
|
||||||
json = await resp.json();
|
parsedResponse = codec.decode(respJson);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
const m = "could not parse response JSON";
|
throw OperationFailedError.fromCode(
|
||||||
throw new OperationFailedError({
|
TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE,
|
||||||
type: "network",
|
"Response invalid",
|
||||||
message: m,
|
{
|
||||||
details: {
|
requestUrl: httpResponse.requestUrl,
|
||||||
status: resp.status,
|
httpStatusCode: httpResponse.status,
|
||||||
|
validationError: e.toString(),
|
||||||
},
|
},
|
||||||
});
|
);
|
||||||
}
|
}
|
||||||
return req.codec.decode(json);
|
return {
|
||||||
|
isError: false,
|
||||||
|
response: parsedResponse,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function throwUnexpectedRequestError(
|
||||||
export interface GetJsonRequest<RespType> {
|
httpResponse: HttpResponse,
|
||||||
http: HttpRequestLibrary;
|
talerErrorResponse: TalerErrorResponse,
|
||||||
url: string;
|
): never {
|
||||||
codec: Codec<RespType>;
|
throw new OperationFailedError(
|
||||||
|
makeErrorDetails(
|
||||||
|
TalerErrorCode.WALLET_UNEXPECTED_REQUEST_ERROR,
|
||||||
|
"Unexpected error code in response",
|
||||||
|
{
|
||||||
|
requestUrl: httpResponse.requestUrl,
|
||||||
|
httpStatusCode: httpResponse.status,
|
||||||
|
errorResponse: talerErrorResponse,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
export async function readSuccessResponseJsonOrThrow<T>(
|
||||||
* Helper for making Taler-style HTTP GET requests with a JSON payload.
|
httpResponse: HttpResponse,
|
||||||
*/
|
codec: Codec<T>,
|
||||||
export async function httpGetTalerJson<RespType>(
|
): Promise<T> {
|
||||||
req: GetJsonRequest<RespType>,
|
const r = await readSuccessResponseJsonOrErrorCode(httpResponse, codec);
|
||||||
): Promise<RespType> {
|
if (!r.isError) {
|
||||||
const resp = await req.http.get(req.url);
|
return r.response;
|
||||||
|
}
|
||||||
|
throwUnexpectedRequestError(httpResponse, r.talerErrorResponse);
|
||||||
|
}
|
||||||
|
|
||||||
if (resp.status !== 200) {
|
export async function readSuccessResponseTextOrErrorCode<T>(
|
||||||
let exc: OperationFailedError | undefined = undefined;
|
httpResponse: HttpResponse,
|
||||||
try {
|
): Promise<ResponseOrError<string>> {
|
||||||
const errorJson = await resp.json();
|
if (!(httpResponse.status >= 200 && httpResponse.status < 300)) {
|
||||||
const m = `received error response (status ${resp.status})`;
|
const errJson = await httpResponse.json();
|
||||||
exc = new OperationFailedError({
|
const talerErrorCode = errJson.code;
|
||||||
type: "protocol",
|
if (typeof talerErrorCode !== "number") {
|
||||||
message: m,
|
throw new OperationFailedError(
|
||||||
details: {
|
makeErrorDetails(
|
||||||
httpStatusCode: resp.status,
|
TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE,
|
||||||
errorResponse: errorJson,
|
"Error response did not contain error code",
|
||||||
},
|
{
|
||||||
});
|
requestUrl: httpResponse.requestUrl,
|
||||||
} catch (e) {
|
},
|
||||||
const m = "could not parse response JSON";
|
),
|
||||||
exc = new OperationFailedError({
|
);
|
||||||
type: "network",
|
|
||||||
message: m,
|
|
||||||
details: {
|
|
||||||
status: resp.status,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
throw exc;
|
return {
|
||||||
|
isError: true,
|
||||||
|
talerErrorResponse: errJson,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
let json: any;
|
const respJson = await httpResponse.text();
|
||||||
try {
|
return {
|
||||||
json = await resp.json();
|
isError: false,
|
||||||
} catch (e) {
|
response: respJson,
|
||||||
const m = "could not parse response JSON";
|
};
|
||||||
throw new OperationFailedError({
|
}
|
||||||
type: "network",
|
|
||||||
message: m,
|
export async function readSuccessResponseTextOrThrow<T>(
|
||||||
details: {
|
httpResponse: HttpResponse,
|
||||||
status: resp.status,
|
): Promise<string> {
|
||||||
},
|
const r = await readSuccessResponseTextOrErrorCode(httpResponse);
|
||||||
});
|
if (!r.isError) {
|
||||||
}
|
return r.response;
|
||||||
return req.codec.decode(json);
|
}
|
||||||
|
throwUnexpectedRequestError(httpResponse, r.talerErrorResponse);
|
||||||
}
|
}
|
||||||
|
@ -52,7 +52,7 @@ import {
|
|||||||
ReserveRecordStatus,
|
ReserveRecordStatus,
|
||||||
CoinSourceType,
|
CoinSourceType,
|
||||||
} from "./types/dbTypes";
|
} from "./types/dbTypes";
|
||||||
import { MerchantRefundDetails, CoinDumpJson } from "./types/talerTypes";
|
import { CoinDumpJson } from "./types/talerTypes";
|
||||||
import {
|
import {
|
||||||
BenchmarkResult,
|
BenchmarkResult,
|
||||||
ConfirmPayResult,
|
ConfirmPayResult,
|
||||||
@ -106,11 +106,7 @@ import {
|
|||||||
} from "./types/pending";
|
} from "./types/pending";
|
||||||
import { WalletNotification, NotificationType } from "./types/notifications";
|
import { WalletNotification, NotificationType } from "./types/notifications";
|
||||||
import { HistoryQuery, HistoryEvent } from "./types/history";
|
import { HistoryQuery, HistoryEvent } from "./types/history";
|
||||||
import {
|
import { processPurchaseQueryRefund, applyRefund } from "./operations/refund";
|
||||||
processPurchaseQueryRefund,
|
|
||||||
getFullRefundFees,
|
|
||||||
applyRefund,
|
|
||||||
} from "./operations/refund";
|
|
||||||
import { durationMin, Duration } from "./util/time";
|
import { durationMin, Duration } from "./util/time";
|
||||||
import { processRecoupGroup } from "./operations/recoup";
|
import { processRecoupGroup } from "./operations/recoup";
|
||||||
import { OperationFailedAndReportedError } from "./operations/errors";
|
import { OperationFailedAndReportedError } from "./operations/errors";
|
||||||
@ -372,12 +368,12 @@ export class Wallet {
|
|||||||
type: NotificationType.InternalError,
|
type: NotificationType.InternalError,
|
||||||
message: "uncaught exception",
|
message: "uncaught exception",
|
||||||
exception: e,
|
exception: e,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.ws.notify({
|
this.ws.notify({
|
||||||
type: NotificationType.PendingOperationProcessed,
|
type: NotificationType.PendingOperationProcessed,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -712,12 +708,6 @@ export class Wallet {
|
|||||||
return this.db.get(Stores.purchases, contractTermsHash);
|
return this.db.get(Stores.purchases, contractTermsHash);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getFullRefundFees(
|
|
||||||
refundPermissions: MerchantRefundDetails[],
|
|
||||||
): Promise<AmountJson> {
|
|
||||||
return getFullRefundFees(this.ws, refundPermissions);
|
|
||||||
}
|
|
||||||
|
|
||||||
async acceptTip(talerTipUri: string): Promise<void> {
|
async acceptTip(talerTipUri: string): Promise<void> {
|
||||||
try {
|
try {
|
||||||
return acceptTip(this.ws, talerTipUri);
|
return acceptTip(this.ws, talerTipUri);
|
||||||
|
@ -35,7 +35,9 @@ import {
|
|||||||
} from "../wxApi";
|
} from "../wxApi";
|
||||||
|
|
||||||
function WithdrawalDialog(props: { talerWithdrawUri: string }): JSX.Element {
|
function WithdrawalDialog(props: { talerWithdrawUri: string }): JSX.Element {
|
||||||
const [details, setDetails] = useState<WithdrawalDetailsResponse | undefined>();
|
const [details, setDetails] = useState<
|
||||||
|
WithdrawalDetailsResponse | undefined
|
||||||
|
>();
|
||||||
const [selectedExchange, setSelectedExchange] = useState<
|
const [selectedExchange, setSelectedExchange] = useState<
|
||||||
string | undefined
|
string | undefined
|
||||||
>();
|
>();
|
||||||
|
@ -29,10 +29,7 @@ import {
|
|||||||
openTalerDatabase,
|
openTalerDatabase,
|
||||||
WALLET_DB_MINOR_VERSION,
|
WALLET_DB_MINOR_VERSION,
|
||||||
} from "../db";
|
} from "../db";
|
||||||
import {
|
import { ReturnCoinsRequest, WalletDiagnostics } from "../types/walletTypes";
|
||||||
ReturnCoinsRequest,
|
|
||||||
WalletDiagnostics,
|
|
||||||
} from "../types/walletTypes";
|
|
||||||
import { BrowserHttpLib } from "../util/http";
|
import { BrowserHttpLib } from "../util/http";
|
||||||
import { OpenedPromise, openPromise } from "../util/promiseUtils";
|
import { OpenedPromise, openPromise } from "../util/promiseUtils";
|
||||||
import { classifyTalerUri, TalerUriType } from "../util/taleruri";
|
import { classifyTalerUri, TalerUriType } from "../util/taleruri";
|
||||||
|
Loading…
Reference in New Issue
Block a user