Skip to content

Commit 54d36ac

Browse files
tfireubs-uiclaude
andcommitted
fix(news): add duplicate-guard to classifyRetryableError, fix log level and dead code
- Add "already exists|duplicate" guard in classifyRetryableError so a 409 response indicating a duplicate signal short-circuits before the NONCE_CONFLICT branch, preventing costly re-payment retries (mirrors the same guard already present in inbox.tools.ts) - Change console.error() to console.warn() for retry progress/info logs in news_file_signal — these are informational, not errors - Annotate the unreachable post-loop throw as dead code (the loop always exits via return on success or throw on the final failed attempt) - Add createFungiblePostCondition to test-news-file-signal.ts to enforce the exact sBTC transfer amount as a post-condition Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 40fd948 commit 54d36ac

2 files changed

Lines changed: 22 additions & 2 deletions

File tree

src/tools/news.tools.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,15 @@ interface RetryInfo {
157157
function classifyRetryableError(status: number, body: unknown): RetryInfo {
158158
const NOT_RETRYABLE: RetryInfo = { retryable: false, delayMs: 0, relaySideConflict: false };
159159

160+
// Duplicate-signal 409 from the news API must NOT be retried —
161+
// the signal was already delivered and retrying would re-pay.
162+
if (status === 409) {
163+
const bodyStr = typeof body === "string" ? body : JSON.stringify(body);
164+
if (/already exists|duplicate/i.test(bodyStr)) {
165+
return NOT_RETRYABLE;
166+
}
167+
}
168+
160169
if (typeof body === "object" && body !== null) {
161170
const b = body as Record<string, unknown>;
162171
const rawRetryAfter = typeof b["retryAfter"] === "number" ? b["retryAfter"] : 0;
@@ -719,7 +728,7 @@ Fields:
719728

720729
for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) {
721730
if (attempt > 0 && nextRetryDelayMs > 0) {
722-
console.error(
731+
console.warn(
723732
`[news_file_signal] Retry attempt ${attempt}/${MAX_ATTEMPTS - 1} after ${nextRetryDelayMs}ms`
724733
);
725734
await sleep(nextRetryDelayMs);
@@ -803,7 +812,7 @@ Fields:
803812
const retry = classifyRetryableError(finalRes.status, parsed);
804813

805814
if (retry.retryable && attempt < MAX_ATTEMPTS - 1) {
806-
console.error(
815+
console.warn(
807816
`[news_file_signal] Retryable error on attempt ${attempt + 1}: status=${finalRes.status} relaySide=${retry.relaySideConflict} body=${responseData}`
808817
);
809818
nextRetryDelayMs = retry.delayMs;
@@ -824,6 +833,8 @@ Fields:
824833
);
825834
}
826835

836+
// Dead code: the loop always exits via return (success) or throw (failure above).
837+
// This path is only reached if MAX_ATTEMPTS is 0, which is not a valid config.
827838
throw new Error(
828839
`Signal filing failed after ${MAX_ATTEMPTS} attempts. Last error: ${lastError}`
829840
);

tests/scripts/test-news-file-signal.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import { getWalletManager } from "../../src/services/wallet-manager.js";
2929
import { getAccount, NETWORK, checkSufficientBalance } from "../../src/services/x402.service.js";
3030
import { getStacksNetwork } from "../../src/config/networks.js";
3131
import { getContracts, parseContractId } from "../../src/config/contracts.js";
32+
import { createFungiblePostCondition } from "../../src/transactions/post-conditions.js";
3233
import { bip322Sign } from "../../src/utils/bip322.js";
3334

3435
const SIGNALS_URL = process.env.SIGNALS_URL || "http://localhost:8787/api/signals";
@@ -180,6 +181,13 @@ async function main() {
180181
console.log("\n[6] Building sponsored tx...");
181182
const contracts = getContracts(NETWORK);
182183
const { address: contractAddress, name: contractName } = parseContractId(contracts.SBTC_TOKEN);
184+
const postCondition = createFungiblePostCondition(
185+
account.address,
186+
contracts.SBTC_TOKEN,
187+
"sbtc-token",
188+
"eq",
189+
amount
190+
);
183191
const transaction = await makeContractCall({
184192
contractAddress,
185193
contractName,
@@ -192,6 +200,7 @@ async function main() {
192200
],
193201
senderKey: account.privateKey,
194202
network: getStacksNetwork(NETWORK),
203+
postConditions: [postCondition],
195204
sponsored: true,
196205
fee: 0n,
197206
});

0 commit comments

Comments
 (0)