diff --git a/services/monitor/src/ChainMonitor.ts b/services/monitor/src/ChainMonitor.ts index f7e5afd9f..ce700f46f 100644 --- a/services/monitor/src/ChainMonitor.ts +++ b/services/monitor/src/ChainMonitor.ts @@ -16,6 +16,7 @@ import type { } from "./types"; import PendingContract from "./PendingContract"; import type { Logger } from "winston"; +import type SimilarityVerificationClient from "./SimilarityVerificationClient"; function createsContract(tx: TransactionResponse): boolean { return !tx.to; @@ -31,6 +32,7 @@ export default class ChainMonitor extends EventEmitter { private sourceFetchers: KnownDecentralizedStorageFetchers; private sourcifyServerURLs: string[]; private sourcifyRequestOptions: SourcifyRequestOptions; + private similarityVerificationClient: SimilarityVerificationClient; private chainLogger: Logger; private startBlock?: number; @@ -47,10 +49,12 @@ export default class ChainMonitor extends EventEmitter { sourcifyChain: SourcifyChain, sourceFetchers: KnownDecentralizedStorageFetchers, monitorConfig: MonitorConfig, + similarityVerificationClient: SimilarityVerificationClient, ) { super(); this.sourcifyChain = sourcifyChain; this.sourceFetchers = sourceFetchers; // TODO: handle multipe + this.similarityVerificationClient = similarityVerificationClient; this.chainLogger = logger.child({ moduleName: "ChainMonitor #" + this.sourcifyChain.chainId, chainId: this.sourcifyChain.chainId, @@ -286,6 +290,11 @@ export default class ChainMonitor extends EventEmitter { address, origin: metadataHash.origin, }); + this.similarityVerificationClient.trigger( + this.sourcifyChain.chainId, + address, + creatorTxHash, + ); return; } @@ -300,6 +309,11 @@ export default class ChainMonitor extends EventEmitter { await pendingContract.assemble(); } catch (err: any) { this.chainLogger.info("Couldn't assemble contract", { address, err }); + this.similarityVerificationClient.trigger( + this.sourcifyChain.chainId, + address, + creatorTxHash, + ); return; } if (!this.isEmpty(pendingContract.pendingSources)) { @@ -307,6 +321,11 @@ export default class ChainMonitor extends EventEmitter { address: pendingContract.address, pendingSources: pendingContract.pendingSources, }); + this.similarityVerificationClient.trigger( + this.sourcifyChain.chainId, + address, + creatorTxHash, + ); return; } diff --git a/services/monitor/src/Monitor.ts b/services/monitor/src/Monitor.ts index f89441c11..a14aaa7a2 100755 --- a/services/monitor/src/Monitor.ts +++ b/services/monitor/src/Monitor.ts @@ -15,6 +15,7 @@ import type { import dotenv from "dotenv"; import defaultConfig from "./defaultConfig"; import path from "path"; +import SimilarityVerificationClient from "./SimilarityVerificationClient"; dotenv.config({ path: path.resolve(__dirname, "..", ".env") }); @@ -22,6 +23,7 @@ export default class Monitor extends EventEmitter { private chainMonitors: ChainMonitor[]; private sourceFetchers: KnownDecentralizedStorageFetchers = {}; private config: MonitorConfig; + private similarityVerificationClient: SimilarityVerificationClient; constructor( chainsToMonitor: MonitorChain[], @@ -53,6 +55,14 @@ export default class Monitor extends EventEmitter { ); } + const similarityBaseUrls = this.config.sourcifyServerURLs + .map((url) => url.replace(/\/+$/, "")) + .filter(Boolean); + this.similarityVerificationClient = new SimilarityVerificationClient( + similarityBaseUrls, + this.config.similarityVerification, + ); + const sourcifyChains = chainsToMonitor.map((chain) => { if (chain instanceof SourcifyChain) { return chain; @@ -100,7 +110,13 @@ export default class Monitor extends EventEmitter { } this.chainMonitors = sourcifyChains.map( - (chain) => new ChainMonitor(chain, this.sourceFetchers, this.config), + (chain) => + new ChainMonitor( + chain, + this.sourceFetchers, + this.config, + this.similarityVerificationClient, + ), ); } diff --git a/services/monitor/src/SimilarityVerificationClient.ts b/services/monitor/src/SimilarityVerificationClient.ts new file mode 100644 index 000000000..c4b03968b --- /dev/null +++ b/services/monitor/src/SimilarityVerificationClient.ts @@ -0,0 +1,68 @@ +import logger from "./logger"; +import type { Logger } from "winston"; +import type { SimilarityVerificationConfig } from "./types"; + +const trimTrailingSlash = (url: string) => url.replace(/\/+$/, ""); + +export default class SimilarityVerificationClient { + private baseUrls: string[]; + private clientLogger: Logger; + private requestDelay: number; + + constructor(baseUrls: string[], options: SimilarityVerificationConfig) { + this.baseUrls = baseUrls.map((url) => trimTrailingSlash(url)); + this.clientLogger = logger.child({ moduleName: "SimilarityVerification" }); + this.requestDelay = options.requestDelay ?? 15 * 1000; + } + + trigger = ( + chainId: number, + address: string, + creationTransactionHash?: string, + ) => { + this.baseUrls.forEach(async (baseUrl) => { + // Give time to the explorer to index the new contract before triggering similarity verification + await new Promise((resolve) => setTimeout(resolve, this.requestDelay)); + const url = `${baseUrl}/v2/verify/similarity/${chainId}/${address}`; + try { + this.clientLogger.info("Triggering similarity verification", { + chainId, + address, + baseUrl, + }); + const response = await fetch(url, { + method: "POST", + headers: { + "Content-Type": "application/json", + "User-Agent": "sourcify-monitor", + }, + body: JSON.stringify({ + ...(creationTransactionHash + ? { creationTransactionHash } + : undefined), + }), + }); + + if (!response.ok) { + const responseText = await response.text(); + throw new Error( + `Similarity verification request failed: ${response.status} ${response.statusText} - ${responseText}`, + ); + } + + this.clientLogger.info("Similarity verification triggered", { + chainId, + address, + baseUrl, + }); + } catch (error: any) { + this.clientLogger.warn("Error triggering similarity verification", { + chainId, + address, + baseUrl, + error, + }); + } + }); + }; +} diff --git a/services/monitor/src/defaultConfig.js b/services/monitor/src/defaultConfig.js index 173bddcad..8a6da200e 100644 --- a/services/monitor/src/defaultConfig.js +++ b/services/monitor/src/defaultConfig.js @@ -13,6 +13,9 @@ const defaultConfig = { maxRetries: 3, retryDelay: 30000, }, + similarityVerification: { + requestDelay: 15000, + }, defaultChainConfig: { startBlock: undefined, blockInterval: 10000, diff --git a/services/monitor/src/types.ts b/services/monitor/src/types.ts index 8291f7fbb..71d670775 100644 --- a/services/monitor/src/types.ts +++ b/services/monitor/src/types.ts @@ -55,16 +55,22 @@ export type MonitorConfig = { sourcifyServerURLs: string[]; sourcifyRequestOptions: SourcifyRequestOptions; defaultChainConfig: DefatultChainMonitorConfig; + similarityVerification: SimilarityVerificationConfig; chainConfigs?: { [chainId: number]: ChainMonitorConfig; }; }; +export interface SimilarityVerificationConfig { + requestDelay?: number; +} + export type PassedMonitorConfig = { - decentralizedStorages?: DecentralizedStorageConfig; + decentralizedStorages?: DecentralizedStorageConfigMap; sourcifyServerURLs?: string[]; sourcifyRequestOptions?: Partial; defaultChainConfig?: DefatultChainMonitorConfig; + similarityVerification?: SimilarityVerificationConfig; chainConfigs?: { [chainId: number]: ChainMonitorConfig; }; diff --git a/services/monitor/test/Monitor.spec.ts b/services/monitor/test/Monitor.spec.ts index be33aecdb..1a75f9071 100644 --- a/services/monitor/test/Monitor.spec.ts +++ b/services/monitor/test/Monitor.spec.ts @@ -4,7 +4,7 @@ import sinon from "sinon"; import Monitor, { authenticateRpcs } from "../src/Monitor"; import logger from "../src/logger"; import type { JsonRpcSigner } from "ethers"; -import { FetchRequest, JsonRpcProvider, Network } from "ethers"; +import { JsonRpcProvider, Network } from "ethers"; import { deployFromAbiAndBytecode, nockInterceptorForVerification, @@ -26,6 +26,7 @@ const HARDHAT_BLOCK_TIME_IN_SEC = 3; const MOCK_SOURCIFY_SERVER = "http://mocksourcifyserver.dev/server/"; const MOCK_SOURCIFY_SERVER_RETURNING_ERRORS = "http://mocksourcifyserver-returning-errors.dev/server/"; +const MOCK_SIMILARITY_SERVER = "http://mocksimilarity.dev/server/"; const localChain = { chainId: 1337, rpc: [`http://localhost:${HARDHAT_PORT}`], @@ -239,5 +240,55 @@ describe("Monitor", function () { monitor.start(); }); }); + + it.only("should trigger similarity verification when contract assembly fails", async () => { + monitor = new Monitor([localChain], { + sourcifyServerURLs: [MOCK_SIMILARITY_SERVER], + decentralizedStorages: { + ipfs: { + enabled: false, + gateways: [], + }, + }, + chainConfigs: { + [localChain.chainId]: { + startBlock: 0, + blockInterval: HARDHAT_BLOCK_TIME_IN_SEC * 1000, + }, + }, + similarityVerification: { + requestDelay: 2000, // Override to 2 seconds for faster tests + }, + }); + + const contractAddress = await deployFromAbiAndBytecode( + signer, + storageContractArtifact.abi, + storageContractArtifact.bytecode, + [], + ); + + const similarityScope = nock("http://mocksimilarity.dev") + .post( + `/server/v2/verify/similarity/${localChain.chainId}/${contractAddress}`, + (body) => { + expect(body).to.have.property("creationTransactionHash"); + return true; + }, + ) + .reply(200, { status: "ok" }); + + await monitor.start(); + + await new Promise((resolve, reject) => { + similarityScope.on("replied", () => resolve()); + setTimeout( + () => reject(new Error("Similarity verification not called")), + 10000, + ); + }); + + expect(similarityScope.isDone()).to.be.true; + }); // Add more test cases as needed });