diff --git a/src/tools/news.tools.ts b/src/tools/news.tools.ts index 6b0dba3..c9c1a2d 100644 --- a/src/tools/news.tools.ts +++ b/src/tools/news.tools.ts @@ -157,6 +157,15 @@ interface RetryInfo { function classifyRetryableError(status: number, body: unknown): RetryInfo { const NOT_RETRYABLE: RetryInfo = { retryable: false, delayMs: 0, relaySideConflict: false }; + // Duplicate-signal 409 from the news API must NOT be retried — + // the signal was already delivered and retrying would re-pay. + if (status === 409) { + const bodyStr = typeof body === "string" ? body : JSON.stringify(body); + if (/already exists|duplicate/i.test(bodyStr)) { + return NOT_RETRYABLE; + } + } + if (typeof body === "object" && body !== null) { const b = body as Record; const rawRetryAfter = typeof b["retryAfter"] === "number" ? b["retryAfter"] : 0; @@ -719,7 +728,7 @@ Fields: for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) { if (attempt > 0 && nextRetryDelayMs > 0) { - console.error( + console.warn( `[news_file_signal] Retry attempt ${attempt}/${MAX_ATTEMPTS - 1} after ${nextRetryDelayMs}ms` ); await sleep(nextRetryDelayMs); @@ -824,9 +833,7 @@ Fields: ); } - throw new Error( - `Signal filing failed after ${MAX_ATTEMPTS} attempts. Last error: ${lastError}` - ); + } catch (error) { return createErrorResponse(error); } diff --git a/tests/scripts/test-news-file-signal.ts b/tests/scripts/test-news-file-signal.ts index 9b61dce..66b871a 100644 --- a/tests/scripts/test-news-file-signal.ts +++ b/tests/scripts/test-news-file-signal.ts @@ -29,6 +29,7 @@ import { getWalletManager } from "../../src/services/wallet-manager.js"; import { getAccount, NETWORK, checkSufficientBalance } from "../../src/services/x402.service.js"; import { getStacksNetwork } from "../../src/config/networks.js"; import { getContracts, parseContractId } from "../../src/config/contracts.js"; +import { createFungiblePostCondition } from "../../src/transactions/post-conditions.js"; import { bip322Sign } from "../../src/utils/bip322.js"; const SIGNALS_URL = process.env.SIGNALS_URL || "http://localhost:8787/api/signals"; @@ -180,6 +181,13 @@ async function main() { console.log("\n[6] Building sponsored tx..."); const contracts = getContracts(NETWORK); const { address: contractAddress, name: contractName } = parseContractId(contracts.SBTC_TOKEN); + const postCondition = createFungiblePostCondition( + account.address, + contracts.SBTC_TOKEN, + "sbtc-token", + "eq", + amount + ); const transaction = await makeContractCall({ contractAddress, contractName, @@ -192,6 +200,7 @@ async function main() { ], senderKey: account.privateKey, network: getStacksNetwork(NETWORK), + postConditions: [postCondition], sponsored: true, fee: 0n, });