summaryrefslogtreecommitdiff
path: root/packages
diff options
context:
space:
mode:
authorFlorian Dold <florian@dold.me>2024-01-22 21:29:47 +0100
committerFlorian Dold <florian@dold.me>2024-01-22 21:29:47 +0100
commitfb5f098f9e60f03cdd6f78aba5aa248ec5889485 (patch)
tree2ddbf078597d48cbf9c85fa3c0491e5e63bb2196 /packages
parent88851f45403c1995c973bcae7ad2976db3c430c7 (diff)
downloadwallet-core-fb5f098f9e60f03cdd6f78aba5aa248ec5889485.tar.gz
wallet-core-fb5f098f9e60f03cdd6f78aba5aa248ec5889485.tar.bz2
wallet-core-fb5f098f9e60f03cdd6f78aba5aa248ec5889485.zip
wallet-core: implement and test balance reporting with scope info
Diffstat (limited to 'packages')
-rw-r--r--packages/taler-harness/src/integrationtests/test-currency-scope.ts192
-rw-r--r--packages/taler-harness/src/integrationtests/test-multiexchange.ts20
-rw-r--r--packages/taler-harness/src/integrationtests/testrunner.ts4
-rw-r--r--packages/taler-util/src/wallet-types.ts2
-rw-r--r--packages/taler-wallet-core/src/db.ts21
-rw-r--r--packages/taler-wallet-core/src/operations/balance.ts333
-rw-r--r--packages/taler-wallet-core/src/operations/common.ts51
-rw-r--r--packages/taler-wallet-core/src/operations/exchanges.ts188
-rw-r--r--packages/taler-wallet-core/src/operations/refresh.ts20
-rw-r--r--packages/taler-wallet-core/src/operations/withdraw.ts69
-rw-r--r--packages/taler-wallet-core/src/util/coinSelection.ts15
-rw-r--r--packages/taler-wallet-core/src/wallet.ts6
12 files changed, 699 insertions, 222 deletions
diff --git a/packages/taler-harness/src/integrationtests/test-currency-scope.ts b/packages/taler-harness/src/integrationtests/test-currency-scope.ts
new file mode 100644
index 000000000..e07a8f47b
--- /dev/null
+++ b/packages/taler-harness/src/integrationtests/test-currency-scope.ts
@@ -0,0 +1,192 @@
+/*
+ This file is part of GNU Taler
+ (C) 2020 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ * Imports.
+ */
+import { Duration, j2s } from "@gnu-taler/taler-util";
+import { Wallet, WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+import { defaultCoinConfig } from "../harness/denomStructures.js";
+import {
+ BankService,
+ ExchangeService,
+ GlobalTestState,
+ MerchantService,
+ generateRandomPayto,
+ setupDb,
+} from "../harness/harness.js";
+import {
+ createWalletDaemonWithClient,
+ makeTestPaymentV2,
+ withdrawViaBankV2,
+} from "../harness/helpers.js";
+
+/**
+ * Run test for basic, bank-integrated withdrawal and payment.
+ */
+export async function runCurrencyScopeTest(t: GlobalTestState) {
+ // Set up test environment
+ const dbDefault = await setupDb(t);
+
+ const dbExchangeTwo = await setupDb(t, {
+ nameSuffix: "exchange2",
+ });
+
+ const bank = await BankService.create(t, {
+ allowRegistrations: true,
+ currency: "TESTKUDOS",
+ database: dbDefault.connStr,
+ httpPort: 8082,
+ });
+
+ const exchangeOne = ExchangeService.create(t, {
+ name: "testexchange-1",
+ currency: "TESTKUDOS",
+ httpPort: 8081,
+ database: dbDefault.connStr,
+ });
+
+ const exchangeTwo = ExchangeService.create(t, {
+ name: "testexchange-2",
+ currency: "TESTKUDOS",
+ httpPort: 8281,
+ database: dbExchangeTwo.connStr,
+ });
+
+ const merchant = await MerchantService.create(t, {
+ name: "testmerchant-1",
+ currency: "TESTKUDOS",
+ httpPort: 8083,
+ database: dbDefault.connStr,
+ });
+
+ const exchangeOneBankAccount = await bank.createExchangeAccount(
+ "myexchange",
+ "x",
+ );
+ await exchangeOne.addBankAccount("1", exchangeOneBankAccount);
+
+ const exchangeTwoBankAccount = await bank.createExchangeAccount(
+ "myexchange2",
+ "x",
+ );
+ await exchangeTwo.addBankAccount("1", exchangeTwoBankAccount);
+
+ bank.setSuggestedExchange(
+ exchangeOne,
+ exchangeOneBankAccount.accountPaytoUri,
+ );
+
+ await bank.start();
+
+ await bank.pingUntilAvailable();
+
+ // Set up the first exchange
+
+ exchangeOne.addOfferedCoins(defaultCoinConfig);
+ await exchangeOne.start();
+ await exchangeOne.pingUntilAvailable();
+
+ // Set up the second exchange
+
+ exchangeTwo.addOfferedCoins(defaultCoinConfig);
+ await exchangeTwo.start();
+ await exchangeTwo.pingUntilAvailable();
+
+ // Start and configure merchant
+
+ merchant.addExchange(exchangeOne);
+ merchant.addExchange(exchangeTwo);
+
+ await merchant.start();
+ await merchant.pingUntilAvailable();
+
+ await merchant.addInstanceWithWireAccount({
+ id: "default",
+ name: "Default Instance",
+ paytoUris: [generateRandomPayto("merchant-default")],
+ defaultWireTransferDelay: Duration.toTalerProtocolDuration(
+ Duration.fromSpec({ minutes: 1 }),
+ ),
+ });
+
+ await merchant.addInstanceWithWireAccount({
+ id: "minst1",
+ name: "minst1",
+ paytoUris: [generateRandomPayto("minst1")],
+ defaultWireTransferDelay: Duration.toTalerProtocolDuration(
+ Duration.fromSpec({ minutes: 1 }),
+ ),
+ });
+
+ const { walletClient } = await createWalletDaemonWithClient(t, {
+ name: "wallet",
+ });
+
+ console.log("setup done!");
+
+ // Withdraw digital cash into the wallet.
+
+ const w1 = await withdrawViaBankV2(t, {
+ walletClient,
+ bank,
+ exchange: exchangeOne,
+ amount: "TESTKUDOS:6",
+ });
+
+ const w2 = await withdrawViaBankV2(t, {
+ walletClient,
+ bank,
+ exchange: exchangeTwo,
+ amount: "TESTKUDOS:6",
+ });
+
+ await w1.withdrawalFinishedCond;
+ await w2.withdrawalFinishedCond;
+
+ const bal = await walletClient.call(WalletApiOperation.GetBalances, {});
+ console.log(j2s(bal));
+
+ // Separate balances, exchange-scope.
+ t.assertDeepEqual(bal.balances.length, 2);
+
+ await walletClient.call(WalletApiOperation.AddGlobalCurrencyExchange, {
+ currency: "TESTKUDOS",
+ exchangeBaseUrl: exchangeOne.baseUrl,
+ exchangeMasterPub: exchangeOne.masterPub,
+ });
+
+ await walletClient.call(WalletApiOperation.AddGlobalCurrencyExchange, {
+ currency: "TESTKUDOS",
+ exchangeBaseUrl: exchangeTwo.baseUrl,
+ exchangeMasterPub: exchangeTwo.masterPub,
+ });
+
+ const ex = walletClient.call(
+ WalletApiOperation.ListGlobalCurrencyExchanges,
+ {},
+ );
+ console.log("global currency exchanges:");
+ console.log(j2s(ex));
+
+ const bal2 = await walletClient.call(WalletApiOperation.GetBalances, {});
+ console.log(j2s(bal2));
+
+ // Global currencies are merged
+ t.assertDeepEqual(bal2.balances.length, 1);
+}
+
+runCurrencyScopeTest.suites = ["wallet"];
diff --git a/packages/taler-harness/src/integrationtests/test-multiexchange.ts b/packages/taler-harness/src/integrationtests/test-multiexchange.ts
index aeda035a8..e27bccc46 100644
--- a/packages/taler-harness/src/integrationtests/test-multiexchange.ts
+++ b/packages/taler-harness/src/integrationtests/test-multiexchange.ts
@@ -17,7 +17,9 @@
/**
* Imports.
*/
+import { Duration } from "@gnu-taler/taler-util";
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+import { defaultCoinConfig } from "../harness/denomStructures.js";
import {
BankService,
ExchangeService,
@@ -27,13 +29,10 @@ import {
setupDb,
} from "../harness/harness.js";
import {
- createSimpleTestkudosEnvironmentV2,
- withdrawViaBankV2,
- makeTestPaymentV2,
createWalletDaemonWithClient,
+ makeTestPaymentV2,
+ withdrawViaBankV2,
} from "../harness/helpers.js";
-import { Duration, j2s } from "@gnu-taler/taler-util";
-import { defaultCoinConfig } from "../harness/denomStructures.js";
/**
* Run test for basic, bank-integrated withdrawal and payment.
@@ -146,14 +145,21 @@ export async function runMultiExchangeTest(t: GlobalTestState) {
walletClient,
bank,
exchange: exchangeOne,
- amount: "TESTKUDOS:20",
+ amount: "TESTKUDOS:6",
+ });
+
+ await withdrawViaBankV2(t, {
+ walletClient,
+ bank,
+ exchange: exchangeTwo,
+ amount: "TESTKUDOS:6",
});
await walletClient.call(WalletApiOperation.TestingWaitTransactionsFinal, {});
const order = {
summary: "Buy me!",
- amount: "TESTKUDOS:5",
+ amount: "TESTKUDOS:10",
fulfillment_url: "taler://fulfillment-success/thx",
};
diff --git a/packages/taler-harness/src/integrationtests/testrunner.ts b/packages/taler-harness/src/integrationtests/testrunner.ts
index 6ab87c756..1b4bdc218 100644
--- a/packages/taler-harness/src/integrationtests/testrunner.ts
+++ b/packages/taler-harness/src/integrationtests/testrunner.ts
@@ -93,12 +93,13 @@ import { runWithdrawalHugeTest } from "./test-withdrawal-huge.js";
import { runWithdrawalManualTest } from "./test-withdrawal-manual.js";
import { runWalletGenDbTest } from "./test-wallet-gendb.js";
import { runLibeufinBankTest } from "./test-libeufin-bank.js";
-import { runMultiExchangeTest } from "./test-multiexchange.js";
+import { runCurrencyScopeTest } from "./test-currency-scope.js";
import { runAgeRestrictionsDepositTest } from "./test-age-restrictions-deposit.js";
import { runWithdrawalConversionTest } from "./test-withdrawal-conversion.js";
import { runPaymentDeletedTest } from "./test-payment-deleted.js";
import { runWithdrawalNotifyBeforeTxTest } from "./test-withdrawal-notify-before-tx.js";
import { runWalletDd48Test } from "./test-wallet-dd48.js";
+import { runMultiExchangeTest } from "./test-multiexchange.js";
/**
* Test runner.
@@ -187,6 +188,7 @@ const allTests: TestMainFunction[] = [
runLibeufinBankTest,
runPaymentDeletedTest,
runWalletDd48Test,
+ runCurrencyScopeTest,
];
export interface TestRunSpec {
diff --git a/packages/taler-util/src/wallet-types.ts b/packages/taler-util/src/wallet-types.ts
index c20290287..12231fb2d 100644
--- a/packages/taler-util/src/wallet-types.ts
+++ b/packages/taler-util/src/wallet-types.ts
@@ -1571,6 +1571,8 @@ export interface ExchangeWithdrawalDetails {
*
*/
ageRestrictionOptions?: number[];
+
+ scopeInfo: ScopeInfo;
}
export interface GetExchangeTosResult {
diff --git a/packages/taler-wallet-core/src/db.ts b/packages/taler-wallet-core/src/db.ts
index ceca24c82..149d73abc 100644
--- a/packages/taler-wallet-core/src/db.ts
+++ b/packages/taler-wallet-core/src/db.ts
@@ -30,6 +30,8 @@ import {
import {
AbsoluteTime,
AgeCommitmentProof,
+ AmountJson,
+ AmountLike,
AmountString,
Amounts,
AttentionInfo,
@@ -1003,6 +1005,13 @@ export interface RefreshReasonDetails {
proposalId?: string;
}
+export interface RefreshGroupPerExchangeInfo {
+ /**
+ * (Expected) output once the refresh group succeeded.
+ */
+ outputEffective: AmountString;
+}
+
/**
* Group of refresh operations. The refreshed coins do not
* have to belong to the same exchange, but must have the same
@@ -1038,6 +1047,8 @@ export interface RefreshGroupRecord {
expectedOutputPerCoin: AmountString[];
+ infoPerExchange?: Record<string, RefreshGroupPerExchangeInfo>;
+
/**
* Flag for each coin whether refreshing finished.
* If a coin can't be refreshed (remaining value too small),
@@ -1717,6 +1728,14 @@ export interface DepositTrackingInfo {
exchangePub: string;
}
+export interface DepositInfoPerExchange {
+ /**
+ * Expected effective amount that will be deposited
+ * from coins of this exchange.
+ */
+ amountEffective: AmountJson;
+}
+
/**
* Group of deposits made by the wallet.
*/
@@ -1768,6 +1787,8 @@ export interface DepositGroupRecord {
statusPerCoin: DepositElementStatus[];
+ infoPerExchange?: Record<string, DepositInfoPerExchange>;
+
/**
* When the deposit transaction was aborted and
* refreshes were tried, we create a refresh
diff --git a/packages/taler-wallet-core/src/operations/balance.ts b/packages/taler-wallet-core/src/operations/balance.ts
index 53ca33fe7..a73476e9c 100644
--- a/packages/taler-wallet-core/src/operations/balance.ts
+++ b/packages/taler-wallet-core/src/operations/balance.ts
@@ -54,6 +54,7 @@ import {
AllowedAuditorInfo,
AllowedExchangeInfo,
AmountJson,
+ AmountLike,
Amounts,
BalanceFlag,
BalancesResponse,
@@ -61,6 +62,7 @@ import {
GetBalanceDetailRequest,
Logger,
parsePaytoUri,
+ ScopeInfo,
ScopeType,
} from "@gnu-taler/taler-util";
import {
@@ -68,6 +70,8 @@ import {
OPERATION_STATUS_ACTIVE_FIRST,
OPERATION_STATUS_ACTIVE_LAST,
RefreshGroupRecord,
+ RefreshOperationStatus,
+ WalletDbReadOnlyTransactionArr,
WalletStoresV1,
WithdrawalGroupStatus,
} from "../db.js";
@@ -75,7 +79,10 @@ import { InternalWalletState } from "../internal-wallet-state.js";
import { assertUnreachable } from "../util/assertUnreachable.js";
import { checkLogicInvariant } from "../util/invariants.js";
import { GetReadOnlyAccess } from "../util/query.js";
-import { getExchangeWireDetailsInTx } from "./exchanges.js";
+import {
+ getExchangeScopeInfo,
+ getExchangeWireDetailsInTx,
+} from "./exchanges.js";
/**
* Logger.
@@ -83,6 +90,7 @@ import { getExchangeWireDetailsInTx } from "./exchanges.js";
const logger = new Logger("operations/balance.ts");
interface WalletBalance {
+ scopeInfo: ScopeInfo;
available: AmountJson;
pendingIncoming: AmountJson;
pendingOutgoing: AmountJson;
@@ -109,67 +117,216 @@ function computeRefreshGroupAvailableAmount(r: RefreshGroupRecord): AmountJson {
return available;
}
-/**
- * Get balance information.
- */
-export async function getBalancesInsideTransaction(
- ws: InternalWalletState,
- tx: GetReadOnlyAccess<{
- coinAvailability: typeof WalletStoresV1.coinAvailability;
- refreshGroups: typeof WalletStoresV1.refreshGroups;
- withdrawalGroups: typeof WalletStoresV1.withdrawalGroups;
- depositGroups: typeof WalletStoresV1.depositGroups;
- }>,
-): Promise<BalancesResponse> {
- const balanceStore: Record<string, WalletBalance> = {};
+function getBalanceKey(scopeInfo: ScopeInfo): string {
+ switch (scopeInfo.type) {
+ case ScopeType.Auditor:
+ return `${scopeInfo.type};${scopeInfo.currency};${scopeInfo.url}`;
+ case ScopeType.Exchange:
+ return `${scopeInfo.type};${scopeInfo.currency};${scopeInfo.url}`;
+ case ScopeType.Global:
+ return `${scopeInfo.type};${scopeInfo.currency}`;
+ }
+}
+
+class BalancesStore {
+ private exchangeScopeCache: Record<string, ScopeInfo> = {};
+ private balanceStore: Record<string, WalletBalance> = {};
+
+ constructor(
+ private ws: InternalWalletState,
+ private tx: WalletDbReadOnlyTransactionArr<
+ [
+ "globalCurrencyAuditors",
+ "globalCurrencyExchanges",
+ "exchanges",
+ "exchangeDetails",
+ ]
+ >,
+ ) {}
/**
* Add amount to a balance field, both for
* the slicing by exchange and currency.
*/
- const initBalance = (currency: string): WalletBalance => {
- const b = balanceStore[currency];
+ private async initBalance(
+ currency: string,
+ exchangeBaseUrl: string,
+ ): Promise<WalletBalance> {
+ let scopeInfo: ScopeInfo | undefined =
+ this.exchangeScopeCache[exchangeBaseUrl];
+ if (!scopeInfo) {
+ scopeInfo = await getExchangeScopeInfo(
+ this.tx,
+ exchangeBaseUrl,
+ currency,
+ );
+ this.exchangeScopeCache[exchangeBaseUrl] = scopeInfo;
+ }
+ const balanceKey = getBalanceKey(scopeInfo);
+ const b = this.balanceStore[balanceKey];
if (!b) {
- balanceStore[currency] = {
- available: Amounts.zeroOfCurrency(currency),
- pendingIncoming: Amounts.zeroOfCurrency(currency),
- pendingOutgoing: Amounts.zeroOfCurrency(currency),
+ const zero = Amounts.zeroOfCurrency(currency);
+ this.balanceStore[balanceKey] = {
+ scopeInfo,
+ available: zero,
+ pendingIncoming: zero,
+ pendingOutgoing: zero,
flagIncomingAml: false,
flagIncomingConfirmation: false,
flagIncomingKyc: false,
flagOutgoingKyc: false,
};
}
- return balanceStore[currency];
- };
+ return this.balanceStore[balanceKey];
+ }
+
+ async addAvailable(
+ currency: string,
+ exchangeBaseUrl: string,
+ amount: AmountLike,
+ ): Promise<void> {
+ const b = await this.initBalance(currency, exchangeBaseUrl);
+ b.available = Amounts.add(b.available, amount).amount;
+ }
+
+ async addPendingIncoming(
+ currency: string,
+ exchangeBaseUrl: string,
+ amount: AmountLike,
+ ): Promise<void> {
+ const b = await this.initBalance(currency, exchangeBaseUrl);
+ b.pendingIncoming = Amounts.add(b.available, amount).amount;
+ }
+
+ async setFlagIncomingAml(
+ currency: string,
+ exchangeBaseUrl: string,
+ ): Promise<void> {
+ const b = await this.initBalance(currency, exchangeBaseUrl);
+ b.flagIncomingAml = true;
+ }
+
+ async setFlagIncomingKyc(
+ currency: string,
+ exchangeBaseUrl: string,
+ ): Promise<void> {
+ const b = await this.initBalance(currency, exchangeBaseUrl);
+ b.flagIncomingKyc = true;
+ }
+
+ async setFlagIncomingConfirmation(
+ currency: string,
+ exchangeBaseUrl: string,
+ ): Promise<void> {
+ const b = await this.initBalance(currency, exchangeBaseUrl);
+ b.flagIncomingConfirmation = true;
+ }
+
+ async setFlagOutgoingKyc(
+ currency: string,
+ exchangeBaseUrl: string,
+ ): Promise<void> {
+ const b = await this.initBalance(currency, exchangeBaseUrl);
+ b.flagOutgoingKyc = true;
+ }
+
+ toBalancesResponse(): BalancesResponse {
+ const balancesResponse: BalancesResponse = {
+ balances: [],
+ };
+
+ const balanceStore = this.balanceStore;
+
+ Object.keys(balanceStore)
+ .sort()
+ .forEach((c) => {
+ const v = balanceStore[c];
+ const flags: BalanceFlag[] = [];
+ if (v.flagIncomingAml) {
+ flags.push(BalanceFlag.IncomingAml);
+ }
+ if (v.flagIncomingKyc) {
+ flags.push(BalanceFlag.IncomingKyc);
+ }
+ if (v.flagIncomingConfirmation) {
+ flags.push(BalanceFlag.IncomingConfirmation);
+ }
+ if (v.flagOutgoingKyc) {
+ flags.push(BalanceFlag.OutgoingKyc);
+ }
+ balancesResponse.balances.push({
+ scopeInfo: v.scopeInfo,
+ available: Amounts.stringify(v.available),
+ pendingIncoming: Amounts.stringify(v.pendingIncoming),
+ pendingOutgoing: Amounts.stringify(v.pendingOutgoing),
+ // FIXME: This field is basically not implemented, do we even need it?
+ hasPendingTransactions: false,
+ // FIXME: This field is basically not implemented, do we even need it?
+ requiresUserInput: false,
+ flags,
+ });
+ });
+ return balancesResponse;
+ }
+}
+
+/**
+ * Get balance information.
+ */
+export async function getBalancesInsideTransaction(
+ ws: InternalWalletState,
+ tx: WalletDbReadOnlyTransactionArr<
+ [
+ "exchanges",
+ "exchangeDetails",
+ "coinAvailability",
+ "refreshGroups",
+ "depositGroups",
+ "withdrawalGroups",
+ "globalCurrencyAuditors",
+ "globalCurrencyExchanges",
+ ]
+ >,
+): Promise<BalancesResponse> {
+ const balanceStore: BalancesStore = new BalancesStore(ws, tx);
const keyRangeActive = GlobalIDB.KeyRange.bound(
OPERATION_STATUS_ACTIVE_FIRST,
OPERATION_STATUS_ACTIVE_LAST,
);
- await tx.coinAvailability.iter().forEach((ca) => {
- const b = initBalance(ca.currency);
+ await tx.coinAvailability.iter().forEachAsync(async (ca) => {
const count = ca.visibleCoinCount ?? 0;
for (let i = 0; i < count; i++) {
- b.available = Amounts.add(b.available, ca.value).amount;
+ await balanceStore.addAvailable(
+ ca.currency,
+ ca.exchangeBaseUrl,
+ ca.value,
+ );
}
});
- await tx.refreshGroups.iter().forEach((r) => {
- const b = initBalance(r.currency);
- b.available = Amounts.add(
- b.available,
- computeRefreshGroupAvailableAmount(r),
- ).amount;
+ await tx.refreshGroups.iter().forEachAsync(async (r) => {
+ switch (r.operationStatus) {
+ case RefreshOperationStatus.Pending:
+ case RefreshOperationStatus.Suspended:
+ break;
+ default:
+ return;
+ }
+ const perExchange = r.infoPerExchange;
+ if (!perExchange) {
+ return;
+ }
+ for (const [e, x] of Object.entries(perExchange)) {
+ await balanceStore.addAvailable(r.currency, e, x.outputEffective);
+ }
});
await tx.withdrawalGroups.indexes.byStatus
.iter(keyRangeActive)
- .forEach((wgRecord) => {
- const b = initBalance(
- Amounts.currencyOf(wgRecord.denomsSel.totalWithdrawCost),
- );
+ .forEachAsync(async (wgRecord) => {
+ const currency = Amounts.currencyOf(wgRecord.denomsSel.totalCoinValue);
switch (wgRecord.status) {
case WithdrawalGroupStatus.AbortedBank:
case WithdrawalGroupStatus.AbortedExchange:
@@ -190,76 +347,53 @@ export async function getBalancesInsideTransaction(
break;
case WithdrawalGroupStatus.SuspendedKyc:
case WithdrawalGroupStatus.PendingKyc:
- b.flagIncomingKyc = true;
+ await balanceStore.setFlagIncomingKyc(
+ currency,
+ wgRecord.exchangeBaseUrl,
+ );
break;
case WithdrawalGroupStatus.PendingAml:
case WithdrawalGroupStatus.SuspendedAml:
- b.flagIncomingAml = true;
+ await balanceStore.setFlagIncomingAml(
+ currency,
+ wgRecord.exchangeBaseUrl,
+ );
break;
case WithdrawalGroupStatus.PendingRegisteringBank:
case WithdrawalGroupStatus.PendingWaitConfirmBank:
- b.flagIncomingConfirmation = true;
+ await balanceStore.setFlagIncomingConfirmation(
+ currency,
+ wgRecord.exchangeBaseUrl,
+ );
break;
default:
assertUnreachable(wgRecord.status);
}
- b.pendingIncoming = Amounts.add(
- b.pendingIncoming,
+ await balanceStore.addPendingIncoming(
+ currency,
+ wgRecord.exchangeBaseUrl,
wgRecord.denomsSel.totalCoinValue,
- ).amount;
+ );
});
- // FIXME: Use indexing to filter out final transactions.
await tx.depositGroups.indexes.byStatus
.iter(keyRangeActive)
- .forEach((dgRecord) => {
- const b = initBalance(Amounts.currencyOf(dgRecord.amount));
- switch (dgRecord.operationStatus) {
- case DepositOperationStatus.SuspendedKyc:
- case DepositOperationStatus.PendingKyc:
- b.flagOutgoingKyc = true;
+ .forEachAsync(async (dgRecord) => {
+ const perExchange = dgRecord.infoPerExchange;
+ if (!perExchange) {
+ return;
}
- });
-
- const balancesResponse: BalancesResponse = {
- balances: [],
- };
-
- Object.keys(balanceStore)
- .sort()
- .forEach((c) => {
- const v = balanceStore[c];
- const flags: BalanceFlag[] = [];
- if (v.flagIncomingAml) {
- flags.push(BalanceFlag.IncomingAml);
- }
- if (v.flagIncomingKyc) {
- flags.push(BalanceFlag.IncomingKyc);
- }
- if (v.flagIncomingConfirmation) {
- flags.push(BalanceFlag.IncomingConfirmation);
- }
- if (v.flagOutgoingKyc) {
- flags.push(BalanceFlag.OutgoingKyc);
+ for (const [e, x] of Object.entries(perExchange)) {
+ const currency = Amounts.currencyOf(dgRecord.amount);
+ switch (dgRecord.operationStatus) {
+ case DepositOperationStatus.SuspendedKyc:
+ case DepositOperationStatus.PendingKyc:
+ await balanceStore.setFlagOutgoingKyc(currency, e);
+ }
}
- balancesResponse.balances.push({
- scopeInfo: {
- // FIXME: obtain REAL scopeInfo instead of faking a global currency
- type: ScopeType.Global,
- currency: Amounts.currencyOf(v.available),
- },
- available: Amounts.stringify(v.available),
- pendingIncoming: Amounts.stringify(v.pendingIncoming),
- pendingOutgoing: Amounts.stringify(v.pendingOutgoing),
- // FIXME: This field is basically not implemented, do we even need it?
- hasPendingTransactions: false,
- // FIXME: This field is basically not implemented, do we even need it?
- requiresUserInput: false,
- flags,
- });
});
- return balancesResponse;
+ return balanceStore.toBalancesResponse();
}
/**
@@ -270,18 +404,23 @@ export async function getBalances(
): Promise<BalancesResponse> {
logger.trace("starting to compute balance");
- const wbal = await ws.db
- .mktx((x) => [
- x.coins,
- x.coinAvailability,
- x.refreshGroups,
- x.purchases,
- x.withdrawalGroups,
- x.depositGroups,
- ])
- .runReadOnly(async (tx) => {
+ const wbal = await ws.db.runReadWriteTx(
+ [
+ "coinAvailability",
+ "coins",
+ "depositGroups",
+ "exchangeDetails",
+ "exchanges",
+ "globalCurrencyAuditors",
+ "globalCurrencyExchanges",
+ "purchases",
+ "refreshGroups",
+ "withdrawalGroups",
+ ],
+ async (tx) => {
return getBalancesInsideTransaction(ws, tx);
- });
+ },
+ );
logger.trace("finished computing wallet balance");
diff --git a/packages/taler-wallet-core/src/operations/common.ts b/packages/taler-wallet-core/src/operations/common.ts
index f34190cef..d626f0056 100644
--- a/packages/taler-wallet-core/src/operations/common.ts
+++ b/packages/taler-wallet-core/src/operations/common.ts
@@ -19,7 +19,6 @@
*/
import {
AbsoluteTime,
- AgeRestriction,
AmountJson,
Amounts,
CancellationToken,
@@ -28,7 +27,6 @@ import {
Duration,
ExchangeEntryState,
ExchangeEntryStatus,
- ExchangeListItem,
ExchangeTosStatus,
ExchangeUpdateStatus,
getErrorDetailFromException,
@@ -36,10 +34,7 @@ import {
Logger,
makeErrorDetail,
NotificationType,
- OperationErrorInfo,
RefreshReason,
- ScopeInfo,
- ScopeType,
TalerError,
TalerErrorCode,
TalerErrorDetail,
@@ -55,7 +50,6 @@ import {
CoinRecord,
DbPreciseTimestamp,
DepositGroupRecord,
- ExchangeDetailsRecord,
ExchangeEntryDbRecordStatus,
ExchangeEntryDbUpdateStatus,
ExchangeEntryRecord,
@@ -653,51 +647,6 @@ export function getExchangeState(r: ExchangeEntryRecord): ExchangeEntryState {
};
}
-/**
- * Mock scope info for an exchange by always returning a regional currency scope.
- */
-function mockExchangeScopeInfo(
- r: ExchangeEntryRecord,
- exchangeDetails: ExchangeDetailsRecord | undefined,
-): ScopeInfo | undefined {
- const currency = r.presetCurrencyHint ?? exchangeDetails?.currency;
- if (currency) {
- return {
- currency,
- type: ScopeType.Exchange,
- url: r.baseUrl,
- };
- }
- return undefined;
-}
-
-export function makeExchangeListItem(
- r: ExchangeEntryRecord,
- exchangeDetails: ExchangeDetailsRecord | undefined,
- lastError: TalerErrorDetail | undefined,
-): ExchangeListItem {
- const lastUpdateErrorInfo: OperationErrorInfo | undefined = lastError
- ? {
- error: lastError,
- }
- : undefined;
-
- return {
- exchangeBaseUrl: r.baseUrl,
- currency: exchangeDetails?.currency ?? r.presetCurrencyHint,
- exchangeUpdateStatus: getExchangeUpdateStatusFromRecord(r),
- exchangeEntryStatus: getExchangeEntryStatusFromRecord(r),
- tosStatus: getExchangeTosStatusFromRecord(r),
- ageRestrictionOptions: exchangeDetails?.ageMask
- ? AgeRestriction.getAgeGroupsFromMask(exchangeDetails.ageMask)
- : [],
- paytoUris: exchangeDetails?.wireInfo.accounts.map((x) => x.payto_uri) ?? [],
- lastUpdateErrorInfo,
- // FIXME: Return real scope info in the future!
- scopeInfo: mockExchangeScopeInfo(r, exchangeDetails),
- };
-}
-
export interface LongpollResult {
ready: boolean;
}
diff --git a/packages/taler-wallet-core/src/operations/exchanges.ts b/packages/taler-wallet-core/src/operations/exchanges.ts
index 67404665c..b4d45db2c 100644
--- a/packages/taler-wallet-core/src/operations/exchanges.ts
+++ b/packages/taler-wallet-core/src/operations/exchanges.ts
@@ -25,6 +25,7 @@
*/
import {
AbsoluteTime,
+ AgeRestriction,
Amounts,
CancellationToken,
DeleteExchangeRequest,
@@ -50,7 +51,10 @@ import {
LibtoolVersion,
Logger,
NotificationType,
+ OperationErrorInfo,
Recoup,
+ ScopeInfo,
+ ScopeType,
TalerError,
TalerErrorCode,
TalerErrorDetail,
@@ -88,7 +92,7 @@ import {
ExchangeEntryDbRecordStatus,
ExchangeEntryDbUpdateStatus,
PendingTaskType,
- WalletDbReadWriteTransaction,
+ WalletDbReadOnlyTransactionArr,
WalletDbReadWriteTransactionArr,
createTimeline,
isWithdrawableDenom,
@@ -103,7 +107,6 @@ import {
import { InternalWalletState } from "../internal-wallet-state.js";
import { checkDbInvariant } from "../util/invariants.js";
import {
- DbReadOnlyTransaction,
DbReadOnlyTransactionArr,
GetReadOnlyAccess,
GetReadWriteAccess,
@@ -114,9 +117,10 @@ import {
TaskRunResult,
TaskRunResultType,
constructTaskIdentifier,
+ getExchangeEntryStatusFromRecord,
getExchangeState,
getExchangeTosStatusFromRecord,
- makeExchangeListItem,
+ getExchangeUpdateStatusFromRecord,
runTaskWithErrorReporting,
} from "./common.js";
@@ -206,6 +210,105 @@ async function getExchangeRecordsInternal(
]);
}
+export async function getExchangeScopeInfo(
+ tx: WalletDbReadOnlyTransactionArr<
+ [
+ "exchanges",
+ "exchangeDetails",
+ "globalCurrencyExchanges",
+ "globalCurrencyAuditors",
+ ]
+ >,
+ exchangeBaseUrl: string,
+ currency: string,
+): Promise<ScopeInfo> {
+ const det = await getExchangeRecordsInternal(tx, exchangeBaseUrl);
+ if (!det) {
+ return {
+ type: ScopeType.Exchange,
+ currency: currency,
+ url: exchangeBaseUrl,
+ };
+ }
+ return internalGetExchangeScopeInfo(tx, det);
+}
+
+async function internalGetExchangeScopeInfo(
+ tx: WalletDbReadOnlyTransactionArr<
+ ["globalCurrencyExchanges", "globalCurrencyAuditors"]
+ >,
+ exchangeDetails: ExchangeDetailsRecord,
+): Promise<ScopeInfo> {
+ const globalExchangeRec =
+ await tx.globalCurrencyExchanges.indexes.byCurrencyAndUrlAndPub.get([
+ exchangeDetails.currency,
+ exchangeDetails.exchangeBaseUrl,
+ exchangeDetails.masterPublicKey,
+ ]);
+ if (globalExchangeRec) {
+ return {
+ currency: exchangeDetails.currency,
+ type: ScopeType.Global,
+ };
+ } else {
+ for (const aud of exchangeDetails.auditors) {
+ const globalAuditorRec =
+ await tx.globalCurrencyAuditors.indexes.byCurrencyAndUrlAndPub.get([
+ exchangeDetails.currency,
+ aud.auditor_url,
+ aud.auditor_pub,
+ ]);
+ if (globalAuditorRec) {
+ return {
+ currency: exchangeDetails.currency,
+ type: ScopeType.Auditor,
+ url: aud.auditor_url,
+ };
+ }
+ }
+ }
+ return {
+ currency: exchangeDetails.currency,
+ type: ScopeType.Exchange,
+ url: exchangeDetails.exchangeBaseUrl,
+ };
+}
+
+async function makeExchangeListItem(
+ tx: WalletDbReadOnlyTransactionArr<
+ ["globalCurrencyExchanges", "globalCurrencyAuditors"]
+ >,
+ r: ExchangeEntryRecord,
+ exchangeDetails: ExchangeDetailsRecord | undefined,
+ lastError: TalerErrorDetail | undefined,
+): Promise<ExchangeListItem> {
+ const lastUpdateErrorInfo: OperationErrorInfo | undefined = lastError
+ ? {
+ error: lastError,
+ }
+ : undefined;
+
+ let scopeInfo: ScopeInfo | undefined = undefined;
+
+ if (exchangeDetails) {
+ scopeInfo = await internalGetExchangeScopeInfo(tx, exchangeDetails);
+ }
+
+ return {
+ exchangeBaseUrl: r.baseUrl,
+ currency: exchangeDetails?.currency ?? r.presetCurrencyHint,
+ exchangeUpdateStatus: getExchangeUpdateStatusFromRecord(r),
+ exchangeEntryStatus: getExchangeEntryStatusFromRecord(r),
+ tosStatus: getExchangeTosStatusFromRecord(r),
+ ageRestrictionOptions: exchangeDetails?.ageMask
+ ? AgeRestriction.getAgeGroupsFromMask(exchangeDetails.ageMask)
+ : [],
+ paytoUris: exchangeDetails?.wireInfo.accounts.map((x) => x.payto_uri) ?? [],
+ lastUpdateErrorInfo,
+ scopeInfo,
+ };
+}
+
export interface ExchangeWireDetails {
currency: string;
masterPublicKey: EddsaPublicKeyString;
@@ -240,14 +343,15 @@ export async function lookupExchangeByUri(
ws: InternalWalletState,
req: GetExchangeEntryByUrlRequest,
): Promise<ExchangeListItem> {
- return await ws.db
- .mktx((x) => [
- x.exchanges,
- x.exchangeDetails,
- x.denominations,
- x.operationRetries,
- ])
- .runReadOnly(async (tx) => {
+ return await ws.db.runReadOnlyTx(
+ [
+ "exchanges",
+ "exchangeDetails",
+ "operationRetries",
+ "globalCurrencyAuditors",
+ "globalCurrencyExchanges",
+ ],
+ async (tx) => {
const exchangeRec = await tx.exchanges.get(req.exchangeBaseUrl);
if (!exchangeRec) {
throw Error("exchange not found");
@@ -259,12 +363,14 @@ export async function lookupExchangeByUri(
const opRetryRecord = await tx.operationRetries.get(
TaskIdentifiers.forExchangeUpdate(exchangeRec),
);
- return makeExchangeListItem(
+ return await makeExchangeListItem(
+ tx,
exchangeRec,
exchangeDetails,
opRetryRecord?.lastError,
);
- });
+ },
+ );
}
/**
@@ -800,6 +906,7 @@ export interface ReadyExchangeSummary {
wireInfo: WireInfo;
protocolVersionRange: string;
tosAcceptedTimestamp: TalerPreciseTimestamp | undefined;
+ scopeInfo: ScopeInfo;
}
/**
@@ -863,14 +970,26 @@ export async function fetchFreshExchange(
);
}
- const { exchange, exchangeDetails, retryInfo } = await ws.db
- .mktx((x) => [x.exchanges, x.exchangeDetails, x.operationRetries])
- .runReadOnly(async (tx) => {
- const exchange = await tx.exchanges.get(canonUrl);
- const exchangeDetails = await getExchangeRecordsInternal(tx, canonUrl);
- const retryInfo = await tx.operationRetries.get(operationId);
- return { exchange, exchangeDetails, retryInfo };
- });
+ const { exchange, exchangeDetails, retryInfo, scopeInfo } =
+ await ws.db.runReadOnlyTx(
+ [
+ "exchanges",
+ "exchangeDetails",
+ "operationRetries",
+ "globalCurrencyAuditors",
+ "globalCurrencyExchanges",
+ ],
+ async (tx) => {
+ const exchange = await tx.exchanges.get(canonUrl);
+ const exchangeDetails = await getExchangeRecordsInternal(tx, canonUrl);
+ const retryInfo = await tx.operationRetries.get(operationId);
+ let scopeInfo: ScopeInfo | undefined = undefined;
+ if (exchange && exchangeDetails) {
+ scopeInfo = await internalGetExchangeScopeInfo(tx, exchangeDetails);
+ }
+ return { exchange, exchangeDetails, retryInfo, scopeInfo };
+ },
+ );
if (!exchange) {
throw Error("exchange entry does not exist anymore");
@@ -891,6 +1010,10 @@ export async function fetchFreshExchange(
throw Error("invariant failed");
}
+ if (!scopeInfo) {
+ throw Error("invariant failed");
+ }
+
const res: ReadyExchangeSummary = {
currency: exchangeDetails.currency,
exchangeBaseUrl: canonUrl,
@@ -903,6 +1026,7 @@ export async function fetchFreshExchange(
tosAcceptedTimestamp: timestampOptionalPreciseFromDb(
exchange.tosAcceptedTimestamp,
),
+ scopeInfo,
};
if (options.expectedMasterPub) {
@@ -1309,9 +1433,15 @@ export async function listExchanges(
ws: InternalWalletState,
): Promise<ExchangesListResponse> {
const exchanges: ExchangeListItem[] = [];
- await ws.db
- .mktx((x) => [x.exchanges, x.exchangeDetails, x.operationRetries])
- .runReadOnly(async (tx) => {
+ await ws.db.runReadOnlyTx(
+ [
+ "exchanges",
+ "operationRetries",
+ "exchangeDetails",
+ "globalCurrencyAuditors",
+ "globalCurrencyExchanges",
+ ],
+ async (tx) => {
const exchangeRecords = await tx.exchanges.iter().toArray();
for (const r of exchangeRecords) {
const taskId = constructTaskIdentifier({
@@ -1321,10 +1451,16 @@ export async function listExchanges(
const exchangeDetails = await getExchangeRecordsInternal(tx, r.baseUrl);
const opRetryRecord = await tx.operationRetries.get(taskId);
exchanges.push(
- makeExchangeListItem(r, exchangeDetails, opRetryRecord?.lastError),
+ await makeExchangeListItem(
+ tx,
+ r,
+ exchangeDetails,
+ opRetryRecord?.lastError,
+ ),
);
}
- });
+ },
+ );
return { exchanges };
}
diff --git a/packages/taler-wallet-core/src/operations/refresh.ts b/packages/taler-wallet-core/src/operations/refresh.ts
index d49c9a1cf..974eb1619 100644
--- a/packages/taler-wallet-core/src/operations/refresh.ts
+++ b/packages/taler-wallet-core/src/operations/refresh.ts
@@ -80,6 +80,7 @@ import {
} from "../db.js";
import {
getCandidateWithdrawalDenomsTx,
+ RefreshGroupPerExchangeInfo,
RefreshSessionRecord,
timestampPreciseToDb,
timestampProtocolFromDb,
@@ -1107,6 +1108,7 @@ async function processRefreshSession(
export interface RefreshOutputInfo {
outputPerCoin: AmountJson[];
+ perExchangeInfo: Record<string, RefreshGroupPerExchangeInfo>;
}
export async function calculateRefreshOutput(
@@ -1124,6 +1126,8 @@ export async function calculateRefreshOutput(
const denomsPerExchange: Record<string, DenominationRecord[]> = {};
+ const infoPerExchange: Record<string, RefreshGroupPerExchangeInfo> = {};
+
// FIXME: Use denom groups instead of querying all denominations!
const getDenoms = async (
exchangeBaseUrl: string,
@@ -1163,11 +1167,21 @@ export async function calculateRefreshOutput(
ws.config.testing.denomselAllowLate,
);
const output = Amounts.sub(refreshAmount, cost).amount;
+ let exchInfo = infoPerExchange[coin.exchangeBaseUrl];
+ if (!exchInfo) {
+ infoPerExchange[coin.exchangeBaseUrl] = exchInfo = {
+ outputEffective: Amounts.stringify(Amounts.zeroOfAmount(cost)),
+ };
+ }
+ exchInfo.outputEffective = Amounts.stringify(
+ Amounts.add(exchInfo.outputEffective, output).amount,
+ );
estimatedOutputPerCoin.push(output);
}
return {
outputPerCoin: estimatedOutputPerCoin,
+ perExchangeInfo: infoPerExchange,
};
}
@@ -1234,6 +1248,10 @@ async function applyRefresh(
}
}
+export interface CreateRefreshGroupResult {
+ refreshGroupId: string;
+}
+
/**
* Create a refresh group for a list of coins.
*
@@ -1252,7 +1270,7 @@ export async function createRefreshGroup(
oldCoinPubs: CoinRefreshRequest[],
reason: RefreshReason,
reasonDetails?: RefreshReasonDetails,
-): Promise<RefreshGroupId> {
+): Promise<CreateRefreshGroupResult> {
const refreshGroupId = encodeCrock(getRandomBytes(32));
const outInfo = await calculateRefreshOutput(ws, tx, currency, oldCoinPubs);
diff --git a/packages/taler-wallet-core/src/operations/withdraw.ts b/packages/taler-wallet-core/src/operations/withdraw.ts
index 29c2cae40..d198cf482 100644
--- a/packages/taler-wallet-core/src/operations/withdraw.ts
+++ b/packages/taler-wallet-core/src/operations/withdraw.ts
@@ -557,9 +557,12 @@ export async function getBankWithdrawalInfo(
throw Error(`can't parse URL ${talerWithdrawUri}`);
}
- const bankApi = new TalerBankIntegrationHttpClient(uriResult.bankIntegrationApiBaseUrl, http);
+ const bankApi = new TalerBankIntegrationHttpClient(
+ uriResult.bankIntegrationApiBaseUrl,
+ http,
+ );
- const { body: config } = await bankApi.getConfig()
+ const { body: config } = await bankApi.getConfig();
if (!bankApi.isCompatible(config.version)) {
throw TalerError.fromDetail(
@@ -572,12 +575,14 @@ export async function getBankWithdrawalInfo(
);
}
- const resp = await bankApi.getWithdrawalOperationById(uriResult.withdrawalOperationId)
+ const resp = await bankApi.getWithdrawalOperationById(
+ uriResult.withdrawalOperationId,
+ );
if (resp.type === "fail") {
throw TalerError.fromUncheckedDetail(resp.detail);
}
- const { body: status } = resp
+ const { body: status } = resp;
logger.info(`bank withdrawal operation status: ${j2s(status)}`);
@@ -1214,7 +1219,8 @@ export async function updateWithdrawalDenoms(
denom.verificationStatus === DenominationVerificationStatus.Unverified
) {
logger.trace(
- `Validating denomination (${current + 1}/${denominations.length
+ `Validating denomination (${current + 1}/${
+ denominations.length
}) signature of ${denom.denomPubHash}`,
);
let valid = false;
@@ -1859,7 +1865,7 @@ export async function getExchangeWithdrawalInfo(
) {
logger.warn(
`wallet's support for exchange protocol version ${WALLET_EXCHANGE_PROTOCOL_VERSION} might be outdated ` +
- `(exchange has ${exchange.protocolVersionRange}), checking for updates`,
+ `(exchange has ${exchange.protocolVersionRange}), checking for updates`,
);
}
}
@@ -1896,6 +1902,7 @@ export async function getExchangeWithdrawalInfo(
ageRestrictionOptions: hasDenomWithAgeRestriction
? AGE_MASK_GROUPS
: undefined,
+ scopeInfo: exchange.scopeInfo,
};
return ret;
}
@@ -1907,9 +1914,8 @@ export interface GetWithdrawalDetailsForUriOpts {
type WithdrawalOperationMemoryMap = {
[uri: string]: boolean | undefined;
-}
-const ongoingChecks: WithdrawalOperationMemoryMap = {
-}
+};
+const ongoingChecks: WithdrawalOperationMemoryMap = {};
/**
* Get more information about a taler://withdraw URI.
*
@@ -1950,28 +1956,41 @@ export async function getWithdrawalDetailsForUri(
);
});
- // FIXME: this should be removed after the extended version of
+ // FIXME: this should be removed after the extended version of
// withdrawal state machine. issue #8099
- if (info.status === "pending" && opts.notifyChangeFromPendingTimeoutMs !== undefined && !ongoingChecks[talerWithdrawUri]) {
+ if (
+ info.status === "pending" &&
+ opts.notifyChangeFromPendingTimeoutMs !== undefined &&
+ !ongoingChecks[talerWithdrawUri]
+ ) {
ongoingChecks[talerWithdrawUri] = true;
- const bankApi = new TalerBankIntegrationHttpClient(info.apiBaseUrl, ws.http);
+ const bankApi = new TalerBankIntegrationHttpClient(
+ info.apiBaseUrl,
+ ws.http,
+ );
console.log(
`waiting operation (${info.operationId}) to change from pending`,
);
- bankApi.getWithdrawalOperationById(info.operationId, {
- old_state: "pending",
- timeoutMs: opts.notifyChangeFromPendingTimeoutMs
- }).then(resp => {
- console.log(
- `operation (${info.operationId}) to change to ${JSON.stringify(resp, undefined, 2)}`,
- );
- ws.notify({
- type: NotificationType.WithdrawalOperationTransition,
- operationId: info.operationId,
- state: resp.type === "fail" ? info.status : resp.body.status,
+ bankApi
+ .getWithdrawalOperationById(info.operationId, {
+ old_state: "pending",
+ timeoutMs: opts.notifyChangeFromPendingTimeoutMs,
+ })
+ .then((resp) => {
+ console.log(
+ `operation (${info.operationId}) to change to ${JSON.stringify(
+ resp,
+ undefined,
+ 2,
+ )}`,
+ );
+ ws.notify({
+ type: NotificationType.WithdrawalOperationTransition,
+ operationId: info.operationId,
+ state: resp.type === "fail" ? info.status : resp.body.status,
+ });
+ ongoingChecks[talerWithdrawUri] = false;
});
- ongoingChecks[talerWithdrawUri] = false
- })
}
return {
diff --git a/packages/taler-wallet-core/src/util/coinSelection.ts b/packages/taler-wallet-core/src/util/coinSelection.ts
index d75450a64..e06d7454b 100644
--- a/packages/taler-wallet-core/src/util/coinSelection.ts
+++ b/packages/taler-wallet-core/src/util/coinSelection.ts
@@ -602,14 +602,9 @@ async function selectPayMerchantCandidates(
ws: InternalWalletState,
req: SelectPayCoinRequestNg,
): Promise<[AvailableDenom[], Record<string, AmountJson>]> {
- return await ws.db
- .mktx((x) => [
- x.exchanges,
- x.exchangeDetails,
- x.denominations,
- x.coinAvailability,
- ])
- .runReadOnly(async (tx) => {
+ return await ws.db.runReadOnlyTx(
+ ["exchanges", "exchangeDetails", "denominations", "coinAvailability"],
+ async (tx) => {
// FIXME: Use the existing helper (from balance.ts) to
// get acceptable exchanges.
const denoms: AvailableDenom[] = [];
@@ -721,6 +716,7 @@ async function selectPayMerchantCandidates(
});
}
}
+ logger.info(`available denoms ${j2s(denoms)}`);
// Sort by available amount (descending), deposit fee (ascending) and
// denomPub (ascending) if deposit fee is the same
// (to guarantee deterministic results)
@@ -731,7 +727,8 @@ async function selectPayMerchantCandidates(
strcmp(o1.denomPubHash, o2.denomPubHash),
);
return [denoms, wfPerExchange];
- });
+ },
+ );
}
/**
diff --git a/packages/taler-wallet-core/src/wallet.ts b/packages/taler-wallet-core/src/wallet.ts
index 87c5aa995..333e42621 100644
--- a/packages/taler-wallet-core/src/wallet.ts
+++ b/packages/taler-wallet-core/src/wallet.ts
@@ -1048,11 +1048,7 @@ async function dispatchRequestInternal<Op extends WalletApiOperation>(
withdrawalAccountsList: wi.exchangeCreditAccountDetails,
numCoins,
// FIXME: Once we have proper scope info support, return correct info here.
- scopeInfo: {
- type: ScopeType.Exchange,
- currency: amt.currency,
- url: req.exchangeBaseUrl,
- },
+ scopeInfo: wi.scopeInfo,
};
return resp;
}