implement common wallet-core API handler

This commit is contained in:
Florian Dold 2020-07-23 19:24:00 +05:30
parent 80433b9399
commit 56ece296e0
No known key found for this signature in database
GPG Key ID: D2E4F00F29D02A4B
9 changed files with 3405 additions and 137 deletions

3079
src/TalerErrorCode.ts Normal file

File diff suppressed because it is too large Load Diff

View File

@ -38,6 +38,7 @@ import {
WALLET_MERCHANT_PROTOCOL_VERSION,
} from "../operations/versions";
import { Amounts } from "../util/amounts";
import { handleCoreApiRequest } from "../walletCoreApiHandler";
// @ts-ignore: special built-in module
//import akono = require("akono");
@ -168,88 +169,8 @@ class AndroidWalletMessageHandler {
},
};
}
case "getTransactions": {
const wallet = await this.wp.promise;
return await wallet.getTransactions(args);
}
case "abortProposal": {
const wallet = await this.wp.promise;
if (typeof args.proposalId !== "string") {
throw Error("propsalId must be a string");
}
return await wallet.refuseProposal(args.proposalId);
}
case "getBalances": {
const wallet = await this.wp.promise;
return await wallet.getBalances();
}
case "getPendingOperations": {
const wallet = await this.wp.promise;
return await wallet.getPendingOperations();
}
case "listExchanges": {
const wallet = await this.wp.promise;
return await wallet.getExchanges();
}
case "addExchange": {
const wallet = await this.wp.promise;
await wallet.updateExchangeFromUrl(args.exchangeBaseUrl);
return {};
}
case "getWithdrawalDetailsForAmount": {
const wallet = await this.wp.promise;
return await wallet.getWithdrawalDetailsForAmount(
args.exchangeBaseUrl,
args.amount,
);
}
case "withdrawTestkudos": {
const wallet = await this.wp.promise;
try {
await withdrawTestBalance(wallet);
} catch (e) {
console.log("error during withdrawTestBalance", e);
}
return {};
}
case "getHistory": {
const wallet = await this.wp.promise;
return await wallet.getHistory();
}
case "getExchangeTos": {
const wallet = await this.wp.promise;
const exchangeBaseUrl = args.exchangeBaseUrl;
return wallet.getExchangeTos(exchangeBaseUrl);
}
case "setExchangeTosAccepted": {
const wallet = await this.wp.promise;
await wallet.acceptExchangeTermsOfService(
args.exchangeBaseUrl,
args.acceptedEtag,
);
return {};
}
case "retryPendingNow": {
const wallet = await this.wp.promise;
await wallet.runPending(true);
return {};
}
case "preparePay": {
const wallet = await this.wp.promise;
return await wallet.preparePayForUri(args.url);
break;
}
case "confirmPay": {
const wallet = await this.wp.promise;
return await wallet.confirmPay(args.proposalId, args.sessionId);
}
case "acceptManualWithdrawal": {
const wallet = await this.wp.promise;
const res = await wallet.acceptManualWithdrawal(
args.exchangeBaseUrl,
Amounts.parseOrThrow(args.amount),
);
return res;
return [];
}
case "startTunnel": {
// this.httpLib.useNfcTunnel = true;
@ -263,31 +184,6 @@ class AndroidWalletMessageHandler {
// httpLib.handleTunnelResponse(msg.args);
throw Error("not implemented");
}
case "getWithdrawDetailsForUri": {
const wallet = await this.wp.promise;
return await wallet.getWithdrawDetailsForUri(
args.talerWithdrawUri,
args.selectedExchange,
);
}
case "applyRefund": {
const wallet = await this.wp.promise;
return await wallet.applyRefund(args.talerRefundUri);
}
case "acceptExchangeTermsOfService": {
const wallet = await this.wp.promise;
return await wallet.acceptExchangeTermsOfService(
args.exchangeBaseUrl,
args.etag,
);
}
case "acceptWithdrawal": {
const wallet = await this.wp.promise;
return await wallet.acceptWithdrawal(
args.talerWithdrawUri,
args.selectedExchange,
);
}
case "reset": {
const oldArgs = this.walletArgs;
this.walletArgs = { ...oldArgs };
@ -312,8 +208,11 @@ class AndroidWalletMessageHandler {
this.wp.resolve(w);
return {};
}
default:
throw Error(`operation "${operation}" not understood`);
default: {
const wallet = await this.wp.promise;
return await handleCoreApiRequest(wallet, operation, id, args);
}
}
}
}

View File

