wallet-core: use testingWaitTransactionsFinal to wait for transactions

This commit is contained in:
Florian Dold 2023-07-01 01:43:29 +02:00
parent f93ab03a1b
commit 5695ae0a9f
No known key found for this signature in database
GPG Key ID: D2E4F00F29D02A4B
7 changed files with 157 additions and 30 deletions

View File

@ -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;

View File

@ -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");
}

View File

@ -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 makeTestPaymentV2(t, { walletClient, merchant, order: order3 });
await walletClient.call(WalletApiOperation.TestingWaitTransactionsFinal, {});
await wallet.runUntilDone();
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");

View File

@ -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) => {

View File

@ -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) {

View File

@ -206,6 +206,7 @@ export enum WalletApiOperation {
Recycle = "recycle",
ApplyDevExperiment = "applyDevExperiment",
ValidateIban = "validateIban",
TestingWaitTransactionsFinal = "testingWaitTransactionsFinal",
}
// group: Initialization
@ -949,6 +950,15 @@ export type DumpCoinsOp = {
response: CoinDumpJson;
};
/**
* 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<

View File

@ -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);
}