test error handling
This commit is contained in:
parent
5e7149f79e
commit
5056da6548
@ -133,6 +133,62 @@ export async function sh(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function shellescape(args: string[]) {
|
||||||
|
const ret = args.map((s) => {
|
||||||
|
if (/[^A-Za-z0-9_\/:=-]/.test(s)) {
|
||||||
|
s = "'" + s.replace(/'/g, "'\\''") + "'";
|
||||||
|
s = s.replace(/^(?:'')+/g, "").replace(/\\'''/g, "\\'");
|
||||||
|
}
|
||||||
|
return s;
|
||||||
|
});
|
||||||
|
return ret.join(" ");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run a shell command, return stdout.
|
||||||
|
*
|
||||||
|
* Log stderr to a log file.
|
||||||
|
*/
|
||||||
|
export async function runCommand(
|
||||||
|
t: GlobalTestState,
|
||||||
|
logName: string,
|
||||||
|
command: string,
|
||||||
|
args: string[],
|
||||||
|
): Promise<string> {
|
||||||
|
console.log("runing command", shellescape([command, ...args]));
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const stdoutChunks: Buffer[] = [];
|
||||||
|
const proc = spawn(command, args, {
|
||||||
|
stdio: ["inherit", "pipe", "pipe"],
|
||||||
|
shell: false,
|
||||||
|
});
|
||||||
|
proc.stdout.on("data", (x) => {
|
||||||
|
if (x instanceof Buffer) {
|
||||||
|
stdoutChunks.push(x);
|
||||||
|
} else {
|
||||||
|
throw Error("unexpected data chunk type");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const stderrLogFileName = path.join(t.testDir, `${logName}-stderr.log`);
|
||||||
|
const stderrLog = fs.createWriteStream(stderrLogFileName, {
|
||||||
|
flags: "a",
|
||||||
|
});
|
||||||
|
proc.stderr.pipe(stderrLog);
|
||||||
|
proc.on("exit", (code, signal) => {
|
||||||
|
console.log(`child process exited (${code} / ${signal})`);
|
||||||
|
if (code != 0) {
|
||||||
|
reject(Error(`Unexpected exit code ${code} for '${command}'`));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const b = Buffer.concat(stdoutChunks).toString("utf-8");
|
||||||
|
resolve(b);
|
||||||
|
});
|
||||||
|
proc.on("error", () => {
|
||||||
|
reject(Error("Child process had error"));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export class ProcessWrapper {
|
export class ProcessWrapper {
|
||||||
private waitPromise: Promise<WaitResult>;
|
private waitPromise: Promise<WaitResult>;
|
||||||
constructor(public proc: ChildProcess) {
|
constructor(public proc: ChildProcess) {
|
||||||
@ -298,7 +354,7 @@ export class GlobalTestState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
assertDeepEqual(actual: any, expected: any): asserts actual is any {
|
assertDeepEqual<T>(actual: any, expected: T): asserts actual is T {
|
||||||
deepStrictEqual(actual, expected);
|
deepStrictEqual(actual, expected);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -349,7 +405,9 @@ export class GlobalTestState {
|
|||||||
args: string[],
|
args: string[],
|
||||||
logName: string,
|
logName: string,
|
||||||
): ProcessWrapper {
|
): ProcessWrapper {
|
||||||
console.log(`spawning process ${command} with arguments ${args})`);
|
console.log(
|
||||||
|
`spawning process (${logName}): ${shellescape([command, ...args])}`,
|
||||||
|
);
|
||||||
const proc = spawn(command, args, {
|
const proc = spawn(command, args, {
|
||||||
stdio: ["inherit", "pipe", "pipe"],
|
stdio: ["inherit", "pipe", "pipe"],
|
||||||
});
|
});
|
||||||
@ -1028,8 +1086,8 @@ export class ExchangeService implements ExchangeServiceInterface {
|
|||||||
await sh(
|
await sh(
|
||||||
this.globalState,
|
this.globalState,
|
||||||
"exchange-wire",
|
"exchange-wire",
|
||||||
`taler-exchange-wire ${this.timetravelArg} -c "${this.configFilename}"`
|
`taler-exchange-wire ${this.timetravelArg} -c "${this.configFilename}"`,
|
||||||
)
|
);
|
||||||
|
|
||||||
this.exchangeWirewatchProc = this.globalState.spawnService(
|
this.exchangeWirewatchProc = this.globalState.spawnService(
|
||||||
"taler-exchange-wirewatch",
|
"taler-exchange-wirewatch",
|
||||||
@ -1403,6 +1461,14 @@ export class WalletCli {
|
|||||||
fs.unlinkSync(this.dbfile);
|
fs.unlinkSync(this.dbfile);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private get timetravelArgArr(): string[] {
|
||||||
|
const tta = this.timetravelArg;
|
||||||
|
if (tta) {
|
||||||
|
return [tta];
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
async apiRequest(
|
async apiRequest(
|
||||||
request: string,
|
request: string,
|
||||||
payload: unknown,
|
payload: unknown,
|
||||||
@ -1420,13 +1486,19 @@ export class WalletCli {
|
|||||||
return JSON.parse(resp) as CoreApiResponse;
|
return JSON.parse(resp) as CoreApiResponse;
|
||||||
}
|
}
|
||||||
|
|
||||||
async runUntilDone(): Promise<void> {
|
async runUntilDone(args: { maxRetries?: number } = {}): Promise<void> {
|
||||||
await sh(
|
await runCommand(
|
||||||
this.globalTestState,
|
this.globalTestState,
|
||||||
`wallet-${this.name}`,
|
`wallet-${this.name}`,
|
||||||
`taler-wallet-cli ${this.timetravelArg ?? ""} --no-throttle --wallet-db ${
|
"taler-wallet-cli",
|
||||||
this.dbfile
|
[
|
||||||
} run-until-done`,
|
"--no-throttle",
|
||||||
|
...this.timetravelArgArr,
|
||||||
|
"--wallet-db",
|
||||||
|
this.dbfile,
|
||||||
|
"run-until-done",
|
||||||
|
...(args.maxRetries ? ["--max-retries", `${args.maxRetries}`] : []),
|
||||||
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -221,7 +221,7 @@ export async function createFaultInjectedMerchantTestkudosEnvironment(
|
|||||||
/**
|
/**
|
||||||
* Withdraw balance.
|
* Withdraw balance.
|
||||||
*/
|
*/
|
||||||
export async function withdrawViaBank(
|
export async function startWithdrawViaBank(
|
||||||
t: GlobalTestState,
|
t: GlobalTestState,
|
||||||
p: {
|
p: {
|
||||||
wallet: WalletCli;
|
wallet: WalletCli;
|
||||||
@ -255,6 +255,26 @@ export async function withdrawViaBank(
|
|||||||
talerWithdrawUri: wop.taler_withdraw_uri,
|
talerWithdrawUri: wop.taler_withdraw_uri,
|
||||||
});
|
});
|
||||||
t.assertTrue(r2.type === "response");
|
t.assertTrue(r2.type === "response");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Withdraw balance.
|
||||||
|
*/
|
||||||
|
export async function withdrawViaBank(
|
||||||
|
t: GlobalTestState,
|
||||||
|
p: {
|
||||||
|
wallet: WalletCli;
|
||||||
|
bank: BankService;
|
||||||
|
exchange: ExchangeService;
|
||||||
|
amount: AmountString;
|
||||||
|
},
|
||||||
|
): Promise<void> {
|
||||||
|
|
||||||
|
const { wallet } = p;
|
||||||
|
|
||||||
|
await startWithdrawViaBank(t, p);
|
||||||
|
|
||||||
await wallet.runUntilDone();
|
await wallet.runUntilDone();
|
||||||
|
|
||||||
// Check balance
|
// Check balance
|
||||||
|
@ -177,7 +177,7 @@ runTest(async (t: GlobalTestState) => {
|
|||||||
// Response is malformed, since it didn't even contain a version code
|
// Response is malformed, since it didn't even contain a version code
|
||||||
// in a format the wallet can understand.
|
// in a format the wallet can understand.
|
||||||
t.assertTrue(
|
t.assertTrue(
|
||||||
err1.operationError.talerErrorCode ===
|
err1.operationError.code ===
|
||||||
TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE,
|
TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE,
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -214,7 +214,7 @@ runTest(async (t: GlobalTestState) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
t.assertTrue(
|
t.assertTrue(
|
||||||
err2.operationError.talerErrorCode ===
|
err2.operationError.code ===
|
||||||
TalerErrorCode.WALLET_EXCHANGE_PROTOCOL_VERSION_INCOMPATIBLE,
|
TalerErrorCode.WALLET_EXCHANGE_PROTOCOL_VERSION_INCOMPATIBLE,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -102,7 +102,7 @@ runTest(async (t: GlobalTestState) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
t.assertTrue(
|
t.assertTrue(
|
||||||
err.operationError.talerErrorCode ===
|
err.operationError.code ===
|
||||||
TalerErrorCode.WALLET_ORDER_ALREADY_CLAIMED,
|
TalerErrorCode.WALLET_ORDER_ALREADY_CLAIMED,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -17,9 +17,18 @@
|
|||||||
/**
|
/**
|
||||||
* Imports.
|
* Imports.
|
||||||
*/
|
*/
|
||||||
import { runTest, GlobalTestState, MerchantPrivateApi, WalletCli } from "./harness";
|
import {
|
||||||
import { createSimpleTestkudosEnvironment, withdrawViaBank } from "./helpers";
|
runTest,
|
||||||
import { PreparePayResultType, durationMin, Duration } from "taler-wallet-core";
|
GlobalTestState,
|
||||||
|
MerchantPrivateApi,
|
||||||
|
WalletCli,
|
||||||
|
} from "./harness";
|
||||||
|
import {
|
||||||
|
createSimpleTestkudosEnvironment,
|
||||||
|
withdrawViaBank,
|
||||||
|
startWithdrawViaBank,
|
||||||
|
} from "./helpers";
|
||||||
|
import { PreparePayResultType, durationMin, Duration, TransactionType } from "taler-wallet-core";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Basic time travel test.
|
* Basic time travel test.
|
||||||
@ -36,7 +45,7 @@ runTest(async (t: GlobalTestState) => {
|
|||||||
|
|
||||||
// Withdraw digital cash into the wallet.
|
// Withdraw digital cash into the wallet.
|
||||||
|
|
||||||
await withdrawViaBank(t, { wallet, bank, exchange, amount: "TESTKUDOS:20" });
|
await withdrawViaBank(t, { wallet, bank, exchange, amount: "TESTKUDOS:15" });
|
||||||
|
|
||||||
// Travel 400 days into the future,
|
// Travel 400 days into the future,
|
||||||
// as the deposit expiration is two years
|
// as the deposit expiration is two years
|
||||||
@ -56,9 +65,28 @@ runTest(async (t: GlobalTestState) => {
|
|||||||
await merchant.pingUntilAvailable();
|
await merchant.pingUntilAvailable();
|
||||||
|
|
||||||
// This should fail, as the wallet didn't time travel yet.
|
// This should fail, as the wallet didn't time travel yet.
|
||||||
await withdrawViaBank(t, { wallet, bank, exchange, amount: "TESTKUDOS:20" });
|
await startWithdrawViaBank(t, {
|
||||||
|
wallet,
|
||||||
|
bank,
|
||||||
|
exchange,
|
||||||
|
amount: "TESTKUDOS:20",
|
||||||
|
});
|
||||||
|
|
||||||
const bal = await wallet.getBalances();
|
// Check that transactions are correct for the failed withdrawal
|
||||||
|
{
|
||||||
|
await wallet.runUntilDone({ maxRetries: 5 });
|
||||||
|
const transactions = await wallet.getTransactions();
|
||||||
|
console.log(transactions);
|
||||||
|
const types = transactions.transactions.map((x) => x.type);
|
||||||
|
t.assertDeepEqual(types, ["withdrawal", "withdrawal"]);
|
||||||
|
const wtrans = transactions.transactions[0];
|
||||||
|
t.assertTrue(wtrans.type === TransactionType.Withdrawal);
|
||||||
|
t.assertTrue(wtrans.pending);
|
||||||
|
}
|
||||||
|
|
||||||
console.log(bal);
|
// Now we also let the wallet time travel
|
||||||
|
|
||||||
|
wallet.setTimetravel(timetravelDuration);
|
||||||
|
|
||||||
|
await wallet.runUntilDone({ maxRetries: 5 });
|
||||||
});
|
});
|
||||||
|
@ -59,7 +59,7 @@ runTest(async (t: GlobalTestState) => {
|
|||||||
});
|
});
|
||||||
t.assertTrue(r2.type === "error");
|
t.assertTrue(r2.type === "error");
|
||||||
t.assertTrue(
|
t.assertTrue(
|
||||||
r2.error.talerErrorCode ===
|
r2.error.code ===
|
||||||
TalerErrorCode.WALLET_WITHDRAWAL_OPERATION_ABORTED_BY_BANK,
|
TalerErrorCode.WALLET_WITHDRAWAL_OPERATION_ABORTED_BY_BANK,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -262,9 +262,13 @@ walletCli
|
|||||||
.subcommand("finishPendingOpt", "run-until-done", {
|
.subcommand("finishPendingOpt", "run-until-done", {
|
||||||
help: "Run until no more work is left.",
|
help: "Run until no more work is left.",
|
||||||
})
|
})
|
||||||
|
.maybeOption("maxRetries", ["--max-retries"], clk.INT)
|
||||||
.action(async (args) => {
|
.action(async (args) => {
|
||||||
await withWallet(args, async (wallet) => {
|
await withWallet(args, async (wallet) => {
|
||||||
await wallet.runUntilDoneAndStop();
|
await wallet.runUntilDone({
|
||||||
|
maxRetries: args.finishPendingOpt.maxRetries,
|
||||||
|
});
|
||||||
|
wallet.stop();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -969,6 +969,13 @@ export enum TalerErrorCode {
|
|||||||
*/
|
*/
|
||||||
REFUND_INVALID_FAILURE_PROOF_BY_EXCHANGE = 1516,
|
REFUND_INVALID_FAILURE_PROOF_BY_EXCHANGE = 1516,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The exchange failed to lookup information for the refund from its database.
|
||||||
|
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
|
||||||
|
* (A value of 0 indicates that the error is generated client-side).
|
||||||
|
*/
|
||||||
|
REFUND_DATABASE_LOOKUP_ERROR = 1517,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The wire format specified in the "sender_account_details" is not understood or not supported by this exchange.
|
* The wire format specified in the "sender_account_details" is not understood or not supported by this exchange.
|
||||||
* Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
|
* Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
|
||||||
@ -1571,6 +1578,20 @@ export enum TalerErrorCode {
|
|||||||
*/
|
*/
|
||||||
FORGET_PATH_NOT_FORGETTABLE = 2182,
|
FORGET_PATH_NOT_FORGETTABLE = 2182,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The merchant backend cannot forget part of an order because it failed to start the database transaction.
|
||||||
|
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
|
||||||
|
* (A value of 0 indicates that the error is generated client-side).
|
||||||
|
*/
|
||||||
|
FORGET_ORDER_DB_START_ERROR = 2183,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The merchant backend cannot forget part of an order because it failed to commit the database transaction.
|
||||||
|
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
|
||||||
|
* (A value of 0 indicates that the error is generated client-side).
|
||||||
|
*/
|
||||||
|
FORGET_ORDER_DB_COMMIT_ERROR = 2184,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Integer overflow with specified timestamp argument detected.
|
* Integer overflow with specified timestamp argument detected.
|
||||||
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
|
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
|
||||||
@ -1991,6 +2012,13 @@ export enum TalerErrorCode {
|
|||||||
*/
|
*/
|
||||||
ORDERS_ALREADY_CLAIMED = 2521,
|
ORDERS_ALREADY_CLAIMED = 2521,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The merchant backend couldn't find a product with the specified id.
|
||||||
|
* Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
|
||||||
|
* (A value of 0 indicates that the error is generated client-side).
|
||||||
|
*/
|
||||||
|
GET_PRODUCTS_NOT_FOUND = 2549,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The merchant backend failed to lookup the products.
|
* The merchant backend failed to lookup the products.
|
||||||
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
|
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
|
||||||
@ -2983,7 +3011,7 @@ export enum TalerErrorCode {
|
|||||||
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
|
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
|
||||||
* (A value of 0 indicates that the error is generated client-side).
|
* (A value of 0 indicates that the error is generated client-side).
|
||||||
*/
|
*/
|
||||||
SYNC_DB_FETCH_ERROR = 6000,
|
SYNC_DB_HARD_FETCH_ERROR = 6000,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The sync service failed find the record in its database.
|
* The sync service failed find the record in its database.
|
||||||
@ -3028,11 +3056,11 @@ export enum TalerErrorCode {
|
|||||||
SYNC_INVALID_SIGNATURE = 6007,
|
SYNC_INVALID_SIGNATURE = 6007,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The "Content-length" field for the upload is either not a number, or too big, or missing.
|
* The "Content-length" field for the upload is either not a number, or too big.
|
||||||
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
|
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
|
||||||
* (A value of 0 indicates that the error is generated client-side).
|
* (A value of 0 indicates that the error is generated client-side).
|
||||||
*/
|
*/
|
||||||
SYNC_BAD_CONTENT_LENGTH = 6008,
|
SYNC_MALFORMED_CONTENT_LENGTH = 6008,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The "Content-length" field for the upload is too big based on the server's terms of service.
|
* The "Content-length" field for the upload is too big based on the server's terms of service.
|
||||||
@ -3111,6 +3139,27 @@ export enum TalerErrorCode {
|
|||||||
*/
|
*/
|
||||||
SYNC_PREVIOUS_BACKUP_UNKNOWN = 6019,
|
SYNC_PREVIOUS_BACKUP_UNKNOWN = 6019,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The sync service had a serialization failure when accessing its database.
|
||||||
|
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
|
||||||
|
* (A value of 0 indicates that the error is generated client-side).
|
||||||
|
*/
|
||||||
|
SYNC_DB_SOFT_FETCH_ERROR = 6020,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The sync service first found information, and then later not. This could happen if a backup was garbage collected just when it was being accessed. Trying again may give a different answer.
|
||||||
|
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
|
||||||
|
* (A value of 0 indicates that the error is generated client-side).
|
||||||
|
*/
|
||||||
|
SYNC_DB_INCONSISTENT_FETCH_ERROR = 6021,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The "Content-length" field for the upload is missing.
|
||||||
|
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
|
||||||
|
* (A value of 0 indicates that the error is generated client-side).
|
||||||
|
*/
|
||||||
|
SYNC_MISSING_CONTENT_LENGTH = 6022,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The wallet does not implement a version of the exchange protocol that is compatible with the protocol version of the exchange.
|
* The wallet does not implement a version of the exchange protocol that is compatible with the protocol version of the exchange.
|
||||||
* Returned with an HTTP status code of #MHD_HTTP_NOT_IMPLEMENTED (501).
|
* Returned with an HTTP status code of #MHD_HTTP_NOT_IMPLEMENTED (501).
|
||||||
@ -3216,6 +3265,13 @@ export enum TalerErrorCode {
|
|||||||
*/
|
*/
|
||||||
WALLET_ORDER_ALREADY_CLAIMED = 7014,
|
WALLET_ORDER_ALREADY_CLAIMED = 7014,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A group of withdrawal operations (typically for the same reserve at the same exchange) has errors and will be tried again later.
|
||||||
|
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
|
||||||
|
* (A value of 0 indicates that the error is generated client-side).
|
||||||
|
*/
|
||||||
|
WALLET_WITHDRAWAL_GROUP_INCOMPLETE = 7015,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* End of error code range.
|
* End of error code range.
|
||||||
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
|
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
|
||||||
|
@ -66,8 +66,8 @@ export function makeErrorDetails(
|
|||||||
details: Record<string, unknown>,
|
details: Record<string, unknown>,
|
||||||
): OperationErrorDetails {
|
): OperationErrorDetails {
|
||||||
return {
|
return {
|
||||||
talerErrorCode: ec,
|
code: ec,
|
||||||
talerErrorHint: `Error: ${TalerErrorCode[ec]}`,
|
hint: `Error: ${TalerErrorCode[ec]}`,
|
||||||
details: details,
|
details: details,
|
||||||
message,
|
message,
|
||||||
};
|
};
|
||||||
|
@ -262,6 +262,7 @@ async function gatherWithdrawalPending(
|
|||||||
source: wsr.source,
|
source: wsr.source,
|
||||||
withdrawalGroupId: wsr.withdrawalGroupId,
|
withdrawalGroupId: wsr.withdrawalGroupId,
|
||||||
lastError: wsr.lastError,
|
lastError: wsr.lastError,
|
||||||
|
retryInfo: wsr.retryInfo,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -165,6 +165,7 @@ export async function getTransactions(
|
|||||||
TransactionType.Withdrawal,
|
TransactionType.Withdrawal,
|
||||||
wsr.withdrawalGroupId,
|
wsr.withdrawalGroupId,
|
||||||
),
|
),
|
||||||
|
...(wsr.lastError ? { error: wsr.lastError} : {}),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
@ -59,6 +59,7 @@ import {
|
|||||||
import { readSuccessResponseJsonOrThrow } from "../util/http";
|
import { readSuccessResponseJsonOrThrow } from "../util/http";
|
||||||
import { URL } from "../util/url";
|
import { URL } from "../util/url";
|
||||||
import { TalerErrorCode } from "../TalerErrorCode";
|
import { TalerErrorCode } from "../TalerErrorCode";
|
||||||
|
import { encodeCrock } from "../crypto/talerCrypto";
|
||||||
|
|
||||||
const logger = new Logger("withdraw.ts");
|
const logger = new Logger("withdraw.ts");
|
||||||
|
|
||||||
@ -558,9 +559,6 @@ async function incrementWithdrawalRetry(
|
|||||||
if (!wsr) {
|
if (!wsr) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!wsr.retryInfo) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
wsr.retryInfo.retryCounter++;
|
wsr.retryInfo.retryCounter++;
|
||||||
updateRetryInfoTimeout(wsr.retryInfo);
|
updateRetryInfoTimeout(wsr.retryInfo);
|
||||||
wsr.lastError = err;
|
wsr.lastError = err;
|
||||||
@ -647,12 +645,13 @@ async function processWithdrawGroupImpl(
|
|||||||
|
|
||||||
let numFinished = 0;
|
let numFinished = 0;
|
||||||
let finishedForFirstTime = false;
|
let finishedForFirstTime = false;
|
||||||
|
let errorsPerCoin: Record<number, OperationErrorDetails> = {};
|
||||||
|
|
||||||
await ws.db.runWithWriteTransaction(
|
await ws.db.runWithWriteTransaction(
|
||||||
[Stores.coins, Stores.withdrawalGroups, Stores.reserves, Stores.planchets],
|
[Stores.coins, Stores.withdrawalGroups, Stores.reserves, Stores.planchets],
|
||||||
async (tx) => {
|
async (tx) => {
|
||||||
const ws = await tx.get(Stores.withdrawalGroups, withdrawalGroupId);
|
const wg = await tx.get(Stores.withdrawalGroups, withdrawalGroupId);
|
||||||
if (!ws) {
|
if (!wg) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -662,22 +661,29 @@ async function processWithdrawGroupImpl(
|
|||||||
if (x.withdrawalDone) {
|
if (x.withdrawalDone) {
|
||||||
numFinished++;
|
numFinished++;
|
||||||
}
|
}
|
||||||
|
if (x.lastError) {
|
||||||
|
errorsPerCoin[x.coinIdx] = x.lastError;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
logger.trace(`now withdrawn ${numFinished} of ${numTotalCoins} coins`);
|
||||||
if (ws.timestampFinish === undefined && numFinished == numTotalCoins) {
|
if (wg.timestampFinish === undefined && numFinished === numTotalCoins) {
|
||||||
finishedForFirstTime = true;
|
finishedForFirstTime = true;
|
||||||
ws.timestampFinish = getTimestampNow();
|
wg.timestampFinish = getTimestampNow();
|
||||||
ws.lastError = undefined;
|
wg.lastError = undefined;
|
||||||
ws.retryInfo = initRetryInfo(false);
|
wg.retryInfo = initRetryInfo(false);
|
||||||
}
|
}
|
||||||
await tx.put(Stores.withdrawalGroups, ws);
|
|
||||||
|
await tx.put(Stores.withdrawalGroups, wg);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
if (numFinished != numTotalCoins) {
|
if (numFinished != numTotalCoins) {
|
||||||
// FIXME: aggregate individual problems into the big error message here.
|
throw OperationFailedError.fromCode(
|
||||||
throw Error(
|
TalerErrorCode.WALLET_WITHDRAWAL_GROUP_INCOMPLETE,
|
||||||
`withdrawal did not finish (${numFinished} / ${numTotalCoins} coins withdrawn)`,
|
`withdrawal did not finish (${numFinished} / ${numTotalCoins} coins withdrawn)`,
|
||||||
|
{
|
||||||
|
errorsPerCoin,
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -210,6 +210,7 @@ export interface PendingWithdrawOperation {
|
|||||||
type: PendingOperationType.Withdraw;
|
type: PendingOperationType.Withdraw;
|
||||||
source: WithdrawalSource;
|
source: WithdrawalSource;
|
||||||
lastError: OperationErrorDetails | undefined;
|
lastError: OperationErrorDetails | undefined;
|
||||||
|
retryInfo: RetryInfo;
|
||||||
withdrawalGroupId: string;
|
withdrawalGroupId: string;
|
||||||
numCoinsWithdrawn: number;
|
numCoinsWithdrawn: number;
|
||||||
numCoinsTotal: number;
|
numCoinsTotal: number;
|
||||||
@ -229,6 +230,12 @@ export interface PendingOperationInfoCommon {
|
|||||||
* as opposed to some regular scheduled operation or a permanent failure.
|
* as opposed to some regular scheduled operation or a permanent failure.
|
||||||
*/
|
*/
|
||||||
givesLifeness: boolean;
|
givesLifeness: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retry info, not available on all pending operations.
|
||||||
|
* If it is available, it must have the same name.
|
||||||
|
*/
|
||||||
|
retryInfo?: RetryInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -42,6 +42,7 @@ import {
|
|||||||
codecForList,
|
codecForList,
|
||||||
codecForAny,
|
codecForAny,
|
||||||
} from "../util/codec";
|
} from "../util/codec";
|
||||||
|
import { OperationErrorDetails } from "./walletTypes";
|
||||||
|
|
||||||
export interface TransactionsRequest {
|
export interface TransactionsRequest {
|
||||||
/**
|
/**
|
||||||
@ -63,24 +64,6 @@ export interface TransactionsResponse {
|
|||||||
transactions: Transaction[];
|
transactions: Transaction[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TransactionError {
|
|
||||||
/**
|
|
||||||
* TALER_EC_* unique error code.
|
|
||||||
* The action(s) offered and message displayed on the transaction item depend on this code.
|
|
||||||
*/
|
|
||||||
ec: number;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* English-only error hint, if available.
|
|
||||||
*/
|
|
||||||
hint?: string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Error details specific to "ec", if applicable/available
|
|
||||||
*/
|
|
||||||
details?: any;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface TransactionCommon {
|
export interface TransactionCommon {
|
||||||
// opaque unique ID for the transaction, used as a starting point for paginating queries
|
// opaque unique ID for the transaction, used as a starting point for paginating queries
|
||||||
// and for invoking actions on the transaction (e.g. deleting/hiding it from the history)
|
// and for invoking actions on the transaction (e.g. deleting/hiding it from the history)
|
||||||
@ -103,7 +86,7 @@ export interface TransactionCommon {
|
|||||||
// Amount added or removed from the wallet's balance (including all fees and other costs)
|
// Amount added or removed from the wallet's balance (including all fees and other costs)
|
||||||
amountEffective: AmountString;
|
amountEffective: AmountString;
|
||||||
|
|
||||||
error?: TransactionError;
|
error?: OperationErrorDetails;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Transaction =
|
export type Transaction =
|
||||||
|
@ -51,7 +51,6 @@ import {
|
|||||||
buildCodecForUnion,
|
buildCodecForUnion,
|
||||||
} from "../util/codec";
|
} from "../util/codec";
|
||||||
import { AmountString, codecForContractTerms, ContractTerms } from "./talerTypes";
|
import { AmountString, codecForContractTerms, ContractTerms } from "./talerTypes";
|
||||||
import { TransactionError, OrderShortInfo, codecForOrderShortInfo } from "./transactions";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Response for the create reserve request to the wallet.
|
* Response for the create reserve request to the wallet.
|
||||||
@ -215,7 +214,7 @@ export interface ConfirmPayResultDone {
|
|||||||
export interface ConfirmPayResultPending {
|
export interface ConfirmPayResultPending {
|
||||||
type: ConfirmPayResultType.Pending;
|
type: ConfirmPayResultType.Pending;
|
||||||
|
|
||||||
lastError: TransactionError;
|
lastError: OperationErrorDetails;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ConfirmPayResult = ConfirmPayResultDone | ConfirmPayResultPending;
|
export type ConfirmPayResult = ConfirmPayResultDone | ConfirmPayResultPending;
|
||||||
|
@ -299,10 +299,15 @@ export class Wallet {
|
|||||||
* liveness left. The wallet will be in a stopped state when this function
|
* liveness left. The wallet will be in a stopped state when this function
|
||||||
* returns without resolving to an exception.
|
* returns without resolving to an exception.
|
||||||
*/
|
*/
|
||||||
public async runUntilDone(): Promise<void> {
|
public async runUntilDone(
|
||||||
|
req: {
|
||||||
|
maxRetries?: number;
|
||||||
|
} = {},
|
||||||
|
): Promise<void> {
|
||||||
let done = false;
|
let done = false;
|
||||||
const p = new Promise((resolve, reject) => {
|
const p = new Promise((resolve, reject) => {
|
||||||
// Run this asynchronously
|
// Monitor for conditions that means we're done or we
|
||||||
|
// should quit with an error (due to exceeded retries).
|
||||||
this.addNotificationListener((n) => {
|
this.addNotificationListener((n) => {
|
||||||
if (done) {
|
if (done) {
|
||||||
return;
|
return;
|
||||||
@ -315,7 +320,29 @@ export class Wallet {
|
|||||||
logger.trace("no liveness-giving operations left");
|
logger.trace("no liveness-giving operations left");
|
||||||
resolve();
|
resolve();
|
||||||
}
|
}
|
||||||
|
const maxRetries = req.maxRetries;
|
||||||
|
if (!maxRetries) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.getPendingOperations({ onlyDue: false })
|
||||||
|
.then((pending) => {
|
||||||
|
for (const p of pending.pendingOperations) {
|
||||||
|
if (p.retryInfo && p.retryInfo.retryCounter > maxRetries) {
|
||||||
|
console.warn(
|
||||||
|
`stopping, as ${maxRetries} retries are exceeded in an operation of type ${p.type}`,
|
||||||
|
);
|
||||||
|
this.stop();
|
||||||
|
done = true;
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
logger.error(e);
|
||||||
|
reject(e);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
// Run this asynchronously
|
||||||
this.runRetryLoop().catch((e) => {
|
this.runRetryLoop().catch((e) => {
|
||||||
logger.error("exception in wallet retry loop");
|
logger.error("exception in wallet retry loop");
|
||||||
reject(e);
|
reject(e);
|
||||||
@ -324,16 +351,6 @@ export class Wallet {
|
|||||||
await p;
|
await p;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Run the wallet until there are no more pending operations that give
|
|
||||||
* liveness left. The wallet will be in a stopped state when this function
|
|
||||||
* returns without resolving to an exception.
|
|
||||||
*/
|
|
||||||
public async runUntilDoneAndStop(): Promise<void> {
|
|
||||||
await this.runUntilDone();
|
|
||||||
logger.trace("stopping after liveness-giving operations done");
|
|
||||||
this.stop();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Process pending operations and wait for scheduled operations in
|
* Process pending operations and wait for scheduled operations in
|
||||||
@ -392,7 +409,7 @@ export class Wallet {
|
|||||||
if (e instanceof OperationFailedAndReportedError) {
|
if (e instanceof OperationFailedAndReportedError) {
|
||||||
logger.warn("operation processed resulted in reported error");
|
logger.warn("operation processed resulted in reported error");
|
||||||
} else {
|
} else {
|
||||||
console.error("Uncaught exception", e);
|
logger.error("Uncaught exception", e);
|
||||||
this.ws.notify({
|
this.ws.notify({
|
||||||
type: NotificationType.InternalError,
|
type: NotificationType.InternalError,
|
||||||
message: "uncaught exception",
|
message: "uncaught exception",
|
||||||
@ -902,10 +919,13 @@ export class Wallet {
|
|||||||
return getTransactions(this.ws, request);
|
return getTransactions(this.ws, request);
|
||||||
}
|
}
|
||||||
|
|
||||||
async withdrawTestBalance(
|
async withdrawTestBalance(req: WithdrawTestBalanceRequest): Promise<void> {
|
||||||
req: WithdrawTestBalanceRequest,
|
await withdrawTestBalance(
|
||||||
): Promise<void> {
|
this.ws,
|
||||||
await withdrawTestBalance(this.ws, req.amount, req.bankBaseUrl, req.exchangeBaseUrl);
|
req.amount,
|
||||||
|
req.bankBaseUrl,
|
||||||
|
req.exchangeBaseUrl,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async runIntegrationtest(args: IntegrationTestArgs): Promise<void> {
|
async runIntegrationtest(args: IntegrationTestArgs): Promise<void> {
|
||||||
@ -940,12 +960,12 @@ export class Wallet {
|
|||||||
case "runIntegrationTest": {
|
case "runIntegrationTest": {
|
||||||
const req = codecForIntegrationTestArgs().decode(payload);
|
const req = codecForIntegrationTestArgs().decode(payload);
|
||||||
await this.runIntegrationtest(req);
|
await this.runIntegrationtest(req);
|
||||||
return {}
|
return {};
|
||||||
}
|
}
|
||||||
case "testPay": {
|
case "testPay": {
|
||||||
const req = codecForTestPayArgs().decode(payload);
|
const req = codecForTestPayArgs().decode(payload);
|
||||||
await this.testPay(req);
|
await this.testPay(req);
|
||||||
return {}
|
return {};
|
||||||
}
|
}
|
||||||
case "getTransactions": {
|
case "getTransactions": {
|
||||||
const req = codecForTransactionsRequest().decode(payload);
|
const req = codecForTransactionsRequest().decode(payload);
|
||||||
@ -988,10 +1008,7 @@ export class Wallet {
|
|||||||
}
|
}
|
||||||
case "setExchangeTosAccepted": {
|
case "setExchangeTosAccepted": {
|
||||||
const req = codecForAcceptExchangeTosRequest().decode(payload);
|
const req = codecForAcceptExchangeTosRequest().decode(payload);
|
||||||
await this.acceptExchangeTermsOfService(
|
await this.acceptExchangeTermsOfService(req.exchangeBaseUrl, req.etag);
|
||||||
req.exchangeBaseUrl,
|
|
||||||
req.etag,
|
|
||||||
);
|
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
case "applyRefund": {
|
case "applyRefund": {
|
||||||
|
Loading…
Reference in New Issue
Block a user