@ -199,23 +199,22 @@ walletCli
});
walletCli
.subcommand("api", "balance", { help: "Call the wallet-core API directly." })
.subcommand("api", "api", { help: "Call the wallet-core API directly." })
.requiredArgument("operation", clk.STRING)
.requiredArgument("request", clk.STRING)
.action(async (args) => {
await withWallet(args, async (wallet) => {
let requestJson;
try {
requestJson = JSON.parse(args.api.operation);
requestJson = JSON.parse(args.api.request);
} catch (e) {
console.error("malformed request");
console.error("Invalid JSON");
process.exit(1);
return;
}
const resp = await handleCoreApiRequest(
wallet,
args.api.operation,
1,
"reqid-1",
requestJson,
);
console.log(JSON.stringify(resp, undefined, 2));

View File

@ -51,6 +51,9 @@ import {
readSuccessResponseJsonOrThrow,
readSuccessResponseTextOrThrow,
} from "../util/http";
import { Logger } from "../util/logging";
const logger = new Logger("exchanges.ts");
async function denominationRecordFromKeys(
ws: InternalWalletState,
@ -197,7 +200,7 @@ async function updateExchangeWithKeys(
// Handle recoup
const recoupDenomList = exchangeKeysJson.recoup ?? [];
const newlyRevokedCoinPubs: string[] = [];
console.log("recoup list from exchange", recoupDenomList);
logger.trace("recoup list from exchange", recoupDenomList);
for (const recoupInfo of recoupDenomList) {
const oldDenom = await tx.getIndexed(
Stores.denominations.denomPubHashIndex,
@ -354,7 +357,7 @@ async function updateExchangeWithWireInfo(
);
for (const a of wireInfo.accounts) {
console.log("validating exchange acct");
logger.trace("validating exchange acct");
const isValid = await ws.cryptoApi.isValidWireAccount(
a.payto_uri,
a.master_sig,

View File

@ -440,11 +440,8 @@ async function incrementReserveRetry(
if (!r.retryInfo) {
return;
}
console.log("updating retry info");
console.log("before", r.retryInfo);
r.retryInfo.retryCounter++;
updateRetryInfoTimeout(r.retryInfo);
console.log("after", r.retryInfo);
r.lastError = err;
await tx.put(Stores.reserves, r);
});
@ -528,16 +525,10 @@ async function updateReserve(
reserveInfo.history,
);
console.log(
"reconciled history:",
JSON.stringify(reconciled, undefined, 2),
);
const summary = summarizeReserveHistory(
reconciled.updatedLocalHistory,
currency,
);
console.log("summary", summary);
if (
reconciled.newAddedItems.length + reconciled.newMatchedItems.length !=
@ -765,7 +756,7 @@ async function depleteReserve(
);
if (newWithdrawalGroup) {
console.log("processing new withdraw group");
logger.trace("processing new withdraw group");
ws.notify({
type: NotificationType.WithdrawGroupCreated,
withdrawalGroupId: newWithdrawalGroup.withdrawalGroupId,

View File

@ -26,6 +26,7 @@
*/
import { Timestamp } from "../util/time";
import { AmountString, Product } from "./talerTypes";
import { Codec, makeCodecForObject, makeCodecOptional, codecForString } from "../util/codec";
export interface TransactionsRequest {
/**
@ -300,3 +301,10 @@ interface TransactionRefresh extends TransactionCommon {
// Amount that will be paid as fees for the refresh
amountEffective: AmountString;
}
export const codecForTransactionsRequest = (): Codec<TransactionsRequest> =>
makeCodecForObject<TransactionsRequest>()
.property("currency", makeCodecOptional(codecForString))
.property("search", makeCodecOptional(codecForString))
.build("TransactionsRequest");

View File

@ -25,6 +25,9 @@
* Imports.
*/
import { Duration } from "./time";
import { Logger } from "./logging";
const logger = new Logger("timer.ts");
/**
* Cancelable timer.
@ -119,7 +122,7 @@ export class TimerGroup {
after(delayMs: number, callback: () => void): TimerHandle {
if (this.stopped) {
console.warn("dropping timer since timer group is stopped");
logger.warn("dropping timer since timer group is stopped");
return nullTimerHandle;
}
const h = after(delayMs, callback);
@ -138,7 +141,7 @@ export class TimerGroup {
every(delayMs: number, callback: () => void): TimerHandle {
if (this.stopped) {
console.warn("dropping timer since timer group is stopped");
logger.warn("dropping timer since timer group is stopped");
return nullTimerHandle;
}
const h = every(delayMs, callback);

View File

@ -195,7 +195,7 @@ export class Wallet {
pending: PendingOperationInfo,
forceNow = false,
): Promise<void> {
console.log("running pending", pending);
logger.trace(`running pending ${JSON.stringify(pending, undefined, 2)}`);
switch (pending.type) {
case PendingOperationType.Bug:
// Nothing to do, will just be displayed to the user
@ -552,15 +552,6 @@ export class Wallet {
return await this.db.get(Stores.exchanges, exchangeBaseUrl);
}
/**
* Retrive the full event history for this wallet.
*/
async getHistory(
historyQuery?: any[],
): Promise<{ history: any[] }> {
return { history: [] };
}
async getPendingOperations({ onlyDue = false } = {}): Promise<
PendingOperationsResponse
> {

295
src/walletCoreApiHandler.ts Normal file
View File

@ -0,0 +1,295 @@
/*
This file is part of GNU Taler
(C) 2020 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
Foundation; either version 3, or (at your option) any later version.
GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
import { Wallet } from "./wallet";
import {
OperationFailedError,
OperationFailedAndReportedError,
makeErrorDetails,
} from "./operations/errors";
import { TalerErrorCode } from "./TalerErrorCode";
import { withdrawTestBalance } from "./headless/helpers";
import { codecForTransactionsRequest } from "./types/transactions";
import {
makeCodecForObject,
codecForString,
Codec,
makeCodecOptional,
} from "./util/codec";
import { Amounts } from "./util/amounts";
interface AddExchangeRequest {
exchangeBaseUrl: string;
}
const codecForAddExchangeRequest = (): Codec<AddExchangeRequest> =>
makeCodecForObject<AddExchangeRequest>()
.property("exchangeBaseUrl", codecForString)
.build("AddExchangeRequest");
interface GetExchangeTosRequest {
exchangeBaseUrl: string;
}
const codecForGetExchangeTosRequest = (): Codec<GetExchangeTosRequest> =>
makeCodecForObject<GetExchangeTosRequest>()
.property("exchangeBaseUrl", codecForString)
.build("GetExchangeTosRequest");
interface AcceptManualWithdrawalRequest {
exchangeBaseUrl: string;
amount: string;
}
const codecForAcceptManualWithdrawalRequet = (): Codec<
AcceptManualWithdrawalRequest
> =>
makeCodecForObject<AcceptManualWithdrawalRequest>()
.property("exchangeBaseUrl", codecForString)
.property("amount", codecForString)
.build("AcceptManualWithdrawalRequest");
interface GetWithdrawalDetailsForAmountRequest {
exchangeBaseUrl: string;
amount: string;
}
interface AcceptBankIntegratedWithdrawalRequest {
talerWithdrawUri: string;
exchangeBaseUrl: string;
}
const codecForAcceptBankIntegratedWithdrawalRequest = (): Codec<
AcceptBankIntegratedWithdrawalRequest
> =>
makeCodecForObject<AcceptBankIntegratedWithdrawalRequest>()
.property("exchangeBaseUrl", codecForString)
.property("talerWithdrawUri", codecForString)
.build("AcceptBankIntegratedWithdrawalRequest");
const codecForGetWithdrawalDetailsForAmountRequest = (): Codec<
GetWithdrawalDetailsForAmountRequest
> =>
makeCodecForObject<GetWithdrawalDetailsForAmountRequest>()
.property("exchangeBaseUrl", codecForString)
.property("amount", codecForString)
.build("GetWithdrawalDetailsForAmountRequest");
interface AcceptExchangeTosRequest {
exchangeBaseUrl: string;
etag: string;
}
const codecForAcceptExchangeTosRequest = (): Codec<AcceptExchangeTosRequest> =>
makeCodecForObject<AcceptExchangeTosRequest>()
.property("exchangeBaseUrl", codecForString)
.property("etag", codecForString)
.build("AcceptExchangeTosRequest");
interface ApplyRefundRequest {
talerRefundUri: string;
}
const codecForApplyRefundRequest = (): Codec<ApplyRefundRequest> =>
makeCodecForObject<ApplyRefundRequest>()
.property("talerRefundUri", codecForString)
.build("ApplyRefundRequest");
interface GetWithdrawUriInfoRequest {
talerWithdrawUri: string;
}
const codecForGetWithdrawUriInfoRequest = (): Codec<
GetWithdrawUriInfoRequest
> =>
makeCodecForObject<GetWithdrawUriInfoRequest>()
.property("talerWithdrawUri", codecForString)
.build("GetWithdrawUriInfoRequest");
interface AbortProposalRequest {
proposalId: string;
}
const codecForAbortProposalRequest = (): Codec<AbortProposalRequest> =>
makeCodecForObject<AbortProposalRequest>()
.property("proposalId", codecForString)
.build("AbortProposalRequest");
interface PreparePayRequest {
talerPayUri: string;
}
const codecForPreparePayRequest = (): Codec<PreparePayRequest> =>
makeCodecForObject<PreparePayRequest>()
.property("talerPayUri", codecForString)
.build("PreparePay");
interface ConfirmPayRequest {
proposalId: string;
sessionId?: string;
}
const codecForConfirmPayRequest = (): Codec<ConfirmPayRequest> =>
makeCodecForObject<ConfirmPayRequest>()
.property("proposalId", codecForString)
.property("sessionId", makeCodecOptional(codecForString))
.build("ConfirmPay");
/**
* Implementation of the "wallet-core" API.
*/
async function dispatchRequestInternal(
wallet: Wallet,
operation: string,
payload: unknown,
): Promise<unknown> {
switch (operation) {
case "withdrawTestkudos":
return await withdrawTestBalance(wallet);
case "getTransactions": {
const req = codecForTransactionsRequest().decode(payload);
return await wallet.getTransactions(req);
}
case "addExchange": {
const req = codecForAddExchangeRequest().decode(payload);
await wallet.updateExchangeFromUrl(req.exchangeBaseUrl);
return {};
}
case "listExchanges": {
return await wallet.getExchanges();
}
case "getWithdrawUriInfo": {
const req = codecForGetWithdrawUriInfoRequest().decode(payload);
// FIXME: implement "natively"
throw Error("not implemented");
}
case "acceptManualWithdrawal": {
const req = codecForAcceptManualWithdrawalRequet().decode(payload);
const res = await wallet.acceptManualWithdrawal(
req.exchangeBaseUrl,
Amounts.parseOrThrow(req.amount),
);
return res;
}
case "getWithdrawalDetailsForAmount": {
const req = codecForGetWithdrawalDetailsForAmountRequest().decode(
payload,
);
return await wallet.getWithdrawalDetailsForAmount(
req.exchangeBaseUrl,
Amounts.parseOrThrow(req.amount),
);
}
case "getBalances": {
return await wallet.getBalances();
}
case "getPendingOperations": {
return await wallet.getPendingOperations();
}
case "acceptExchangeTermsOfService": {
const req = codecForAcceptExchangeTosRequest().decode(payload);
return await wallet.acceptExchangeTermsOfService(
req.exchangeBaseUrl,
req.etag,
);
}
case "applyRefund": {
const req = codecForApplyRefundRequest().decode(payload);
return await wallet.applyRefund(req.talerRefundUri);
}
case "acceptBankIntegratedWithdrawal": {
const req = codecForAcceptBankIntegratedWithdrawalRequest().decode(
payload,
);
return await wallet.acceptWithdrawal(
req.talerWithdrawUri,
req.exchangeBaseUrl,
);
}
case "getExchangeTos": {
const req = codecForGetExchangeTosRequest().decode(payload);
return wallet.getExchangeTos(req.exchangeBaseUrl);
}
case "abortProposal": {
const req = codecForAbortProposalRequest().decode(payload);
return await wallet.refuseProposal(req.proposalId);
}
case "retryPendingNow": {
await wallet.runPending(true);
return {};
}
case "preparePay": {
const req = codecForPreparePayRequest().decode(payload);
return await wallet.preparePayForUri(req.talerPayUri);
}
case "confirmPay": {
const req = codecForConfirmPayRequest().decode(payload);
return await wallet.confirmPay(req.proposalId, req.sessionId);
}
}
throw OperationFailedError.fromCode(
TalerErrorCode.WALLET_CORE_API_OPERATION_UNKNOWN,
"unknown operation",
{
operation,
},
);
}
/**
* Handle a request to the wallet-core API.
*/
export async function handleCoreApiRequest(
w: Wallet,
operation: string,
id: string,
payload: unknown,
): Promise<unknown> {
try {
const result = await dispatchRequestInternal(w, operation, payload);
const respMsg = {
isError: false,
operation,
id,
result,
};
return respMsg;
} catch (e) {
if (
e instanceof OperationFailedError ||
e instanceof OperationFailedAndReportedError
) {
return {
isError: true,
operation,
id,
error: e.operationError,
};
} else {
return {
isError: true,
operation,
id,
error: makeErrorDetails(
TalerErrorCode.WALLET_UNEXPECTED_EXCEPTION,
`unexpected exception: ${e}`,
{},
),
};
}
}
}