summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--packages/taler-harness/src/harness/harness.ts32
-rw-r--r--packages/taler-harness/src/harness/helpers.ts70
-rw-r--r--packages/taler-harness/src/integrationtests/test-payment.ts29
-rw-r--r--packages/taler-wallet-core/src/operations/refresh.ts28
-rw-r--r--packages/taler-wallet-core/src/operations/testing.ts14
-rw-r--r--packages/taler-wallet-core/src/wallet-api-types.ts11
-rw-r--r--packages/taler-wallet-core/src/wallet.ts3
7 files changed, 157 insertions, 30 deletions
diff --git a/packages/taler-harness/src/harness/harness.ts b/packages/taler-harness/src/harness/harness.ts
index 7b2f980cc..1120eae84 100644
--- a/packages/taler-harness/src/harness/harness.ts
+++ b/packages/taler-harness/src/harness/harness.ts
@@ -41,17 +41,21 @@ import {
Logger,
MerchantReserveCreateConfirmation,
MerchantTemplateAddDetails,
+ NotificationType,
parsePaytoUri,
stringToBytes,
TalerError,
TalerProtocolDuration,
+ TransactionMajorState,
WalletNotification,
} from "@gnu-taler/taler-util";
import {
BankApi,
BankServiceHandle,
HarnessExchangeBankAccount,
+ OpenedPromise,
openPromise,
+ WalletApiOperation,
WalletCoreApiClient,
WalletCoreRequestType,
WalletCoreResponseType,
@@ -934,7 +938,12 @@ export class FakebankService
);
await this.pingUntilAvailable();
for (const acc of this.accounts) {
- await BankApi.registerAccount(this, acc.accountName, acc.accountPassword, {});
+ await BankApi.registerAccount(
+ this,
+ acc.accountName,
+ acc.accountPassword,
+ {},
+ );
}
}
@@ -2246,9 +2255,26 @@ export interface WalletClientArgs {
onNotification?(n: WalletNotification): void;
}
+export type CancelFn = () => void;
+export type NotificationHandler = (n: WalletNotification) => void;
+
+/**
+ * Convenience wrapper around a (remote) wallet handle.
+ */
export class WalletClient {
remoteWallet: RemoteWallet | undefined = undefined;
private waiter: WalletNotificationWaiter = makeNotificationWaiter();
+ notificationHandlers: NotificationHandler[] = [];
+
+ addNotificationListener(f: NotificationHandler): CancelFn {
+ this.notificationHandlers.push(f);
+ return () => {
+ const idx = this.notificationHandlers.indexOf(f);
+ if (idx >= 0) {
+ this.notificationHandlers.splice(idx, 1);
+ }
+ };
+ }
async call<Op extends keyof WalletOperations>(
operation: Op,
@@ -2260,6 +2286,7 @@ export class WalletClient {
const client = getClientFromRemoteWallet(this.remoteWallet);
return client.call(operation, payload);
}
+
constructor(private args: WalletClientArgs) {}
async connect(): Promise<void> {
@@ -2272,6 +2299,9 @@ export class WalletClient {
walletClient.args.onNotification(n);
}
waiter.notify(n);
+ for (const h of walletClient.notificationHandlers) {
+ h(n);
+ }
},
});
this.remoteWallet = w;
diff --git a/packages/taler-harness/src/harness/helpers.ts b/packages/taler-harness/src/harness/helpers.ts
index fd6e9aa2e..8c62aef37 100644
--- a/packages/taler-harness/src/harness/helpers.ts
+++ b/packages/taler-harness/src/harness/helpers.ts
@@ -689,3 +689,73 @@ export async function makeTestPayment(
t.assertTrue(orderStatus.order_status === "paid");
}
+
+/**
+ * Make a simple payment and check that it succeeded.
+ */
+export async function makeTestPaymentV2(
+ t: GlobalTestState,
+ args: {
+ merchant: MerchantServiceInterface;
+ walletClient: WalletClient;
+ order: Partial<MerchantContractTerms>;
+ instance?: string;
+ },
+ auth: WithAuthorization = {},
+): Promise<void> {
+ // Set up order.
+
+ const { walletClient, merchant } = args;
+ const instance = args.instance ?? "default";
+
+ const orderResp = await MerchantPrivateApi.createOrder(
+ merchant,
+ instance,
+ {
+ order: args.order,
+ },
+ auth,
+ );
+
+ let orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(
+ merchant,
+ {
+ orderId: orderResp.order_id,
+ },
+ auth,
+ );
+
+ t.assertTrue(orderStatus.order_status === "unpaid");
+
+ // Make wallet pay for the order
+
+ const preparePayResult = await walletClient.call(
+ WalletApiOperation.PreparePayForUri,
+ {
+ talerPayUri: orderStatus.taler_pay_uri,
+ },
+ );
+
+ t.assertTrue(
+ preparePayResult.status === PreparePayResultType.PaymentPossible,
+ );
+
+ const r2 = await walletClient.call(WalletApiOperation.ConfirmPay, {
+ proposalId: preparePayResult.proposalId,
+ });
+
+ t.assertTrue(r2.type === ConfirmPayResultType.Done);
+
+ // Check if payment was successful.
+
+ orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(
+ merchant,
+ {
+ orderId: orderResp.order_id,
+ instance,
+ },
+ auth,
+ );
+
+ t.assertTrue(orderStatus.order_status === "paid");
+}
diff --git a/packages/taler-harness/src/integrationtests/test-payment.ts b/packages/taler-harness/src/integrationtests/test-payment.ts
index f184e57e7..9d1ce0e22 100644
--- a/packages/taler-harness/src/integrationtests/test-payment.ts
+++ b/packages/taler-harness/src/integrationtests/test-payment.ts
@@ -20,9 +20,9 @@
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
import { GlobalTestState } from "../harness/harness.js";
import {
- createSimpleTestkudosEnvironment,
- withdrawViaBank,
- makeTestPayment,
+ createSimpleTestkudosEnvironmentV2,
+ withdrawViaBankV2,
+ makeTestPaymentV2,
} from "../harness/helpers.js";
import { j2s } from "@gnu-taler/taler-util";
@@ -32,12 +32,14 @@ import { j2s } from "@gnu-taler/taler-util";
export async function runPaymentTest(t: GlobalTestState) {
// Set up test environment
- const { wallet, bank, exchange, merchant } =
- await createSimpleTestkudosEnvironment(t);
+ const { walletClient, bank, exchange, merchant } =
+ await createSimpleTestkudosEnvironmentV2(t);
// Withdraw digital cash into the wallet.
- await withdrawViaBank(t, { wallet, bank, exchange, amount: "TESTKUDOS:20" });
+ await withdrawViaBankV2(t, { walletClient, bank, exchange, amount: "TESTKUDOS:20" });
+
+ await walletClient.call(WalletApiOperation.TestingWaitTransactionsFinal, {});
const order = {
summary: "Buy me!",
@@ -45,8 +47,8 @@ export async function runPaymentTest(t: GlobalTestState) {
fulfillment_url: "taler://fulfillment-success/thx",
};
- await makeTestPayment(t, { wallet, merchant, order });
- await wallet.runUntilDone();
+ await makeTestPaymentV2(t, { walletClient, merchant, order });
+ await walletClient.call(WalletApiOperation.TestingWaitTransactionsFinal, {});
// Test JSON normalization of contract terms: Does the wallet
// agree with the merchant?
@@ -56,8 +58,8 @@ export async function runPaymentTest(t: GlobalTestState) {
fulfillment_url: "taler://fulfillment-success/thx",
};
- await makeTestPayment(t, { wallet, merchant, order: order2 });
- await wallet.runUntilDone();
+ await makeTestPaymentV2(t, { walletClient, merchant, order: order2 });
+ await walletClient.call(WalletApiOperation.TestingWaitTransactionsFinal, {});
// Test JSON normalization of contract terms: Does the wallet
// agree with the merchant?
@@ -67,11 +69,10 @@ export async function runPaymentTest(t: GlobalTestState) {
fulfillment_url: "taler://fulfillment-success/thx",
};
- await makeTestPayment(t, { wallet, merchant, order: order3 });
-
- await wallet.runUntilDone();
+ await makeTestPaymentV2(t, { walletClient, merchant, order: order3 });
+ await walletClient.call(WalletApiOperation.TestingWaitTransactionsFinal, {});
- const bal = await wallet.client.call(WalletApiOperation.GetBalances, {});
+ const bal = await walletClient.call(WalletApiOperation.GetBalances, {});
console.log(`balance after 3 payments: ${j2s(bal)}`);
t.assertAmountEquals(bal.balances[0].available, "TESTKUDOS:3.8");
t.assertAmountEquals(bal.balances[0].pendingIncoming, "TESTKUDOS:0");
diff --git a/packages/taler-wallet-core/src/operations/refresh.ts b/packages/taler-wallet-core/src/operations/refresh.ts
index fd6281eda..6eb221c1c 100644
--- a/packages/taler-wallet-core/src/operations/refresh.ts
+++ b/packages/taler-wallet-core/src/operations/refresh.ts
@@ -263,22 +263,25 @@ async function refreshCreateSession(
availableAmount,
)} too small`,
);
- // FIXME: State transition notification missing.
- await ws.db
+ const transitionInfo = await ws.db
.mktx((x) => [x.coins, x.coinAvailability, x.refreshGroups])
.runReadWrite(async (tx) => {
const rg = await tx.refreshGroups.get(refreshGroupId);
if (!rg) {
return;
}
+ const oldTxState = computeRefreshTransactionState(rg);
rg.statusPerCoin[coinIndex] = RefreshCoinStatus.Finished;
const updateRes = updateGroupStatus(rg);
if (updateRes.final) {
await makeCoinsVisible(ws, tx, transactionId);
}
await tx.refreshGroups.put(rg);
+ const newTxState = computeRefreshTransactionState(rg);
+ return { oldTxState, newTxState };
});
ws.notify({ type: NotificationType.BalanceChange });
+ notifyTransition(ws, transactionId, transitionInfo);
return;
}
@@ -438,7 +441,7 @@ async function refreshMelt(
if (resp.status === HttpStatusCode.NotFound) {
const errDetails = await readUnexpectedResponseDetails(resp);
- await ws.db
+ const transitionInfo = await ws.db
.mktx((x) => [x.refreshGroups, x.coins, x.coinAvailability])
.runReadWrite(async (tx) => {
const rg = await tx.refreshGroups.get(refreshGroupId);
@@ -451,6 +454,7 @@ async function refreshMelt(
if (rg.statusPerCoin[coinIndex] !== RefreshCoinStatus.Pending) {
return;
}
+ const oldTxState = computeRefreshTransactionState(rg);
rg.statusPerCoin[coinIndex] = RefreshCoinStatus.Failed;
rg.lastErrorPerCoin[coinIndex] = errDetails;
const updateRes = updateGroupStatus(rg);
@@ -458,8 +462,14 @@ async function refreshMelt(
await makeCoinsVisible(ws, tx, transactionId);
}
await tx.refreshGroups.put(rg);
+ const newTxState = computeRefreshTransactionState(rg);
+ return {
+ oldTxState,
+ newTxState,
+ };
});
ws.notify({ type: NotificationType.BalanceChange });
+ notifyTransition(ws, transactionId, transitionInfo);
return;
}
@@ -739,7 +749,7 @@ async function refreshReveal(
}
}
- await ws.db
+ const transitionInfo = await ws.db
.mktx((x) => [
x.coins,
x.denominations,
@@ -756,6 +766,7 @@ async function refreshReveal(
if (!rs) {
return;
}
+ const oldTxState = computeRefreshTransactionState(rg);
rg.statusPerCoin[coinIndex] = RefreshCoinStatus.Finished;
updateGroupStatus(rg);
for (const coin of coins) {
@@ -763,7 +774,10 @@ async function refreshReveal(
}
await makeCoinsVisible(ws, tx, transactionId);
await tx.refreshGroups.put(rg);
+ const newTxState = computeRefreshTransactionState(rg);
+ return { oldTxState, newTxState };
});
+ notifyTransition(ws, transactionId, transitionInfo);
logger.trace("refresh finished (end of reveal)");
}
@@ -778,7 +792,7 @@ export async function processRefreshGroup(
.mktx((x) => [x.refreshGroups])
.runReadOnly(async (tx) => tx.refreshGroups.get(refreshGroupId));
if (!refreshGroup) {
- return TaskRunResult.finished()
+ return TaskRunResult.finished();
}
if (refreshGroup.timestampFinished) {
return TaskRunResult.finished();
@@ -1235,10 +1249,6 @@ export async function suspendRefreshGroup(
tag: TransactionType.Refresh,
refreshGroupId,
});
- const retryTag = constructTaskIdentifier({
- tag: PendingTaskType.Refresh,
- refreshGroupId,
- });
let res = await ws.db
.mktx((x) => [x.refreshGroups])
.runReadWrite(async (tx) => {
diff --git a/packages/taler-wallet-core/src/operations/testing.ts b/packages/taler-wallet-core/src/operations/testing.ts
index 8c84702b8..ea373e914 100644
--- a/packages/taler-wallet-core/src/operations/testing.ts
+++ b/packages/taler-wallet-core/src/operations/testing.ts
@@ -450,7 +450,7 @@ export async function runIntegrationTest(
logger.trace("integration test: all done!");
}
-async function waitUntilDone(ws: InternalWalletState): Promise<void> {
+export async function waitUntilDone(ws: InternalWalletState): Promise<void> {
logger.info("waiting until all transactions are in a final state");
ws.ensureTaskLoopRunning();
let p: OpenedPromise<void> | undefined = undefined;
@@ -459,11 +459,13 @@ async function waitUntilDone(ws: InternalWalletState): Promise<void> {
return;
}
if (notif.type === NotificationType.TransactionStateTransition) {
- p.resolve();
- }
- // Work-around, refresh transactions don't properly emit transition notifications yet.
- if (notif.type === NotificationType.PendingOperationProcessed) {
- p.resolve();
+ switch (notif.newTxState.major) {
+ case TransactionMajorState.Pending:
+ case TransactionMajorState.Aborting:
+ break;
+ default:
+ p.resolve();
+ }
}
});
while (1) {
diff --git a/packages/taler-wallet-core/src/wallet-api-types.ts b/packages/taler-wallet-core/src/wallet-api-types.ts
index 6bcee0299..cea548db6 100644
--- a/packages/taler-wallet-core/src/wallet-api-types.ts
+++ b/packages/taler-wallet-core/src/wallet-api-types.ts
@@ -206,6 +206,7 @@ export enum WalletApiOperation {
Recycle = "recycle",
ApplyDevExperiment = "applyDevExperiment",
ValidateIban = "validateIban",
+ TestingWaitTransactionsFinal = "testingWaitTransactionsFinal",
}
// group: Initialization
@@ -950,6 +951,15 @@ export type DumpCoinsOp = {
};
/**
+ * Wait until all transactions are in a final state.
+ */
+export type TestingWaitTransactionsFinal = {
+ op: WalletApiOperation.TestingWaitTransactionsFinal;
+ request: EmptyObject;
+ response: EmptyObject;
+};
+
+/**
* Set a coin as (un-)suspended.
* Suspended coins won't be used for payments.
*/
@@ -1051,6 +1061,7 @@ export type WalletOperations = {
[WalletApiOperation.Recycle]: RecycleOp;
[WalletApiOperation.ApplyDevExperiment]: ApplyDevExperimentOp;
[WalletApiOperation.ValidateIban]: ValidateIbanOp;
+ [WalletApiOperation.TestingWaitTransactionsFinal]: TestingWaitTransactionsFinal;
};
export type WalletCoreRequestType<
diff --git a/packages/taler-wallet-core/src/wallet.ts b/packages/taler-wallet-core/src/wallet.ts
index 8f11a3d28..11030af2b 100644
--- a/packages/taler-wallet-core/src/wallet.ts
+++ b/packages/taler-wallet-core/src/wallet.ts
@@ -242,6 +242,7 @@ import {
runIntegrationTest,
runIntegrationTest2,
testPay,
+ waitUntilDone,
withdrawTestBalance,
} from "./operations/testing.js";
import {
@@ -1550,6 +1551,8 @@ async function dispatchRequestInternal<Op extends WalletApiOperation>(
case WalletApiOperation.GetVersion: {
return getVersion(ws);
}
+ case WalletApiOperation.TestingWaitTransactionsFinal:
+ return await waitUntilDone(ws);
// default:
// assertUnreachable(operation);
}