wallet-core: use testingWaitTransactionsFinal to wait for transactions
This commit is contained in:
parent
f93ab03a1b
commit
5695ae0a9f
@ -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;
|
||||
|
@ -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");
|
||||
}
|
||||
|
@ -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");
|
||||
|
@ -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) => {
|
||||
|
@ -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) {
|
||||
|
@ -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<
|
||||
|
@ -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);
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user