wallet-core,harness: introduce reserveIsReady flag, test tx lifeycle

This commit is contained in:
Florian Dold 2023-02-13 13:15:47 +01:00
parent 79b77a0c3c
commit 22cb8adaa6
No known key found for this signature in database
GPG Key ID: D2E4F00F29D02A4B
8 changed files with 200 additions and 33 deletions

View File

@ -1169,6 +1169,29 @@ export class ExchangeService implements ExchangeServiceInterface {
return !!this.exchangeWirewatchProc || !!this.exchangeHttpProc;
}
/**
* Stop the wirewatch service (which runs by default).
*
* Useful for some tests.
*/
async stopWirewatch(): Promise<void> {
const wirewatch = this.exchangeWirewatchProc;
if (wirewatch) {
wirewatch.proc.kill("SIGTERM");
await wirewatch.wait();
this.exchangeWirewatchProc = undefined;
}
}
async startWirewatch(): Promise<void> {
const wirewatch = this.exchangeWirewatchProc;
if (wirewatch) {
logger.warn("wirewatch already running");
} else {
this.internalCreateWirewatchProc();
}
}
async stop(): Promise<void> {
const wirewatch = this.exchangeWirewatchProc;
if (wirewatch) {
@ -1332,6 +1355,19 @@ export class ExchangeService implements ExchangeServiceInterface {
);
}
private internalCreateWirewatchProc() {
this.exchangeWirewatchProc = this.globalState.spawnService(
"taler-exchange-wirewatch",
[
"-c",
this.configFilename,
"--longpoll-timeout=5s",
...this.timetravelArgArr,
],
`exchange-wirewatch-${this.name}`,
);
}
async start(): Promise<void> {
if (this.isRunning()) {
throw Error("exchange is already running");
@ -1360,16 +1396,7 @@ export class ExchangeService implements ExchangeServiceInterface {
`exchange-crypto-rsa-${this.name}`,
);
this.exchangeWirewatchProc = this.globalState.spawnService(
"taler-exchange-wirewatch",
[
"-c",
this.configFilename,
"--longpoll-timeout=5s",
...this.timetravelArgArr,
],
`exchange-wirewatch-${this.name}`,
);
this.internalCreateWirewatchProc();
this.exchangeHttpProc = this.globalState.spawnService(
"taler-exchange-httpd",

View File

@ -209,7 +209,7 @@ export async function createSimpleTestkudosEnvironmentV2(
t: GlobalTestState,
coinConfig: CoinConfig[] = defaultCoinConfig.map((x) => x("TESTKUDOS")),
opts: EnvOptions = {},
): Promise<SimpleTestEnvironment> {
): Promise<SimpleTestEnvironmentNg> {
const db = await setupDb(t);
const bank = await BankService.create(t, {
@ -293,15 +293,32 @@ export async function createSimpleTestkudosEnvironmentV2(
),
});
console.log("setup done!");
const walletService = new WalletService(t, {
name: "wallet",
useInMemoryDb: true,
});
await walletService.start();
await walletService.pingUntilAvailable();
const wallet = new WalletCli(t);
const walletClient = new WalletClient({
unixPath: walletService.socketPath,
onNotification(n) {
console.log("got notification", n);
},
});
await walletClient.connect();
await walletClient.client.call(WalletApiOperation.InitWallet, {
skipDefaults: true,
});
console.log("setup done!");
return {
commonDb: db,
exchange,
merchant,
wallet,
walletClient,
walletService,
bank,
exchangeBankAccount,
};

View File

@ -307,7 +307,7 @@ export async function runKycTest(t: GlobalTestState) {
// Withdraw
const kycNotificationCond = walletClient.waitForNotificationCond((x) => {
if (x.type === NotificationType.WithdrawalKycRequested) {
if (x.type === NotificationType.WithdrawalGroupKycRequested) {
return x;
}
return false;

View File

@ -18,13 +18,13 @@
* Imports.
*/
import { GlobalTestState } from "../harness/harness.js";
import { createSimpleTestkudosEnvironment } from "../harness/helpers.js";
import { createSimpleTestkudosEnvironmentV2 } from "../harness/helpers.js";
import {
WalletApiOperation,
BankApi,
BankAccessApi,
} from "@gnu-taler/taler-wallet-core";
import { j2s } from "@gnu-taler/taler-util";
import { j2s, NotificationType, TransactionType, WithdrawalType } from "@gnu-taler/taler-util";
/**
* Run test for basic, bank-integrated withdrawal.
@ -32,7 +32,8 @@ import { j2s } from "@gnu-taler/taler-util";
export async function runWithdrawalBankIntegratedTest(t: GlobalTestState) {
// Set up test environment
const { wallet, bank, exchange } = await createSimpleTestkudosEnvironment(t);
const { walletClient, bank, exchange } =
await createSimpleTestkudosEnvironmentV2(t);
// Create a withdrawal operation
@ -45,46 +46,117 @@ export async function runWithdrawalBankIntegratedTest(t: GlobalTestState) {
// Hand it to the wallet
const r1 = await wallet.client.call(
const r1 = await walletClient.client.call(
WalletApiOperation.GetWithdrawalDetailsForUri,
{
talerWithdrawUri: wop.taler_withdraw_uri,
},
);
await wallet.runPending();
// Withdraw
const r2 = await wallet.client.call(
const withdrawalBankConfirmedCond = walletClient.waitForNotificationCond(
(x) => {
return x.type === NotificationType.WithdrawalGroupBankConfirmed;
},
);
const withdrawalFinishedCond = walletClient.waitForNotificationCond((x) => {
return x.type === NotificationType.WithdrawGroupFinished;
});
const withdrawalReserveReadyCond = walletClient.waitForNotificationCond(
(x) => {
return x.type === NotificationType.WithdrawalGroupReserveReady;
},
);
const r2 = await walletClient.client.call(
WalletApiOperation.AcceptBankIntegratedWithdrawal,
{
exchangeBaseUrl: exchange.baseUrl,
talerWithdrawUri: wop.taler_withdraw_uri,
},
);
// Do it twice to check idempotency
const r3 = await wallet.client.call(
const r3 = await walletClient.client.call(
WalletApiOperation.AcceptBankIntegratedWithdrawal,
{
exchangeBaseUrl: exchange.baseUrl,
talerWithdrawUri: wop.taler_withdraw_uri,
},
);
await wallet.runPending();
await exchange.stopWirewatch();
// Check status before withdrawal is confirmed by bank.
{
const txn = await walletClient.client.call(
WalletApiOperation.GetTransactions,
{},
);
console.log("transactions before confirmation:", j2s(txn));
const tx0 = txn.transactions[0];
t.assertTrue(tx0.type === TransactionType.Withdrawal);
t.assertTrue(tx0.withdrawalDetails.type === WithdrawalType.TalerBankIntegrationApi);
t.assertTrue(tx0.withdrawalDetails.confirmed === false);
t.assertTrue(tx0.withdrawalDetails.reserveIsReady === false);
}
// Confirm it
await BankApi.confirmWithdrawalOperation(bank, user, wop);
await wallet.runUntilDone();
await withdrawalBankConfirmedCond;
// Check status after withdrawal is confirmed by bank,
// but before funds are wired to the exchange.
{
const txn = await walletClient.client.call(
WalletApiOperation.GetTransactions,
{},
);
console.log("transactions after confirmation:", j2s(txn));
const tx0 = txn.transactions[0];
t.assertTrue(tx0.type === TransactionType.Withdrawal);
t.assertTrue(tx0.withdrawalDetails.type === WithdrawalType.TalerBankIntegrationApi);
t.assertTrue(tx0.withdrawalDetails.confirmed === true);
t.assertTrue(tx0.withdrawalDetails.reserveIsReady === false);
}
await exchange.startWirewatch();
await withdrawalReserveReadyCond;
// Check status after funds were wired.
{
const txn = await walletClient.client.call(
WalletApiOperation.GetTransactions,
{},
);
console.log("transactions after reserve ready:", j2s(txn));
const tx0 = txn.transactions[0];
t.assertTrue(tx0.type === TransactionType.Withdrawal);
t.assertTrue(tx0.withdrawalDetails.type === WithdrawalType.TalerBankIntegrationApi);
t.assertTrue(tx0.withdrawalDetails.confirmed === true);
t.assertTrue(tx0.withdrawalDetails.reserveIsReady === true);
}
await withdrawalFinishedCond;
// Check balance
const balResp = await wallet.client.call(WalletApiOperation.GetBalances, {});
const balResp = await walletClient.client.call(
WalletApiOperation.GetBalances,
{},
);
t.assertAmountEquals("TESTKUDOS:9.72", balResp.balances[0].available);
const txn = await wallet.client.call(WalletApiOperation.GetTransactions, {});
const txn = await walletClient.client.call(
WalletApiOperation.GetTransactions,
{},
);
console.log(`transactions: ${j2s(txn)}`);
}

View File

@ -62,7 +62,9 @@ export enum NotificationType {
PendingOperationProcessed = "pending-operation-processed",
ProposalRefused = "proposal-refused",
ReserveRegisteredWithBank = "reserve-registered-with-bank",
WithdrawalKycRequested = "withdrawal-kyc-requested",
WithdrawalGroupKycRequested = "withdrawal-group-kyc-requested",
WithdrawalGroupBankConfirmed = "withdrawal-group-bank-confirmed",
WithdrawalGroupReserveReady = "withdrawal-group-reserve-ready",
DepositOperationError = "deposit-operation-error",
}
@ -118,12 +120,22 @@ export interface RefreshMeltedNotification {
type: NotificationType.RefreshMelted;
}
export interface WithdrawalKycRequested {
type: NotificationType.WithdrawalKycRequested;
export interface WithdrawalGroupKycRequested {
type: NotificationType.WithdrawalGroupKycRequested;
transactionId: string;
kycUrl: string;
}
export interface WithdrawalGroupBankConfirmed {
type: NotificationType.WithdrawalGroupBankConfirmed;
transactionId: string;
}
export interface WithdrawalGroupReserveReady {
type: NotificationType.WithdrawalGroupReserveReady;
transactionId: string;
}
export interface RefreshRevealedNotification {
type: NotificationType.RefreshRevealed;
}
@ -293,4 +305,6 @@ export type WalletNotification =
| ReserveRegisteredWithBankNotification
| ReserveNotYetFoundNotification
| PayOperationSuccessNotification
| WithdrawalKycRequested;
| WithdrawalGroupKycRequested
| WithdrawalGroupBankConfirmed
| WithdrawalGroupReserveReady;

View File

@ -167,6 +167,11 @@ interface WithdrawalDetailsForManualTransfer {
// Public key of the reserve
reservePub: string;
/**
* Is the reserve ready for withdrawal?
*/
reserveIsReady: boolean;
}
interface WithdrawalDetailsForTalerBankIntegrationApi {
@ -187,6 +192,11 @@ interface WithdrawalDetailsForTalerBankIntegrationApi {
// Public key of the reserve
reservePub: string;
/**
* Is the reserve ready for withdrawal?
*/
reserveIsReady: boolean;
}
// This should only be used for actual withdrawals

View File

@ -55,6 +55,7 @@ import {
PeerPushPaymentInitiationStatus,
PeerPullPaymentIncomingStatus,
TransactionStatus,
WithdrawalGroupStatus,
} from "../db.js";
import { InternalWalletState } from "../internal-wallet-state.js";
import { assertUnreachable } from "../util/assertUnreachable.js";
@ -515,6 +516,9 @@ function buildTransactionForBankIntegratedWithdraw(
confirmed: wsr.wgInfo.bankInfo.timestampBankConfirmed ? true : false,
reservePub: wsr.reservePub,
bankConfirmationUrl: wsr.wgInfo.bankInfo.confirmUrl,
reserveIsReady:
wsr.status === WithdrawalGroupStatus.Finished ||
wsr.status === WithdrawalGroupStatus.Ready,
},
exchangeBaseUrl: wsr.exchangeBaseUrl,
extendedStatus: wsr.timestampFinish
@ -558,6 +562,9 @@ function buildTransactionForManualWithdraw(
type: WithdrawalType.ManualTransfer,
reservePub: withdrawalGroup.reservePub,
exchangePaytoUris,
reserveIsReady:
withdrawalGroup.status === WithdrawalGroupStatus.Finished ||
withdrawalGroup.status === WithdrawalGroupStatus.Ready,
},
exchangeBaseUrl: withdrawalGroup.exchangeBaseUrl,
extendedStatus: withdrawalGroup.timestampFinish

View File

@ -987,6 +987,14 @@ async function queryReserve(
await tx.withdrawalGroups.put(wg);
});
ws.notify({
type: NotificationType.WithdrawalGroupReserveReady,
transactionId: makeTransactionId(
TransactionType.Withdrawal,
withdrawalGroupId,
),
});
return { ready: true };
}
@ -1250,7 +1258,12 @@ export async function processWithdrawalGroup(
if (numKycRequired > 0) {
if (kycInfo) {
await checkWithdrawalKycStatus(ws, withdrawalGroup, kycInfo, "individual");
await checkWithdrawalKycStatus(
ws,
withdrawalGroup,
kycInfo,
"individual",
);
return {
type: OperationAttemptResultType.Pending,
result: undefined,
@ -1310,7 +1323,7 @@ export async function checkWithdrawalKycStatus(
const kycStatus = await kycStatusReq.json();
logger.info(`kyc status: ${j2s(kycStatus)}`);
ws.notify({
type: NotificationType.WithdrawalKycRequested,
type: NotificationType.WithdrawalGroupKycRequested,
kycUrl: kycStatus.kyc_url,
transactionId: makeTransactionId(
TransactionType.Withdrawal,
@ -1794,6 +1807,13 @@ async function processReserveBankStatus(
const now = AbsoluteTime.toTimestamp(AbsoluteTime.now());
r.wgInfo.bankInfo.timestampBankConfirmed = now;
r.status = WithdrawalGroupStatus.QueryingStatus;
ws.notify({
type: NotificationType.WithdrawalGroupBankConfirmed,
transactionId: makeTransactionId(
TransactionType.Withdrawal,
r.withdrawalGroupId,
),
});
} else {
logger.info("withdrawal: transfer not yet confirmed by bank");
r.wgInfo.bankInfo.confirmUrl = status.confirm_transfer_url;