commit a72cb0208c7202a69af1c886d08f2f568041df89
parent acf564b9e4996043b8ce9cd8dcdca1fb291d211a
Author: Florian Dold <dold@taler.net>
Date: Thu, 30 Jul 2026 14:18:38 +0200
wallet-core: raise coded errors for client-visible failures
Bad arguments, missing records, KYC limits and peer-payment outcomes were
plain exceptions, so every one of them reached clients as
WALLET_UNEXPECTED_EXCEPTION with nothing to branch on. Paying an invoice
also reported the peer-push insufficient-balance code rather than the pull
one, and parseTransactionIdentifier threw for some malformed identifiers
while returning undefined for others, so the throwing paths escaped past
its callers' own checks.
Diffstat:
24 files changed, 828 insertions(+), 297 deletions(-)
diff --git a/packages/taler-harness/src/integrationtests/test-peer-pull.ts b/packages/taler-harness/src/integrationtests/test-peer-pull.ts
@@ -144,7 +144,7 @@ export async function runPeerPullTest(t: GlobalTestState) {
);
t.assertTrue(
insufficient_balance.errorDetail.code ===
- TalerErrorCode.WALLET_PEER_PUSH_PAYMENT_INSUFFICIENT_BALANCE,
+ TalerErrorCode.WALLET_PEER_PULL_PAYMENT_INSUFFICIENT_BALANCE,
);
const unknown_purse = await t.assertThrowsTalerErrorAsync(async () =>
@@ -153,10 +153,9 @@ export async function runPeerPullTest(t: GlobalTestState) {
"taler+http://pay-pull/localhost:8081/MQP1DP1J94ZZWNQS7TRDF1KJZ7V8H74CZF41V90FKXBPN5GNRN6G",
}),
);
- // FIXME this should fail with a proper error code
t.assertTrue(
unknown_purse.errorDetail.code ===
- TalerErrorCode.WALLET_UNEXPECTED_EXCEPTION,
+ TalerErrorCode.WALLET_PEER_CONTRACT_NOT_FOUND,
);
}
@@ -231,10 +230,9 @@ export async function runPeerPullTest(t: GlobalTestState) {
talerUri: tx.talerUri!,
}),
);
- // FIXME this should fail with a proper error code
t.assertTrue(
completed_purse.errorDetail.code ===
- TalerErrorCode.WALLET_UNEXPECTED_EXCEPTION,
+ TalerErrorCode.WALLET_PEER_PULL_DEBIT_ALREADY_PAID,
);
}
@@ -387,10 +385,14 @@ export async function runPeerPullTest(t: GlobalTestState) {
talerUri: tx.talerUri!,
}),
);
- // FIXME this should fail with a proper error code
+ // Whether the exchange drops the contract along with the purse or keeps
+ // it and only reports the purse as gone decides which of the two the
+ // wallet sees, so accept either.
t.assertTrue(
aborted_contract.errorDetail.code ===
- TalerErrorCode.WALLET_UNEXPECTED_EXCEPTION,
+ TalerErrorCode.WALLET_PEER_CONTRACT_NOT_FOUND ||
+ aborted_contract.errorDetail.code ===
+ TalerErrorCode.WALLET_PEER_PULL_DEBIT_PURSE_GONE,
);
}
diff --git a/packages/taler-harness/src/integrationtests/test-peer-push.ts b/packages/taler-harness/src/integrationtests/test-peer-push.ts
@@ -158,10 +158,9 @@ export async function runPeerPushTest(t: GlobalTestState) {
"taler+http://pay-push/localhost:8081/MQP1DP1J94ZZWNQS7TRDF1KJZ7V8H74CZF41V90FKXBPN5GNRN6G",
}),
);
- // FIXME this should fail with a proper error code
t.assertTrue(
unknown_purse.errorDetail.code ===
- TalerErrorCode.WALLET_UNEXPECTED_EXCEPTION,
+ TalerErrorCode.WALLET_PEER_CONTRACT_NOT_FOUND,
);
}
@@ -403,57 +402,63 @@ export async function runPeerPushTest(t: GlobalTestState) {
});
});
- await t.runSpanAsync("P2P push abort after the purse was merged", async () => {
- const tx = await initPeerPushDebit("abort after merge");
+ await t.runSpanAsync(
+ "P2P push abort after the purse was merged",
+ async () => {
+ const tx = await initPeerPushDebit("abort after merge");
- const prepare4 = await wallet4.call(
- WalletApiOperation.PreparePeerPushCredit,
- {
- talerUri: tx.talerUri!,
- },
- );
+ const prepare4 = await wallet4.call(
+ WalletApiOperation.PreparePeerPushCredit,
+ {
+ talerUri: tx.talerUri!,
+ },
+ );
- // Suspend the sender so that it does not observe the merge on its own.
- // The abort below is then the first request that learns about it, which is
- // the ordering that leaves the exchange answering the purse deletion with
- // a conflict.
- await wallet1.call(WalletApiOperation.SuspendTransaction, {
- transactionId: tx.transactionId,
- });
- await wallet1.call(WalletApiOperation.TestingWaitTransactionState, {
- transactionId: tx.transactionId,
- txState: {
- major: TransactionMajorState.Suspended,
- minor: TransactionMinorState.Ready,
- },
- });
+ // Suspend the sender so that it does not observe the merge on its own.
+ // The abort below is then the first request that learns about it, which is
+ // the ordering that leaves the exchange answering the purse deletion with
+ // a conflict.
+ await wallet1.call(WalletApiOperation.SuspendTransaction, {
+ transactionId: tx.transactionId,
+ });
+ await wallet1.call(WalletApiOperation.TestingWaitTransactionState, {
+ transactionId: tx.transactionId,
+ txState: {
+ major: TransactionMajorState.Suspended,
+ minor: TransactionMinorState.Ready,
+ },
+ });
- await wallet4.call(WalletApiOperation.ConfirmPeerPushCredit, {
- transactionId: prepare4.transactionId,
- });
- await wallet4.call(WalletApiOperation.TestingWaitTransactionState, {
- transactionId: prepare4.transactionId,
- txState: {
- major: TransactionMajorState.Done,
- },
- });
+ await wallet4.call(WalletApiOperation.ConfirmPeerPushCredit, {
+ transactionId: prepare4.transactionId,
+ });
+ await wallet4.call(WalletApiOperation.TestingWaitTransactionState, {
+ transactionId: prepare4.transactionId,
+ txState: {
+ major: TransactionMajorState.Done,
+ },
+ });
- // The recipient has the money, so the payment cannot be taken back.
- await wallet1.call(WalletApiOperation.AbortTransaction, {
- transactionId: tx.transactionId,
- });
- await wallet1.call(WalletApiOperation.TestingWaitTransactionState, {
- transactionId: tx.transactionId,
- txState: {
- major: TransactionMajorState.Done,
- },
- });
+ // The recipient has the money, so the payment cannot be taken back.
+ await wallet1.call(WalletApiOperation.AbortTransaction, {
+ transactionId: tx.transactionId,
+ });
+ await wallet1.call(WalletApiOperation.TestingWaitTransactionState, {
+ transactionId: tx.transactionId,
+ txState: {
+ major: TransactionMajorState.Done,
+ },
+ });
- const finalTx = await wallet1.call(WalletApiOperation.GetTransactionById, {
- transactionId: tx.transactionId,
- });
- t.assertDeepEqual(finalTx.abortReason, undefined);
- });
+ const finalTx = await wallet1.call(
+ WalletApiOperation.GetTransactionById,
+ {
+ transactionId: tx.transactionId,
+ },
+ );
+ t.assertDeepEqual(finalTx.abortReason, undefined);
+ },
+ );
await t.runSpanAsync("P2P push abort before create purse", async () => {
// Make sure the reserve can't be created.
diff --git a/packages/taler-wallet-core/src/coinSelection.ts b/packages/taler-wallet-core/src/coinSelection.ts
@@ -228,12 +228,7 @@ async function internalSelectPayCoins(
> {
let restrictWireMethod;
if (req.depositPaytoUri) {
- const parsedPayto = Result.orUndefined(
- Paytos.fromString(req.depositPaytoUri),
- );
- if (!parsedPayto) {
- throw Error("invalid payto URI");
- }
+ const parsedPayto = Paytos.parseOrThrow(req.depositPaytoUri);
restrictWireMethod = parsedPayto.targetType;
if (restrictWireMethod !== req.restrictWireMethod) {
logger.warn(`conflicting payto URI and wire method restriction`);
@@ -1601,10 +1596,7 @@ export async function getMaxDepositAmount(
},
);
if (req.depositPaytoUri) {
- const p = Result.orUndefined(Paytos.fromString(req.depositPaytoUri));
- if (!p) {
- throw Error("invalid payto URI");
- }
+ const p = Paytos.parseOrThrow(req.depositPaytoUri);
restrictWireMethod = p.targetType;
}
const candidateRes = await selectPayCandidates(wex, tx, {
diff --git a/packages/taler-wallet-core/src/deposits.ts b/packages/taler-wallet-core/src/deposits.ts
@@ -147,6 +147,7 @@ import {
applyNotifyTransition,
constructTransactionIdentifier,
isUnsuccessfulTransaction,
+ makeInvalidTransactionIdError,
parseTransactionIdentifier,
} from "./transactions.js";
import {
@@ -1996,10 +1997,7 @@ export async function internalCheckDepositGroup(
wex: WalletExecutionContext,
req: CheckDepositRequest,
): Promise<CheckDepositResponse> {
- const p = Result.orUndefined(Paytos.fromString(req.depositPaytoUri));
- if (!p) {
- throw Error("invalid payto URI");
- }
+ const p = Paytos.parseOrThrow(req.depositPaytoUri);
const amount = Amounts.parseOrThrow(req.amount);
const currency = Amounts.currencyOf(amount);
@@ -2009,7 +2007,11 @@ export async function internalCheckDepositGroup(
});
if (exchangeInfos.length == 0) {
- throw Error("no exchanges possible for deposit");
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_NO_SUITABLE_EXCHANGE,
+ { currency, wireMethod: p.targetType },
+ "no exchange in the wallet can be used for this deposit",
+ );
}
const depositFeeLimit = amount;
@@ -2099,12 +2101,7 @@ async function internalCreateDepositGroup(
wex: WalletExecutionContext,
req: CreateDepositGroupRequest,
): Promise<CreateDepositGroupResponse> {
- const depositPayto = Result.orUndefined(
- Paytos.fromString(req.depositPaytoUri),
- );
- if (!depositPayto) {
- throw Error("invalid payto URI");
- }
+ const depositPayto = Paytos.parseOrThrow(req.depositPaytoUri);
const amount = Amounts.parseOrThrow(req.amount);
const currency = amount.currency;
@@ -2115,7 +2112,11 @@ async function internalCreateDepositGroup(
});
if (exchangeInfos.length == 0) {
- throw Error("no exchanges possible for deposit");
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_NO_SUITABLE_EXCHANGE,
+ { currency, wireMethod: depositPayto.targetType },
+ "no exchange in the wallet can be used for this deposit",
+ );
}
const now = AbsoluteTime.now();
@@ -2168,7 +2169,14 @@ async function internalCreateDepositGroup(
}
if (checkDepositHardLimitExceeded(exchanges, req.amount)) {
- throw Error("deposit would exceed hard KYC limit");
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_KYC_LIMIT_EXCEEDED,
+ {
+ exchangeBaseUrl: exchanges[0]?.exchangeBaseUrl,
+ requestedAmount: req.amount,
+ },
+ "deposit would exceed a hard limit of the exchange",
+ );
}
let merchantPair: EddsaKeyPairStrings;
@@ -2186,7 +2194,11 @@ async function internalCreateDepositGroup(
coins[0].exchangeBaseUrl,
);
} else {
- throw Error("refusing to create deposit with zero coins");
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_CORE_API_BAD_REQUEST,
+ { parameter: "amount" },
+ "refusing to create a deposit that would not spend any coins",
+ );
}
const noncePair = await wex.cryptoApi.createEddsaKeypair({});
@@ -2232,8 +2244,10 @@ async function internalCreateDepositGroup(
contractData.contractTerms.version !== undefined &&
contractData.contractTerms.version !== MerchantContractVersion.V0
) {
- throw Error(
- `unsupported contract version ${contractData.contractTerms.version}`,
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_CONTRACT_TERMS_UNSUPPORTED,
+ {},
+ `this wallet does not support contract version ${contractData.contractTerms.version}`,
);
}
@@ -2243,7 +2257,10 @@ async function internalCreateDepositGroup(
if (req.transactionId) {
const txId = parseTransactionIdentifier(req.transactionId);
if (!txId || txId.tag !== TransactionType.Deposit) {
- throw Error("invalid transaction ID");
+ throw makeInvalidTransactionIdError(
+ req.transactionId,
+ TransactionType.Deposit,
+ );
}
depositGroupId = txId.depositGroupId;
} else {
diff --git a/packages/taler-wallet-core/src/donau.ts b/packages/taler-wallet-core/src/donau.ts
@@ -53,6 +53,8 @@ import {
SignedTokenEnvelope,
stringToBytes,
succeedOrThrow,
+ TalerError,
+ TalerErrorCode,
} from "@gnu-taler/taler-util";
import {
ConfigRecordKey,
@@ -552,7 +554,11 @@ export async function acceptDonauBlindSigs(
donauBlindedSigs: SignedTokenEnvelope[],
): Promise<void> {
if (donauPlanchets.length != donauBlindedSigs.length) {
- throw Error();
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_TRANSACTION_PROTOCOL_VIOLATION,
+ {},
+ `donau returned ${donauBlindedSigs.length} signatures for ${donauPlanchets.length} planchets`,
+ );
}
const client = new DonauHttpClient(donauBaseUrl);
@@ -581,11 +587,19 @@ export async function acceptDonauBlindSigs(
}
}
if (!unitKey) {
- throw Error("donation unit key not found");
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_TRANSACTION_PROTOCOL_VIOLATION,
+ {},
+ "donau signed with a donation unit key that it does not offer",
+ );
}
logger.info(`found unit key ${j2s(unitKey)}`);
if (myBlindSig.cipher !== DenomKeyType.Rsa) {
- throw Error("only RSA supported");
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_TRANSACTION_PROTOCOL_VIOLATION,
+ {},
+ `this wallet only supports RSA donation units, donau used ${myBlindSig.cipher}`,
+ );
}
const unblindSig = await wex.cryptoApi.rsaUnblind({
pk: unitKey.donation_unit_pub.rsa_public_key,
@@ -608,7 +622,11 @@ export async function acceptDonauBlindSigs(
sig: unblindSig.sig,
});
if (!verifyRes.valid) {
- throw Error("invalid donau signature");
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_TRANSACTION_PROTOCOL_VIOLATION,
+ {},
+ "the donation receipt signature from donau is invalid",
+ );
}
sigs.push({
cipher: "RSA",
diff --git a/packages/taler-wallet-core/src/exchanges.ts b/packages/taler-wallet-core/src/exchanges.ts
@@ -171,6 +171,7 @@ import {
BalanceEffect,
applyNotifyTransition,
constructTransactionIdentifier,
+ makeTransactionActionUnsupportedError,
rematerializeTransactions,
} from "./transactions.js";
import { WALLET_EXCHANGE_PROTOCOL_VERSION } from "./versions.js";
@@ -616,7 +617,11 @@ async function validateWireInfo(
isValid = v;
}
if (!isValid) {
- throw Error("exchange acct signature invalid");
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_EXCHANGE_SIGNATURE_INVALID,
+ {},
+ `signature of exchange account ${a.payto_uri} is invalid`,
+ );
}
}
logger.trace("account validation done");
@@ -645,7 +650,11 @@ async function validateWireInfo(
isValid = v;
}
if (!isValid) {
- throw Error("exchange wire fee signature invalid");
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_EXCHANGE_SIGNATURE_INVALID,
+ {},
+ `signature of the exchange's wire fee for ${wireMethod} is invalid`,
+ );
}
feeList.push(fee);
}
@@ -684,7 +693,11 @@ async function validateSignKeys(
isValid = v;
}
if (!isValid) {
- throw Error("exchange signing key signature invalid: " + sk.key);
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_EXCHANGE_SIGNATURE_INVALID,
+ {},
+ `signature of the exchange's signing key ${sk.key} is invalid`,
+ );
}
}
}
@@ -709,7 +722,11 @@ async function validateGlobalFees(
}
if (!isValid) {
- throw Error("exchange global fees signature invalid: " + gf.master_sig);
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_EXCHANGE_SIGNATURE_INVALID,
+ {},
+ `signature of the exchange's global fees is invalid`,
+ );
}
egf.push({
accountFee: Amounts.stringify(gf.account_fee),
@@ -1267,8 +1284,10 @@ function checkExpectedMasterPub(
expectedMasterPub: string | undefined,
): void {
if (expectedMasterPub && summary.masterPub !== expectedMasterPub) {
- throw Error(
- "public key of the exchange does not match expected public key",
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_EXCHANGE_SIGNATURE_INVALID,
+ {},
+ "the exchange's master public key does not match the expected one",
);
}
}
@@ -2589,27 +2608,19 @@ export class DenomLossTransactionContext implements TransactionContext {
}
userAbortTransaction(): Promise<void> {
- throw new Error(
- "Method not implemented - DenomLossTransactionContext.userAbortTransaction",
- );
+ throw makeTransactionActionUnsupportedError(this.transactionId, "abort");
}
userSuspendTransaction(): Promise<void> {
- throw new Error(
- "Method not implemented - DenomLossTransactionContext.userSuspendTransaction",
- );
+ throw makeTransactionActionUnsupportedError(this.transactionId, "suspend");
}
userResumeTransaction(): Promise<void> {
- throw new Error(
- "Method not implemented - DenomLossTransactionContext.userResumeTransaction",
- );
+ throw makeTransactionActionUnsupportedError(this.transactionId, "resume");
}
userFailTransaction(): Promise<void> {
- throw new Error(
- "Method not implemented - DenomLossTransactionContext.userResumeTransaction",
- );
+ throw makeTransactionActionUnsupportedError(this.transactionId, "fail");
}
async userDeleteTransaction(): Promise<void> {
@@ -2751,7 +2762,9 @@ export async function getExchangePaytoUri(
return account.payto_uri;
}
}
- throw Error(
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_NO_SUITABLE_EXCHANGE,
+ { wireMethod: supportedTargetTypes[0] },
`no matching account found at exchange ${exchangeBaseUrl} for wire types ${j2s(
supportedTargetTypes,
)}`,
@@ -3010,7 +3023,11 @@ export async function getExchangeDetailedInfo(
});
if (!exchange) {
- throw Error(`exchange with base url "${exchangeBaseurl}" not found`);
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_EXCHANGE_ENTRY_NOT_FOUND,
+ {},
+ `exchange with base URL "${exchangeBaseurl}" not found`,
+ );
}
const denoms = exchange.denominations.map((d) => ({
@@ -4092,7 +4109,11 @@ export async function checkExchangeInScopeTx(
return gr != null;
}
case ScopeType.Auditor:
- throw Error("auditor scope not supported yet");
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_CORE_API_BAD_REQUEST,
+ { parameter: "scopeInfo" },
+ "the auditor scope is not supported yet",
+ );
}
}
diff --git a/packages/taler-wallet-core/src/instructedAmountConversion.ts b/packages/taler-wallet-core/src/instructedAmountConversion.ts
@@ -22,6 +22,8 @@ import {
Amounts,
ConvertAmountRequest,
Duration,
+ TalerError,
+ TalerErrorCode,
TransactionAmountMode,
TransactionType,
checkDbInvariant,
@@ -71,7 +73,11 @@ function getOperationType(txType: TransactionType): OperationType {
? OperationType.Debit
: undefined;
if (!operationType) {
- throw Error(`operation type ${txType} not yet supported`);
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_CORE_API_BAD_REQUEST,
+ { parameter: "type" },
+ `conversion for operation type ${txType} is not supported`,
+ );
}
return operationType;
}
@@ -149,8 +155,10 @@ async function getAvailableCoins(
exchangeDetails.wireInfo.feesForType[filters.wireMethod];
if (!wireMethodWithDates) {
- throw Error(
- `exchange ${exchangeBaseUrl} doesn't have wire method ${filters.wireMethod}`,
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_NO_SUITABLE_EXCHANGE,
+ { wireMethod: filters.wireMethod },
+ `exchange ${exchangeBaseUrl} does not support wire method ${filters.wireMethod}`,
);
}
const wireMethodFee = wireMethodWithDates.find((x) => {
diff --git a/packages/taler-wallet-core/src/mailbox.ts b/packages/taler-wallet-core/src/mailbox.ts
@@ -40,6 +40,8 @@ import {
NotificationType,
Result,
SendTalerUriMailboxMessageRequest,
+ TalerError,
+ TalerErrorCode,
TalerMailboxInstanceHttpClient,
TalerProtocolTimestamp,
TalerSignaturePurpose,
@@ -130,7 +132,11 @@ export async function registerMailbox(
const vMsg = new DataView(messageHeader);
const vExpNbo = new DataView(expNboBuffer);
if (mailboxConf.expiration.t_s == "never") {
- throw Error("mailbox can not expire, invalid");
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_MAILBOX_UNAVAILABLE,
+ { mailboxBaseUrl: mailboxConf.mailboxBaseUrl },
+ "a mailbox registration must have an expiration",
+ );
}
vExpNbo.setBigUint64(0, BigInt(mailboxConf.expiration.t_s));
const digestBuffer = new Uint8Array([
@@ -167,7 +173,11 @@ export async function registerMailbox(
case "ok":
return resp.body;
case HttpStatusCode.Forbidden:
- throw Error("Access to Mailbox API unauthorized");
+ throw TalerError.fromDetail(
+ TalerErrorCode.GENERIC_FORBIDDEN,
+ {},
+ "the mailbox service refused the registration",
+ );
case HttpStatusCode.PaymentRequired:
return resp.body;
}
@@ -218,8 +228,14 @@ export async function createNewMailbox(
const resp = await registerMailbox(wex, mailboxConf);
if (resp.status == "payment-required") {
if (!resp.talerUri) {
- throw Error(
- "payment required to register mailbox but no Taler URI given",
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE,
+ {
+ requestUrl: mailboxConf.mailboxBaseUrl,
+ requestMethod: "POST",
+ httpStatusCode: HttpStatusCode.PaymentRequired,
+ },
+ "the mailbox service asked for payment but did not say how to pay",
);
}
mailboxConf.payUri = Result.orElse(
@@ -275,7 +291,11 @@ export async function refreshMailbox(
message_size = resConf.body.message_body_bytes;
break;
default:
- throw Error("unable to get mailbox service config");
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_MAILBOX_UNAVAILABLE,
+ { mailboxBaseUrl: mailboxConf.mailboxBaseUrl },
+ "could not read the configuration of the mailbox service",
+ );
}
const res = await mailboxClient.getMessages({
hMailbox: mailboxConf.hAddress,
@@ -287,8 +307,14 @@ export async function refreshMailbox(
const messages = res.body.messages;
const now = TalerProtocolTimestamp.now();
if (messages.byteLength % message_size !== 0) {
- throw Error(
- `mailbox messages response not a multiple of message size! (${messages.byteLength} % ${message_size} != 0)`,
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE,
+ {
+ requestUrl: mailboxConf.mailboxBaseUrl,
+ requestMethod: "GET",
+ httpStatusCode: HttpStatusCode.Ok,
+ },
+ `mailbox messages response is not a multiple of the message size (${messages.byteLength} % ${message_size} != 0)`,
);
}
// FIXME: if we have reached the maximum number of
@@ -343,7 +369,11 @@ export async function refreshMailbox(
messages: [],
}; // No new messages;
default:
- throw Error("unexpected mailbox messages response empty");
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_MAILBOX_UNAVAILABLE,
+ { mailboxBaseUrl: mailboxConf.mailboxBaseUrl },
+ "the mailbox service did not return the messages",
+ );
}
}
@@ -358,7 +388,11 @@ function encryptTalerUriMessage(
const talerUriBytes = stringToBytes(talerUri);
const paddingLength = paddedMessageSize - 4 - talerUriBytes.length - 16 - 32;
if (paddingLength < 0) {
- throw new Error("talerUri is too long for the requested paddedMessageSize");
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_MAILBOX_UNAVAILABLE,
+ {},
+ "the taler:// URI does not fit into a message of this mailbox service",
+ );
}
const header = new Uint8Array(4);
const v = new DataView(header.buffer);
@@ -393,7 +427,11 @@ export async function sendTalerUriMessage(
paddedMessageSize = resConf.body.message_body_bytes;
break;
default:
- throw Error("unable to get mailbox service config");
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_MAILBOX_UNAVAILABLE,
+ { mailboxBaseUrl: req.contact.mailboxBaseUri },
+ "could not read the configuration of the mailbox service",
+ );
}
const resKeys = await mailboxClient.getMailboxInfo(
req.contact.mailboxAddress,
@@ -404,7 +442,11 @@ export async function sendTalerUriMessage(
keys = resKeys.body;
break;
default:
- throw Error("unable to get mailbox keys");
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_MAILBOX_UNAVAILABLE,
+ { mailboxBaseUrl: req.contact.mailboxBaseUri },
+ "could not read the keys of the recipient's mailbox",
+ );
}
const encryptedMessage = encryptTalerUriMessage(
decodeCrock(keys.encryption_key),
@@ -419,6 +461,10 @@ export async function sendTalerUriMessage(
case "ok":
return {};
default:
- throw Error("Failed to send message");
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_MAILBOX_UNAVAILABLE,
+ { mailboxBaseUrl: req.contact.mailboxBaseUri },
+ "the mailbox service did not accept the message",
+ );
}
}
diff --git a/packages/taler-wallet-core/src/pay-merchant.ts b/packages/taler-wallet-core/src/pay-merchant.ts
@@ -170,6 +170,9 @@ import {
BalanceEffect,
constructTransactionIdentifier,
isUnsuccessfulTransaction,
+ makeInvalidTransactionIdError,
+ makeTransactionActionUnsupportedError,
+ makeTransactionNotFoundError,
parseTransactionIdentifier,
} from "./transactions.js";
import {
@@ -240,7 +243,7 @@ export class PayMerchantTransactionContext implements TransactionContext {
const proposalId = this.proposalId;
const purchaseRec = await tx.getPurchase(proposalId);
if (!purchaseRec) {
- throw Error("not found");
+ throw makeTransactionNotFoundError(this.transactionId);
}
const txState = computePayMerchantTransactionState(purchaseRec);
@@ -685,7 +688,7 @@ export class RefundTransactionContext implements TransactionContext {
): Promise<Transaction | undefined> {
const refundRecord = await tx.getRefundGroup(this.refundGroupId);
if (!refundRecord) {
- throw Error("not found");
+ throw makeTransactionNotFoundError(this.transactionId);
}
const maybeContractData = await lookupMaybeContractData(
tx,
@@ -774,19 +777,19 @@ export class RefundTransactionContext implements TransactionContext {
}
userSuspendTransaction(): Promise<void> {
- throw new Error("Unsupported operation");
+ throw makeTransactionActionUnsupportedError(this.transactionId, "suspend");
}
userAbortTransaction(): Promise<void> {
- throw new Error("Unsupported operation");
+ throw makeTransactionActionUnsupportedError(this.transactionId, "abort");
}
userResumeTransaction(): Promise<void> {
- throw new Error("Unsupported operation");
+ throw makeTransactionActionUnsupportedError(this.transactionId, "resume");
}
userFailTransaction(): Promise<void> {
- throw new Error("Unsupported operation");
+ throw makeTransactionActionUnsupportedError(this.transactionId, "fail");
}
}
@@ -1415,7 +1418,11 @@ async function generateSlate(
const choice = contractData.choices[choiceIndex];
const output = choice.outputs[outputIndex];
if (output.type !== MerchantContractOutputType.Token) {
- throw new Error(`unsupported contract output type ${output.type}`);
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_CONTRACT_TERMS_UNSUPPORTED,
+ {},
+ `this wallet does not support contract output type "${output.type}"`,
+ );
}
const family = contractData.token_families[output.token_family_slug];
@@ -2211,7 +2218,7 @@ export async function getChoicesForPayment(
): Promise<GetChoicesForPaymentResult> {
const parsedTx = parseTransactionIdentifier(transactionId);
if (parsedTx?.tag !== TransactionType.Payment) {
- throw Error("expected payment transaction ID");
+ throw makeInvalidTransactionIdError(transactionId, TransactionType.Payment);
}
const proposalId = parsedTx.proposalId;
const { proposal, d } = await wex.runWalletDbTx(async (tx) => {
@@ -2239,7 +2246,7 @@ export async function getChoicesForPayment(
});
if (!proposal) {
- throw Error(`proposal with id ${proposalId} not found`);
+ throw makeTransactionNotFoundError(transactionId);
}
if (!d) {
@@ -2299,7 +2306,11 @@ export async function getChoicesForPayment(
choiceIndex,
);
if (!available) {
- throw Error("choice index not specified for contract v1");
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_CORE_API_BAD_REQUEST,
+ { parameter: "choiceIndex" },
+ "a contract v1 payment requires a choice index",
+ );
}
let amountEffective: AmountJson | undefined = undefined;
@@ -2410,7 +2421,11 @@ async function calculateDefaultChoice(
};
case MerchantContractVersion.V1:
if (contractTerms.choices.length === 0)
- throw Error(`contract v1 has no choices`);
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_CONTRACT_TERMS_MALFORMED,
+ {},
+ "a contract v1 must offer at least one choice",
+ );
// If there's only one choice, use it.
if (
@@ -2502,7 +2517,7 @@ export async function confirmPay(
const parsedTx = parseTransactionIdentifier(transactionId);
if (parsedTx?.tag !== TransactionType.Payment) {
- throw Error("expected payment transaction ID");
+ throw makeInvalidTransactionIdError(transactionId, TransactionType.Payment);
}
const proposalId = parsedTx.proposalId;
const ctx = new PayMerchantTransactionContext(wex, proposalId);
@@ -2514,12 +2529,19 @@ export async function confirmPay(
});
if (!proposal) {
- throw Error(`proposal with id ${proposalId} not found`);
+ throw makeTransactionNotFoundError(transactionId);
}
const d = await expectProposalDownload(wex, proposal);
if (!d) {
- throw Error("proposal is in invalid state");
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_REQUEST_TRANSACTION_STATE_UNSUPPORTED,
+ {
+ txState: computePayMerchantTransactionState(proposal),
+ debugStateNum: proposal.purchaseStatus,
+ },
+ "the contract terms of this payment have not been downloaded yet",
+ );
}
const existingPurchase = await wex.runWalletDbTx(async (tx) => {
@@ -2545,7 +2567,11 @@ export async function confirmPay(
choiceIndex !== undefined &&
choiceIndex !== existingPurchase.choiceIndex
) {
- throw Error(`cannot change choice index of existing purchase`);
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_CORE_API_BAD_REQUEST,
+ { parameter: "choiceIndex" },
+ "the choice index of an existing purchase cannot be changed",
+ );
}
logger.trace("confirmPay: submitting payment for existing purchase");
@@ -2573,7 +2599,11 @@ export async function confirmPay(
choiceIndex,
);
if (!available) {
- throw Error("choice index not specified for contract v1");
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_CORE_API_BAD_REQUEST,
+ { parameter: "choiceIndex" },
+ "a contract v1 payment requires a choice index",
+ );
}
const currency = Amounts.currencyOf(amountRaw);
@@ -2607,7 +2637,14 @@ export async function confirmPay(
switch (selectTokensResult.type) {
case "failure": {
logger.warn("not confirming payment, insufficient tokens");
- throw Error("insufficient tokens");
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_REQUEST_TRANSACTION_STATE_UNSUPPORTED,
+ {
+ txState: computePayMerchantTransactionState(proposal),
+ debugStateNum: proposal.purchaseStatus,
+ },
+ "the wallet does not hold the tokens that this choice requires",
+ );
}
}
@@ -3326,7 +3363,10 @@ async function processPurchasePay(
wex,
);
const resp = await wex.ws.runSequentialized([EXCHANGE_COINS_LOCK], () =>
- merchantClient.demostratePayment(download.contractTerms.order_id, reqBody),
+ merchantClient.demostratePayment(
+ download.contractTerms.order_id,
+ reqBody,
+ ),
);
logger.trace(`/paid response status: ${resp.response.status}`);
if (isOrderUnknown(resp)) {
@@ -3821,23 +3861,29 @@ export async function sharePayment(
});
if (!proposalId) {
- throw Error(`no proposal found for order id ${orderId}`);
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_PURCHASE_NOT_FOUND,
+ { merchantBaseUrl, orderId },
+ `no purchase found for order ${orderId}`,
+ );
}
const ctx = new PayMerchantTransactionContext(wex, proposalId);
+ // "gone" and "wrong state" need different error codes, so they cannot both
+ // come back as undefined.
const result = await wex.runWalletDbTx(async (tx) => {
const [p, h] = await ctx.getRecordHandle(tx);
if (!p) {
logger.warn("purchase does not exist anymore");
- return undefined;
+ return { type: "gone" as const };
}
if (
p.purchaseStatus !== PurchaseStatus.DialogProposed &&
p.purchaseStatus !== PurchaseStatus.DialogShared
) {
// FIXME: purchase can be shared before being paid
- return undefined;
+ return { type: "bad-state" as const, purchase: p };
}
if (p.purchaseStatus === PurchaseStatus.DialogProposed) {
p.purchaseStatus = PurchaseStatus.DialogShared;
@@ -3845,6 +3891,7 @@ export async function sharePayment(
await h.update(p, "share");
}
return {
+ type: "ok" as const,
proposalId: p.proposalId,
nonce: p.noncePriv,
session: p.lastSessionId ?? p.downloadSessionId,
@@ -3852,8 +3899,22 @@ export async function sharePayment(
};
});
- if (result === undefined) {
- throw Error("This purchase can't be shared");
+ switch (result.type) {
+ case "ok":
+ break;
+ case "gone":
+ throw makeTransactionNotFoundError(ctx.transactionId);
+ case "bad-state":
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_REQUEST_TRANSACTION_STATE_UNSUPPORTED,
+ {
+ txState: computePayMerchantTransactionState(result.purchase),
+ debugStateNum: result.purchase.purchaseStatus,
+ },
+ "this payment can only be shared before it is paid",
+ );
+ default:
+ assertUnreachable(result);
}
// schedule a task to watch for the status
@@ -4230,7 +4291,9 @@ async function processPurchaseAbortingRefund(
coins: abortingCoins,
};
- logger.trace(`making order abort request for ${download.contractTerms.order_id}`);
+ logger.trace(
+ `making order abort request for ${download.contractTerms.order_id}`,
+ );
const merchantClient = walletMerchantClient(
download.contractTerms.merchant_base_url,
@@ -4276,8 +4339,11 @@ async function processPurchaseAbortingRefund(
const refunds: MerchantCoinRefundStatus[] = [];
if (abortResp.refunds.length != abortingCoins.length) {
- // FIXME: define error code!
- throw Error("invalid order abort response");
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_TRANSACTION_PROTOCOL_VIOLATION,
+ {},
+ `merchant returned ${abortResp.refunds.length} refunds for ${abortingCoins.length} aborted coins`,
+ );
}
for (let i = 0; i < abortResp.refunds.length; i++) {
@@ -4427,11 +4493,12 @@ export async function startRefundQueryForUri(
talerUri: string,
): Promise<StartRefundQueryForUriResponse> {
const parsedUri = Result.orUndefined(TalerUris.parse(talerUri));
- if (!parsedUri) {
- throw Error("invalid taler:// URI");
- }
- if (parsedUri.type !== TalerUriAction.Refund) {
- throw Error("expected taler://refund URI");
+ if (parsedUri?.type !== TalerUriAction.Refund) {
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_TALER_URI_MALFORMED,
+ { talerUri, expectedAction: TalerUriAction.Refund },
+ "expected a taler://refund URI",
+ );
}
const purchaseRecord = await wex.runWalletDbTx(async (tx) => {
return tx.getPurchaseByUrlAndOrderId(
diff --git a/packages/taler-wallet-core/src/pay-peer-pull-credit.ts b/packages/taler-wallet-core/src/pay-peer-pull-credit.ts
@@ -30,6 +30,7 @@ import {
PeerContractTerms,
ScopeInfo,
ScopeType,
+ TalerError,
TalerErrorCode,
TalerErrorDetail,
TalerPreciseTimestamp,
@@ -1096,7 +1097,11 @@ export async function internalCheckPeerPullCredit(
url: req.exchangeBaseUrl,
};
} else {
- throw Error("client must either specify exchangeBaseUrl or restrictScope");
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_CORE_API_BAD_REQUEST,
+ { parameter: "restrictScope" },
+ "either exchangeBaseUrl or restrictScope must be specified",
+ );
}
logger.trace("checking peer-pull-credit fees");
@@ -1108,7 +1113,11 @@ export async function internalCheckPeerPullCredit(
);
if (!exchangeUrl) {
- throw Error("no exchange found for initiating a peer pull payment");
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_NO_SUITABLE_EXCHANGE,
+ { currency: restrictScope.currency },
+ "no exchange in the wallet can be used to create an invoice",
+ );
}
logger.trace(`found ${exchangeUrl} as preferred exchange`);
@@ -1168,7 +1177,11 @@ async function internalInitiatePeerPullPayment(
}
if (!maybeExchangeBaseUrl) {
- throw Error("no exchange found for initiating a peer pull payment");
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_NO_SUITABLE_EXCHANGE,
+ { currency },
+ "no exchange in the wallet can be used to create an invoice",
+ );
}
const exchangeBaseUrl = maybeExchangeBaseUrl;
@@ -1179,7 +1192,14 @@ async function internalInitiatePeerPullPayment(
if (
checkPeerCreditHardLimitExceeded(exchange, req.partialContractTerms.amount)
) {
- throw Error("peer credit would exceed hard KYC limit");
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_KYC_LIMIT_EXCEEDED,
+ {
+ exchangeBaseUrl,
+ requestedAmount: req.partialContractTerms.amount,
+ },
+ "this invoice would exceed a hard limit of the exchange",
+ );
}
const mergeReserveInfo = await getMergeReserveInfo(wex, {
diff --git a/packages/taler-wallet-core/src/pay-peer-pull-debit.ts b/packages/taler-wallet-core/src/pay-peer-pull-debit.ts
@@ -99,6 +99,8 @@ import {
BalanceEffect,
constructTransactionIdentifier,
isUnsuccessfulTransaction,
+ makeInvalidTransactionIdError,
+ makeTransactionNotFoundError,
parseTransactionIdentifier,
} from "./transactions.js";
import { WalletExecutionContext, walletExchangeClient } from "./wallet.js";
@@ -569,7 +571,7 @@ async function processPeerPullDebitPendingDeposit(
switch (coinSelRes.type) {
case "failure":
throw TalerError.fromDetail(
- TalerErrorCode.WALLET_PEER_PUSH_PAYMENT_INSUFFICIENT_BALANCE,
+ TalerErrorCode.WALLET_PEER_PULL_PAYMENT_INSUFFICIENT_BALANCE,
{
insufficientBalanceDetails: coinSelRes.insufficientBalanceDetails,
},
@@ -726,7 +728,12 @@ export async function processPeerPullDebit(
tx.getPeerPullDebit(peerPullDebitId),
);
if (!peerPullInc) {
- throw Error("peer pull debit not found");
+ throw makeTransactionNotFoundError(
+ constructTransactionIdentifier({
+ tag: TransactionType.PeerPullDebit,
+ peerPullDebitId,
+ }),
+ );
}
switch (peerPullInc.status) {
@@ -754,7 +761,10 @@ export async function confirmPeerPullDebit(
): Promise<AcceptPeerPullPaymentResponse> {
const parsed = parseTransactionIdentifier(req.transactionId);
if (!parsed || parsed.tag !== TransactionType.PeerPullDebit) {
- throw Error("invalid peer-pull-debit transaction identifier");
+ throw makeInvalidTransactionIdError(
+ req.transactionId,
+ TransactionType.PeerPullDebit,
+ );
}
const peerPullInc = await wex.runWalletDbTx(async (tx) =>
@@ -762,9 +772,7 @@ export async function confirmPeerPullDebit(
);
if (peerPullInc == null) {
- throw Error(
- `can't accept unknown incoming p2p pull payment (${req.transactionId})`,
- );
+ throw makeTransactionNotFoundError(req.transactionId);
}
const ctx = new PeerPullDebitTransactionContext(wex, parsed.peerPullDebitId);
@@ -791,7 +799,7 @@ export async function confirmPeerPullDebit(
switch (coinSelRes.type) {
case "failure":
throw TalerError.fromDetail(
- TalerErrorCode.WALLET_PEER_PUSH_PAYMENT_INSUFFICIENT_BALANCE,
+ TalerErrorCode.WALLET_PEER_PULL_PAYMENT_INSUFFICIENT_BALANCE,
{
insufficientBalanceDetails: coinSelRes.insufficientBalanceDetails,
},
@@ -860,7 +868,11 @@ async function internalPreparePeerPullDebit(
req: PreparePeerPullDebitRequest,
): Promise<PreparePeerPullDebitResponse> {
if (!req.talerUri && !req.transactionId) {
- throw Error("either talerUri or transactionId must be specified");
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_CORE_API_BAD_REQUEST,
+ { parameter: "talerUri" },
+ "either talerUri or transactionId must be specified",
+ );
}
let uri: TalerPayPullUri | undefined;
@@ -869,7 +881,11 @@ async function internalPreparePeerPullDebit(
TalerUris.parseRestricted(req.talerUri, TalerUriAction.PayPull),
);
if (!uri) {
- throw Error("got invalid taler://pay-push URI");
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_TALER_URI_MALFORMED,
+ { talerUri: req.talerUri, expectedAction: TalerUriAction.PayPull },
+ "expected a taler://pay-pull URI",
+ );
}
}
@@ -877,7 +893,10 @@ async function internalPreparePeerPullDebit(
if (req.transactionId) {
const parsedRes = parseTransactionIdentifier(req.transactionId);
if (!parsedRes || parsedRes.tag !== TransactionType.PeerPullDebit) {
- throw Error("got invalid transaction ID");
+ throw makeInvalidTransactionIdError(
+ req.transactionId,
+ TransactionType.PeerPullDebit,
+ );
}
parsedTxId = parsedRes.peerPullDebitId;
}
@@ -933,7 +952,7 @@ async function internalPreparePeerPullDebit(
}
if (!uri) {
- throw Error("transaction not found, use talerUri instead");
+ throw makeTransactionNotFoundError(req.transactionId!);
}
const exchangeBaseUrl = uri.exchangeBaseUrl;
@@ -951,8 +970,11 @@ async function internalPreparePeerPullDebit(
case "ok":
break;
case HttpStatusCode.NotFound:
- // FIXME: appropriated error code
- throw Error("unknown P2P contract");
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_PEER_CONTRACT_NOT_FOUND,
+ {},
+ "the exchange does not know this peer-to-peer contract",
+ );
default:
assertUnreachable(contractResp);
}
@@ -976,16 +998,22 @@ async function internalPreparePeerPullDebit(
{},
);
case HttpStatusCode.NotFound:
- // FIXME: appropriated error code
- throw Error("unknown peer pull debit");
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_PEER_CONTRACT_NOT_FOUND,
+ {},
+ "the exchange does not know the purse of this invoice",
+ );
default:
assertUnreachable(resp);
}
if (isPurseDeposited(resp.body)) {
logger.info("purse completed by another wallet");
- // FIXME: appropriated error code
- throw Error("peer pull debit already completed");
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_PEER_PULL_DEBIT_ALREADY_PAID,
+ {},
+ "this invoice has already been paid",
+ );
}
const peerPullDebitId = encodeCrock(getRandomBytes(32));
@@ -998,7 +1026,11 @@ async function internalPreparePeerPullDebit(
} else {
// FIXME: In this case, where do we get the purse expiration from?!
// https://bugs.gnunet.org/view.php?id=7706
- throw Error("pull payments without contract terms not supported yet");
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_CONTRACT_TERMS_UNSUPPORTED,
+ {},
+ "invoices without contract terms are not supported yet",
+ );
}
const contractTermsHash = ContractTermsUtil.hashContractTerms(contractTerms);
@@ -1025,7 +1057,7 @@ async function internalPreparePeerPullDebit(
switch (coinSelRes.type) {
case "failure":
throw TalerError.fromDetail(
- TalerErrorCode.WALLET_PEER_PUSH_PAYMENT_INSUFFICIENT_BALANCE,
+ TalerErrorCode.WALLET_PEER_PULL_PAYMENT_INSUFFICIENT_BALANCE,
{
insufficientBalanceDetails: coinSelRes.insufficientBalanceDetails,
},
diff --git a/packages/taler-wallet-core/src/pay-peer-push-credit.ts b/packages/taler-wallet-core/src/pay-peer-push-credit.ts
@@ -30,6 +30,8 @@ import {
PreparePeerPushCreditRequest,
PreparePeerPushCreditResponse,
Result,
+ TalerError,
+ TalerErrorCode,
TalerErrorDetail,
TalerPayPushUri,
TalerPreciseTimestamp,
@@ -101,6 +103,8 @@ import {
BalanceEffect,
constructTransactionIdentifier,
isUnsuccessfulTransaction,
+ makeInvalidTransactionIdError,
+ makeTransactionNotFoundError,
parseTransactionIdentifier,
} from "./transactions.js";
import { WalletExecutionContext, walletExchangeClient } from "./wallet.js";
@@ -489,7 +493,11 @@ async function internalPreparePeerPushCredit(
req: PreparePeerPushCreditRequest,
): Promise<PreparePeerPushCreditResponse> {
if (!req.talerUri && !req.transactionId) {
- throw Error("either talerUri or transactionId must be specified");
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_CORE_API_BAD_REQUEST,
+ { parameter: "talerUri" },
+ "either talerUri or transactionId must be specified",
+ );
}
let uri: TalerPayPushUri | undefined;
@@ -498,7 +506,11 @@ async function internalPreparePeerPushCredit(
TalerUris.parseRestricted(req.talerUri, TalerUriAction.PayPush),
);
if (!uri) {
- throw Error("got invalid taler://pay-push URI");
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_TALER_URI_MALFORMED,
+ { talerUri: req.talerUri, expectedAction: TalerUriAction.PayPush },
+ "expected a taler://pay-push URI",
+ );
}
}
@@ -506,7 +518,10 @@ async function internalPreparePeerPushCredit(
if (req.transactionId) {
const parsedRes = parseTransactionIdentifier(req.transactionId);
if (!parsedRes || parsedRes.tag !== TransactionType.PeerPushCredit) {
- throw Error("got invalid transaction ID");
+ throw makeInvalidTransactionIdError(
+ req.transactionId,
+ TransactionType.PeerPushCredit,
+ );
}
parsedTxId = parsedRes.peerPushCreditId;
}
@@ -568,7 +583,7 @@ async function internalPreparePeerPushCredit(
}
if (!uri) {
- throw Error("transaction not found, use talerUri instead");
+ throw makeTransactionNotFoundError(req.transactionId!);
}
const exchangeBaseUrl = uri.exchangeBaseUrl;
@@ -587,8 +602,11 @@ async function internalPreparePeerPushCredit(
case "ok":
break;
case HttpStatusCode.NotFound:
- // FIXME: appropriated error code
- throw Error("unknown P2P contract");
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_PEER_CONTRACT_NOT_FOUND,
+ {},
+ "the exchange does not know this peer-to-peer contract",
+ );
default:
assertUnreachable(contractResp);
}
@@ -608,11 +626,17 @@ async function internalPreparePeerPushCredit(
case "ok":
break;
case HttpStatusCode.Gone:
- // FIXME: appropriated error code
- throw Error("aborted peer push credit");
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_PEER_PUSH_CREDIT_PURSE_GONE,
+ {},
+ "the sender aborted this payment",
+ );
case HttpStatusCode.NotFound:
- // FIXME: appropriated error code
- throw Error("unknown peer push credit");
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_PEER_CONTRACT_NOT_FOUND,
+ {},
+ "the exchange does not know the purse of this payment",
+ );
default:
assertUnreachable(resp);
}
@@ -1269,10 +1293,16 @@ async function internalConfirmPeerPushCredit(
): Promise<AcceptPeerPushPaymentResponse> {
const parsedTx = parseTransactionIdentifier(req.transactionId);
if (!parsedTx) {
- throw Error("invalid transaction ID");
+ throw makeInvalidTransactionIdError(
+ req.transactionId,
+ TransactionType.PeerPushCredit,
+ );
}
if (parsedTx.tag !== TransactionType.PeerPushCredit) {
- throw Error("invalid transaction ID type");
+ throw makeInvalidTransactionIdError(
+ req.transactionId,
+ TransactionType.PeerPushCredit,
+ );
}
const ctx = new PeerPushCreditTransactionContext(
wex,
@@ -1297,9 +1327,7 @@ async function internalConfirmPeerPushCredit(
});
if (!res) {
- throw Error(
- `can't accept unknown incoming p2p push payment (${req.transactionId})`,
- );
+ throw makeTransactionNotFoundError(req.transactionId);
}
const peerInc = res.peerInc;
@@ -1311,7 +1339,14 @@ async function internalConfirmPeerPushCredit(
requireExchangeTosAcceptedOrThrow(wex, exchange);
if (checkPeerCreditHardLimitExceeded(exchange, res.contractTerms.amount)) {
- throw Error("peer credit would exceed hard KYC limit");
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_KYC_LIMIT_EXCEEDED,
+ {
+ exchangeBaseUrl: peerInc.exchangeBaseUrl,
+ requestedAmount: res.contractTerms.amount,
+ },
+ "accepting this payment would exceed a hard limit of the exchange",
+ );
}
await wex.runWalletDbTx(async (tx) => {
diff --git a/packages/taler-wallet-core/src/pay-peer-push-debit.ts b/packages/taler-wallet-core/src/pay-peer-push-debit.ts
@@ -457,7 +457,11 @@ async function internalCheckPeerPushDebit(
restrictScope,
);
if (!exchangeBaseUrl) {
- throw Error("no exchange found for payment");
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_NO_SUITABLE_EXCHANGE,
+ { currency },
+ "no exchange in the wallet can be used for this payment",
+ );
}
return {
@@ -1091,8 +1095,10 @@ export async function initiatePeerPushDebit(
const currency = Amounts.currencyOf(instructedAmount);
if (req.exchangeBaseUrl != null && req.restrictScope != null) {
- throw Error(
- "initiatePeerPushDebit: exchangeBaseUrl and restrictScope are mutually exclusive",
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_CORE_API_BAD_REQUEST,
+ { parameter: "restrictScope" },
+ "exchangeBaseUrl and restrictScope are mutually exclusive",
);
}
diff --git a/packages/taler-wallet-core/src/pay-template.ts b/packages/taler-wallet-core/src/pay-template.ts
@@ -25,6 +25,8 @@ import {
PreparePayTemplateRequest,
Result,
succeedOrThrow,
+ TalerError,
+ TalerErrorCode,
TalerMerchantApi,
TalerMerchantInstanceHttpClient,
TalerPayTemplateUri,
@@ -114,7 +116,11 @@ export function applyTemplateUriOverrides(
// We don't support overrides for this template type.
break;
default:
- throw Error("unsupported template type");
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_CONTRACT_TERMS_UNSUPPORTED,
+ {},
+ "this wallet does not support this order template type",
+ );
}
}
@@ -156,7 +162,11 @@ export function applyTemplateUserOverrides(
// We don't support overrides for this template type.
break;
default:
- throw Error("unsupported template type");
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_CONTRACT_TERMS_UNSUPPORTED,
+ {},
+ "this wallet does not support this order template type",
+ );
}
}
@@ -183,7 +193,14 @@ async function internalCheckPayForTemplate(
),
);
if (!parsedUri) {
- throw Error("invalid taler-template URI");
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_TALER_URI_MALFORMED,
+ {
+ talerUri: req.talerPayTemplateUri,
+ expectedAction: TalerUriAction.PayTemplate,
+ },
+ "expected a taler://pay-template URI",
+ );
}
const merchantApi = new TalerMerchantInstanceHttpClient(
@@ -239,7 +256,14 @@ export async function instantiateTemplateRaw(
);
if (!parsedUri) {
- throw Error("invalid taler-template URI");
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_TALER_URI_MALFORMED,
+ {
+ talerUri: req.talerPayTemplateUri,
+ expectedAction: TalerUriAction.PayTemplate,
+ },
+ "expected a taler://pay-template URI",
+ );
}
const merchantApi = new TalerMerchantInstanceHttpClient(
diff --git a/packages/taler-wallet-core/src/recoup.ts b/packages/taler-wallet-core/src/recoup.ts
@@ -58,7 +58,10 @@ import {
} from "./db-common.js";
import { CoinSourceType } from "./db-indexeddb.js";
import { createRefreshGroup } from "./refresh.js";
-import { constructTransactionIdentifier } from "./transactions.js";
+import {
+ constructTransactionIdentifier,
+ makeTransactionActionUnsupportedError,
+} from "./transactions.js";
import {
WalletExecutionContext,
getDenomInfo,
@@ -431,27 +434,19 @@ export class RecoupTransactionContext implements TransactionContext {
}
userAbortTransaction(): Promise<void> {
- throw new Error(
- "Method not implemented RecoupTransactionContext.userAbortTransaction",
- );
+ throw makeTransactionActionUnsupportedError(this.transactionId, "abort");
}
userSuspendTransaction(): Promise<void> {
- throw new Error(
- "Method not implemented RecoupTransactionContext.userSuspendTransaction",
- );
+ throw makeTransactionActionUnsupportedError(this.transactionId, "suspend");
}
userResumeTransaction(): Promise<void> {
- throw new Error(
- "Method not implemented RecoupTransactionContext.userResumeTransaction",
- );
+ throw makeTransactionActionUnsupportedError(this.transactionId, "resume");
}
userFailTransaction(): Promise<void> {
- throw new Error(
- "Method not implemented RecoupTransactionContext.userFailTransaction",
- );
+ throw makeTransactionActionUnsupportedError(this.transactionId, "fail");
}
async userDeleteTransaction(): Promise<void> {
@@ -479,7 +474,11 @@ export class RecoupTransactionContext implements TransactionContext {
lookupFullTransaction(
tx: WalletDbTransaction,
): Promise<Transaction | undefined> {
- throw new Error("Method not implemented.");
+ throw makeTransactionActionUnsupportedError(
+ this.transactionId,
+ "lookup",
+ "recoup transactions are not materialized and cannot be looked up",
+ );
}
}
diff --git a/packages/taler-wallet-core/src/refresh.ts b/packages/taler-wallet-core/src/refresh.ts
@@ -108,6 +108,7 @@ import {
import {
constructTransactionIdentifier,
isUnsuccessfulTransaction,
+ makeTransactionActionUnsupportedError,
} from "./transactions.js";
import {
EXCHANGE_COINS_LOCK,
@@ -284,7 +285,11 @@ export class RefreshTransactionContext implements TransactionContext {
async userAbortTransaction(): Promise<void> {
// Refresh transactions only support fail, not abort.
- throw new Error("refresh transactions cannot be aborted");
+ throw makeTransactionActionUnsupportedError(
+ this.transactionId,
+ "abort",
+ "refresh transactions cannot be aborted, only failed",
+ );
}
async userResumeTransaction(): Promise<void> {
diff --git a/packages/taler-wallet-core/src/requests.ts b/packages/taler-wallet-core/src/requests.ts
@@ -475,7 +475,11 @@ async function handleGetBankAccountById(
return tx.getBankAccount(req.bankAccountId);
});
if (!acct) {
- throw Error(`bank account ${req.bankAccountId} not found`);
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_BANK_ACCOUNT_NOT_FOUND,
+ { bankAccountId: req.bankAccountId },
+ `bank account ${req.bankAccountId} not found`,
+ );
}
return acct;
}
@@ -490,7 +494,11 @@ async function forgetBankAccount(
await wex.runWalletDbTx(async (tx) => {
const account = await tx.getBankAccount(bankAccountId);
if (!account) {
- throw Error(`account not found: ${bankAccountId}`);
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_BANK_ACCOUNT_NOT_FOUND,
+ { bankAccountId },
+ `bank account ${bankAccountId} not found`,
+ );
}
await tx.deleteBankAccount(account.bankAccountId);
});
@@ -610,7 +618,11 @@ function requireIdbBackend(
): IdbWalletDbHandle {
const idb = wex.ws.idbOnly;
if (!idb) {
- throw Error(`${what} is only supported on the IndexedDB backend`);
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_DB_BACKEND_UNSUPPORTED,
+ { backend: wex.ws.db.name },
+ `${what} is only supported on the IndexedDB backend`,
+ );
}
return idb;
}
@@ -680,7 +692,11 @@ async function recoverStoredBackup(
const bd = await backupsDb.runAllStoresReadWriteTx({}, async (tx) => {
const backupMeta = await tx.backupMeta.get(name);
if (!backupMeta) {
- throw Error("backup not found");
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_CORE_API_BAD_REQUEST,
+ { parameter: "name" },
+ `stored backup ${name} not found`,
+ );
}
const backupData = await tx.backupData.get(name);
if (!backupData) {
@@ -708,7 +724,14 @@ async function handlePrepareWithdrawExchange(
): Promise<PrepareWithdrawExchangeResponse> {
const parsedUri = Result.orUndefined(TalerUris.parse(req.talerUri));
if (parsedUri?.type !== TalerUriAction.WithdrawExchange) {
- throw Error("expected a taler://withdraw-exchange URI");
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_TALER_URI_MALFORMED,
+ {
+ talerUri: req.talerUri,
+ expectedAction: TalerUriAction.WithdrawExchange,
+ },
+ "expected a taler://withdraw-exchange URI",
+ );
}
const exchangeBaseUrl = parsedUri.exchangeBaseUrl;
@@ -728,7 +751,11 @@ async function handlePrepareWithdrawExchange(
if (parsedUri.amount) {
const amt = Amounts.parseOrThrow(parsedUri.amount);
if (amt.currency !== exchange.currency) {
- throw Error("mismatch of currency (URI vs exchange)");
+ throw TalerError.fromDetail(
+ TalerErrorCode.GENERIC_CURRENCY_MISMATCH,
+ {},
+ `mismatch of currency (URI says ${amt.currency}, exchange uses ${exchange.currency})`,
+ );
}
}
return {
@@ -906,7 +933,11 @@ async function handleAddExchange(
if (req.uri.startsWith("taler")) {
const p = Result.orUndefined(TalerUris.parse(req.uri));
if (p?.type !== TalerUriAction.AddExchange) {
- throw Error("invalid taler://add-exchange/ URI");
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_TALER_URI_MALFORMED,
+ { talerUri: req.uri, expectedAction: TalerUriAction.AddExchange },
+ "expected a taler://add-exchange/ URI",
+ );
}
exchangeBaseUrl = p.exchangeBaseUrl;
} else if (req.allowCompletion) {
@@ -920,14 +951,24 @@ async function handleAddExchange(
} else if (req.uri.startsWith("http")) {
const canonUrl = canonicalizeBaseUrl(req.uri);
if (req.uri != canonUrl) {
- throw Error("exchange base URL must be canonicalized");
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_CORE_API_BAD_REQUEST,
+ { parameter: "uri" },
+ `exchange base URL must be canonicalized (expected "${canonUrl}")`,
+ );
}
exchangeBaseUrl = req.uri;
} else {
- throw Error("AddExchangeRequest.uri must be http(s):// or taler://");
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_CORE_API_BAD_REQUEST,
+ { parameter: "uri" },
+ "AddExchangeRequest.uri must be http(s):// or taler://",
+ );
}
} else {
- throw Error(
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_CORE_API_BAD_REQUEST,
+ { parameter: "uri" },
"AddExchangeRequest must either specify uri or exchangeBaseUrl",
);
}
@@ -994,9 +1035,7 @@ async function handleAddBankAccount(
wex: WalletExecutionContext,
req: AddBankAccountRequest,
): Promise<AddBankAccountResponse> {
- if (Result.isError(Paytos.fromString(req.paytoUri))) {
- throw Error("invalid payto");
- }
+ Paytos.parseOrThrow(req.paytoUri);
const acctId = await wex.runWalletDbTx(async (tx) => {
let currencies = req.currencies;
let myId: string;
@@ -1048,7 +1087,11 @@ async function handleTestingGetReserveHistory(
return tx.getReserveByReservePub(req.reservePub);
});
if (!reserve) {
- throw Error("no reserve pub found");
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_CORE_API_BAD_REQUEST,
+ { parameter: "reservePub" },
+ `no reserve found for reserve public key ${req.reservePub}`,
+ );
}
const sigResp = await wex.cryptoApi.signReserveHistoryReq({
reservePriv: reserve.reservePriv,
@@ -1113,10 +1156,7 @@ async function handleGetBankingChoicesForPayto(
wex: WalletExecutionContext,
req: GetBankingChoicesForPaytoRequest,
): Promise<GetBankingChoicesForPaytoResponse> {
- const parsedPayto = Result.orUndefined(Paytos.fromString(req.paytoUri));
- if (!parsedPayto) {
- throw Error("invalid payto URI");
- }
+ const parsedPayto = Paytos.parseOrThrow(req.paytoUri);
const amount = parsedPayto.params["amount"];
if (!amount) {
logger.warn("payto URI has no amount");
@@ -1284,11 +1324,12 @@ async function handleStartRefundQuery(
req: StartRefundQueryRequest,
): Promise<EmptyObject> {
const txIdParsed = parseTransactionIdentifier(req.transactionId);
- if (!txIdParsed) {
- throw Error("invalid transaction ID");
- }
- if (txIdParsed.tag !== TransactionType.Payment) {
- throw Error("expected payment transaction ID");
+ if (txIdParsed?.tag !== TransactionType.Payment) {
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_CORE_API_BAD_REQUEST,
+ { parameter: "transactionId" },
+ `expected a payment transaction ID, got "${req.transactionId}"`,
+ );
}
await startQueryRefund(wex, txIdParsed.proposalId);
return {};
@@ -1636,7 +1677,11 @@ async function handleExportDbToFile(
): Promise<ExportDbToFileResponse> {
const db = wex.ws.db;
if (!db.exportToFile) {
- throw Error(`the ${db.name} backend cannot export the database to a file`);
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_DB_BACKEND_UNSUPPORTED,
+ { backend: db.name },
+ `the ${db.name} backend cannot export the database to a file`,
+ );
}
// Called as a method: the sqlite implementation reads this.ndb, and
// extracting the function first would drop the receiver.
@@ -1674,7 +1719,9 @@ async function handleImportDbFromFile(
if (req.path.endsWith(".json")) {
const db = wex.ws.db;
if (!db.readBackupJson) {
- throw Error(
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_DB_BACKEND_UNSUPPORTED,
+ { backend: db.name },
`the ${db.name} backend cannot read a database dump from a file`,
);
}
@@ -1683,7 +1730,11 @@ async function handleImportDbFromFile(
dump,
});
} else {
- throw Error("DB file import only supports .json files at the moment");
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_CORE_API_BAD_REQUEST,
+ { parameter: "path" },
+ "DB file import only supports .json files at the moment",
+ );
}
}
@@ -1800,7 +1851,11 @@ export async function handleTestingRunFixup(
});
return {};
}
- throw Error("fixup not found");
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_CORE_API_BAD_REQUEST,
+ { parameter: "id" },
+ `fixup "${req.id}" not found`,
+ );
}
async function handleGetVersion(
@@ -1955,7 +2010,11 @@ export async function handleTestingCorruptWithdrawalCoinSel(
): Promise<EmptyObject> {
const txId = parseTransactionIdentifier(req.transactionId);
if (txId?.tag !== TransactionType.Withdrawal) {
- throw Error("expected withdrawal transaction ID");
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_CORE_API_BAD_REQUEST,
+ { parameter: "transactionId" },
+ `expected a withdrawal transaction ID, got "${req.transactionId}"`,
+ );
}
await wex.runWalletDbTx(async (tx) => {
const wg = await tx.getWithdrawalGroup(txId.withdrawalGroupId);
@@ -2526,7 +2585,11 @@ const handlers: { [T in WalletApiOperation]: HandlerWithValidator<T> } = {
[WalletApiOperation.Recycle]: {
codec: codecForEmptyObject(),
handler: async (wex, req) => {
- throw Error("not implemented");
+ throw TalerError.fromDetail(
+ TalerErrorCode.GENERIC_FEATURE_NOT_IMPLEMENTED,
+ {},
+ "this wallet-core operation is declared but not implemented yet",
+ );
},
},
[WalletApiOperation.ExportDb]: {
@@ -2685,7 +2748,11 @@ const handlers: { [T in WalletApiOperation]: HandlerWithValidator<T> } = {
[WalletApiOperation.ListAssociatedRefreshes]: {
codec: codecForAny(),
handler: async (wex, req) => {
- throw Error("not implemented");
+ throw TalerError.fromDetail(
+ TalerErrorCode.GENERIC_FEATURE_NOT_IMPLEMENTED,
+ {},
+ "this wallet-core operation is declared but not implemented yet",
+ );
},
},
[WalletApiOperation.GetBankingChoicesForPayto]: {
@@ -2727,7 +2794,9 @@ export async function dispatchRequestInternal(
payload: unknown,
): Promise<WalletCoreResponseType<typeof operation>> {
if (!wex.ws.initCalled && !isWalletInitOperation(operation)) {
- throw Error(
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_CORE_NOT_AVAILABLE,
+ {},
`wallet must be initialized before running operation ${operation}`,
);
}
diff --git a/packages/taler-wallet-core/src/taldir.ts b/packages/taler-wallet-core/src/taldir.ts
@@ -80,7 +80,11 @@ export async function lookupAlias(
}
break;
default:
- throw Error("unable to get directory service config");
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_ALIAS_REGISTRATION_FAILED,
+ { directoryBaseUrl: lookupRequest.taldirBaseUrl },
+ "could not read the configuration of the directory service",
+ );
}
// SHA-512(len($ALIASTYPE)+len($ALIAS)||$ALIASTYPE||$ALIAS)
const hAliasBuffer = createHAliasBuffer(
@@ -97,7 +101,11 @@ export async function lookupAlias(
case HttpStatusCode.NotFound:
return {};
default:
- throw Error("unexpected taldir messages response empty");
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_ALIAS_REGISTRATION_FAILED,
+ { directoryBaseUrl: lookupRequest.taldirBaseUrl },
+ "the directory service did not answer the alias lookup",
+ );
}
}
@@ -122,11 +130,17 @@ export async function registerAlias(
case "ok":
return res.body;
case HttpStatusCode.NotFound:
- throw Error(
- "taldir reported not found, this means that the alias type was not supported",
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_ALIAS_REGISTRATION_FAILED,
+ { directoryBaseUrl: registerRequest.taldirBaseUrl },
+ `the directory service does not support the alias type "${registerRequest.aliasType}"`,
);
default:
- throw Error("unexpected taldir messages response empty");
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_ALIAS_REGISTRATION_FAILED,
+ { directoryBaseUrl: registerRequest.taldirBaseUrl },
+ "the directory service did not accept the registration request",
+ );
}
}
@@ -162,14 +176,28 @@ export async function completeAliasRegistration(
case "ok":
return {};
case HttpStatusCode.NotFound:
- throw Error("taldir reported that this registration was not found");
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_ALIAS_REGISTRATION_FAILED,
+ { directoryBaseUrl: completionRequest.taldirBaseUrl },
+ "the directory service does not know this registration, it may have expired",
+ );
case HttpStatusCode.Forbidden:
- throw TalerError.fromDetail(TalerErrorCode.GENERIC_FORBIDDEN, {});
+ throw TalerError.fromDetail(
+ TalerErrorCode.GENERIC_FORBIDDEN,
+ {},
+ "the directory service rejected the answer to the challenge",
+ );
case HttpStatusCode.TooManyRequests:
- throw Error(
- "taldir reported that too many tries have been made to solve this registration challenge",
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_ALIAS_REGISTRATION_FAILED,
+ { directoryBaseUrl: completionRequest.taldirBaseUrl },
+ "too many attempts have been made to answer this registration challenge",
);
default:
- throw Error("unexpected taldir messages response empty");
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_ALIAS_REGISTRATION_FAILED,
+ { directoryBaseUrl: completionRequest.taldirBaseUrl },
+ "the directory service did not accept the registration",
+ );
}
}
diff --git a/packages/taler-wallet-core/src/tokenFamilies.ts b/packages/taler-wallet-core/src/tokenFamilies.ts
@@ -28,6 +28,8 @@ import {
MerchantContractTokenKind,
MerchantInfo,
SubscriptionListDetail,
+ TalerError,
+ TalerErrorCode,
} from "@gnu-taler/taler-util";
import { WalletToken } from "./db-common.js";
import { WalletExecutionContext } from "./index.js";
@@ -214,9 +216,12 @@ export async function deleteDiscount(
}
}
- // FIXME: proper GANA error
if (inUse) {
- throw Error("One or more tokens in this family are in use");
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_TOKENS_IN_USE,
+ { tokenFamilyHash },
+ "one or more tokens in this family are in use",
+ );
}
for (const token of tokens) {
@@ -250,9 +255,12 @@ export async function deleteSubscription(
}
}
- // FIXME: proper GANA error
if (inUse) {
- throw Error("One or more tokens in this family are in use");
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_TOKENS_IN_USE,
+ { tokenFamilyHash },
+ "one or more tokens in this family are in use",
+ );
}
for (const token of tokens) {
diff --git a/packages/taler-wallet-core/src/tokenSelection.ts b/packages/taler-wallet-core/src/tokenSelection.ts
@@ -25,6 +25,8 @@ import {
MerchantContractTokenDetails,
MerchantContractTokenKind,
PaymentTokenAvailabilityDetails,
+ TalerError,
+ TalerErrorCode,
TalerProtocolTimestamp,
TokenAvailabilityHint,
} from "@gnu-taler/taler-util";
@@ -182,7 +184,11 @@ export async function selectPayTokensInTx(
const proposal = await tx.getPurchase(req.proposalId);
if (!proposal) {
- throw Error(`proposal ${req.proposalId} could not be found`);
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_PURCHASE_NOT_FOUND,
+ {},
+ `proposal ${req.proposalId} could not be found`,
+ );
}
var tokensRequested = 0;
diff --git a/packages/taler-wallet-core/src/transactions.test.ts b/packages/taler-wallet-core/src/transactions.test.ts
@@ -53,6 +53,10 @@ test("an unknown transaction type does not parse", (t) => {
});
test("a malformed transaction identifier is rejected", (t) => {
- assert.throws(() => parseTransactionIdentifier("txn:deposit"));
- assert.throws(() => parseTransactionIdentifier("nottxn:deposit:x"));
+ // Every kind of rejection reports the same way, so that callers raise one
+ // coded error instead of the shape of the failure deciding whether they see
+ // an exception or a missing value.
+ assert.strictEqual(parseTransactionIdentifier("txn:deposit"), undefined);
+ assert.strictEqual(parseTransactionIdentifier("nottxn:deposit:x"), undefined);
+ assert.strictEqual(parseTransactionIdentifier(""), undefined);
});
diff --git a/packages/taler-wallet-core/src/transactions.ts b/packages/taler-wallet-core/src/transactions.ts
@@ -27,6 +27,8 @@ import {
Logger,
NotificationType,
ScopeType,
+ TalerError,
+ TalerErrorCode,
Transaction,
TransactionByIdRequest,
TransactionIdStr,
@@ -98,7 +100,11 @@ function shouldSkipCurrency(
}
case ScopeType.Auditor: {
// same currency and same auditor
- throw Error("filering balance in auditor scope is not implemented");
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_CORE_API_BAD_REQUEST,
+ { parameter: "scopeInfo" },
+ "filtering transactions by auditor scope is not implemented",
+ );
}
default:
assertUnreachable(transactionsRequest.scopeInfo);
@@ -130,6 +136,59 @@ const txOrder: { [t in TransactionType]: number } = {
[TransactionType.DenomLoss]: 13,
};
+/**
+ * Error for a transaction identifier that the client made up: either it does
+ * not parse at all, or it denotes a different transaction type than the
+ * operation accepts.
+ */
+export function makeInvalidTransactionIdError(
+ transactionId: string,
+ expectedType?: TransactionType,
+): TalerError {
+ const hint =
+ expectedType == null
+ ? `invalid transaction identifier "${transactionId}"`
+ : `expected a ${expectedType} transaction identifier, got "${transactionId}"`;
+ return TalerError.fromDetail(
+ TalerErrorCode.WALLET_CORE_API_BAD_REQUEST,
+ { parameter: "transactionId" },
+ hint,
+ );
+}
+
+/**
+ * Error for a well-formed transaction identifier that the wallet has no
+ * record for. Distinct from {@link makeInvalidTransactionIdError}: the client
+ * asked for something reasonable, it just isn't (or is no longer) there.
+ */
+export function makeTransactionNotFoundError(
+ transactionId: string,
+): TalerError {
+ return TalerError.fromDetail(
+ TalerErrorCode.WALLET_TRANSACTION_NOT_FOUND,
+ { transactionId },
+ `transaction ${transactionId} not found`,
+ );
+}
+
+/**
+ * Error for an action (abort/suspend/resume/fail) that this kind of
+ * transaction does not offer at all. Not to be confused with
+ * WALLET_REQUEST_TRANSACTION_STATE_UNSUPPORTED, which means the action exists
+ * but the transaction is currently in a state that does not allow it.
+ */
+export function makeTransactionActionUnsupportedError(
+ transactionId: string,
+ action: "abort" | "suspend" | "resume" | "fail" | "lookup",
+ hint?: string,
+): TalerError {
+ return TalerError.fromDetail(
+ TalerErrorCode.WALLET_TRANSACTION_ACTION_UNSUPPORTED,
+ { transactionId, action },
+ hint ?? `transaction ${transactionId} cannot be ${action}ed`,
+ );
+}
+
export async function getTransactionById(
wex: WalletExecutionContext,
req: TransactionByIdRequest,
@@ -137,7 +196,7 @@ export async function getTransactionById(
const parsedTx = parseTransactionIdentifier(req.transactionId);
if (!parsedTx) {
- throw Error("invalid transaction ID");
+ throw makeInvalidTransactionIdError(req.transactionId);
}
switch (parsedTx.tag) {
@@ -160,7 +219,11 @@ export async function getTransactionById(
}),
);
if (!txDetails) {
- throw Error("transaction not found");
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_TRANSACTION_NOT_FOUND,
+ { transactionId: req.transactionId },
+ `transaction ${req.transactionId} not found`,
+ );
}
return txDetails;
}
@@ -318,8 +381,10 @@ async function findOffsetTransaction(
if (req.offsetTimestamp) {
closestTimestamp = timestampPreciseToDb(req.offsetTimestamp);
} else {
- throw Error(
- "offset transaction not found and no offset timestamp specified",
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_TRANSACTION_NOT_FOUND,
+ { transactionId: req.offsetTransactionId },
+ `offset transaction ${req.offsetTransactionId} not found and no offset timestamp specified`,
);
}
} else if (req?.offsetTimestamp) {
@@ -629,6 +694,10 @@ export function constructTransactionIdentifier(
/**
* Parse a transaction identifier string into a typed, structured representation.
+ *
+ * Returns undefined for every kind of malformed identifier, so that callers can
+ * report one coded error instead of the shape of the failure deciding whether
+ * they see an exception or a missing value.
*/
export function parseTransactionIdentifier(
transactionId: string,
@@ -636,13 +705,13 @@ export function parseTransactionIdentifier(
const txnParts = transactionId.split(":");
if (txnParts.length < 3) {
- throw Error("id should have al least 3 parts separated by ':'");
+ return undefined;
}
const [prefix, type, ...rest] = txnParts;
if (prefix != "txn") {
- throw Error("invalid transaction identifier");
+ return undefined;
}
switch (type) {
@@ -702,7 +771,7 @@ function maybeTaskFromTransaction(
const parsedTx = parseTransactionIdentifier(transactionId);
if (!parsedTx) {
- throw Error("invalid transaction identifier");
+ throw makeInvalidTransactionIdError(transactionId);
}
// FIXME: We currently don't cancel active long-polling tasks here.
@@ -804,7 +873,7 @@ async function getContextForTransaction(
): Promise<TransactionContext> {
const tx = parseTransactionIdentifier(transactionId);
if (!tx) {
- throw Error("invalid transaction ID");
+ throw makeInvalidTransactionIdError(transactionId);
}
switch (tx.tag) {
case TransactionType.Deposit:
@@ -828,7 +897,11 @@ async function getContextForTransaction(
return new RefundTransactionContext(wex, tx.refundGroupId);
case TransactionType.Recoup:
//return new RecoupTransactionContext(ws, tx.recoupGroupId);
- throw new Error("not yet supported");
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_TRANSACTION_ACTION_UNSUPPORTED,
+ { transactionId, action: "lookup" },
+ "recoup transactions cannot be inspected yet",
+ );
case TransactionType.DenomLoss:
return new DenomLossTransactionContext(wex, tx.denomLossEventId);
default:
diff --git a/packages/taler-wallet-core/src/wallet.ts b/packages/taler-wallet-core/src/wallet.ts
@@ -371,7 +371,16 @@ async function handleTxRetries<T>(
logger.error(
`updating exchange already failed in execution context, not retrying`,
);
- throw exn;
+ // UnverifiedDenomError and OutdatedExchangeError are internal
+ // signals for "update the exchange and try again". Once that has
+ // been tried, they escape to the client, and there they have to
+ // carry a code instead of surfacing as an unexpected exception.
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_EXCHANGE_ENTRY_OUTDATED,
+ { exchangeBaseUrl: url },
+ `the wallet's information about ${url} is outdated and could not be refreshed`,
+ exn,
+ );
}
// Prevent both recursion and multiple updates
// per wallet execution context.
diff --git a/packages/taler-wallet-core/src/withdraw.ts b/packages/taler-wallet-core/src/withdraw.ts
@@ -187,6 +187,8 @@ import {
import {
constructTransactionIdentifier,
isUnsuccessfulTransaction,
+ makeInvalidTransactionIdError,
+ makeTransactionNotFoundError,
parseTransactionIdentifier,
} from "./transactions.js";
import { WALLET_EXCHANGE_PROTOCOL_VERSION } from "./versions.js";
@@ -2195,7 +2197,9 @@ async function processWithdrawalGroupAbortingBank(
),
);
if (!uriResult) {
- throw Error(`can't parse withdrawal URL ${wgInfo.bankInfo.talerWithdrawUri}`);
+ throw Error(
+ `can't parse withdrawal URL ${wgInfo.bankInfo.talerWithdrawUri}`,
+ );
}
logger.info(`aborting withdrawal ${uriResult.withdrawalOperationId}`);
const bankClient = walletBankIntegrationClient(
@@ -2798,8 +2802,10 @@ export async function getExchangeWithdrawalInfo(
if (exchange.currency != instructedAmount.currency) {
// Specifying the amount in the conversion input currency is not yet supported.
// We might add support for it later.
- throw new Error(
- `withdrawal only supported when specifying target currency ${exchange.currency}`,
+ throw TalerError.fromDetail(
+ TalerErrorCode.GENERIC_CURRENCY_MISMATCH,
+ {},
+ `withdrawal is only supported when the amount is given in the exchange's currency (${exchange.currency})`,
);
}
@@ -3075,7 +3081,10 @@ async function registerReserveWithBank(
);
}
const uriResult = Result.orUndefined(
- TalerUris.parseRestricted(bankInfo.talerWithdrawUri, TalerUriAction.Withdraw),
+ TalerUris.parseRestricted(
+ bankInfo.talerWithdrawUri,
+ TalerUriAction.Withdraw,
+ ),
);
if (!uriResult) {
throw Error(`can't parse withdrawal URL ${bankInfo.talerWithdrawUri}`);
@@ -3761,21 +3770,27 @@ export async function confirmWithdrawal(
req.amount == null ? undefined : Amounts.parseOrThrow(req.amount);
if (parsedTx?.tag !== TransactionType.Withdrawal) {
- throw Error("invalid withdrawal transaction ID");
+ throw makeInvalidTransactionIdError(
+ req.transactionId,
+ TransactionType.Withdrawal,
+ );
}
const withdrawalGroup = await wex.runWalletDbTx((tx) =>
tx.getWithdrawalGroup(parsedTx.withdrawalGroupId),
);
if (!withdrawalGroup) {
- throw Error("withdrawal group not found");
+ throw makeTransactionNotFoundError(req.transactionId);
}
if (
withdrawalGroup.wgInfo.withdrawalType !==
WithdrawalRecordType.BankIntegrated
) {
- throw Error("not a bank integrated withdrawal");
+ throw makeInvalidTransactionIdError(
+ req.transactionId,
+ TransactionType.Withdrawal,
+ );
}
await wex.runWalletDbTx(async (tx) => {
@@ -3799,7 +3814,14 @@ export async function confirmWithdrawal(
requireExchangeTosAcceptedOrThrow(wex, exchange);
if (req.amount && checkWithdrawalHardLimitExceeded(exchange, req.amount)) {
- throw Error("withdrawal would exceed hard KYC limit");
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_KYC_LIMIT_EXCEEDED,
+ {
+ exchangeBaseUrl: exchange.exchangeBaseUrl,
+ requestedAmount: req.amount,
+ },
+ "withdrawal would exceed a hard limit of the exchange",
+ );
}
const talerWithdrawUri = withdrawalGroup.wgInfo.bankInfo.talerWithdrawUri;
@@ -3844,8 +3866,10 @@ export async function confirmWithdrawal(
instructedAmount,
});
} else if (!withdrawalGroup.isForeignAccount) {
- throw Error(
- "Confirming withdrawals with non-foreign accounts and flexible amount is not supported. Consider adding external-confirmation=1 to the taler://withdraw URI.",
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_CORE_API_BAD_REQUEST,
+ { parameter: "amount" },
+ "confirming a withdrawal with a non-foreign account and a flexible amount is not supported; consider adding external-confirmation=1 to the taler://withdraw URI",
);
}
@@ -3853,10 +3877,7 @@ export async function confirmWithdrawal(
if (senderWire && !withdrawalGroup.isForeignAccount) {
logger.info(`sender wire is ${senderWire}`);
- const parsedSenderWire = Result.orUndefined(Paytos.fromString(senderWire));
- if (!parsedSenderWire) {
- throw Error("invalid payto URI");
- }
+ const parsedSenderWire = Paytos.parseOrThrow(senderWire);
let acceptable = false;
if (withdrawalAccountList.length == 0) {
logger.warn(`empty list of withdrawal accounts`);
@@ -3888,8 +3909,10 @@ export async function confirmWithdrawal(
// Might be acceptable if it's a withdrawal from a
// foreign account that is not properly marked as such.
logger.warn("no account acceptable by the exchange");
- throw Error(
- `Exchange ${selectedExchange} not usable for withdrawal, as account ${senderWire} is not acceptable to the exchange`,
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_NO_SUITABLE_EXCHANGE,
+ { currency: instructedCurrency },
+ `exchange ${selectedExchange} does not accept withdrawals to account ${senderWire}`,
);
}
@@ -4049,8 +4072,10 @@ export async function acceptBankIntegratedWithdrawal(
if (p.info.amount == null) {
if (req.amount == null) {
if (p.info.editableAmount) {
- throw Error(
- "amount required, as withdrawal operation has flexible amount",
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_CORE_API_BAD_REQUEST,
+ { parameter: "amount" },
+ "an amount is required, as this withdrawal operation has a flexible amount",
);
}
// Amount will be determined by the bank only after withdrawal has
@@ -4067,8 +4092,10 @@ export async function acceptBankIntegratedWithdrawal(
Amounts.cmp(p.info.amount, req.amount) != 0 &&
!p.info.editableAmount
) {
- throw Error(
- `mismatched amount, amount is fixed by bank (${p.info.amount}) but client provided different amount (${req.amount})`,
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_CORE_API_BAD_REQUEST,
+ { parameter: "amount" },
+ `the amount is fixed by the bank (${p.info.amount}), but the client asked for ${req.amount}`,
);
}
amount = Amounts.stringify(req.amount);
@@ -4250,13 +4277,19 @@ export async function createManualWithdrawal(
const exchange = await fetchFreshExchangeWithRetryNow(wex, exchangeBaseUrl);
if (exchange.currency != amount.currency) {
- throw Error(
- "manual withdrawal with conversion from foreign currency is not yet supported",
+ throw TalerError.fromDetail(
+ TalerErrorCode.GENERIC_CURRENCY_MISMATCH,
+ {},
+ `manual withdrawal in ${amount.currency} from an exchange that uses ${exchange.currency} is not yet supported`,
);
}
if (checkWithdrawalHardLimitExceeded(exchange, req.amount)) {
- throw Error("withdrawal would exceed hard KYC limit");
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_KYC_LIMIT_EXCEEDED,
+ { exchangeBaseUrl, requestedAmount: Amounts.stringify(req.amount) },
+ "withdrawal would exceed a hard limit of the exchange",
+ );
}
let reserveKeyPair: EddsaKeyPairStrings;
@@ -4381,7 +4414,11 @@ export async function internalGetWithdrawalDetailsForAmount(
);
}
if (!exchangeBaseUrl) {
- throw Error("could not find exchange for withdrawal");
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_NO_SUITABLE_EXCHANGE,
+ { currency: req.restrictScope?.currency },
+ "no exchange in the wallet can be used for this withdrawal",
+ );
}
const wi = await getExchangeWithdrawalInfo(
wex,