mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 07:52:24 +00:00
Handle zap allowance exhaustion
This commit is contained in:
1 parent
2c5213aa17
commit
8879c3a46d
4 files changed
+305
-58
No files matched your search
+53
-15
@@ -37,10 +37,12 @@ import {
|
||||
getCachedLightningEntry,
|
||||
getCachedMetadataByUrl,
|
||||
getCachedPlatformLightningAddress,
|
||||
isZapAllowanceExhaustedError,
|
||||
isMetadataEntryFresh,
|
||||
normalizeLightningAddressKey,
|
||||
rememberLightningMetadata,
|
||||
setCachedPlatformLightningAddress,
|
||||
buildZapAllowanceExhaustedMessage,
|
||||
validateInvoiceAmount
|
||||
} from "./payments/zapSharedState.js";
|
||||
import { splitAndZap } from "./payments/zapSplit.js";
|
||||
@@ -2570,13 +2572,18 @@ function createZapDependencies({
|
||||
}
|
||||
return payment;
|
||||
} catch (error) {
|
||||
const allowanceExhausted = isZapAllowanceExhaustedError(error);
|
||||
if (allowanceExhausted) {
|
||||
error.__zapAllowanceExhausted = true;
|
||||
}
|
||||
if (Array.isArray(shareTracker)) {
|
||||
shareTracker.push({
|
||||
type: shareType,
|
||||
status: "error",
|
||||
amount: shareAmount,
|
||||
address,
|
||||
error
|
||||
error,
|
||||
allowanceExhausted
|
||||
});
|
||||
}
|
||||
if (
|
||||
@@ -2993,23 +3000,54 @@ async function handleZapSend(event) {
|
||||
const failureShares = tracker.filter(
|
||||
(entry) => entry && entry.status !== "success" && entry.amount > 0
|
||||
);
|
||||
const allowanceExhausted =
|
||||
isZapAllowanceExhaustedError(error) ||
|
||||
failureShares.some((share) =>
|
||||
isZapAllowanceExhaustedError(share?.error)
|
||||
);
|
||||
if (allowanceExhausted) {
|
||||
resetZapRetryState();
|
||||
}
|
||||
|
||||
if (failureShares.length) {
|
||||
markZapRetryPending(failureShares);
|
||||
const summary = failureShares
|
||||
.map((share) => `${describeShareType(share.type)} ${share.amount} sats`)
|
||||
.join(", ");
|
||||
const tone = tracker.length > failureShares.length ? "warning" : "error";
|
||||
const statusMessage =
|
||||
tracker.length > failureShares.length
|
||||
? `Partial zap failure. Press Send again to retry: ${summary}.`
|
||||
: `Zap failed. Press Send again to retry: ${summary}.`;
|
||||
setZapStatus(statusMessage, tone);
|
||||
app?.showError?.(error?.message || statusMessage);
|
||||
if (allowanceExhausted) {
|
||||
const allowanceShare = failureShares.find((share) =>
|
||||
isZapAllowanceExhaustedError(share?.error)
|
||||
);
|
||||
const allowanceMessage = buildZapAllowanceExhaustedMessage(
|
||||
isZapAllowanceExhaustedError(error) ? error : allowanceShare?.error
|
||||
);
|
||||
const partialSuccess = tracker.length > failureShares.length;
|
||||
const tone = partialSuccess ? "warning" : "error";
|
||||
const statusMessage = partialSuccess
|
||||
? `Some zap shares succeeded before your wallet allowance was exhausted. ${allowanceMessage}`
|
||||
: allowanceMessage;
|
||||
setZapStatus(statusMessage, tone);
|
||||
app?.showError?.(statusMessage);
|
||||
} else {
|
||||
markZapRetryPending(failureShares);
|
||||
const summary = failureShares
|
||||
.map((share) => `${describeShareType(share.type)} ${share.amount} sats`)
|
||||
.join(", ");
|
||||
const tone = tracker.length > failureShares.length ? "warning" : "error";
|
||||
const statusMessage =
|
||||
tracker.length > failureShares.length
|
||||
? `Partial zap failure. Press Send again to retry: ${summary}.`
|
||||
: `Zap failed. Press Send again to retry: ${summary}.`;
|
||||
setZapStatus(statusMessage, tone);
|
||||
app?.showError?.(error?.message || statusMessage);
|
||||
}
|
||||
} else {
|
||||
resetZapRetryState();
|
||||
const message = error?.message || "Zap failed. Please try again.";
|
||||
setZapStatus(message, "error");
|
||||
app?.showError?.(message);
|
||||
if (allowanceExhausted) {
|
||||
const allowanceMessage = buildZapAllowanceExhaustedMessage(error);
|
||||
setZapStatus(allowanceMessage, "error");
|
||||
app?.showError?.(allowanceMessage);
|
||||
} else {
|
||||
const message = error?.message || "Zap failed. Please try again.";
|
||||
setZapStatus(message, "error");
|
||||
app?.showError?.(message);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
zapInFlight = false;
|
||||
|
||||
@@ -162,3 +162,97 @@ export function clearZapCaches() {
|
||||
}
|
||||
|
||||
export { validateInvoiceAmount };
|
||||
|
||||
function extractErrorMessage(error) {
|
||||
if (!error) {
|
||||
return "";
|
||||
}
|
||||
if (typeof error === "string") {
|
||||
return error;
|
||||
}
|
||||
if (typeof error?.message === "string") {
|
||||
return error.message;
|
||||
}
|
||||
if (typeof error?.reason === "string") {
|
||||
return error.reason;
|
||||
}
|
||||
if (typeof error?.error?.message === "string") {
|
||||
return error.error.message;
|
||||
}
|
||||
if (typeof error?.data?.error === "string") {
|
||||
return error.data.error;
|
||||
}
|
||||
if (typeof error?.data?.error?.message === "string") {
|
||||
return error.data.error.message;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
const ALLOWANCE_ERROR_PATTERNS = [
|
||||
"budget exceeded",
|
||||
"allowance exceeded",
|
||||
"allowance exhausted",
|
||||
"allowance spent",
|
||||
"allowance depleted",
|
||||
"allowance limit reached",
|
||||
"spending allowance exceeded",
|
||||
"spending allowance reached",
|
||||
"spending limit reached",
|
||||
"quota exceeded",
|
||||
];
|
||||
|
||||
export function isZapAllowanceExhaustedError(error) {
|
||||
const message = extractErrorMessage(error);
|
||||
if (!message) {
|
||||
return false;
|
||||
}
|
||||
const normalized = message.toLowerCase();
|
||||
if (ALLOWANCE_ERROR_PATTERNS.some((pattern) => normalized.includes(pattern))) {
|
||||
return true;
|
||||
}
|
||||
if (normalized.includes("allowance") && normalized.includes("zap")) {
|
||||
if (
|
||||
normalized.includes("exceed") ||
|
||||
normalized.includes("exhaust") ||
|
||||
normalized.includes("spent") ||
|
||||
normalized.includes("deplet")
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (normalized.includes("budget") && normalized.includes("zap")) {
|
||||
if (
|
||||
normalized.includes("exceed") ||
|
||||
normalized.includes("spent") ||
|
||||
normalized.includes("deplet")
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
const ALLOWANCE_GUIDANCE_SUFFIX =
|
||||
"Increase your wallet zap limit or reduce the platform split to continue.";
|
||||
const DEFAULT_ALLOWANCE_MESSAGE =
|
||||
"Your wallet's zap allowance has been exhausted. " + ALLOWANCE_GUIDANCE_SUFFIX;
|
||||
|
||||
export function buildZapAllowanceExhaustedMessage(error) {
|
||||
const baseMessage = extractErrorMessage(error).trim();
|
||||
if (!baseMessage) {
|
||||
return DEFAULT_ALLOWANCE_MESSAGE;
|
||||
}
|
||||
const normalized = baseMessage.toLowerCase();
|
||||
const punctuation = /[.!?]$/.test(baseMessage) ? "" : ".";
|
||||
if (
|
||||
normalized.includes("zap allowance") &&
|
||||
normalized.includes("increase") &&
|
||||
normalized.includes("platform split")
|
||||
) {
|
||||
return baseMessage;
|
||||
}
|
||||
if (normalized.includes("zap") && normalized.includes("allowance")) {
|
||||
return `${baseMessage}${punctuation} ${ALLOWANCE_GUIDANCE_SUFFIX}`;
|
||||
}
|
||||
return `${baseMessage}${punctuation} ${DEFAULT_ALLOWANCE_MESSAGE}`;
|
||||
}
|
||||
+84
-43
@@ -7,10 +7,12 @@ import {
|
||||
getCachedLightningEntry,
|
||||
getCachedMetadataByUrl,
|
||||
getCachedPlatformLightningAddress,
|
||||
isZapAllowanceExhaustedError,
|
||||
isMetadataEntryFresh,
|
||||
normalizeLightningAddressKey,
|
||||
rememberLightningMetadata,
|
||||
setCachedPlatformLightningAddress,
|
||||
buildZapAllowanceExhaustedMessage,
|
||||
validateInvoiceAmount,
|
||||
} from "../payments/zapSharedState.js";
|
||||
import {
|
||||
@@ -557,31 +559,36 @@ export default class ZapController {
|
||||
shareType === "platform"
|
||||
? platformEntry?.address || getCachedPlatformLightningAddress()
|
||||
: creatorEntry?.address || lightningAddress;
|
||||
try {
|
||||
const payment = await sendPaymentFn(bolt11, params);
|
||||
if (Array.isArray(shareTracker)) {
|
||||
shareTracker.push({
|
||||
type: shareType,
|
||||
try {
|
||||
const payment = await sendPaymentFn(bolt11, params);
|
||||
if (Array.isArray(shareTracker)) {
|
||||
shareTracker.push({
|
||||
type: shareType,
|
||||
status: "success",
|
||||
amount,
|
||||
address,
|
||||
payment,
|
||||
});
|
||||
}
|
||||
return payment;
|
||||
} catch (error) {
|
||||
if (Array.isArray(shareTracker)) {
|
||||
shareTracker.push({
|
||||
type: shareType,
|
||||
status: "error",
|
||||
amount,
|
||||
address,
|
||||
error,
|
||||
});
|
||||
}
|
||||
if (
|
||||
typeof error?.message === "string" &&
|
||||
error.message.toLowerCase().includes("timed out")
|
||||
});
|
||||
}
|
||||
return payment;
|
||||
} catch (error) {
|
||||
const allowanceExhausted = isZapAllowanceExhaustedError(error);
|
||||
if (allowanceExhausted) {
|
||||
error.__zapAllowanceExhausted = true;
|
||||
}
|
||||
if (Array.isArray(shareTracker)) {
|
||||
shareTracker.push({
|
||||
type: shareType,
|
||||
status: "error",
|
||||
amount,
|
||||
address,
|
||||
error,
|
||||
allowanceExhausted,
|
||||
});
|
||||
}
|
||||
if (
|
||||
typeof error?.message === "string" &&
|
||||
error.message.toLowerCase().includes("timed out")
|
||||
) {
|
||||
try {
|
||||
resetWalletClientFn();
|
||||
@@ -982,31 +989,65 @@ export default class ZapController {
|
||||
const failureShares = tracker.filter(
|
||||
(entry) => entry && entry.status !== "success" && entry.amount > 0
|
||||
);
|
||||
const allowanceExhausted =
|
||||
isZapAllowanceExhaustedError(error) ||
|
||||
failureShares.some((share) =>
|
||||
isZapAllowanceExhaustedError(share?.error)
|
||||
);
|
||||
if (allowanceExhausted) {
|
||||
this.resetRetryState();
|
||||
}
|
||||
|
||||
if (failureShares.length) {
|
||||
this.markRetryPending(failureShares, comment);
|
||||
const summary = failureShares
|
||||
.map((share) => {
|
||||
const label =
|
||||
share.type === "platform"
|
||||
? "Platform"
|
||||
: share.type === "creator"
|
||||
? "Creator"
|
||||
: "Lightning";
|
||||
return `${label} ${share.amount} sats`;
|
||||
})
|
||||
.join(", ");
|
||||
const tone = tracker.length > failureShares.length ? "warning" : "error";
|
||||
const statusMessage =
|
||||
tracker.length > failureShares.length
|
||||
? `Partial zap failure. Press Send again to retry: ${summary}.`
|
||||
: `Zap failed. Press Send again to retry: ${summary}.`;
|
||||
this.videoModal?.setZapStatus(statusMessage, tone);
|
||||
this.notifyError(error?.message || statusMessage);
|
||||
if (allowanceExhausted) {
|
||||
const allowanceShare = failureShares.find((share) =>
|
||||
isZapAllowanceExhaustedError(share?.error)
|
||||
);
|
||||
const allowanceSource = isZapAllowanceExhaustedError(error)
|
||||
? error
|
||||
: allowanceShare?.error;
|
||||
const allowanceMessage = buildZapAllowanceExhaustedMessage(
|
||||
allowanceSource
|
||||
);
|
||||
const partialSuccess = tracker.length > failureShares.length;
|
||||
const tone = partialSuccess ? "warning" : "error";
|
||||
const statusMessage = partialSuccess
|
||||
? `Some zap shares succeeded before your wallet allowance was exhausted. ${allowanceMessage}`
|
||||
: allowanceMessage;
|
||||
this.videoModal?.setZapStatus(statusMessage, tone);
|
||||
this.notifyError(statusMessage);
|
||||
} else {
|
||||
this.markRetryPending(failureShares, comment);
|
||||
const summary = failureShares
|
||||
.map((share) => {
|
||||
const label =
|
||||
share.type === "platform"
|
||||
? "Platform"
|
||||
: share.type === "creator"
|
||||
? "Creator"
|
||||
: "Lightning";
|
||||
return `${label} ${share.amount} sats`;
|
||||
})
|
||||
.join(", ");
|
||||
const tone = tracker.length > failureShares.length ? "warning" : "error";
|
||||
const statusMessage =
|
||||
tracker.length > failureShares.length
|
||||
? `Partial zap failure. Press Send again to retry: ${summary}.`
|
||||
: `Zap failed. Press Send again to retry: ${summary}.`;
|
||||
this.videoModal?.setZapStatus(statusMessage, tone);
|
||||
this.notifyError(error?.message || statusMessage);
|
||||
}
|
||||
} else {
|
||||
this.resetRetryState();
|
||||
const message = error?.message || "Zap failed. Please try again.";
|
||||
this.videoModal?.setZapStatus(message, "error");
|
||||
this.notifyError(message);
|
||||
if (allowanceExhausted) {
|
||||
const allowanceMessage = buildZapAllowanceExhaustedMessage(error);
|
||||
this.videoModal?.setZapStatus(allowanceMessage, "error");
|
||||
this.notifyError(allowanceMessage);
|
||||
} else {
|
||||
const message = error?.message || "Zap failed. Please try again.";
|
||||
this.videoModal?.setZapStatus(message, "error");
|
||||
this.notifyError(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -543,4 +543,78 @@ await (async () => {
|
||||
app.destroy();
|
||||
})();
|
||||
|
||||
await (async () => {
|
||||
// Test: allowance exhaustion surfaces guidance and skips retry queue
|
||||
const { app, modalStub, creatorAddress, platformAddress } = await createApp({
|
||||
splitAndZap: async () => ({ receipts: [] }),
|
||||
});
|
||||
|
||||
const shareError = new Error("Budget exceeded");
|
||||
const tracker = [
|
||||
{
|
||||
type: "creator",
|
||||
status: "success",
|
||||
amount: 900,
|
||||
address: creatorAddress,
|
||||
payment: { preimage: "aa".repeat(16) },
|
||||
},
|
||||
{
|
||||
type: "platform",
|
||||
status: "error",
|
||||
amount: 100,
|
||||
address: platformAddress,
|
||||
error: shareError,
|
||||
},
|
||||
];
|
||||
|
||||
const walletError = new Error("Budget exceeded");
|
||||
walletError.__zapShareTracker = tracker;
|
||||
|
||||
modalStub.statusMessages = [];
|
||||
modalStub.retryState = undefined;
|
||||
|
||||
app.zapController.handleZapError({
|
||||
error: walletError,
|
||||
amount: 1000,
|
||||
comment: "",
|
||||
walletSettings: { nwcUri: "nostr+walletconnect://example" },
|
||||
});
|
||||
|
||||
const lastStatus =
|
||||
modalStub.statusMessages[modalStub.statusMessages.length - 1];
|
||||
assert.ok(lastStatus, "status message should be recorded");
|
||||
assert.equal(
|
||||
lastStatus.tone,
|
||||
"warning",
|
||||
"partial allowance exhaustion should produce a warning tone",
|
||||
);
|
||||
assert.ok(
|
||||
/zap allowance/i.test(lastStatus.message),
|
||||
"status message should mention zap allowance exhaustion",
|
||||
);
|
||||
assert.ok(
|
||||
/increase your wallet zap limit or reduce the platform split/i.test(
|
||||
lastStatus.message,
|
||||
),
|
||||
"status message should advise adjusting allowance or platform split",
|
||||
);
|
||||
assert.equal(
|
||||
lastStatus.message.includes("Press Send again"),
|
||||
false,
|
||||
"allowance exhaustion should not prompt a retry",
|
||||
);
|
||||
assert.equal(
|
||||
app.zapController.modalZapRetryState,
|
||||
null,
|
||||
"controller should clear retry state when allowance exhausted",
|
||||
);
|
||||
assert.equal(
|
||||
modalStub.retryState?.pending,
|
||||
false,
|
||||
"modal should not mark retry pending when allowance exhausted",
|
||||
);
|
||||
|
||||
app.destroy();
|
||||
})();
|
||||
|
||||
process.exit(0);
|
||||
Reference in new issue
Block a user