From 22b987c0324e898c9e97a7da4352df86cf6d8078 Mon Sep 17 00:00:00 2001 From: Marco Castignoli Date: Thu, 20 Nov 2025 10:15:11 +0100 Subject: [PATCH] Implement retry logic for Etherscan verification submission if contract is not yet indexed --- .../EtherscanVerifyApiService.ts | 75 +++++++++++++++---- .../storage/EtherscanVerifyApiService.spec.ts | 47 ++++++++++++ 2 files changed, 107 insertions(+), 15 deletions(-) diff --git a/services/server/src/server/services/storageServices/EtherscanVerifyApiService.ts b/services/server/src/server/services/storageServices/EtherscanVerifyApiService.ts index 804a53104..d1df2d1e1 100644 --- a/services/server/src/server/services/storageServices/EtherscanVerifyApiService.ts +++ b/services/server/src/server/services/storageServices/EtherscanVerifyApiService.ts @@ -502,22 +502,37 @@ export class EtherscanVerifyApiService implements WStorageService { return; } - let response: EtherscanRpcResponse; - try { - response = await this.submitVerification(verification, apiBaseUrl); - } catch (error: any) { - // Store the external verification result if we have job data - if (jobData) { - this.storeExternalVerificationResult(jobData, { - error: error.message, - }); - } - logger.error("Failed to submit verification to explorer", { - ...submissionContext, - error, + let response = await this.handleEtherscanApiVerification( + verification, + apiBaseUrl, + submissionContext, + jobData, + ); + + // Case in which Etherscan explorer hasn't indexed the contract yet + const maxRetries = 6; + const retryDelayMs = 5000; + let retries = 0; + while ( + response.message.startsWith("Unable to locate ContractCode at") && + retries < maxRetries + ) { + retries += 1; + logger.info( + "Explorer has not indexed contract yet; retrying verification submission", + { + ...submissionContext, + attempt: retries, + maxRetries, + }, + ); + await new Promise((resolve) => setTimeout(resolve, retryDelayMs)); + response = await this.handleEtherscanApiVerification( + verification, apiBaseUrl, - }); - throw error; + submissionContext, + jobData, + ); } logger.info("Submitted verification to explorer", { @@ -609,6 +624,36 @@ export class EtherscanVerifyApiService implements WStorageService { return (await response.json()) as EtherscanRpcResponse; } + private async handleEtherscanApiVerification( + verification: VerificationExport, + apiBaseUrl: string, + submissionContext: { + identifier: string; + chainId: number; + address: string; + }, + jobData?: { + verificationId: string; + finishTime: Date; + }, + ): Promise { + try { + return await this.submitVerification(verification, apiBaseUrl); + } catch (error: any) { + if (jobData) { + this.storeExternalVerificationResult(jobData, { + error: error.message, + }); + } + logger.error("Failed to submit verification to explorer", { + ...submissionContext, + apiBaseUrl, + error, + }); + throw error; + } + } + private async handleBlockscoutVyperVerification( verification: VerificationExport, apiBaseUrl: string, diff --git a/services/server/test/unit/storage/EtherscanVerifyApiService.spec.ts b/services/server/test/unit/storage/EtherscanVerifyApiService.spec.ts index 9b3a0404b..18839883c 100644 --- a/services/server/test/unit/storage/EtherscanVerifyApiService.spec.ts +++ b/services/server/test/unit/storage/EtherscanVerifyApiService.spec.ts @@ -325,4 +325,51 @@ describe("EtherscanVerifyApiService", function () { }, ); }); + + it("retries submission when contract is not yet indexed", async () => { + const clock = sandbox.useFakeTimers(); + const baseUrl = "https://etherscan.example/api"; + const upsertStub = sandbox.stub().resolves(); + const service = createService( + WStorageIdentifiers.EtherscanVerify, + baseUrl, + upsertStub, + ); + const jobData = { + verificationId: "verification-job-id", + finishTime: new Date(), + }; + + const unindexedResponse = mockFetchResponse({ + status: "0", + message: "Unable to locate ContractCode at 0xabc", + result: "N/A", + }); + const successResponse = mockFetchResponse({ + status: "1", + message: "OK", + result: "receipt-789", + }); + + fetchStub.onCall(0).resolves(unindexedResponse); + fetchStub.onCall(1).resolves(unindexedResponse); + fetchStub.onCall(2).resolves(successResponse); + + const verification = structuredClone(MockVerificationExport); + const storePromise = service.storeVerification(verification, jobData); + + await clock.tickAsync(10000); + await storePromise; + + expect(fetchStub.callCount).to.equal(3); + expect(clock.now).to.equal(10000); + sinon.assert.calledOnceWithExactly( + upsertStub, + jobData.verificationId, + WStorageIdentifiers.EtherscanVerify, + { + verificationId: "receipt-789", + }, + ); + }); });