summaryrefslogtreecommitdiff
path: root/packages
diff options
context:
space:
mode:
authorFlorian Dold <florian@dold.me>2021-06-15 18:52:43 +0200
committerFlorian Dold <florian@dold.me>2021-06-15 18:52:43 +0200
commitd41ae5eb97a5264b1d61321354eac049ca317c97 (patch)
treea8e78bee0ff2a92f0b3f1cb9230442186dd17358 /packages
parent4b16d7bd342dbb5376fd2cef08b14ebabbe4ed10 (diff)
downloadwallet-core-d41ae5eb97a5264b1d61321354eac049ca317c97.tar.gz
wallet-core-d41ae5eb97a5264b1d61321354eac049ca317c97.tar.bz2
wallet-core-d41ae5eb97a5264b1d61321354eac049ca317c97.zip
separate wallet state from wallet client
Diffstat (limited to 'packages')
-rw-r--r--packages/taler-util/src/walletTypes.ts14
-rw-r--r--packages/taler-wallet-android/src/index.ts40
-rw-r--r--packages/taler-wallet-cli/src/index.ts362
-rw-r--r--packages/taler-wallet-cli/src/integrationtests/harness.ts5
-rw-r--r--packages/taler-wallet-cli/src/integrationtests/test-payment-fault.ts2
-rw-r--r--packages/taler-wallet-core/src/db.ts2
-rw-r--r--packages/taler-wallet-core/src/headless/helpers.ts7
-rw-r--r--packages/taler-wallet-core/src/index.ts34
-rw-r--r--packages/taler-wallet-core/src/operations/backup/index.ts16
-rw-r--r--packages/taler-wallet-core/src/operations/exchanges.ts7
-rw-r--r--packages/taler-wallet-core/src/operations/refresh.ts5
-rw-r--r--packages/taler-wallet-core/src/operations/state.ts26
-rw-r--r--packages/taler-wallet-core/src/operations/testing.ts88
-rw-r--r--packages/taler-wallet-core/src/operations/transactions.ts2
-rw-r--r--packages/taler-wallet-core/src/util/coinSelection.test.ts2
-rw-r--r--packages/taler-wallet-core/src/util/http.ts5
-rw-r--r--packages/taler-wallet-core/src/wallet.ts1929
-rw-r--r--packages/taler-wallet-webextension/src/wxBackend.ts16
18 files changed, 1284 insertions, 1278 deletions
diff --git a/packages/taler-util/src/walletTypes.ts b/packages/taler-util/src/walletTypes.ts
index 6a0c57139..4d49db029 100644
--- a/packages/taler-util/src/walletTypes.ts
+++ b/packages/taler-util/src/walletTypes.ts
@@ -622,11 +622,13 @@ export const codecForIntegrationTestArgs = (): Codec<IntegrationTestArgs> =>
export interface AddExchangeRequest {
exchangeBaseUrl: string;
+ forceUpdate?: boolean;
}
export const codecForAddExchangeRequest = (): Codec<AddExchangeRequest> =>
buildCodecForObject<AddExchangeRequest>()
.property("exchangeBaseUrl", codecForString())
+ .property("forceUpdate", codecOptional(codecForBoolean()))
.build("AddExchangeRequest");
export interface ForceExchangeUpdateRequest {
@@ -962,3 +964,15 @@ export const codecForRetryTransactionRequest = (): Codec<RetryTransactionRequest
buildCodecForObject<RetryTransactionRequest>()
.property("transactionId", codecForString())
.build("RetryTransactionRequest");
+
+export interface SetWalletDeviceIdRequest {
+ /**
+ * New wallet device ID to set.
+ */
+ walletDeviceId: string;
+}
+
+export const codecForSetWalletDeviceIdRequest = (): Codec<SetWalletDeviceIdRequest> =>
+ buildCodecForObject<SetWalletDeviceIdRequest>()
+ .property("walletDeviceId", codecForString())
+ .build("SetWalletDeviceIdRequest");
diff --git a/packages/taler-wallet-android/src/index.ts b/packages/taler-wallet-android/src/index.ts
index 0be45ae7c..6f6439fb9 100644
--- a/packages/taler-wallet-android/src/index.ts
+++ b/packages/taler-wallet-android/src/index.ts
@@ -18,7 +18,6 @@
* Imports.
*/
import {
- Wallet,
getDefaultNodeWallet,
DefaultNodeWalletArgs,
NodeHttpLib,
@@ -33,7 +32,10 @@ import {
Headers,
WALLET_EXCHANGE_PROTOCOL_VERSION,
WALLET_MERCHANT_PROTOCOL_VERSION,
+ runRetryLoop,
+ handleCoreApiRequest,
} from "@gnu-taler/taler-wallet-core";
+import { InternalWalletState } from "@gnu-taler/taler-wallet-core/lib/operations/state";
import fs from "fs";
import { WalletNotification } from "../../taler-wallet-core/node_modules/@gnu-taler/taler-util/lib/notifications.js";
@@ -154,8 +156,8 @@ function sendAkonoMessage(ev: CoreApiEnvelope): void {
class AndroidWalletMessageHandler {
walletArgs: DefaultNodeWalletArgs | undefined;
- maybeWallet: Wallet | undefined;
- wp = openPromise<Wallet>();
+ maybeWallet: InternalWalletState | undefined;
+ wp = openPromise<InternalWalletState>();
httpLib = new NodeHttpLib();
/**
@@ -174,6 +176,17 @@ class AndroidWalletMessageHandler {
result,
};
};
+
+ const reinit = async () => {
+ const w = await getDefaultNodeWallet(this.walletArgs);
+ this.maybeWallet = w;
+ await handleCoreApiRequest(w, "initWallet", "akono-init", {});
+ runRetryLoop(w).catch((e) => {
+ console.error("Error during wallet retry loop", e);
+ });
+ this.wp.resolve(w);
+ };
+
switch (operation) {
case "init": {
this.walletArgs = {
@@ -183,12 +196,7 @@ class AndroidWalletMessageHandler {
persistentStoragePath: args.persistentStoragePath,
httpLib: this.httpLib,
};
- const w = await getDefaultNodeWallet(this.walletArgs);
- this.maybeWallet = w;
- w.runRetryLoop().catch((e) => {
- console.error("Error during wallet retry loop", e);
- });
- this.wp.resolve(w);
+ await reinit();
return wrapResponse({
supported_protocol_versions: {
exchange: WALLET_EXCHANGE_PROTOCOL_VERSION,
@@ -196,9 +204,6 @@ class AndroidWalletMessageHandler {
},
});
}
- case "getHistory": {
- return wrapResponse({ history: [] });
- }
case "startTunnel": {
// this.httpLib.useNfcTunnel = true;
throw Error("not implemented");
@@ -225,19 +230,14 @@ class AndroidWalletMessageHandler {
}
const wallet = await this.wp.promise;
wallet.stop();
- this.wp = openPromise<Wallet>();
+ this.wp = openPromise<InternalWalletState>();
this.maybeWallet = undefined;
- const w = await getDefaultNodeWallet(this.walletArgs);
- this.maybeWallet = w;
- w.runRetryLoop().catch((e) => {
- console.error("Error during wallet retry loop", e);
- });
- this.wp.resolve(w);
+ await reinit();
return wrapResponse({});
}
default: {
const wallet = await this.wp.promise;
- return await wallet.handleCoreApiRequest(operation, id, args);
+ return await handleCoreApiRequest(wallet, operation, id, args);
}
}
}
diff --git a/packages/taler-wallet-cli/src/index.ts b/packages/taler-wallet-cli/src/index.ts
index c98ece941..63b969f18 100644
--- a/packages/taler-wallet-cli/src/index.ts
+++ b/packages/taler-wallet-cli/src/index.ts
@@ -33,9 +33,9 @@ import {
codecForList,
codecForString,
Logger,
+ WithdrawalType,
} from "@gnu-taler/taler-util";
import {
- Wallet,
NodeHttpLib,
getDefaultNodeWallet,
OperationFailedAndReportedError,
@@ -45,7 +45,14 @@ import {
NodeThreadCryptoWorkerFactory,
CryptoApi,
walletCoreDebugFlags,
+ WalletCoreApiClient,
+ WalletApiOperation,
+ handleCoreApiRequest,
+ runPending,
+ runUntilDone,
+ getClientFromWalletState,
} from "@gnu-taler/taler-wallet-core";
+import { InternalWalletState } from "@gnu-taler/taler-wallet-core/src/operations/state";
// This module also serves as the entry point for the crypto
// thread worker, and thus must expose these two handlers.
@@ -63,11 +70,13 @@ function assertUnreachable(x: never): never {
}
async function doPay(
- wallet: Wallet,
+ wallet: WalletCoreApiClient,
payUrl: string,
options: { alwaysYes: boolean } = { alwaysYes: true },
): Promise<void> {
- const result = await wallet.preparePayForUri(payUrl);
+ const result = await wallet.call(WalletApiOperation.PreparePayForUri, {
+ talerPayUri: payUrl,
+ });
if (result.status === PreparePayResultType.InsufficientBalance) {
console.log("contract", result.contractTerms);
console.error("insufficient balance");
@@ -111,7 +120,9 @@ async function doPay(
}
if (pay) {
- await wallet.confirmPay(result.proposalId, undefined);
+ await wallet.call(WalletApiOperation.ConfirmPay, {
+ proposalId: result.proposalId,
+ });
} else {
console.log("not paying");
}
@@ -161,7 +172,10 @@ type WalletCliArgsType = clk.GetArgType<typeof walletCli>;
async function withWallet<T>(
walletCliArgs: WalletCliArgsType,
- f: (w: Wallet) => Promise<T>,
+ f: (w: {
+ client: WalletCoreApiClient;
+ ws: InternalWalletState;
+ }) => Promise<T>,
): Promise<T> {
const dbPath = walletCliArgs.wallet.walletDbFile ?? defaultWalletDbPath;
const myHttpLib = new NodeHttpLib();
@@ -174,8 +188,11 @@ async function withWallet<T>(
});
applyVerbose(walletCliArgs.wallet.verbose);
try {
- await wallet.fillDefaults();
- const ret = await f(wallet);
+ const w = {
+ ws: wallet,
+ client: await getClientFromWalletState(wallet),
+ };
+ const ret = await f(w);
return ret;
} catch (e) {
if (
@@ -204,7 +221,10 @@ walletCli
})
.action(async (args) => {
await withWallet(args, async (wallet) => {
- const balance = await wallet.getBalances();
+ const balance = await wallet.client.call(
+ WalletApiOperation.GetBalances,
+ {},
+ );
console.log(JSON.stringify(balance, undefined, 2));
});
});
@@ -222,7 +242,8 @@ walletCli
console.error("Invalid JSON");
process.exit(1);
}
- const resp = await wallet.handleCoreApiRequest(
+ const resp = await handleCoreApiRequest(
+ wallet.ws,
args.api.operation,
"reqid-1",
requestJson,
@@ -235,7 +256,10 @@ walletCli
.subcommand("", "pending", { help: "Show pending operations." })
.action(async (args) => {
await withWallet(args, async (wallet) => {
- const pending = await wallet.getPendingOperations();
+ const pending = await wallet.client.call(
+ WalletApiOperation.GetPendingOperations,
+ {},
+ );
console.log(JSON.stringify(pending, undefined, 2));
});
});
@@ -246,10 +270,13 @@ walletCli
.maybeOption("search", ["--search"], clk.STRING)
.action(async (args) => {
await withWallet(args, async (wallet) => {
- const pending = await wallet.getTransactions({
- currency: args.transactions.currency,
- search: args.transactions.search,
- });
+ const pending = await wallet.client.call(
+ WalletApiOperation.GetTransactions,
+ {
+ currency: args.transactions.currency,
+ search: args.transactions.search,
+ },
+ );
console.log(JSON.stringify(pending, undefined, 2));
});
});
@@ -267,7 +294,20 @@ walletCli
.flag("forceNow", ["-f", "--force-now"])
.action(async (args) => {
await withWallet(args, async (wallet) => {
- await wallet.runPending(args.runPendingOpt.forceNow);
+ await runPending(wallet.ws, args.runPendingOpt.forceNow);
+ });
+ });
+
+walletCli
+ .subcommand("retryTransaction", "retry-transaction", {
+ help: "Retry a transaction.",
+ })
+ .requiredArgument("transactionId", clk.STRING)
+ .action(async (args) => {
+ await withWallet(args, async (wallet) => {
+ await wallet.client.call(WalletApiOperation.RetryTransaction, {
+ transactionId: args.retryTransaction.transactionId,
+ });
});
});
@@ -278,10 +318,10 @@ walletCli
.maybeOption("maxRetries", ["--max-retries"], clk.INT)
.action(async (args) => {
await withWallet(args, async (wallet) => {
- await wallet.runUntilDone({
+ await runUntilDone(wallet.ws, {
maxRetries: args.finishPendingOpt.maxRetries,
});
- wallet.stop();
+ wallet.ws.stop();
});
});
@@ -294,7 +334,7 @@ walletCli
})
.action(async (args) => {
await withWallet(args, async (wallet) => {
- await wallet.deleteTransaction({
+ await wallet.client.call(WalletApiOperation.DeleteTransaction, {
transactionId: args.deleteTransaction.transactionId,
});
});
@@ -312,29 +352,51 @@ walletCli
const uriType = classifyTalerUri(uri);
switch (uriType) {
case TalerUriType.TalerPay:
- await doPay(wallet, uri, { alwaysYes: args.handleUri.autoYes });
+ await doPay(wallet.client, uri, {
+ alwaysYes: args.handleUri.autoYes,
+ });
break;
case TalerUriType.TalerTip:
{
- const res = await wallet.prepareTip(uri);
+ const res = await wallet.client.call(
+ WalletApiOperation.PrepareTip,
+ {
+ talerTipUri: uri,
+ },
+ );
console.log("tip status", res);
- await wallet.acceptTip(res.walletTipId);
+ await wallet.client.call(WalletApiOperation.AcceptTip, {
+ walletTipId: res.walletTipId,
+ });
}
break;
case TalerUriType.TalerRefund:
- await wallet.applyRefund(uri);
+ await wallet.client.call(WalletApiOperation.ApplyRefund, {
+ talerRefundUri: uri,
+ });
break;
case TalerUriType.TalerWithdraw:
{
- const withdrawInfo = await wallet.getWithdrawalDetailsForUri(uri);
+ const withdrawInfo = await wallet.client.call(
+ WalletApiOperation.GetWithdrawalDetailsForUri,
+ {
+ talerWithdrawUri: uri,
+ },
+ );
+ console.log("withdrawInfo", withdrawInfo);
const selectedExchange = withdrawInfo.defaultExchangeBaseUrl;
if (!selectedExchange) {
console.error("no suggested exchange!");
process.exit(1);
return;
}
- const res = await wallet.acceptWithdrawal(uri, selectedExchange);
- await wallet.processReserve(res.reservePub);
+ const res = await wallet.client.call(
+ WalletApiOperation.AcceptBankIntegratedWithdrawal,
+ {
+ exchangeBaseUrl: selectedExchange,
+ talerWithdrawUri: uri,
+ },
+ );
}
break;
default:
@@ -356,7 +418,10 @@ exchangesCli
.action(async (args) => {
console.log("Listing exchanges ...");
await withWallet(args, async (wallet) => {
- const exchanges = await wallet.getExchanges();
+ const exchanges = await wallet.client.call(
+ WalletApiOperation.ListExchanges,
+ {},
+ );
console.log(JSON.stringify(exchanges, undefined, 2));
});
});
@@ -371,10 +436,10 @@ exchangesCli
.flag("force", ["-f", "--force"])
.action(async (args) => {
await withWallet(args, async (wallet) => {
- await wallet.updateExchangeFromUrl(
- args.exchangesUpdateCmd.url,
- args.exchangesUpdateCmd.force,
- );
+ await wallet.client.call(WalletApiOperation.AddExchange, {
+ exchangeBaseUrl: args.exchangesUpdateCmd.url,
+ forceUpdate: args.exchangesUpdateCmd.force,
+ });
});
});
@@ -387,7 +452,9 @@ exchangesCli
})
.action(async (args) => {
await withWallet(args, async (wallet) => {
- await wallet.updateExchangeFromUrl(args.exchangesAddCmd.url);
+ await wallet.client.call(WalletApiOperation.AddExchange, {
+ exchangeBaseUrl: args.exchangesAddCmd.url,
+ });
});
});
@@ -403,10 +470,10 @@ exchangesCli
})
.action(async (args) => {
await withWallet(args, async (wallet) => {
- await wallet.acceptExchangeTermsOfService(
- args.exchangesAcceptTosCmd.url,
- args.exchangesAcceptTosCmd.etag,
- );
+ await wallet.client.call(WalletApiOperation.SetExchangeTosAccepted, {
+ etag: args.exchangesAcceptTosCmd.etag,
+ exchangeBaseUrl: args.exchangesAcceptTosCmd.url,
+ });
});
});
@@ -419,7 +486,12 @@ exchangesCli
})
.action(async (args) => {
await withWallet(args, async (wallet) => {
- const tosResult = await wallet.getExchangeTos(args.exchangesTosCmd.url);
+ const tosResult = await wallet.client.call(
+ WalletApiOperation.GetExchangeTos,
+ {
+ exchangeBaseUrl: args.exchangesTosCmd.url,
+ },
+ );
console.log(JSON.stringify(tosResult, undefined, 2));
});
});
@@ -435,65 +507,44 @@ backupCli
})
.action(async (args) => {
await withWallet(args, async (wallet) => {
- const backup = await wallet.setDeviceId(args.setDeviceId.deviceId);
- console.log(JSON.stringify(backup, undefined, 2));
+ await wallet.client.call(WalletApiOperation.SetWalletDeviceId, {
+ walletDeviceId: args.setDeviceId.deviceId,
+ });
});
});
backupCli.subcommand("exportPlain", "export-plain").action(async (args) => {
await withWallet(args, async (wallet) => {
- const backup = await wallet.exportBackupPlain();
+ const backup = await wallet.client.call(
+ WalletApiOperation.ExportBackupPlain,
+ {},
+ );
console.log(JSON.stringify(backup, undefined, 2));
});
});
-backupCli
- .subcommand("export", "export")
- .requiredArgument("filename", clk.STRING, {
- help: "backup filename",
- })
- .action(async (args) => {
- await withWallet(args, async (wallet) => {
- const backup = await wallet.exportBackupEncrypted();
- fs.writeFileSync(args.export.filename, backup);
- });
- });
-
-backupCli
- .subcommand("import", "import")
- .requiredArgument("filename", clk.STRING, {
- help: "backup filename",
- })
- .action(async (args) => {
- await withWallet(args, async (wallet) => {
- const backupEncBlob = fs.readFileSync(args.import.filename);
- await wallet.importBackupEncrypted(backupEncBlob);
- });
- });
-
-backupCli.subcommand("importPlain", "import-plain").action(async (args) => {
- await withWallet(args, async (wallet) => {
- const data = JSON.parse(await read(process.stdin));
- await wallet.importBackupPlain(data);
- });
-});
-
backupCli.subcommand("recoverySave", "save-recovery").action(async (args) => {
await withWallet(args, async (wallet) => {
- const recoveryJson = await wallet.getBackupRecovery();
+ const recoveryJson = await wallet.client.call(
+ WalletApiOperation.ExportBackupRecovery,
+ {},
+ );
console.log(JSON.stringify(recoveryJson, undefined, 2));
});
});
backupCli.subcommand("run", "run").action(async (args) => {
await withWallet(args, async (wallet) => {
- await wallet.runBackupCycle();
+ await wallet.client.call(WalletApiOperation.RunBackupCycle, {});
});
});
backupCli.subcommand("status", "status").action(async (args) => {
await withWallet(args, async (wallet) => {
- const status = await wallet.getBackupStatus();
+ const status = await wallet.client.call(
+ WalletApiOperation.GetBackupInfo,
+ {},
+ );
console.log(JSON.stringify(status, undefined, 2));
});
});
@@ -518,7 +569,7 @@ backupCli
throw Error("invalid recovery strategy");
}
}
- await wallet.loadBackupRecovery({
+ await wallet.client.call(WalletApiOperation.ImportBackupRecovery, {
recovery: data,
strategy,
});
@@ -531,7 +582,7 @@ backupCli
.flag("activate", ["--activate"])
.action(async (args) => {
await withWallet(args, async (wallet) => {
- await wallet.addBackupProvider({
+ await wallet.client.call(WalletApiOperation.AddBackupProvider, {
backupProviderBaseUrl: args.addProvider.url,
activate: args.addProvider.activate,
});
@@ -548,12 +599,15 @@ depositCli
.requiredArgument("targetPayto", clk.STRING)
.action(async (args) => {
await withWallet(args, async (wallet) => {
- const resp = await wallet.createDepositGroup({
- amount: args.createDepositArgs.amount,
- depositPaytoUri: args.createDepositArgs.targetPayto,
- });
+ const resp = await wallet.client.call(
+ WalletApiOperation.CreateDepositGroup,
+ {
+ amount: args.createDepositArgs.amount,
+ depositPaytoUri: args.createDepositArgs.targetPayto,
+ },
+ );
console.log(`Created deposit ${resp.depositGroupId}`);
- await wallet.runPending();
+ await runPending(wallet.ws);
});
});
@@ -562,9 +616,12 @@ depositCli
.requiredArgument("depositGroupId", clk.STRING)
.action(async (args) => {
await withWallet(args, async (wallet) => {
- const resp = await wallet.trackDepositGroup({
- depositGroupId: args.trackDepositArgs.depositGroupId,
- });
+ const resp = await wallet.client.call(
+ WalletApiOperation.TrackDepositGroup,
+ {
+ depositGroupId: args.trackDepositArgs.depositGroupId,
+ },
+ );
console.log(JSON.stringify(resp, undefined, 2));
});
});
@@ -582,9 +639,12 @@ advancedCli
.requiredArgument("amount", clk.STRING)
.action(async (args) => {
await withWallet(args, async (wallet) => {
- const details = await wallet.getWithdrawalDetailsForAmount(
- args.manualWithdrawalDetails.exchange,
- Amounts.parseOrThrow(args.manualWithdrawalDetails.amount),
+ const details = await wallet.client.call(
+ WalletApiOperation.GetWithdrawalDetailsForAmount,
+ {
+ amount: args.manualWithdrawalDetails.amount,
+ exchangeBaseUrl: args.manualWithdrawalDetails.exchange,
+ },
);
console.log(JSON.stringify(details, undefined, 2));
});
@@ -611,23 +671,33 @@ advancedCli
})
.action(async (args) => {
await withWallet(args, async (wallet) => {
- const { exchange, exchangeDetails } = await wallet.updateExchangeFromUrl(
- args.withdrawManually.exchange,
+ const exchangeBaseUrl = args.withdrawManually.exchange;
+ const amount = args.withdrawManually.amount;
+ const d = await wallet.client.call(
+ WalletApiOperation.GetWithdrawalDetailsForAmount,
+ {
+ amount: args.withdrawManually.amount,
+ exchangeBaseUrl: exchangeBaseUrl,
+ },
);
- const acct = exchangeDetails.wireInfo.accounts[0];
+ const acct = d.paytoUris[0];
if (!acct) {
console.log("exchange has no accounts");
return;
}
- const reserve = await wallet.acceptManualWithdrawal(
- exchange.baseUrl,
- Amounts.parseOrThrow(args.withdrawManually.amount),
+ const resp = await wallet.client.call(
+ WalletApiOperation.AcceptManualWithdrawal,
+ {
+ amount,
+ exchangeBaseUrl,
+ },
);
- const completePaytoUri = addPaytoQueryParams(acct.payto_uri, {
+ const reservePub = resp.reservePub;
+ const completePaytoUri = addPaytoQueryParams(acct, {
amount: args.withdrawManually.amount,
- message: `Taler top-up ${reserve.reservePub}`,
+ message: `Taler top-up ${reservePub}`,
});
- console.log("Created reserve", reserve.reservePub);
+ console.log("Created reserve", reservePub);
console.log("Payto URI", completePaytoUri);
});
});
@@ -640,37 +710,14 @@ currenciesCli
.subcommand("show", "show", { help: "Show currencies." })
.action(async (args) => {
await withWallet(args, async (wallet) => {
- const currencies = await wallet.getCurrencies();
+ const currencies = await wallet.client.call(
+ WalletApiOperation.ListCurrencies,
+ {},
+ );
console.log(JSON.stringify(currencies, undefined, 2));
});
});
-const reservesCli = advancedCli.subcommand("reserves", "reserves", {
- help: "Manage reserves.",
-});
-
-reservesCli
- .subcommand("list", "list", {
- help: "List reserves.",
- })
- .action(async (args) => {
- await withWallet(args, async (wallet) => {
- const reserves = await wallet.getReservesForExchange();
- console.log(JSON.stringify(reserves, undefined, 2));
- });
- });
-
-reservesCli
- .subcommand("update", "update", {
- help: "Update reserve status via exchange.",
- })
- .requiredArgument("reservePub", clk.STRING)
- .action(async (args) => {
- await withWallet(args, async (wallet) => {
- await wallet.updateReserve(args.update.reservePub);
- });
- });
-
advancedCli
.subcommand("payPrepare", "pay-prepare", {
help: "Claim an order but don't pay yet.",
@@ -678,7 +725,12 @@ advancedCli
.requiredArgument("url", clk.STRING)
.action(async (args) => {
await withWallet(args, async (wallet) => {
- const res = await wallet.preparePayForUri(args.payPrepare.url);
+ const res = await wallet.client.call(
+ WalletApiOperation.PreparePayForUri,
+ {
+ talerPayUri: args.payPrepare.url,
+ },
+ );
switch (res.status) {
case PreparePayResultType.InsufficientBalance:
console.log("insufficient balance");
@@ -707,10 +759,10 @@ advancedCli
.maybeOption("sessionIdOverride", ["--session-id"], clk.STRING)
.action(async (args) => {
await withWallet(args, async (wallet) => {
- wallet.confirmPay(
- args.payConfirm.proposalId,
- args.payConfirm.sessionIdOverride,
- );
+ await wallet.client.call(WalletApiOperation.ConfirmPay, {
+ proposalId: args.payConfirm.proposalId,
+ sessionId: args.payConfirm.sessionIdOverride,
+ });
});
});
@@ -721,7 +773,9 @@ advancedCli
.requiredArgument("coinPub", clk.STRING)
.action(async (args) => {
await withWallet(args, async (wallet) => {
- await wallet.refresh(args.refresh.coinPub);
+ await wallet.client.call(WalletApiOperation.ForceRefresh, {
+ coinPubList: [args.refresh.coinPub],
+ });
});
});
@@ -731,7 +785,10 @@ advancedCli
})
.action(async (args) => {
await withWallet(args, async (wallet) => {
- const coinDump = await wallet.dumpCoins();
+ const coinDump = await wallet.client.call(
+ WalletApiOperation.DumpCoins,
+ {},
+ );
console.log(JSON.stringify(coinDump, undefined, 2));
});
});
@@ -755,7 +812,10 @@ advancedCli
process.exit(1);
}
for (const c of coinPubList) {
- await wallet.setCoinSuspended(c, true);
+ await wallet.client.call(WalletApiOperation.SetCoinSuspended, {
+ coinPub: c,
+ suspended: true,
+ });
}
});
});
@@ -777,7 +837,10 @@ advancedCli
process.exit(1);
}
for (const c of coinPubList) {
- await wallet.setCoinSuspended(c, false);
+ await wallet.client.call(WalletApiOperation.SetCoinSuspended, {
+ coinPub: c,
+ suspended: false,
+ });
}
});
});
@@ -788,43 +851,18 @@ advancedCli
})
.action(async (args) => {
await withWallet(args, async (wallet) => {
- const coins = await wallet.getCoins();
- for (const coin of coins) {
- console.log(`coin ${coin.coinPub}`);
- console.log(` status ${coin.status}`);
- console.log(` exchange ${coin.exchangeBaseUrl}`);
- console.log(` denomPubHash ${coin.denomPubHash}`);
+ const coins = await wallet.client.call(WalletApiOperation.DumpCoins, {});
+ for (const coin of coins.coins) {
+ console.log(`coin ${coin.coin_pub}`);
+ console.log(` exchange ${coin.exchange_base_url}`);
+ console.log(` denomPubHash ${coin.denom_pub_hash}`);
console.log(
- ` remaining amount ${Amounts.stringify(coin.currentAmount)}`,
+ ` remaining amount ${Amounts.stringify(coin.remaining_value)}`,
);
}
});
});
-advancedCli
- .subcommand("updateReserve", "update-reserve", {
- help: "Update reserve status.",
- })
- .requiredArgument("reservePub", clk.STRING)
- .action(async (args) => {
- await withWallet(args, async (wallet) => {
- const r = await wallet.updateReserve(args.updateReserve.reservePub);
- console.log("updated reserve:", JSON.stringify(r, undefined, 2));
- });
- });
-
-advancedCli
- .subcommand("updateReserve", "show-reserve", {
- help: "Show the current reserve status.",
- })
- .requiredArgument("reservePub", clk.STRING)
- .action(async (args) => {
- await withWallet(args, async (wallet) => {
- const r = await wallet.getReserve(args.updateReserve.reservePub);
- console.log("updated reserve:", JSON.stringify(r, undefined, 2));
- });
- });
-
const testCli = walletCli.subcommand("testingArgs", "testing", {
help: "Subcommands for testing GNU Taler deployments.",
});
diff --git a/packages/taler-wallet-cli/src/integrationtests/harness.ts b/packages/taler-wallet-cli/src/integrationtests/harness.ts
index b6ea02696..b0a538a72 100644
--- a/packages/taler-wallet-cli/src/integrationtests/harness.ts
+++ b/packages/taler-wallet-cli/src/integrationtests/harness.ts
@@ -1783,7 +1783,10 @@ export class WalletCli {
}
async forceUpdateExchange(req: ForceExchangeUpdateRequest): Promise<void> {
- const resp = await this.apiRequest("forceUpdateExchange", req);
+ const resp = await this.apiRequest("addExchange", {
+ exchangeBaseUrl: req.exchangeBaseUrl,
+ forceUpdate: true,
+ });
if (resp.type === "response") {
return;
}
diff --git a/packages/taler-wallet-cli/src/integrationtests/test-payment-fault.ts b/packages/taler-wallet-cli/src/integrationtests/test-payment-fault.ts
index 26b8566bb..37ae0739c 100644
--- a/packages/taler-wallet-cli/src/integrationtests/test-payment-fault.ts
+++ b/packages/taler-wallet-cli/src/integrationtests/test-payment-fault.ts
@@ -213,4 +213,4 @@ export async function runPaymentFaultTest(t: GlobalTestState) {
}
runPaymentFaultTest.suites = ["wallet"];
-runPaymentFaultTest.timeoutMs = 120000; \ No newline at end of file
+runPaymentFaultTest.timeoutMs = 120000;
diff --git a/packages/taler-wallet-core/src/db.ts b/packages/taler-wallet-core/src/db.ts
index 8f9d5757d..2d2c0615c 100644
--- a/packages/taler-wallet-core/src/db.ts
+++ b/packages/taler-wallet-core/src/db.ts
@@ -576,7 +576,7 @@ export interface ExchangeDetailsRecord {
/**
* Timestamp when the ToS was accepted.
- *
+ *
* Used during backup merging.
*/
termsOfServiceAcceptedTimestamp: Timestamp | undefined;
diff --git a/packages/taler-wallet-core/src/headless/helpers.ts b/packages/taler-wallet-core/src/headless/helpers.ts
index 7b918d5d9..8125ef6b0 100644
--- a/packages/taler-wallet-core/src/headless/helpers.ts
+++ b/packages/taler-wallet-core/src/headless/helpers.ts
@@ -22,7 +22,6 @@
/**
* Imports.
*/
-import { Wallet } from "../wallet";
import {
MemoryBackend,
BridgeIDBFactory,
@@ -36,6 +35,7 @@ import { Logger } from "@gnu-taler/taler-util";
import { SynchronousCryptoWorkerFactory } from "../crypto/workers/synchronousWorker";
import type { IDBFactory } from "@gnu-taler/idb-bridge";
import { WalletNotification } from "@gnu-taler/taler-util";
+import { InternalWalletState } from "../operations/state.js";
const logger = new Logger("headless/helpers.ts");
@@ -93,7 +93,7 @@ function makeId(length: number): string {
*/
export async function getDefaultNodeWallet(
args: DefaultNodeWalletArgs = {},
-): Promise<Wallet> {
+): Promise<InternalWalletState> {
BridgeIDBFactory.enableTracing = false;
const myBackend = new MemoryBackend();
myBackend.enableTracing = false;
@@ -172,7 +172,8 @@ export async function getDefaultNodeWallet(
workerFactory = new SynchronousCryptoWorkerFactory();
}
- const w = new Wallet(myDb, myHttpLib, workerFactory);
+ const w = new InternalWalletState(myDb, myHttpLib, workerFactory);
+
if (args.notifyHandler) {
w.addNotificationListener(args.notifyHandler);
}
diff --git a/packages/taler-wallet-core/src/index.ts b/packages/taler-wallet-core/src/index.ts
index 459c4c07c..24109d9b8 100644
--- a/packages/taler-wallet-core/src/index.ts
+++ b/packages/taler-wallet-core/src/index.ts
@@ -19,34 +19,34 @@
*/
// Errors
-export * from "./operations/errors";
+export * from "./operations/errors.js";
// Util functionality
-export { URL } from "./util/url";
-export * from "./util/promiseUtils";
-export * from "./util/query";
-export * from "./util/http";
+export { URL } from "./util/url.js";
+export * from "./util/promiseUtils.js";
+export * from "./util/query.js";
+export * from "./util/http.js";
// Utils for using the wallet under node
-export { NodeHttpLib } from "./headless/NodeHttpLib";
+export { NodeHttpLib } from "./headless/NodeHttpLib.js";
export {
getDefaultNodeWallet,
DefaultNodeWalletArgs,
-} from "./headless/helpers";
+} from "./headless/helpers.js";
-export * from "./operations/versions";
+export * from "./operations/versions.js";
-export * from "./db";
+export * from "./db.js";
// Crypto and crypto workers
-export * from "./crypto/workers/nodeThreadWorker";
-export { CryptoImplementation } from "./crypto/workers/cryptoImplementation";
-export type { CryptoWorker } from "./crypto/workers/cryptoWorker";
-export { CryptoWorkerFactory, CryptoApi } from "./crypto/workers/cryptoApi";
-export * from "./crypto/talerCrypto";
+export * from "./crypto/workers/nodeThreadWorker.js";
+export { CryptoImplementation } from "./crypto/workers/cryptoImplementation.js";
+export type { CryptoWorker } from "./crypto/workers/cryptoWorker.js";
+export { CryptoWorkerFactory, CryptoApi } from "./crypto/workers/cryptoApi.js";
+export * from "./crypto/talerCrypto.js";
-export * from "./pending-types";
+export * from "./pending-types.js";
-export * from "./util/debugFlags";
+export * from "./util/debugFlags.js";
-export { Wallet } from "./wallet";
+export * from "./wallet.js";
diff --git a/packages/taler-wallet-core/src/operations/backup/index.ts b/packages/taler-wallet-core/src/operations/backup/index.ts
index 00a76bd19..2cc056721 100644
--- a/packages/taler-wallet-core/src/operations/backup/index.ts
+++ b/packages/taler-wallet-core/src/operations/backup/index.ts
@@ -38,7 +38,10 @@ import {
WalletBackupConfState,
WALLET_BACKUP_STATE_KEY,
} from "../../db.js";
-import { checkDbInvariant, checkLogicInvariant } from "../../util/invariants.js";
+import {
+ checkDbInvariant,
+ checkLogicInvariant,
+} from "../../util/invariants.js";
import {
bytesToString,
decodeCrock,
@@ -83,8 +86,15 @@ import {
TalerErrorDetails,
} from "@gnu-taler/taler-util";
import { CryptoApi } from "../../crypto/workers/cryptoApi.js";
-import { secretbox, secretbox_open } from "../../crypto/primitives/nacl-fast.js";
-import { checkPaymentByProposalId, confirmPay, preparePayForUri } from "../pay.js";
+import {
+ secretbox,
+ secretbox_open,
+} from "../../crypto/primitives/nacl-fast.js";
+import {
+ checkPaymentByProposalId,
+ confirmPay,
+ preparePayForUri,
+} from "../pay.js";
import { exportBackup } from "./export.js";
import { BackupCryptoPrecomputedData, importBackup } from "./import.js";
import { provideBackupState, getWalletBackupState } from "./state.js";
diff --git a/packages/taler-wallet-core/src/operations/exchanges.ts b/packages/taler-wallet-core/src/operations/exchanges.ts
index 1948f70e1..c8dfcbc17 100644
--- a/packages/taler-wallet-core/src/operations/exchanges.ts
+++ b/packages/taler-wallet-core/src/operations/exchanges.ts
@@ -60,7 +60,12 @@ import {
WALLET_CACHE_BREAKER_CLIENT_VERSION,
WALLET_EXCHANGE_PROTOCOL_VERSION,
} from "./versions.js";
-import { getExpiryTimestamp, HttpRequestLibrary, readSuccessResponseJsonOrThrow, readSuccessResponseTextOrThrow } from "../util/http.js";
+import {
+ getExpiryTimestamp,
+ HttpRequestLibrary,
+ readSuccessResponseJsonOrThrow,
+ readSuccessResponseTextOrThrow,
+} from "../util/http.js";
import { CryptoApi } from "../crypto/workers/cryptoApi.js";
import { DbAccess, GetReadOnlyAccess } from "../util/query.js";
import { decodeCrock, encodeCrock, hash } from "../crypto/talerCrypto.js";
diff --git a/packages/taler-wallet-core/src/operations/refresh.ts b/packages/taler-wallet-core/src/operations/refresh.ts
index c442a7c90..3c81362ce 100644
--- a/packages/taler-wallet-core/src/operations/refresh.ts
+++ b/packages/taler-wallet-core/src/operations/refresh.ts
@@ -55,7 +55,10 @@ import { URL } from "../util/url.js";
import { guardOperationException } from "./errors.js";
import { updateExchangeFromUrl } from "./exchanges.js";
import { EXCHANGE_COINS_LOCK, InternalWalletState } from "./state.js";
-import { isWithdrawableDenom, selectWithdrawalDenominations } from "./withdraw.js";
+import {
+ isWithdrawableDenom,
+ selectWithdrawalDenominations,
+} from "./withdraw.js";
import { RefreshNewDenomInfo } from "../crypto/cryptoTypes.js";
import { GetReadWriteAccess } from "../util/query.js";
diff --git a/packages/taler-wallet-core/src/operations/state.ts b/packages/taler-wallet-core/src/operations/state.ts
index 66baa95a4..ee7ceb8af 100644
--- a/packages/taler-wallet-core/src/operations/state.ts
+++ b/packages/taler-wallet-core/src/operations/state.ts
@@ -27,8 +27,13 @@ import { WalletStoresV1 } from "../db.js";
import { PendingOperationsResponse } from "../pending-types.js";
import { AsyncOpMemoMap, AsyncOpMemoSingle } from "../util/asyncMemo.js";
import { HttpRequestLibrary } from "../util/http";
-import { OpenedPromise, openPromise } from "../util/promiseUtils.js";
+import {
+ AsyncCondition,
+ OpenedPromise,
+ openPromise,
+} from "../util/promiseUtils.js";
import { DbAccess } from "../util/query.js";
+import { TimerGroup } from "../util/timer.js";
type NotificationListener = (n: WalletNotification) => void;
@@ -37,6 +42,9 @@ const logger = new Logger("state.ts");
export const EXCHANGE_COINS_LOCK = "exchange-coins-lock";
export const EXCHANGE_RESERVES_LOCK = "exchange-reserves-lock";
+/**
+ * Internal state of the wallet.
+ */
export class InternalWalletState {
memoProcessReserve: AsyncOpMemoMap<void> = new AsyncOpMemoMap();
memoMakePlanchet: AsyncOpMemoMap<void> = new AsyncOpMemoMap();
@@ -47,8 +55,15 @@ export class InternalWalletState {
memoProcessDeposit: AsyncOpMemoMap<void> = new AsyncOpMemoMap();
cryptoApi: CryptoApi;
+ timerGroup: TimerGroup = new TimerGroup();
+ latch = new AsyncCondition();
+ stopped = false;
+ memoRunRetryLoop = new AsyncOpMemoSingle<void>();
+
listeners: NotificationListener[] = [];
+ initCalled: boolean = false;
+
/**
* Promises that are waiting for a particular resource.
*/
@@ -86,6 +101,15 @@ export class InternalWalletState {
}
/**
+ * Stop ongoing processing.
+ */
+ stop(): void {
+ this.stopped = true;
+ this.timerGroup.stopCurrentAndFutureTimers();
+ this.cryptoApi.stop();
+ }
+
+ /**
* Run an async function after acquiring a list of locks, identified
* by string tokens.
*/
diff --git a/packages/taler-wallet-core/src/operations/testing.ts b/packages/taler-wallet-core/src/operations/testing.ts
index b163569ae..ce3a47f36 100644
--- a/packages/taler-wallet-core/src/operations/testing.ts
+++ b/packages/taler-wallet-core/src/operations/testing.ts
@@ -33,10 +33,13 @@ import {
TestPayArgs,
PreparePayResultType,
} from "@gnu-taler/taler-util";
-import { Wallet } from "../wallet.js";
import { createTalerWithdrawReserve } from "./reserves.js";
import { InternalWalletState } from "./state.js";
import { URL } from "../util/url.js";
+import { confirmPay, preparePayForUri } from "./pay.js";
+import { getBalances } from "./balance.js";
+import { runUntilDone } from "../wallet.js";
+import { applyRefund } from "./refund.js";
const logger = new Logger("operations/testing.ts");
@@ -261,14 +264,13 @@ interface BankWithdrawalResponse {
}
async function makePayment(
- http: HttpRequestLibrary,
- wallet: Wallet,
+ ws: InternalWalletState,
merchant: MerchantBackendInfo,
amount: string,
summary: string,
): Promise<{ orderId: string }> {
const orderResp = await createOrder(
- http,
+ ws.http,
merchant,
amount,
summary,
@@ -277,7 +279,7 @@ async function makePayment(
logger.trace("created order with orderId", orderResp.orderId);
- let paymentStatus = await checkPayment(http, merchant, orderResp.orderId);
+ let paymentStatus = await checkPayment(ws.http, merchant, orderResp.orderId);
logger.trace("payment status", paymentStatus);
@@ -286,7 +288,7 @@ async function makePayment(
throw Error("no taler://pay/ URI in payment response");
}
- const preparePayResult = await wallet.preparePayForUri(talerPayUri);
+ const preparePayResult = await preparePayForUri(ws, talerPayUri);
logger.trace("prepare pay result", preparePayResult);
@@ -294,14 +296,15 @@ async function makePayment(
throw Error("payment not possible");
}
- const confirmPayResult = await wallet.confirmPay(
+ const confirmPayResult = await confirmPay(
+ ws,
preparePayResult.proposalId,
undefined,
);
logger.trace("confirmPayResult", confirmPayResult);
- paymentStatus = await checkPayment(http, merchant, orderResp.orderId);
+ paymentStatus = await checkPayment(ws.http, merchant, orderResp.orderId);
logger.trace("payment status after wallet payment:", paymentStatus);
@@ -315,8 +318,7 @@ async function makePayment(
}
export async function runIntegrationTest(
- http: HttpRequestLibrary,
- wallet: Wallet,
+ ws: InternalWalletState,
args: IntegrationTestArgs,
): Promise<void> {
logger.info("running test with arguments", args);
@@ -325,15 +327,16 @@ export async function runIntegrationTest(
const currency = parsedSpendAmount.currency;
logger.info("withdrawing test balance");
- await wallet.withdrawTestBalance({
- amount: args.amountToWithdraw,
- bankBaseUrl: args.bankBaseUrl,
- exchangeBaseUrl: args.exchangeBaseUrl,
- });
- await wallet.runUntilDone();
+ await withdrawTestBalance(
+ ws,
+ args.amountToWithdraw,
+ args.bankBaseUrl,
+ args.exchangeBaseUrl,
+ );
+ await runUntilDone(ws);
logger.info("done withdrawing test balance");
- const balance = await wallet.getBalances();
+ const balance = await getBalances(ws);
logger.trace(JSON.stringify(balance, null, 2));
@@ -342,16 +345,10 @@ export async function runIntegrationTest(
authToken: args.merchantAuthToken,
};
- await makePayment(
- http,
- wallet,
- myMerchant,
- args.amountToSpend,
- "hello world",
- );
+ await makePayment(ws, myMerchant, args.amountToSpend, "hello world");
// Wait until the refresh is done
- await wallet.runUntilDone();
+ await runUntilDone(ws);
logger.trace("withdrawing test balance for refund");
const withdrawAmountTwo = Amounts.parseOrThrow(`${currency}:18`);
@@ -359,25 +356,25 @@ export async function runIntegrationTest(
const refundAmount = Amounts.parseOrThrow(`${currency}:6`);
const spendAmountThree = Amounts.parseOrThrow(`${currency}:3`);
- await wallet.withdrawTestBalance({
- amount: Amounts.stringify(withdrawAmountTwo),
- bankBaseUrl: args.bankBaseUrl,
- exchangeBaseUrl: args.exchangeBaseUrl,
- });
+ await withdrawTestBalance(
+ ws,
+ Amounts.stringify(withdrawAmountTwo),
+ args.bankBaseUrl,
+ args.exchangeBaseUrl,
+ );
// Wait until the withdraw is done
- await wallet.runUntilDone();
+ await runUntilDone(ws);
const { orderId: refundOrderId } = await makePayment(
- http,
- wallet,
+ ws,
myMerchant,
Amounts.stringify(spendAmountTwo),
"order that will be refunded",
);
const refundUri = await refund(
- http,
+ ws.http,
myMerchant,
refundOrderId,
"test refund",
@@ -386,18 +383,17 @@ export async function runIntegrationTest(
logger.trace("refund URI", refundUri);
- await wallet.applyRefund(refundUri);
+ await applyRefund(ws, refundUri);
logger.trace("integration test: applied refund");
// Wait until the refund is done
- await wallet.runUntilDone();
+ await runUntilDone(ws);
logger.trace("integration test: making payment after refund");
await makePayment(
- http,
- wallet,
+ ws,
myMerchant,
Amounts.stringify(spendAmountThree),
"payment after refund",
@@ -405,30 +401,26 @@ export async function runIntegrationTest(
logger.trace("integration test: make payment done");
- await wallet.runUntilDone();
+ await runUntilDone(ws);
logger.trace("integration test: all done!");
}
-export async function testPay(
- http: HttpRequestLibrary,
- wallet: Wallet,
- args: TestPayArgs,
-) {
+export async function testPay(ws: InternalWalletState, args: TestPayArgs) {
logger.trace("creating order");
const merchant = {
authToken: args.merchantAuthToken,
baseUrl: args.merchantBaseUrl,
};
const orderResp = await createOrder(
- http,
+ ws.http,
merchant,
args.amount,
args.summary,
"taler://fulfillment-success/thank+you",
);
logger.trace("created new order with order ID", orderResp.orderId);
- const checkPayResp = await checkPayment(http, merchant, orderResp.orderId);
+ const checkPayResp = await checkPayment(ws.http, merchant, orderResp.orderId);
const talerPayUri = checkPayResp.taler_pay_uri;
if (!talerPayUri) {
console.error("fatal: no taler pay URI received from backend");
@@ -436,9 +428,9 @@ export async function testPay(
return;
}
logger.trace("taler pay URI:", talerPayUri);
- const result = await wallet.preparePayForUri(talerPayUri);
+ const result = await preparePayForUri(ws, talerPayUri);
if (result.status !== PreparePayResultType.PaymentPossible) {
throw Error(`unexpected prepare pay status: ${result.status}`);
}
- await wallet.confirmPay(result.proposalId, undefined);
+ await confirmPay(ws, result.proposalId, undefined);
}
diff --git a/packages/taler-wallet-core/src/operations/transactions.ts b/packages/taler-wallet-core/src/operations/transactions.ts
index 1b2c8477f..5836a6ee3 100644
--- a/packages/taler-wallet-core/src/operations/transactions.ts
+++ b/packages/taler-wallet-core/src/operations/transactions.ts
@@ -424,7 +424,7 @@ export async function retryTransaction(
break;
}
case TransactionType.Payment: {
- const proposalId = rest[0]
+ const proposalId = rest[0];
await processPurchasePay(ws, proposalId, true);
break;
}
diff --git a/packages/taler-wallet-core/src/util/coinSelection.test.ts b/packages/taler-wallet-core/src/util/coinSelection.test.ts
index 1e87bc1f3..ed48b8dd1 100644
--- a/packages/taler-wallet-core/src/util/coinSelection.test.ts
+++ b/packages/taler-wallet-core/src/util/coinSelection.test.ts
@@ -17,7 +17,7 @@
/**
* Imports.
*/
- import test from "ava";
+import test from "ava";
import { AmountJson, Amounts } from "@gnu-taler/taler-util";
import { AvailableCoinInfo, selectPayCoins } from "./coinSelection.js";
diff --git a/packages/taler-wallet-core/src/util/http.ts b/packages/taler-wallet-core/src/util/http.ts
index 868619ada..92a9e4396 100644
--- a/packages/taler-wallet-core/src/util/http.ts
+++ b/packages/taler-wallet-core/src/util/http.ts
@@ -24,7 +24,10 @@
/**
* Imports
*/
-import { OperationFailedError, makeErrorDetails } from "../operations/errors.js";
+import {
+ OperationFailedError,
+ makeErrorDetails,
+} from "../operations/errors.js";
import {
Logger,
Duration,
diff --git a/packages/taler-wallet-core/src/wallet.ts b/packages/taler-wallet-core/src/wallet.ts
index 33e431f37..82bc8b44b 100644
--- a/packages/taler-wallet-core/src/wallet.ts
+++ b/packages/taler-wallet-core/src/wallet.ts
@@ -23,33 +23,46 @@
* Imports.
*/
import {
+ AcceptBankIntegratedWithdrawalRequest,
+ AcceptExchangeTosRequest,
+ AcceptManualWithdrawalRequest,
+ AcceptTipRequest,
+ AddExchangeRequest,
+ ApplyRefundRequest,
BackupRecovery,
codecForAny,
codecForDeleteTransactionRequest,
codecForRetryTransactionRequest,
+ codecForSetWalletDeviceIdRequest,
+ ConfirmPayRequest,
DeleteTransactionRequest,
durationFromSpec,
- durationMax,
durationMin,
+ ForceRefreshRequest,
getDurationRemaining,
+ GetExchangeTosRequest,
+ GetWithdrawalDetailsForAmountRequest,
+ GetWithdrawalDetailsForUriRequest,
isTimestampExpired,
j2s,
+ PreparePayRequest,
+ PrepareTipRequest,
+ RetryTransactionRequest,
+ SetCoinSuspendedRequest,
+ SetWalletDeviceIdRequest,
TalerErrorCode,
Timestamp,
timestampMin,
+ WalletBackupContentV1,
WalletCurrencyInfo,
} from "@gnu-taler/taler-util";
-import { CryptoWorkerFactory } from "./crypto/workers/cryptoApi";
import {
addBackupProvider,
AddBackupProviderRequest,
BackupInfo,
codecForAddBackupProviderRequest,
- exportBackupEncrypted,
getBackupInfo,
getBackupRecovery,
- importBackupEncrypted,
- importBackupPlain,
loadBackupRecovery,
runBackupCycle,
} from "./operations/backup";
@@ -68,7 +81,6 @@ import {
import {
acceptExchangeTermsOfService,
getExchangeDetails,
- getExchangePaytoUri,
updateExchangeFromUrl,
} from "./operations/exchanges";
import {
@@ -76,7 +88,6 @@ import {
preparePayForUri,
processDownloadProposal,
processPurchasePay,
- refuseProposal,
} from "./operations/pay";
import { getPendingOperations } from "./operations/pending";
import { processRecoupGroup } from "./operations/recoup";
@@ -93,7 +104,6 @@ import {
import {
createReserve,
createTalerWithdrawReserve,
- forceQueryReserve,
getFundingPaytoUris,
processReserve,
} from "./operations/reserves";
@@ -104,7 +114,11 @@ import {
withdrawTestBalance,
} from "./operations/testing";
import { acceptTip, prepareTip, processTip } from "./operations/tip";
-import { deleteTransaction, getTransactions, retryTransaction } from "./operations/transactions";
+import {
+ deleteTransaction,
+ getTransactions,
+ retryTransaction,
+} from "./operations/transactions";
import {
getExchangeWithdrawalInfo,
getWithdrawalDetailsForUri,
@@ -112,15 +126,10 @@ import {
} from "./operations/withdraw";
import {
AuditorTrustRecord,
- CoinRecord,
CoinSourceType,
- ExchangeDetailsRecord,
- ExchangeRecord,
- ReserveRecord,
ReserveRecordStatus,
- WalletStoresV1,
} from "./db.js";
-import { NotificationType, WalletNotification } from "@gnu-taler/taler-util";
+import { NotificationType } from "@gnu-taler/taler-util";
import {
PendingOperationInfo,
PendingOperationsResponse,
@@ -137,7 +146,6 @@ import {
AcceptWithdrawalResponse,
ApplyRefundResponse,
BalancesResponse,
- BenchmarkResult,
codecForAbortPayWithRefundRequest,
codecForAcceptBankIntegratedWithdrawalRequest,
codecForAcceptExchangeTosRequest,
@@ -147,7 +155,6 @@ import {
codecForApplyRefundRequest,
codecForConfirmPayRequest,
codecForCreateDepositGroupRequest,
- codecForForceExchangeUpdateRequest,
codecForForceRefreshRequest,
codecForGetExchangeTosRequest,
codecForGetWithdrawalDetailsForAmountRequest,
@@ -166,28 +173,18 @@ import {
ExchangeListItem,
ExchangesListRespose,
GetExchangeTosResult,
- IntegrationTestArgs,
ManualWithdrawalDetails,
PreparePayResult,
PrepareTipResult,
RecoveryLoadRequest,
RefreshReason,
- ReturnCoinsRequest,
- TestPayArgs,
TrackDepositGroupRequest,
TrackDepositGroupResponse,
- WithdrawTestBalanceRequest,
WithdrawUriInfoResponse,
} from "@gnu-taler/taler-util";
import { AmountJson, Amounts } from "@gnu-taler/taler-util";
import { assertUnreachable } from "./util/assertUnreachable";
-import { AsyncOpMemoSingle } from "./util/asyncMemo";
-import { HttpRequestLibrary } from "./util/http";
import { Logger } from "@gnu-taler/taler-util";
-import { AsyncCondition } from "./util/promiseUtils";
-import { TimerGroup } from "./util/timer";
-import { getExchangeTrust } from "./operations/currencies.js";
-import { DbAccess } from "./util/query.js";
import { setWalletDeviceId } from "./operations/backup/state.js";
const builtinAuditors: AuditorTrustRecord[] = [
@@ -201,440 +198,815 @@ const builtinAuditors: AuditorTrustRecord[] = [
const logger = new Logger("wallet.ts");
-/**
- * The platform-independent wallet implementation.
- */
-export class Wallet {
- private ws: InternalWalletState;
- private timerGroup: TimerGroup = new TimerGroup();
- private latch = new AsyncCondition();
- private stopped = false;
- private memoRunRetryLoop = new AsyncOpMemoSingle<void>();
-
- get db(): DbAccess<typeof WalletStoresV1> {
- return this.ws.db;
- }
-
- constructor(
- db: DbAccess<typeof WalletStoresV1>,
- http: HttpRequestLibrary,
- cryptoWorkerFactory: CryptoWorkerFactory,
- ) {
- this.ws = new InternalWalletState(db, http, cryptoWorkerFactory);
+async function getWithdrawalDetailsForAmount(
+ ws: InternalWalletState,
+ exchangeBaseUrl: string,
+ amount: AmountJson,
+): Promise<ManualWithdrawalDetails> {
+ const wi = await getExchangeWithdrawalInfo(ws, exchangeBaseUrl, amount);
+ const paytoUris = wi.exchangeDetails.wireInfo.accounts.map(
+ (x) => x.payto_uri,
+ );
+ if (!paytoUris) {
+ throw Error("exchange is in invalid state");
}
+ return {
+ amountRaw: Amounts.stringify(amount),
+ amountEffective: Amounts.stringify(wi.selectedDenoms.totalCoinValue),
+ paytoUris,
+ tosAccepted: wi.termsOfServiceAccepted,
+ };
+}
- getExchangePaytoUri(
- exchangeBaseUrl: string,
- supportedTargetTypes: string[],
- ): Promise<string> {
- return getExchangePaytoUri(this.ws, exchangeBaseUrl, supportedTargetTypes);
+/**
+ * Execute one operation based on the pending operation info record.
+ */
+async function processOnePendingOperation(
+ ws: InternalWalletState,
+ pending: PendingOperationInfo,
+ forceNow = false,
+): Promise<void> {
+ logger.trace(`running pending ${JSON.stringify(pending, undefined, 2)}`);
+ switch (pending.type) {
+ case PendingOperationType.ExchangeUpdate:
+ await updateExchangeFromUrl(ws, pending.exchangeBaseUrl, forceNow);
+ break;
+ case PendingOperationType.Refresh:
+ await processRefreshGroup(ws, pending.refreshGroupId, forceNow);
+ break;
+ case PendingOperationType.Reserve:
+ await processReserve(ws, pending.reservePub, forceNow);
+ break;
+ case PendingOperationType.Withdraw:
+ await processWithdrawGroup(ws, pending.withdrawalGroupId, forceNow);
+ break;
+ case PendingOperationType.ProposalDownload:
+ await processDownloadProposal(ws, pending.proposalId, forceNow);
+ break;
+ case PendingOperationType.TipPickup:
+ await processTip(ws, pending.tipId, forceNow);
+ break;
+ case PendingOperationType.Pay:
+ await processPurchasePay(ws, pending.proposalId, forceNow);
+ break;
+ case PendingOperationType.RefundQuery:
+ await processPurchaseQueryRefund(ws, pending.proposalId, forceNow);
+ break;
+ case PendingOperationType.Recoup:
+ await processRecoupGroup(ws, pending.recoupGroupId, forceNow);
+ break;
+ case PendingOperationType.ExchangeCheckRefresh:
+ await autoRefresh(ws, pending.exchangeBaseUrl);
+ break;
+ case PendingOperationType.Deposit:
+ await processDepositGroup(ws, pending.depositGroupId);
+ break;
+ default:
+ assertUnreachable(pending);
}
+}
- async getWithdrawalDetailsForAmount(
- exchangeBaseUrl: string,
- amount: AmountJson,
- ): Promise<ManualWithdrawalDetails> {
- const wi = await getExchangeWithdrawalInfo(
- this.ws,
- exchangeBaseUrl,
- amount,
- );
- const paytoUris = wi.exchangeDetails.wireInfo.accounts.map(
- (x) => x.payto_uri,
- );
- if (!paytoUris) {
- throw Error("exchange is in invalid state");
+/**
+ * Process pending operations.
+ */
+export async function runPending(
+ ws: InternalWalletState,
+ forceNow = false,
+): Promise<void> {
+ const pendingOpsResponse = await getPendingOperations(ws);
+ for (const p of pendingOpsResponse.pendingOperations) {
+ if (!forceNow && !isTimestampExpired(p.timestampDue)) {
+ continue;
}
- return {
- amountRaw: Amounts.stringify(amount),
- amountEffective: Amounts.stringify(wi.selectedDenoms.totalCoinValue),
- paytoUris,
- tosAccepted: wi.termsOfServiceAccepted,
- };
- }
-
- addNotificationListener(f: (n: WalletNotification) => void): void {
- this.ws.addNotificationListener(f);
- }
-
- /**
- * Execute one operation based on the pending operation info record.
- */
- async processOnePendingOperation(
- pending: PendingOperationInfo,
- forceNow = false,
- ): Promise<void> {
- logger.trace(`running pending ${JSON.stringify(pending, undefined, 2)}`);
- switch (pending.type) {
- case PendingOperationType.ExchangeUpdate:
- await updateExchangeFromUrl(this.ws, pending.exchangeBaseUrl, forceNow);
- break;
- case PendingOperationType.Refresh:
- await processRefreshGroup(this.ws, pending.refreshGroupId, forceNow);
- break;
- case PendingOperationType.Reserve:
- await processReserve(this.ws, pending.reservePub, forceNow);
- break;
- case PendingOperationType.Withdraw:
- await processWithdrawGroup(
- this.ws,
- pending.withdrawalGroupId,
- forceNow,
+ try {
+ await processOnePendingOperation(ws, p, forceNow);
+ } catch (e) {
+ if (e instanceof OperationFailedAndReportedError) {
+ console.error(
+ "Operation failed:",
+ JSON.stringify(e.operationError, undefined, 2),
);
- break;
- case PendingOperationType.ProposalDownload:
- await processDownloadProposal(this.ws, pending.proposalId, forceNow);
- break;
- case PendingOperationType.TipPickup:
- await processTip(this.ws, pending.tipId, forceNow);
- break;
- case PendingOperationType.Pay:
- await processPurchasePay(this.ws, pending.proposalId, forceNow);
- break;
- case PendingOperationType.RefundQuery:
- await processPurchaseQueryRefund(this.ws, pending.proposalId, forceNow);
- break;
- case PendingOperationType.Recoup:
- await processRecoupGroup(this.ws, pending.recoupGroupId, forceNow);
- break;
- case PendingOperationType.ExchangeCheckRefresh:
- await autoRefresh(this.ws, pending.exchangeBaseUrl);
- break;
- case PendingOperationType.Deposit:
- await processDepositGroup(this.ws, pending.depositGroupId);
- break;
- default:
- assertUnreachable(pending);
+ } else {
+ console.error(e);
+ }
}
}
+}
- /**
- * Process pending operations.
- */
- public async runPending(forceNow = false): Promise<void> {
- const pendingOpsResponse = await this.getPendingOperations();
- for (const p of pendingOpsResponse.pendingOperations) {
- if (!forceNow && !isTimestampExpired(p.timestampDue)) {
- continue;
+/**
+ * Run the wallet until there are no more pending operations that give
+ * liveness left. The wallet will be in a stopped state when this function
+ * returns without resolving to an exception.
+ */
+export async function runUntilDone(
+ ws: InternalWalletState,
+ req: {
+ maxRetries?: number;
+ } = {},
+): Promise<void> {
+ let done = false;
+ const p = new Promise<void>((resolve, reject) => {
+ // Monitor for conditions that means we're done or we
+ // should quit with an error (due to exceeded retries).
+ ws.addNotificationListener((n) => {
+ if (done) {
+ return;
}
- try {
- await this.processOnePendingOperation(p, forceNow);
- } catch (e) {
- if (e instanceof OperationFailedAndReportedError) {
- console.error(
- "Operation failed:",
- JSON.stringify(e.operationError, undefined, 2),
- );
- } else {
- console.error(e);
- }
+ if (
+ n.type === NotificationType.WaitingForRetry &&
+ n.numGivingLiveness == 0
+ ) {
+ done = true;
+ logger.trace("no liveness-giving operations left");
+ resolve();
}
- }
- }
-
- /**
- * Run the wallet until there are no more pending operations that give
- * liveness left. The wallet will be in a stopped state when this function
- * returns without resolving to an exception.
- */
- public async runUntilDone(
- req: {
- maxRetries?: number;
- } = {},
- ): Promise<void> {
- let done = false;
- const p = new Promise<void>((resolve, reject) => {
- // Monitor for conditions that means we're done or we
- // should quit with an error (due to exceeded retries).
- this.addNotificationListener((n) => {
- if (done) {
- return;
- }
- if (
- n.type === NotificationType.WaitingForRetry &&
- n.numGivingLiveness == 0
- ) {
- done = true;
- logger.trace("no liveness-giving operations left");
- resolve();
- }
- const maxRetries = req.maxRetries;
- if (!maxRetries) {
- return;
- }
- this.getPendingOperations()
- .then((pending) => {
- for (const p of pending.pendingOperations) {
- if (p.retryInfo && p.retryInfo.retryCounter > maxRetries) {
- console.warn(
- `stopping, as ${maxRetries} retries are exceeded in an operation of type ${p.type}`,
- );
- this.stop();
- done = true;
- resolve();
- }
+ const maxRetries = req.maxRetries;
+ if (!maxRetries) {
+ return;
+ }
+ getPendingOperations(ws)
+ .then((pending) => {
+ for (const p of pending.pendingOperations) {
+ if (p.retryInfo && p.retryInfo.retryCounter > maxRetries) {
+ console.warn(
+ `stopping, as ${maxRetries} retries are exceeded in an operation of type ${p.type}`,
+ );
+ ws.stop();
+ done = true;
+ resolve();
}
- })
- .catch((e) => {
- logger.error(e);
- reject(e);
- });
- });
- // Run this asynchronously
- this.runRetryLoop().catch((e) => {
- logger.error("exception in wallet retry loop");
- reject(e);
- });
+ }
+ })
+ .catch((e) => {
+ logger.error(e);
+ reject(e);
+ });
});
- await p;
- }
-
- /**
- * Process pending operations and wait for scheduled operations in
- * a loop until the wallet is stopped explicitly.
- */
- public async runRetryLoop(): Promise<void> {
- // Make sure we only run one main loop at a time.
- return this.memoRunRetryLoop.memo(async () => {
- try {
- await this.runRetryLoopImpl();
- } catch (e) {
- console.error("error during retry loop execution", e);
- throw e;
- }
+ // Run this asynchronously
+ runRetryLoop(ws).catch((e) => {
+ logger.error("exception in wallet retry loop");
+ reject(e);
});
- }
+ });
+ await p;
+}
- private async runRetryLoopImpl(): Promise<void> {
- for (let iteration = 0; !this.stopped; iteration++) {
- const pending = await this.getPendingOperations();
- logger.trace(`pending operations: ${j2s(pending)}`);
- let numGivingLiveness = 0;
- let numDue = 0;
- let minDue: Timestamp = { t_ms: "never" };
+/**
+ * Process pending operations and wait for scheduled operations in
+ * a loop until the wallet is stopped explicitly.
+ */
+export async function runRetryLoop(ws: InternalWalletState): Promise<void> {
+ // Make sure we only run one main loop at a time.
+ return ws.memoRunRetryLoop.memo(async () => {
+ try {
+ await runRetryLoopImpl(ws);
+ } catch (e) {
+ console.error("error during retry loop execution", e);
+ throw e;
+ }
+ });
+}
+
+async function runRetryLoopImpl(ws: InternalWalletState): Promise<void> {
+ for (let iteration = 0; !ws.stopped; iteration++) {
+ const pending = await getPendingOperations(ws);
+ logger.trace(`pending operations: ${j2s(pending)}`);
+ let numGivingLiveness = 0;
+ let numDue = 0;
+ let minDue: Timestamp = { t_ms: "never" };
+ for (const p of pending.pendingOperations) {
+ minDue = timestampMin(minDue, p.timestampDue);
+ if (isTimestampExpired(p.timestampDue)) {
+ numDue++;
+ }
+ if (p.givesLifeness) {
+ numGivingLiveness++;
+ }
+ }
+ // Make sure that we run tasks that don't give lifeness at least
+ // one time.
+ if (iteration !== 0 && numDue === 0) {
+ // We've executed pending, due operations at least one.
+ // Now we don't have any more operations available,
+ // and need to wait.
+
+ // Wait for at most 5 seconds to the next check.
+ const dt = durationMin(
+ durationFromSpec({
+ seconds: 5,
+ }),
+ getDurationRemaining(minDue),
+ );
+ logger.trace(`waiting for at most ${dt.d_ms} ms`);
+ const timeout = ws.timerGroup.resolveAfter(dt);
+ ws.notify({
+ type: NotificationType.WaitingForRetry,
+ numGivingLiveness,
+ numPending: pending.pendingOperations.length,
+ });
+ // Wait until either the timeout, or we are notified (via the latch)
+ // that more work might be available.
+ await Promise.race([timeout, ws.latch.wait()]);
+ } else {
+ logger.trace(
+ `running ${pending.pendingOperations.length} pending operations`,
+ );
for (const p of pending.pendingOperations) {
- minDue = timestampMin(minDue, p.timestampDue);
- if (isTimestampExpired(p.timestampDue)) {
- numDue++;
- }
- if (p.givesLifeness) {
- numGivingLiveness++;
+ if (!isTimestampExpired(p.timestampDue)) {
+ continue;
}
- }
- // Make sure that we run tasks that don't give lifeness at least
- // one time.
- if (iteration !== 0 && numDue === 0) {
- // We've executed pending, due operations at least one.
- // Now we don't have any more operations available,
- // and need to wait.
-
- // Wait for at most 5 seconds to the next check.
- const dt = durationMin(
- durationFromSpec({
- seconds: 5,
- }),
- getDurationRemaining(minDue),
- );
- logger.trace(`waiting for at most ${dt.d_ms} ms`)
- const timeout = this.timerGroup.resolveAfter(dt);
- this.ws.notify({
- type: NotificationType.WaitingForRetry,
- numGivingLiveness,
- numPending: pending.pendingOperations.length,
- });
- // Wait until either the timeout, or we are notified (via the latch)
- // that more work might be available.
- await Promise.race([timeout, this.latch.wait()]);
- } else {
- logger.trace(
- `running ${pending.pendingOperations.length} pending operations`,
- );
- for (const p of pending.pendingOperations) {
- if (!isTimestampExpired(p.timestampDue)) {
- continue;
- }
- try {
- await this.processOnePendingOperation(p);
- } catch (e) {
- if (e instanceof OperationFailedAndReportedError) {
- logger.warn("operation processed resulted in reported error");
- } else {
- logger.error("Uncaught exception", e);
- this.ws.notify({
- type: NotificationType.InternalError,
- message: "uncaught exception",
- exception: e,
- });
- }
+ try {
+ await processOnePendingOperation(ws, p);
+ } catch (e) {
+ if (e instanceof OperationFailedAndReportedError) {
+ logger.warn("operation processed resulted in reported error");
+ } else {
+ logger.error("Uncaught exception", e);
+ ws.notify({
+ type: NotificationType.InternalError,
+ message: "uncaught exception",
+ exception: e,
+ });
}
- this.ws.notify({
- type: NotificationType.PendingOperationProcessed,
- });
}
+ ws.notify({
+ type: NotificationType.PendingOperationProcessed,
+ });
}
}
- logger.trace("exiting wallet retry loop");
}
+ logger.trace("exiting wallet retry loop");
+}
- /**
- * Insert the hard-coded defaults for exchanges, coins and
- * auditors into the database, unless these defaults have
- * already been applied.
- */
- async fillDefaults(): Promise<void> {
- await this.db
- .mktx((x) => ({ config: x.config, auditorTrustStore: x.auditorTrust }))
- .runReadWrite(async (tx) => {
- let applied = false;
- await tx.config.iter().forEach((x) => {
- if (x.key == "currencyDefaultsApplied" && x.value == true) {
- applied = true;
- }
- });
- if (!applied) {
- for (const c of builtinAuditors) {
- await tx.auditorTrustStore.put(c);
- }
+/**
+ * Insert the hard-coded defaults for exchanges, coins and
+ * auditors into the database, unless these defaults have
+ * already been applied.
+ */
+async function fillDefaults(ws: InternalWalletState): Promise<void> {
+ await ws.db
+ .mktx((x) => ({ config: x.config, auditorTrustStore: x.auditorTrust }))
+ .runReadWrite(async (tx) => {
+ let applied = false;
+ await tx.config.iter().forEach((x) => {
+ if (x.key == "currencyDefaultsApplied" && x.value == true) {
+ applied = true;
}
});
- }
+ if (!applied) {
+ for (const c of builtinAuditors) {
+ await tx.auditorTrustStore.put(c);
+ }
+ }
+ });
+}
- /**
- * Check if a payment for the given taler://pay/ URI is possible.
- *
- * If the payment is possible, the signature are already generated but not
- * yet send to the merchant.
- */
- async preparePayForUri(talerPayUri: string): Promise<PreparePayResult> {
- return preparePayForUri(this.ws, talerPayUri);
+/**
+ * Create a reserve, but do not flag it as confirmed yet.
+ *
+ * Adds the corresponding exchange as a trusted exchange if it is neither
+ * audited nor trusted already.
+ */
+async function acceptManualWithdrawal(
+ ws: InternalWalletState,
+ exchangeBaseUrl: string,
+ amount: AmountJson,
+): Promise<AcceptManualWithdrawalResult> {
+ try {
+ const resp = await createReserve(ws, {
+ amount,
+ exchange: exchangeBaseUrl,
+ });
+ const exchangePaytoUris = await ws.db
+ .mktx((x) => ({
+ exchanges: x.exchanges,
+ exchangeDetails: x.exchangeDetails,
+ reserves: x.reserves,
+ }))
+ .runReadWrite((tx) => getFundingPaytoUris(tx, resp.reservePub));
+ return {
+ reservePub: resp.reservePub,
+ exchangePaytoUris,
+ };
+ } finally {
+ ws.latch.trigger();
}
+}
- /**
- * Add a contract to the wallet and sign coins, and send them.
- */
- async confirmPay(
- proposalId: string,
- sessionIdOverride: string | undefined,
- ): Promise<ConfirmPayResult> {
- try {
- return await confirmPay(this.ws, proposalId, sessionIdOverride);
- } finally {
- this.latch.trigger();
- }
+async function getExchangeTos(
+ ws: InternalWalletState,
+ exchangeBaseUrl: string,
+): Promise<GetExchangeTosResult> {
+ const { exchange, exchangeDetails } = await updateExchangeFromUrl(
+ ws,
+ exchangeBaseUrl,
+ );
+ const tos = exchangeDetails.termsOfServiceText;
+ const currentEtag = exchangeDetails.termsOfServiceLastEtag;
+ if (!tos || !currentEtag) {
+ throw Error("exchange is in invalid state");
}
+ return {
+ acceptedEtag: exchangeDetails.termsOfServiceAcceptedEtag,
+ currentEtag,
+ tos,
+ };
+}
- /**
- * First fetch information required to withdraw from the reserve,
- * then deplete the reserve, withdrawing coins until it is empty.
- *
- * The returned promise resolves once the reserve is set to the
- * state DORMANT.
- */
- async processReserve(reservePub: string): Promise<void> {
- try {
- return await processReserve(this.ws, reservePub);
- } finally {
- this.latch.trigger();
- }
+async function getExchanges(
+ ws: InternalWalletState,
+): Promise<ExchangesListRespose> {
+ const exchanges: ExchangeListItem[] = [];
+ await ws.db
+ .mktx((x) => ({
+ exchanges: x.exchanges,
+ exchangeDetails: x.exchangeDetails,
+ }))
+ .runReadOnly(async (tx) => {
+ const exchangeRecords = await tx.exchanges.iter().toArray();
+ for (const r of exchangeRecords) {
+ const dp = r.detailsPointer;
+ if (!dp) {
+ continue;
+ }
+ const { currency, masterPublicKey } = dp;
+ const exchangeDetails = await getExchangeDetails(tx, r.baseUrl);
+ if (!exchangeDetails) {
+ continue;
+ }
+ exchanges.push({
+ exchangeBaseUrl: r.baseUrl,
+ currency,
+ paytoUris: exchangeDetails.wireInfo.accounts.map((x) => x.payto_uri),
+ });
+ }
+ });
+ return { exchanges };
+}
+
+async function acceptWithdrawal(
+ ws: InternalWalletState,
+ talerWithdrawUri: string,
+ selectedExchange: string,
+): Promise<AcceptWithdrawalResponse> {
+ try {
+ return createTalerWithdrawReserve(ws, talerWithdrawUri, selectedExchange);
+ } finally {
+ ws.latch.trigger();
}
+}
- /**
- * Create a reserve, but do not flag it as confirmed yet.
- *
- * Adds the corresponding exchange as a trusted exchange if it is neither
- * audited nor trusted already.
- */
- async acceptManualWithdrawal(
- exchangeBaseUrl: string,
- amount: AmountJson,
- ): Promise<AcceptManualWithdrawalResult> {
- try {
- const resp = await createReserve(this.ws, {
- amount,
- exchange: exchangeBaseUrl,
- });
- const exchangePaytoUris = await this.db
- .mktx((x) => ({
- exchanges: x.exchanges,
- exchangeDetails: x.exchangeDetails,
- reserves: x.reserves,
- }))
- .runReadWrite((tx) => getFundingPaytoUris(tx, resp.reservePub));
- return {
- reservePub: resp.reservePub,
- exchangePaytoUris,
- };
- } finally {
- this.latch.trigger();
+/**
+ * Inform the wallet that the status of a reserve has changed (e.g. due to a
+ * confirmation from the bank.).
+ */
+export async function handleNotifyReserve(
+ ws: InternalWalletState,
+): Promise<void> {
+ const reserves = await ws.db
+ .mktx((x) => ({
+ reserves: x.reserves,
+ }))
+ .runReadOnly(async (tx) => {
+ return tx.reserves.iter().toArray();
+ });
+ for (const r of reserves) {
+ if (r.reserveStatus === ReserveRecordStatus.WAIT_CONFIRM_BANK) {
+ try {
+ processReserve(ws, r.reservePub);
+ } catch (e) {
+ console.error(e);
+ }
}
}
+}
- /**
- * Check if and how an exchange is trusted and/or audited.
- */
- async getExchangeTrust(
- exchangeInfo: ExchangeRecord,
- ): Promise<{ isTrusted: boolean; isAudited: boolean }> {
- return getExchangeTrust(this.ws, exchangeInfo);
- }
+async function setCoinSuspended(
+ ws: InternalWalletState,
+ coinPub: string,
+ suspended: boolean,
+): Promise<void> {
+ await ws.db
+ .mktx((x) => ({
+ coins: x.coins,
+ }))
+ .runReadWrite(async (tx) => {
+ const c = await tx.coins.get(coinPub);
+ if (!c) {
+ logger.warn(`coin ${coinPub} not found, won't suspend`);
+ return;
+ }
+ c.suspended = suspended;
+ await tx.coins.put(c);
+ });
+}
- async getWithdrawalDetailsForUri(
- talerWithdrawUri: string,
- ): Promise<WithdrawUriInfoResponse> {
- return getWithdrawalDetailsForUri(this.ws, talerWithdrawUri);
- }
+/**
+ * Dump the public information of coins we have in an easy-to-process format.
+ */
+async function dumpCoins(ws: InternalWalletState): Promise<CoinDumpJson> {
+ const coinsJson: CoinDumpJson = { coins: [] };
+ await ws.db
+ .mktx((x) => ({
+ coins: x.coins,
+ denominations: x.denominations,
+ withdrawalGroups: x.withdrawalGroups,
+ }))
+ .runReadOnly(async (tx) => {
+ const coins = await tx.coins.iter().toArray();
+ for (const c of coins) {
+ const denom = await tx.denominations.get([
+ c.exchangeBaseUrl,
+ c.denomPubHash,
+ ]);
+ if (!denom) {
+ console.error("no denom session found for coin");
+ continue;
+ }
+ const cs = c.coinSource;
+ let refreshParentCoinPub: string | undefined;
+ if (cs.type == CoinSourceType.Refresh) {
+ refreshParentCoinPub = cs.oldCoinPub;
+ }
+ let withdrawalReservePub: string | undefined;
+ if (cs.type == CoinSourceType.Withdraw) {
+ const ws = await tx.withdrawalGroups.get(cs.withdrawalGroupId);
+ if (!ws) {
+ console.error("no withdrawal session found for coin");
+ continue;
+ }
+ withdrawalReservePub = ws.reservePub;
+ }
+ coinsJson.coins.push({
+ coin_pub: c.coinPub,
+ denom_pub: c.denomPub,
+ denom_pub_hash: c.denomPubHash,
+ denom_value: Amounts.stringify(denom.value),
+ exchange_base_url: c.exchangeBaseUrl,
+ refresh_parent_coin_pub: refreshParentCoinPub,
+ remaining_value: Amounts.stringify(c.currentAmount),
+ withdrawal_reserve_pub: withdrawalReservePub,
+ coin_suspended: c.suspended,
+ });
+ }
+ });
+ return coinsJson;
+}
- async deleteTransaction(req: DeleteTransactionRequest): Promise<void> {
- return deleteTransaction(this.ws, req.transactionId);
- }
+export enum WalletApiOperation {
+ InitWallet = "initWallet",
+ WithdrawTestkudos = "withdrawTestkudos",
+ WithdrawTestBalance = "withdrawTestBalance",
+ PreparePayForUri = "preparePayForUri",
+ RunIntegrationTest = "runIntegrationTest",
+ TestPay = "testPay",
+ AddExchange = "addExchange",
+ GetTransactions = "getTransactions",
+ ListExchanges = "listExchanges",
+ GetWithdrawalDetailsForUri = "getWithdrawalDetailsForUri",
+ GetWithdrawalDetailsForAmount = "getWithdrawalDetailsForAmount",
+ AcceptManualWithdrawal = "acceptManualWithdrawal",
+ GetBalances = "getBalances",
+ GetPendingOperations = "getPendingOperations",
+ SetExchangeTosAccepted = "setExchangeTosAccepted",
+ ApplyRefund = "applyRefund",
+ AcceptBankIntegratedWithdrawal = "acceptBankIntegratedWithdrawal",
+ GetExchangeTos = "getExchangeTos",
+ RetryPendingNow = "retryPendingNow",
+ PreparePay = "preparePay",
+ ConfirmPay = "confirmPay",
+ DumpCoins = "dumpCoins",
+ SetCoinSuspended = "setCoinSuspended",
+ ForceRefresh = "forceRefresh",
+ PrepareTip = "prepareTip",
+ AcceptTip = "acceptTip",
+ ExportBackup = "exportBackup",
+ AddBackupProvider = "addBackupProvider",
+ RunBackupCycle = "runBackupCycle",
+ ExportBackupRecovery = "exportBackupRecovery",
+ ImportBackupRecovery = "importBackupRecovery",
+ GetBackupInfo = "getBackupInfo",
+ TrackDepositGroup = "trackDepositGroup",
+ DeleteTransaction = "deleteTransaction",
+ RetryTransaction = "retryTransaction",
+ GetCoins = "getCoins",
+ ListCurrencies = "listCurrencies",
+ CreateDepositGroup = "createDepositGroup",
+ SetWalletDeviceId = "setWalletDeviceId",
+ ExportBackupPlain = "exportBackupPlain",
+}
- async setDeviceId(newDeviceId: string): Promise<void> {
- return setWalletDeviceId(this.ws, newDeviceId);
- }
+export type WalletOperations = {
+ [WalletApiOperation.PreparePayForUri]: {
+ request: PreparePayRequest;
+ response: PreparePayResult;
+ };
+ [WalletApiOperation.WithdrawTestkudos]: {
+ request: {};
+ response: {};
+ };
+ [WalletApiOperation.PreparePay]: {
+ request: PreparePayRequest;
+ response: PreparePayResult;
+ };
+ [WalletApiOperation.ConfirmPay]: {
+ request: ConfirmPayRequest;
+ response: ConfirmPayResult;
+ };
+ [WalletApiOperation.GetBalances]: {
+ request: {};
+ response: BalancesResponse;
+ };
+ [WalletApiOperation.GetTransactions]: {
+ request: TransactionsRequest;
+ response: TransactionsResponse;
+ };
+ [WalletApiOperation.GetPendingOperations]: {
+ request: {};
+ response: PendingOperationsResponse;
+ };
+ [WalletApiOperation.DumpCoins]: {
+ request: {};
+ response: CoinDumpJson;
+ };
+ [WalletApiOperation.SetCoinSuspended]: {
+ request: SetCoinSuspendedRequest;
+ response: {};
+ };
+ [WalletApiOperation.ForceRefresh]: {
+ request: ForceRefreshRequest;
+ response: {};
+ };
+ [WalletApiOperation.DeleteTransaction]: {
+ request: DeleteTransactionRequest;
+ response: {};
+ };
+ [WalletApiOperation.RetryTransaction]: {
+ request: RetryTransactionRequest;
+ response: {};
+ };
+ [WalletApiOperation.PrepareTip]: {
+ request: PrepareTipRequest;
+ response: PrepareTipResult;
+ };
+ [WalletApiOperation.AcceptTip]: {
+ request: AcceptTipRequest;
+ response: {};
+ };
+ [WalletApiOperation.ApplyRefund]: {
+ request: ApplyRefundRequest;
+ response: ApplyRefundResponse;
+ };
+ [WalletApiOperation.ListCurrencies]: {
+ request: {};
+ response: WalletCurrencyInfo;
+ };
+ [WalletApiOperation.GetWithdrawalDetailsForAmount]: {
+ request: GetWithdrawalDetailsForAmountRequest;
+ response: ManualWithdrawalDetails;
+ };
+ [WalletApiOperation.GetWithdrawalDetailsForUri]: {
+ request: GetWithdrawalDetailsForUriRequest;
+ response: WithdrawUriInfoResponse;
+ };
+ [WalletApiOperation.AcceptBankIntegratedWithdrawal]: {
+ request: AcceptBankIntegratedWithdrawalRequest;
+ response: AcceptWithdrawalResponse;
+ };
+ [WalletApiOperation.AcceptManualWithdrawal]: {
+ request: AcceptManualWithdrawalRequest;
+ response: AcceptManualWithdrawalResult;
+ };
+ [WalletApiOperation.ListExchanges]: {
+ request: {};
+ response: ExchangesListRespose;
+ };
+ [WalletApiOperation.AddExchange]: {
+ request: AddExchangeRequest;
+ response: {};
+ };
+ [WalletApiOperation.SetExchangeTosAccepted]: {
+ request: AcceptExchangeTosRequest;
+ response: {};
+ };
+ [WalletApiOperation.GetExchangeTos]: {
+ request: GetExchangeTosRequest;
+ response: GetExchangeTosResult;
+ };
+ [WalletApiOperation.TrackDepositGroup]: {
+ request: TrackDepositGroupRequest;
+ response: TrackDepositGroupResponse;
+ };
+ [WalletApiOperation.CreateDepositGroup]: {
+ request: CreateDepositGroupRequest;
+ response: CreateDepositGroupResponse;
+ };
+ [WalletApiOperation.SetWalletDeviceId]: {
+ request: SetWalletDeviceIdRequest;
+ response: {};
+ };
+ [WalletApiOperation.ExportBackupPlain]: {
+ request: {};
+ response: WalletBackupContentV1;
+ };
+ [WalletApiOperation.ExportBackupRecovery]: {
+ request: {};
+ response: BackupRecovery;
+ };
+ [WalletApiOperation.ImportBackupRecovery]: {
+ request: RecoveryLoadRequest;
+ response: {};
+ };
+ [WalletApiOperation.RunBackupCycle]: {
+ request: {};
+ response: {};
+ };
+ [WalletApiOperation.AddBackupProvider]: {
+ request: AddBackupProviderRequest;
+ response: {};
+ };
+ [WalletApiOperation.GetBackupInfo]: {
+ request: {};
+ response: BackupInfo;
+ };
+};
+
+export type RequestType<
+ Op extends WalletApiOperation & keyof WalletOperations
+> = WalletOperations[Op] extends { request: infer T } ? T : never;
+
+export type ResponseType<
+ Op extends WalletApiOperation & keyof WalletOperations
+> = WalletOperations[Op] extends { response: infer T } ? T : never;
+
+export interface WalletCoreApiClient {
+ call<Op extends WalletApiOperation & keyof WalletOperations>(
+ operation: Op,
+ payload: RequestType<Op>,
+ ): Promise<ResponseType<Op>>;
+}
- /**
- * Update or add exchange DB entry by fetching the /keys and /wire information.
- */
- async updateExchangeFromUrl(
- baseUrl: string,
- force = false,
- ): Promise<{
- exchange: ExchangeRecord;
- exchangeDetails: ExchangeDetailsRecord;
- }> {
- try {
- return updateExchangeFromUrl(this.ws, baseUrl, force);
- } finally {
- this.latch.trigger();
- }
- }
+/**
+ * Get an API client from an internal wallet state object.
+ */
+export async function getClientFromWalletState(
+ ws: InternalWalletState,
+): Promise<WalletCoreApiClient> {
+ let id = 0;
+ const client: WalletCoreApiClient = {
+ async call(op, payload): Promise<any> {
+ const res = await handleCoreApiRequest(ws, op, `${id++}`, payload);
+ switch (res.type) {
+ case "error":
+ throw new OperationFailedError(res.error);
+ case "response":
+ return res.result;
+ }
+ },
+ };
+ return client;
+}
- async getExchangeTos(exchangeBaseUrl: string): Promise<GetExchangeTosResult> {
- const { exchange, exchangeDetails } = await this.updateExchangeFromUrl(
- exchangeBaseUrl,
+/**
+ * Implementation of the "wallet-core" API.
+ */
+async function dispatchRequestInternal(
+ ws: InternalWalletState,
+ operation: string,
+ payload: unknown,
+): Promise<Record<string, any>> {
+ if (ws.initCalled && operation !== "initWallet") {
+ throw Error(
+ `wallet must be initialized before running operation ${operation}`,
);
- const tos = exchangeDetails.termsOfServiceText;
- const currentEtag = exchangeDetails.termsOfServiceLastEtag;
- if (!tos || !currentEtag) {
- throw Error("exchange is in invalid state");
- }
- return {
- acceptedEtag: exchangeDetails.termsOfServiceAcceptedEtag,
- currentEtag,
- tos,
- };
}
-
- /**
- * Get detailed balance information, sliced by exchange and by currency.
- */
- async getBalances(): Promise<BalancesResponse> {
- return this.ws.memoGetBalance.memo(() => getBalances(this.ws));
- }
-
- async refresh(oldCoinPub: string): Promise<void> {
- try {
- const refreshGroupId = await this.db
+ switch (operation) {
+ case "initWallet": {
+ ws.initCalled = true;
+ return {};
+ }
+ case "withdrawTestkudos": {
+ await withdrawTestBalance(
+ ws,
+ "TESTKUDOS:10",
+ "https://bank.test.taler.net/",
+ "https://exchange.test.taler.net/",
+ );
+ return {};
+ }
+ case "withdrawTestBalance": {
+ const req = codecForWithdrawTestBalance().decode(payload);
+ await withdrawTestBalance(
+ ws,
+ req.amount,
+ req.bankBaseUrl,
+ req.exchangeBaseUrl,
+ );
+ return {};
+ }
+ case "runIntegrationTest": {
+ const req = codecForIntegrationTestArgs().decode(payload);
+ await runIntegrationTest(ws, req);
+ return {};
+ }
+ case "testPay": {
+ const req = codecForTestPayArgs().decode(payload);
+ await testPay(ws, req);
+ return {};
+ }
+ case "getTransactions": {
+ const req = codecForTransactionsRequest().decode(payload);
+ return await getTransactions(ws, req);
+ }
+ case "addExchange": {
+ const req = codecForAddExchangeRequest().decode(payload);
+ await updateExchangeFromUrl(ws, req.exchangeBaseUrl, req.forceUpdate);
+ return {};
+ }
+ case "listExchanges": {
+ return await getExchanges(ws);
+ }
+ case "getWithdrawalDetailsForUri": {
+ const req = codecForGetWithdrawalDetailsForUri().decode(payload);
+ return await getWithdrawalDetailsForUri(ws, req.talerWithdrawUri);
+ }
+ case "acceptManualWithdrawal": {
+ const req = codecForAcceptManualWithdrawalRequet().decode(payload);
+ const res = await acceptManualWithdrawal(
+ ws,
+ req.exchangeBaseUrl,
+ Amounts.parseOrThrow(req.amount),
+ );
+ return res;
+ }
+ case "getWithdrawalDetailsForAmount": {
+ const req = codecForGetWithdrawalDetailsForAmountRequest().decode(
+ payload,
+ );
+ return await getWithdrawalDetailsForAmount(
+ ws,
+ req.exchangeBaseUrl,
+ Amounts.parseOrThrow(req.amount),
+ );
+ }
+ case "getBalances": {
+ return await getBalances(ws);
+ }
+ case "getPendingOperations": {
+ return await getPendingOperations(ws);
+ }
+ case "setExchangeTosAccepted": {
+ const req = codecForAcceptExchangeTosRequest().decode(payload);
+ await acceptExchangeTermsOfService(ws, req.exchangeBaseUrl, req.etag);
+ return {};
+ }
+ case "applyRefund": {
+ const req = codecForApplyRefundRequest().decode(payload);
+ return await applyRefund(ws, req.talerRefundUri);
+ }
+ case "acceptBankIntegratedWithdrawal": {
+ const req = codecForAcceptBankIntegratedWithdrawalRequest().decode(
+ payload,
+ );
+ return await acceptWithdrawal(
+ ws,
+ req.talerWithdrawUri,
+ req.exchangeBaseUrl,
+ );
+ }
+ case "getExchangeTos": {
+ const req = codecForGetExchangeTosRequest().decode(payload);
+ return getExchangeTos(ws, req.exchangeBaseUrl);
+ }
+ case "retryPendingNow": {
+ await runPending(ws, true);
+ return {};
+ }
+ case "preparePay": {
+ const req = codecForPreparePayRequest().decode(payload);
+ return await preparePayForUri(ws, req.talerPayUri);
+ }
+ case "confirmPay": {
+ const req = codecForConfirmPayRequest().decode(payload);
+ return await confirmPay(ws, req.proposalId, req.sessionId);
+ }
+ case "abortFailedPayWithRefund": {
+ const req = codecForAbortPayWithRefundRequest().decode(payload);
+ await abortFailedPayWithRefund(ws, req.proposalId);
+ return {};
+ }
+ case "dumpCoins": {
+ return await dumpCoins(ws);
+ }
+ case "setCoinSuspended": {
+ const req = codecForSetCoinSuspendedRequest().decode(payload);
+ await setCoinSuspended(ws, req.coinPub, req.suspended);
+ return {};
+ }
+ case "forceRefresh": {
+ const req = codecForForceRefreshRequest().decode(payload);
+ const coinPubs = req.coinPubList.map((x) => ({ coinPub: x }));
+ const refreshGroupId = await ws.db
.mktx((x) => ({
refreshGroups: x.refreshGroups,
denominations: x.denominations,
@@ -642,613 +1014,150 @@ export class Wallet {
}))
.runReadWrite(async (tx) => {
return await createRefreshGroup(
- this.ws,
+ ws,
tx,
- [{ coinPub: oldCoinPub }],
+ coinPubs,
RefreshReason.Manual,
);
});
- await processRefreshGroup(this.ws, refreshGroupId.refreshGroupId);
- } catch (e) {
- this.latch.trigger();
+ processRefreshGroup(ws, refreshGroupId.refreshGroupId, true).catch(
+ (x) => {
+ logger.error(x);
+ },
+ );
+ return {
+ refreshGroupId,
+ };
}
- }
-
- async getPendingOperations(): Promise<PendingOperationsResponse> {
- return this.ws.memoGetPending.memo(() => getPendingOperations(this.ws));
- }
-
- async acceptExchangeTermsOfService(
- exchangeBaseUrl: string,
- etag: string | undefined,
- ): Promise<void> {
- return acceptExchangeTermsOfService(this.ws, exchangeBaseUrl, etag);
- }
-
- async getExchanges(): Promise<ExchangesListRespose> {
- const exchanges: ExchangeListItem[] = [];
- await this.db
- .mktx((x) => ({
- exchanges: x.exchanges,
- exchangeDetails: x.exchangeDetails,
- }))
- .runReadOnly(async (tx) => {
- const exchangeRecords = await tx.exchanges.iter().toArray();
- for (const r of exchangeRecords) {
- const dp = r.detailsPointer;
- if (!dp) {
- continue;
- }
- const { currency, masterPublicKey } = dp;
- const exchangeDetails = await getExchangeDetails(tx, r.baseUrl);
- if (!exchangeDetails) {
- continue;
- }
- exchanges.push({
- exchangeBaseUrl: r.baseUrl,
- currency,
- paytoUris: exchangeDetails.wireInfo.accounts.map(
- (x) => x.payto_uri,
- ),
- });
- }
- });
- return { exchanges };
- }
-
- async getCurrencies(): Promise<WalletCurrencyInfo> {
- return await this.ws.db
- .mktx((x) => ({
- auditorTrust: x.auditorTrust,
- exchangeTrust: x.exchangeTrust,
- }))
- .runReadOnly(async (tx) => {
- const trustedAuditors = await tx.auditorTrust.iter().toArray();
- const trustedExchanges = await tx.exchangeTrust.iter().toArray();
- return {
- trustedAuditors: trustedAuditors.map((x) => ({
- currency: x.currency,
- auditorBaseUrl: x.auditorBaseUrl,
- auditorPub: x.auditorPub,
- })),
- trustedExchanges: trustedExchanges.map((x) => ({
- currency: x.currency,
- exchangeBaseUrl: x.exchangeBaseUrl,
- exchangeMasterPub: x.exchangeMasterPub,
- })),
- };
- });
- }
-
- /**
- * Stop ongoing processing.
- */
- stop(): void {
- this.stopped = true;
- this.timerGroup.stopCurrentAndFutureTimers();
- this.ws.cryptoApi.stop();
- }
-
- /**
- * Trigger paying coins back into the user's account.
- */
- async returnCoins(req: ReturnCoinsRequest): Promise<void> {
- throw Error("not implemented");
- }
-
- /**
- * Accept a refund, return the contract hash for the contract
- * that was involved in the refund.
- */
- async applyRefund(talerRefundUri: string): Promise<ApplyRefundResponse> {
- return applyRefund(this.ws, talerRefundUri);
- }
-
- async acceptTip(talerTipUri: string): Promise<void> {
- try {
- return acceptTip(this.ws, talerTipUri);
- } catch (e) {
- this.latch.trigger();
+ case "prepareTip": {
+ const req = codecForPrepareTipRequest().decode(payload);
+ return await prepareTip(ws, req.talerTipUri);
}
- }
-
- async prepareTip(talerTipUri: string): Promise<PrepareTipResult> {
- return prepareTip(this.ws, talerTipUri);
- }
-
- async abortFailedPayWithRefund(proposalId: string): Promise<void> {
- return abortFailedPayWithRefund(this.ws, proposalId);
- }
-
- /**
- * Inform the wallet that the status of a reserve has changed (e.g. due to a
- * confirmation from the bank.).
- */
- public async handleNotifyReserve(): Promise<void> {
- const reserves = await this.ws.db
- .mktx((x) => ({
- reserves: x.reserves,
- }))
- .runReadOnly(async (tx) => {
- return tx.reserves.iter().toArray();
- });
- for (const r of reserves) {
- if (r.reserveStatus === ReserveRecordStatus.WAIT_CONFIRM_BANK) {
- try {
- this.processReserve(r.reservePub);
- } catch (e) {
- console.error(e);
- }
- }
+ case "acceptTip": {
+ const req = codecForAcceptTipRequest().decode(payload);
+ await acceptTip(ws, req.walletTipId);
+ return {};
}
- }
-
- /**
- * Remove unreferenced / expired data from the wallet's database
- * based on the current system time.
- */
- async collectGarbage(): Promise<void> {
- // FIXME(#5845)
- // We currently do not garbage-collect the wallet database. This might change
- // after the feature has been properly re-designed, and we have come up with a
- // strategy to test it.
- }
-
- async acceptWithdrawal(
- talerWithdrawUri: string,
- selectedExchange: string,
- ): Promise<AcceptWithdrawalResponse> {
- try {
- return createTalerWithdrawReserve(
- this.ws,
- talerWithdrawUri,
- selectedExchange,
- );
- } finally {
- this.latch.trigger();
+ case "exportBackupPlain": {
+ return exportBackup(ws);
}
- }
-
- async refuseProposal(proposalId: string): Promise<void> {
- return refuseProposal(this.ws, proposalId);
- }
-
- benchmarkCrypto(repetitions: number): Promise<BenchmarkResult> {
- return this.ws.cryptoApi.benchmark(repetitions);
- }
-
- async setCoinSuspended(coinPub: string, suspended: boolean): Promise<void> {
- await this.db
- .mktx((x) => ({
- coins: x.coins,
- }))
- .runReadWrite(async (tx) => {
- const c = await tx.coins.get(coinPub);
- if (!c) {
- logger.warn(`coin ${coinPub} not found, won't suspend`);
- return;
- }
- c.suspended = suspended;
- await tx.coins.put(c);
- });
- }
-
- /**
- * Dump the public information of coins we have in an easy-to-process format.
- */
- async dumpCoins(): Promise<CoinDumpJson> {
- const coinsJson: CoinDumpJson = { coins: [] };
- await this.ws.db
- .mktx((x) => ({
- coins: x.coins,
- denominations: x.denominations,
- withdrawalGroups: x.withdrawalGroups,
- }))
- .runReadOnly(async (tx) => {
- const coins = await tx.coins.iter().toArray();
- for (const c of coins) {
- const denom = await tx.denominations.get([
- c.exchangeBaseUrl,
- c.denomPubHash,
- ]);
- if (!denom) {
- console.error("no denom session found for coin");
- continue;
- }
- const cs = c.coinSource;
- let refreshParentCoinPub: string | undefined;
- if (cs.type == CoinSourceType.Refresh) {
- refreshParentCoinPub = cs.oldCoinPub;
- }
- let withdrawalReservePub: string | undefined;
- if (cs.type == CoinSourceType.Withdraw) {
- const ws = await tx.withdrawalGroups.get(cs.withdrawalGroupId);
- if (!ws) {
- console.error("no withdrawal session found for coin");
- continue;
- }
- withdrawalReservePub = ws.reservePub;
- }
- coinsJson.coins.push({
- coin_pub: c.coinPub,
- denom_pub: c.denomPub,
- denom_pub_hash: c.denomPubHash,
- denom_value: Amounts.stringify(denom.value),
- exchange_base_url: c.exchangeBaseUrl,
- refresh_parent_coin_pub: refreshParentCoinPub,
- remaining_value: Amounts.stringify(c.currentAmount),
- withdrawal_reserve_pub: withdrawalReservePub,
- coin_suspended: c.suspended,
- });
- }
- });
- return coinsJson;
- }
-
- async getTransactions(
- request: TransactionsRequest,
- ): Promise<TransactionsResponse> {
- return getTransactions(this.ws, request);
- }
-
- async withdrawTestBalance(req: WithdrawTestBalanceRequest): Promise<void> {
- await withdrawTestBalance(
- this.ws,
- req.amount,
- req.bankBaseUrl,
- req.exchangeBaseUrl,
- );
- }
-
- async updateReserve(reservePub: string): Promise<ReserveRecord | undefined> {
- await forceQueryReserve(this.ws, reservePub);
- return await this.ws.db
- .mktx((x) => ({
- reserves: x.reserves,
- }))
- .runReadOnly(async (tx) => {
- return tx.reserves.get(reservePub);
- });
- }
-
- async getCoins(): Promise<CoinRecord[]> {
- return await this.db
- .mktx((x) => ({
- coins: x.coins,
- }))
- .runReadOnly(async (tx) => {
- return tx.coins.iter().toArray();
- });
- }
-
- async getReservesForExchange(
- exchangeBaseUrl?: string,
- ): Promise<ReserveRecord[]> {
- return await this.db
- .mktx((x) => ({
- reserves: x.reserves,
- }))
- .runReadOnly(async (tx) => {
- if (exchangeBaseUrl) {
- return await tx.reserves
- .iter()
- .filter((r) => r.exchangeBaseUrl === exchangeBaseUrl);
- } else {
- return await tx.reserves.iter().toArray();
- }
- });
- }
-
- async getReserve(reservePub: string): Promise<ReserveRecord | undefined> {
- return await this.db
- .mktx((x) => ({
- reserves: x.reserves,
- }))
- .runReadOnly(async (tx) => {
- return tx.reserves.get(reservePub);
- });
- }
-
- async runIntegrationtest(args: IntegrationTestArgs): Promise<void> {
- return runIntegrationTest(this.ws.http, this, args);
- }
-
- async testPay(args: TestPayArgs) {
- return testPay(this.ws.http, this, args);
- }
-
- async exportBackupPlain() {
- return exportBackup(this.ws);
- }
-
- async importBackupPlain(backup: any) {
- return importBackupPlain(this.ws, backup);
- }
-
- async exportBackupEncrypted() {
- return exportBackupEncrypted(this.ws);
- }
-
- async importBackupEncrypted(backup: Uint8Array) {
- return importBackupEncrypted(this.ws, backup);
- }
-
- async getBackupRecovery(): Promise<BackupRecovery> {
- return getBackupRecovery(this.ws);
- }
-
- async loadBackupRecovery(req: RecoveryLoadRequest): Promise<void> {
- return loadBackupRecovery(this.ws, req);
- }
-
- async addBackupProvider(req: AddBackupProviderRequest): Promise<void> {
- return addBackupProvider(this.ws, req);
- }
-
- async createDepositGroup(
- req: CreateDepositGroupRequest,
- ): Promise<CreateDepositGroupResponse> {
- return createDepositGroup(this.ws, req);
- }
-
- async runBackupCycle(): Promise<void> {
- return runBackupCycle(this.ws);
- }
-
- async getBackupStatus(): Promise<BackupInfo> {
- return getBackupInfo(this.ws);
- }
-
- async trackDepositGroup(
- req: TrackDepositGroupRequest,
- ): Promise<TrackDepositGroupResponse> {
- return trackDepositGroup(this.ws, req);
- }
-
- /**
- * Implementation of the "wallet-core" API.
- */
- private async dispatchRequestInternal(
- operation: string,
- payload: unknown,
- ): Promise<Record<string, any>> {
- switch (operation) {
- case "withdrawTestkudos": {
- await this.withdrawTestBalance({
- amount: "TESTKUDOS:10",
- bankBaseUrl: "https://bank.test.taler.net/",
- exchangeBaseUrl: "https://exchange.test.taler.net/",
+ case "addBackupProvider": {
+ const req = codecForAddBackupProviderRequest().decode(payload);
+ await addBackupProvider(ws, req);
+ return {};
+ }
+ case "runBackupCycle": {
+ await runBackupCycle(ws);
+ return {};
+ }
+ case "exportBackupRecovery": {
+ const resp = await getBackupRecovery(ws);
+ return resp;
+ }
+ case "importBackupRecovery": {
+ const req = codecForAny().decode(payload);
+ await loadBackupRecovery(ws, req);
+ return {};
+ }
+ case "getBackupInfo": {
+ const resp = await getBackupInfo(ws);
+ return resp;
+ }
+ case "createDepositGroup": {
+ const req = codecForCreateDepositGroupRequest().decode(payload);
+ return await createDepositGroup(ws, req);
+ }
+ case "trackDepositGroup": {
+ const req = codecForTrackDepositGroupRequest().decode(payload);
+ return trackDepositGroup(ws, req);
+ }
+ case "deleteTransaction": {
+ const req = codecForDeleteTransactionRequest().decode(payload);
+ await deleteTransaction(ws, req.transactionId);
+ return {};
+ }
+ case "retryTransaction": {
+ const req = codecForRetryTransactionRequest().decode(payload);
+ await retryTransaction(ws, req.transactionId);
+ return {};
+ }
+ case "setWalletDeviceId": {
+ const req = codecForSetWalletDeviceIdRequest().decode(payload);
+ await setWalletDeviceId(ws, req.walletDeviceId);
+ return {};
+ }
+ case "listCurrencies": {
+ return await ws.db
+ .mktx((x) => ({
+ auditorTrust: x.auditorTrust,
+ exchangeTrust: x.exchangeTrust,
+ }))
+ .runReadOnly(async (tx) => {
+ const trustedAuditors = await tx.auditorTrust.iter().toArray();
+ const trustedExchanges = await tx.exchangeTrust.iter().toArray();
+ return {
+ trustedAuditors: trustedAuditors.map((x) => ({
+ currency: x.currency,
+ auditorBaseUrl: x.auditorBaseUrl,
+ auditorPub: x.auditorPub,
+ })),
+ trustedExchanges: trustedExchanges.map((x) => ({
+ currency: x.currency,
+ exchangeBaseUrl: x.exchangeBaseUrl,
+ exchangeMasterPub: x.exchangeMasterPub,
+ })),
+ };
});
- return {};
- }
- case "withdrawTestBalance": {
- const req = codecForWithdrawTestBalance().decode(payload);
- await this.withdrawTestBalance(req);
- return {};
- }
- case "runIntegrationTest": {
- const req = codecForIntegrationTestArgs().decode(payload);
- await this.runIntegrationtest(req);
- return {};
- }
- case "testPay": {
- const req = codecForTestPayArgs().decode(payload);
- await this.testPay(req);
- return {};
- }
- case "getTransactions": {
- const req = codecForTransactionsRequest().decode(payload);
- return await this.getTransactions(req);
- }
- case "addExchange": {
- const req = codecForAddExchangeRequest().decode(payload);
- await this.updateExchangeFromUrl(req.exchangeBaseUrl);
- return {};
- }
- case "forceUpdateExchange": {
- const req = codecForForceExchangeUpdateRequest().decode(payload);
- await this.updateExchangeFromUrl(req.exchangeBaseUrl, true);
- return {};
- }
- case "listExchanges": {
- return await this.getExchanges();
- }
- case "getWithdrawalDetailsForUri": {
- const req = codecForGetWithdrawalDetailsForUri().decode(payload);
- return await this.getWithdrawalDetailsForUri(req.talerWithdrawUri);
- }
- case "acceptManualWithdrawal": {
- const req = codecForAcceptManualWithdrawalRequet().decode(payload);
- const res = await this.acceptManualWithdrawal(
- req.exchangeBaseUrl,
- Amounts.parseOrThrow(req.amount),
- );
- return res;
- }
- case "getWithdrawalDetailsForAmount": {
- const req = codecForGetWithdrawalDetailsForAmountRequest().decode(
- payload,
- );
- return await this.getWithdrawalDetailsForAmount(
- req.exchangeBaseUrl,
- Amounts.parseOrThrow(req.amount),
- );
- }
- case "getBalances": {
- return await this.getBalances();
- }
- case "getPendingOperations": {
- return await this.getPendingOperations();
- }
- case "setExchangeTosAccepted": {
- const req = codecForAcceptExchangeTosRequest().decode(payload);
- await this.acceptExchangeTermsOfService(req.exchangeBaseUrl, req.etag);
- return {};
- }
- case "applyRefund": {
- const req = codecForApplyRefundRequest().decode(payload);
- return await this.applyRefund(req.talerRefundUri);
- }
- case "acceptBankIntegratedWithdrawal": {
- const req = codecForAcceptBankIntegratedWithdrawalRequest().decode(
- payload,
- );
- return await this.acceptWithdrawal(
- req.talerWithdrawUri,
- req.exchangeBaseUrl,
- );
- }
- case "getExchangeTos": {
- const req = codecForGetExchangeTosRequest().decode(payload);
- return this.getExchangeTos(req.exchangeBaseUrl);
- }
- case "retryPendingNow": {
- await this.runPending(true);
- return {};
- }
- case "preparePay": {
- const req = codecForPreparePayRequest().decode(payload);
- return await this.preparePayForUri(req.talerPayUri);
- }
- case "confirmPay": {
- const req = codecForConfirmPayRequest().decode(payload);
- return await this.confirmPay(req.proposalId, req.sessionId);
- }
- case "abortFailedPayWithRefund": {
- const req = codecForAbortPayWithRefundRequest().decode(payload);
- await this.abortFailedPayWithRefund(req.proposalId);
- return {};
- }
- case "dumpCoins": {
- return await this.dumpCoins();
- }
- case "setCoinSuspended": {
- const req = codecForSetCoinSuspendedRequest().decode(payload);
- await this.setCoinSuspended(req.coinPub, req.suspended);
- return {};
- }
- case "forceRefresh": {
- const req = codecForForceRefreshRequest().decode(payload);
- const coinPubs = req.coinPubList.map((x) => ({ coinPub: x }));
- const refreshGroupId = await this.db
- .mktx((x) => ({
- refreshGroups: x.refreshGroups,
- denominations: x.denominations,
- coins: x.coins,
- }))
- .runReadWrite(async (tx) => {
- return await createRefreshGroup(
- this.ws,
- tx,
- coinPubs,
- RefreshReason.Manual,
- );
- });
- return {
- refreshGroupId,
- };
- }
- case "prepareTip": {
- const req = codecForPrepareTipRequest().decode(payload);
- return await this.prepareTip(req.talerTipUri);
- }
- case "acceptTip": {
- const req = codecForAcceptTipRequest().decode(payload);
- await this.acceptTip(req.walletTipId);
- return {};
- }
- case "exportBackup": {
- return exportBackup(this.ws);
- }
- case "addBackupProvider": {
- const req = codecForAddBackupProviderRequest().decode(payload);
- await addBackupProvider(this.ws, req);
- return {};
- }
- case "runBackupCycle": {
- await runBackupCycle(this.ws);
- return {};
- }
- case "exportBackupRecovery": {
- const resp = await getBackupRecovery(this.ws);
- return resp;
- }
- case "importBackupRecovery": {
- const req = codecForAny().decode(payload);
- await loadBackupRecovery(this.ws, req);
- return {};
- }
- case "getBackupInfo": {
- const resp = await getBackupInfo(this.ws);
- return resp;
- }
- case "createDepositGroup": {
- const req = codecForCreateDepositGroupRequest().decode(payload);
- return await createDepositGroup(this.ws, req);
- }
- case "trackDepositGroup": {
- const req = codecForTrackDepositGroupRequest().decode(payload);
- return trackDepositGroup(this.ws, req);
- }
- case "deleteTransaction": {
- const req = codecForDeleteTransactionRequest().decode(payload);
- await deleteTransaction(this.ws, req.transactionId);
- return {};
- }
- case "retryTransaction": {
- const req = codecForRetryTransactionRequest().decode(payload);
- await retryTransaction(this.ws, req.transactionId);
- return {};
- }
}
- throw OperationFailedError.fromCode(
- TalerErrorCode.WALLET_CORE_API_OPERATION_UNKNOWN,
- "unknown operation",
- {
- operation,
- },
- );
}
+ throw OperationFailedError.fromCode(
+ TalerErrorCode.WALLET_CORE_API_OPERATION_UNKNOWN,
+ "unknown operation",
+ {
+ operation,
+ },
+ );
+}
- /**
- * Handle a request to the wallet-core API.
- */
- async handleCoreApiRequest(
- operation: string,
- id: string,
- payload: unknown,
- ): Promise<CoreApiResponse> {
- try {
- const result = await this.dispatchRequestInternal(operation, payload);
+/**
+ * Handle a request to the wallet-core API.
+ */
+export async function handleCoreApiRequest(
+ ws: InternalWalletState,
+ operation: string,
+ id: string,
+ payload: unknown,
+): Promise<CoreApiResponse> {
+ try {
+ const result = await dispatchRequestInternal(ws, operation, payload);
+ return {
+ type: "response",
+ operation,
+ id,
+ result,
+ };
+ } catch (e) {
+ if (
+ e instanceof OperationFailedError ||
+ e instanceof OperationFailedAndReportedError
+ ) {
return {
- type: "response",
+ type: "error",
operation,
id,
- result,
+ error: e.operationError,
+ };
+ } else {
+ return {
+ type: "error",
+ operation,
+ id,
+ error: makeErrorDetails(
+ TalerErrorCode.WALLET_UNEXPECTED_EXCEPTION,
+ `unexpected exception: ${e}`,
+ {},
+ ),
};
- } catch (e) {
- if (
- e instanceof OperationFailedError ||
- e instanceof OperationFailedAndReportedError
- ) {
- return {
- type: "error",
- operation,
- id,
- error: e.operationError,
- };
- } else {
- return {
- type: "error",
- operation,
- id,
- error: makeErrorDetails(
- TalerErrorCode.WALLET_UNEXPECTED_EXCEPTION,
- `unexpected exception: ${e}`,
- {},
- ),
- };
- }
}
}
}
diff --git a/packages/taler-wallet-webextension/src/wxBackend.ts b/packages/taler-wallet-webextension/src/wxBackend.ts
index 51a44ee67..d3f99d9cb 100644
--- a/packages/taler-wallet-webextension/src/wxBackend.ts
+++ b/packages/taler-wallet-webextension/src/wxBackend.ts
@@ -26,7 +26,6 @@
import { isFirefox, getPermissionsApi } from "./compat";
import { extendedPermissions } from "./permissions";
import {
- Wallet,
OpenedPromise,
openPromise,
openTalerDatabase,
@@ -34,6 +33,9 @@ import {
deleteTalerDatabase,
DbAccess,
WalletStoresV1,
+ handleCoreApiRequest,
+ runRetryLoop,
+ handleNotifyReserve,
} from "@gnu-taler/taler-wallet-core";
import {
classifyTalerUri,
@@ -45,12 +47,13 @@ import {
} from "@gnu-taler/taler-util";
import { BrowserHttpLib } from "./browserHttpLib";
import { BrowserCryptoWorkerFactory } from "./browserCryptoWorkerFactory";
+import { InternalWalletState } from "@gnu-taler/taler-wallet-core/src/operations/state";
/**
* Currently active wallet instance. Might be unloaded and
* re-instantiated when the database is reset.
*/
-let currentWallet: Wallet | undefined;
+let currentWallet: InternalWalletState | undefined;
let currentDatabase: DbAccess<typeof WalletStoresV1> | undefined;
@@ -167,7 +170,7 @@ async function dispatch(
};
break;
}
- r = await w.handleCoreApiRequest(req.operation, req.id, req.payload);
+ r = await handleCoreApiRequest(w, req.operation, req.id, req.payload);
break;
}
}
@@ -253,7 +256,7 @@ async function reinitWallet(): Promise<void> {
}
const http = new BrowserHttpLib();
console.log("setting wallet");
- const wallet = new Wallet(
+ const wallet = new InternalWalletState(
currentDatabase,
http,
new BrowserCryptoWorkerFactory(),
@@ -267,7 +270,7 @@ async function reinitWallet(): Promise<void> {
}
}
});
- wallet.runRetryLoop().catch((e) => {
+ runRetryLoop(wallet).catch((e) => {
console.log("error during wallet retry loop", e);
});
// Useful for debugging in the background page.
@@ -357,7 +360,7 @@ function headerListener(
if (!w) {
return;
}
- w.handleNotifyReserve();
+ handleNotifyReserve(w);
});
break;
default:
@@ -448,3 +451,4 @@ export async function wxMain(): Promise<void> {
setupHeaderListener();
});
}
+