import { mkdir, readFile, writeFile } from "node:fs/promises";
import path from "node:path";
import { bytesToHex, createPublicClient, createWalletClient, formatUnits, http, keccak256, parseEventLogs, parseUnits } from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { bountyManagerAbi, erc20Abi } from "./abi.js";
import { ProtocolApiClient } from "./api.js";
import { loadConfig } from "./config.js";
import { extractSatAssignment, runCryptoMiniSat } from "./cryptominisat.js";
import { assignmentFromLiterals, assignmentSatisfies, dimacsCnfText, parseDimacsCnfFile, unitClauseCnfText } from "./dimacs.js";
import { Logger } from "./logger.js";
import { SolverState } from "./state.js";
const zeroAddress = "0x0000000000000000000000000000000000000000";
const zeroHash = "0x0000000000000000000000000000000000000000000000000000000000000000";
const solutionKindSatAssignment = 1;
const solutionKindUnsatProof = 2;
const proofFormatNone = 0;
const proofFormatFrat = 2;
function chainFromConfig(config) {
    return {
        id: config.chainId,
        name: `3SAT target chain ${config.chainId}`,
        nativeCurrency: { name: "Ether", symbol: "ETH", decimals: 18 },
        rpcUrls: {
            default: { http: [config.rpcUrl] },
            public: { http: [config.rpcUrl] }
        }
    };
}
function nowSeconds() {
    return Math.floor(Date.now() / 1000);
}
function seconds(value) {
    return Number(value);
}
function blocksNewCommit(submission) {
    return submission.state === 1 || submission.state === 2 || submission.state === 3;
}
function sleep(ms) {
    return new Promise((resolve) => setTimeout(resolve, ms));
}
function stateLabel(state) {
    const labels = [
        "None",
        "Committed",
        "Revealed",
        "PendingAccepted",
        "PendingRejected",
        "Invalid",
        "Finalized"
    ];
    return state === undefined ? "-" : labels[state] || `Unknown (${state})`;
}
function tokenTextFor(value, token) {
    return `${formatUnits(typeof value === "bigint" ? value : BigInt(value), token.decimals)} ${token.symbol}`;
}
function solutionFileName(bountyId) {
    return `bounty-${bountyId}-solution-${Date.now()}.cnf`;
}
function fratProofFileName(bountyId) {
    return `bounty-${bountyId}-unsat-proof-${Date.now()}.frat`;
}
function resultKindLabel(solutionKind, proofFormat) {
    if (solutionKind === solutionKindUnsatProof) {
        return proofFormat === proofFormatFrat ? "UNSAT proof (FRAT)" : "UNSAT proof";
    }
    return "SAT assignment";
}
export class SolverClient {
    config = loadConfig();
    account = privateKeyToAccount(this.config.solverPrivateKey);
    chain = chainFromConfig(this.config);
    publicClient = createPublicClient({ transport: http(this.config.rpcUrl) });
    walletClient = createWalletClient({
        account: this.account,
        chain: this.chain,
        transport: http(this.config.rpcUrl)
    });
    api = new ProtocolApiClient(this.config);
    logger = new Logger(this.config.logFile);
    state = new SolverState(this.config.stateFile);
    foreignJobWarnings = new Set();
    tokenInfo(address) {
        const normalized = address?.toLowerCase();
        if (normalized && normalized === this.config.usdcToken.toLowerCase()) {
            return { address: this.config.usdcToken, symbol: "USDC", decimals: 6 };
        }
        if (normalized && normalized === this.config.satToken.toLowerCase()) {
            return { address: this.config.satToken, symbol: "$3SAT", decimals: 18 };
        }
        return {
            address: (address || this.config.satToken),
            symbol: "ERC20",
            decimals: 18
        };
    }
    minRewardForToken(token) {
        const value = this.config.minRewardTokens.trim();
        if (!value) {
            return 0n;
        }
        if (/^\d+$/.test(value) && value.length > 24) {
            return BigInt(value);
        }
        return parseUnits(value, token.decimals);
    }
    async start() {
        await this.state.load();
        await this.logger.write("info", "Solver client starting.", {
            address: this.account.address,
            dryRun: this.config.dryRun,
            apiUrl: this.config.apiUrl,
            chainId: this.config.chainId,
            satToken: this.config.satToken,
            usdcToken: this.config.usdcToken,
            bountyManager: this.config.bountyManager,
            cryptoMiniSatPath: this.config.cryptoMiniSatPath,
            minRewardToken: this.config.minRewardTokens
        });
        await this.assertChainConnection();
        do {
            await this.pollOnce();
            if (!this.config.runOnce) {
                await sleep(this.config.pollIntervalMs);
            }
        } while (!this.config.runOnce);
    }
    async assertChainConnection() {
        const chainId = await this.publicClient.getChainId();
        if (chainId !== this.config.chainId) {
            throw new Error(`RPC chain id mismatch. Expected ${this.config.chainId}, got ${chainId}.`);
        }
        const [bountyManagerCode, satTokenCode, usdcTokenCode, nativeBalance, satBalance, usdcBalance] = await Promise.all([
            this.publicClient.getCode({ address: this.config.bountyManager }),
            this.publicClient.getCode({ address: this.config.satToken }),
            this.publicClient.getCode({ address: this.config.usdcToken }),
            this.publicClient.getBalance({ address: this.account.address }),
            this.publicClient.readContract({
                address: this.config.satToken,
                abi: erc20Abi,
                functionName: "balanceOf",
                args: [this.account.address]
            }),
            this.publicClient.readContract({
                address: this.config.usdcToken,
                abi: erc20Abi,
                functionName: "balanceOf",
                args: [this.account.address]
            })
        ]);
        if (!bountyManagerCode || bountyManagerCode === "0x") {
            throw new Error(`BOUNTY_MANAGER_ADDRESS has no contract code on chain ${chainId}.`);
        }
        if (!satTokenCode || satTokenCode === "0x") {
            throw new Error(`SAT_TOKEN_ADDRESS has no contract code on chain ${chainId}.`);
        }
        if (!usdcTokenCode || usdcTokenCode === "0x") {
            throw new Error(`USDC_TOKEN_ADDRESS has no contract code on chain ${chainId}.`);
        }
        if (nativeBalance === 0n && !this.config.dryRun) {
            throw new Error("Solver wallet has zero native gas balance. Fund it before live commits/reveals.");
        }
        await this.logger.write("info", "Chain connection confirmed.", {
            chainId,
            solver: this.account.address,
            nativeBalanceWei: nativeBalance.toString(),
            satBalance: tokenTextFor(satBalance, this.tokenInfo(this.config.satToken)),
            usdcBalance: tokenTextFor(usdcBalance, this.tokenInfo(this.config.usdcToken))
        });
    }
    async pollOnce() {
        await this.processExistingJobs();
        if (this.activeJobCount() >= this.config.maxActiveJobs) {
            await this.logger.write("info", "Active job limit reached; skipping new bounty discovery.", {
                activeJobs: this.activeJobCount(),
                maxActiveJobs: this.config.maxActiveJobs
            });
            return;
        }
        const { status, bounties } = await this.api.marketplace();
        await this.logger.write("info", "Marketplace polled.", {
            configured: status.configured,
            cachedBounties: bounties.length,
            indexStorage: status.storage
        });
        const candidates = bounties.filter((bounty) => this.shouldTryBounty(bounty)).slice(0, this.config.maxBountiesPerPoll);
        for (const bounty of candidates) {
            if (this.activeJobCount() >= this.config.maxActiveJobs) {
                break;
            }
            try {
                await this.solveAndCommit(bounty);
            }
            catch (err) {
                await this.logger.write("error", "Bounty solve/commit failed.", {
                    bountyId: bounty.bountyId,
                    bountyCode: bounty.bountyCode,
                    error: err instanceof Error ? err.message : String(err)
                });
            }
        }
    }
    shouldTryBounty(bounty) {
        const now = nowSeconds();
        const commitDeadline = seconds(bounty.commitDeadline);
        const reward = BigInt(bounty.reward);
        const rewardToken = this.tokenInfo(bounty.paymentToken);
        if (bounty.finalized || this.hasLiveJobForCurrentWallet(bounty.bountyId)) {
            return false;
        }
        if (reward < this.minRewardForToken(rewardToken)) {
            return false;
        }
        if (now + this.config.minCommitSecondsLeft >= commitDeadline) {
            return false;
        }
        const submissions = Object.values(bounty.submissions || {});
        if (submissions.some((submission) => submission.solver.toLowerCase() === this.account.address.toLowerCase())) {
            return false;
        }
        if (!this.config.allowCompetingSubmissions && submissions.some(blocksNewCommit)) {
            return false;
        }
        return true;
    }
    async solveAndCommit(candidate) {
        const detail = await this.api.bounty(candidate.bountyCode || candidate.bountyId);
        const now = nowSeconds();
        if (detail.bounty.finalized) {
            return;
        }
        const blockingSubmission = detail.submissions.find(blocksNewCommit);
        if (!this.config.allowCompetingSubmissions && blockingSubmission) {
            await this.logger.write("info", "Bounty already has an active submission; skipping for now.", {
                bountyId: detail.bountyId,
                bountyCode: detail.bountyCode,
                submissionId: blockingSubmission.submissionId,
                state: blockingSubmission.stateLabel
            });
            return;
        }
        if (now + this.config.minCommitSecondsLeft >= seconds(detail.bounty.commitDeadline)) {
            await this.logger.write("warn", "Bounty commit window is too close; skipping.", {
                bountyId: detail.bountyId,
                bountyCode: detail.bountyCode,
                commitDeadline: detail.bounty.commitDeadline
            });
            return;
        }
        const bountyPaymentToken = this.tokenInfo(detail.bounty.paymentToken);
        await this.logger.write("info", "Solving bounty.", {
            bountyId: detail.bountyId,
            bountyCode: detail.bountyCode,
            reward: tokenTextFor(detail.bounty.reward, bountyPaymentToken),
            paymentToken: bountyPaymentToken.address,
            instanceRef: detail.bounty.instanceRef
        });
        const instancePath = await this.api.downloadInstance({
            bountyId: detail.bountyId,
            instanceDownloadUrl: detail.bounty.instanceDownloadUrl,
            expectedDigest: detail.bounty.instanceDigest
        });
        const instance = await parseDimacsCnfFile(instancePath);
        await mkdir(this.config.artifactDir, { recursive: true });
        const solverInputPath = path.join(this.config.artifactDir, `bounty-${detail.bountyId}-solver-input-${Date.now()}.cnf`);
        const proofFileName = fratProofFileName(detail.bountyId);
        const proofPath = path.join(this.config.artifactDir, proofFileName);
        await writeFile(solverInputPath, dimacsCnfText(instance), "utf8");
        const solverResult = await runCryptoMiniSat({
            binaryPath: this.config.cryptoMiniSatPath,
            cnfPath: solverInputPath,
            proofPath,
            timeoutMs: this.config.solveTimeoutMs
        });
        if (solverResult.status !== "SAT" && solverResult.status !== "UNSAT") {
            await this.logger.write("warn", "CryptoMiniSat did not find a SAT model.", {
                bountyId: detail.bountyId,
                bountyCode: detail.bountyCode,
                status: solverResult.status,
                exitCode: solverResult.exitCode
            });
            return;
        }
        let fileName;
        let solutionPath;
        let solutionKind;
        let proofFormat;
        let assignedVariables;
        if (solverResult.status === "UNSAT") {
            const proofBytes = await readFile(proofPath);
            if (proofBytes.byteLength === 0) {
                throw new Error("CryptoMiniSat reported UNSAT but did not write a FRAT proof.");
            }
            fileName = proofFileName;
            solutionPath = proofPath;
            solutionKind = solutionKindUnsatProof;
            proofFormat = proofFormatFrat;
        }
        else {
            const literals = extractSatAssignment(solverResult, instance.variables);
            const assignment = assignmentFromLiterals(literals);
            const check = assignmentSatisfies(instance, assignment);
            if (!check.ok) {
                throw new Error(check.reason || "Generated solver model does not satisfy the instance.");
            }
            fileName = solutionFileName(detail.bountyId);
            solutionPath = path.join(this.config.artifactDir, fileName);
            await writeFile(solutionPath, unitClauseCnfText(instance.variables, literals), "utf8");
            solutionKind = solutionKindSatAssignment;
            proofFormat = proofFormatNone;
            assignedVariables = literals.length;
        }
        const solutionBytes = new Uint8Array(await readFile(solutionPath));
        const localSolutionDigest = keccak256(bytesToHex(solutionBytes));
        await this.logger.write("info", "Result generated locally.", {
            bountyId: detail.bountyId,
            solutionPath: path.relative(this.config.rootDir, solutionPath),
            solutionDigest: localSolutionDigest,
            solutionKind: resultKindLabel(solutionKind, proofFormat),
            assignedVariables
        });
        if (this.config.dryRun) {
            await this.state.upsert({
                bountyId: detail.bountyId,
                bountyCode: detail.bountyCode,
                phase: "dry-run",
                solver: this.account.address,
                solutionRef: "dry-run://not-uploaded",
                solutionDigest: localSolutionDigest,
                solutionKind,
                proofFormat,
                salt: "0x0000000000000000000000000000000000000000000000000000000000000000",
                commitHash: "0x0000000000000000000000000000000000000000000000000000000000000000",
                createdAt: new Date().toISOString(),
                updatedAt: new Date().toISOString()
            });
            await this.logger.write("info", "Dry run enabled; solution was not uploaded and commit transaction was not sent.", {
                bountyId: detail.bountyId,
                bountyCode: detail.bountyCode,
                solutionKind: resultKindLabel(solutionKind, proofFormat)
            });
            return;
        }
        const solution = await this.api.uploadSolution(solutionPath, fileName, { solutionKind, proofFormat });
        if (solution.digest.toLowerCase() !== localSolutionDigest.toLowerCase()) {
            throw new Error("Uploaded solution digest does not match local digest.");
        }
        const commit = await this.api.prepareCommit({
            bounty: detail.bountyCode,
            solver: this.account.address,
            solutionKind,
            proofFormat,
            solutionRef: solution.ref,
            solutionDigest: solution.digest
        });
        const bondToken = commit.bondToken || commit.token || detail.bounty.paymentToken;
        if (!bondToken) {
            throw new Error("Prepare commit response did not include a bond token.");
        }
        await this.assertPreparedContext(commit.bountyManager);
        await this.assertBondAndAllowance(bondToken, BigInt(commit.solverBond), commit.transactions);
        const commitTx = commit.transactions.find((tx) => tx.functionName === "commitSolution");
        if (!commitTx) {
            throw new Error("Prepare commit response did not include commitSolution transaction.");
        }
        const commitReceipt = await this.sendPreparedTransaction(commitTx);
        const submissionId = this.submissionIdFromCommitReceipt(commitReceipt, detail.bountyId);
        const job = {
            bountyId: detail.bountyId,
            bountyCode: detail.bountyCode,
            phase: "committed",
            solver: this.account.address,
            solutionRef: solution.ref,
            solutionDigest: solution.digest,
            solutionKind,
            proofFormat,
            salt: commit.salt,
            commitHash: commit.commitHash,
            submissionId,
            commitTxHash: commitReceipt.transactionHash,
            createdAt: new Date().toISOString(),
            updatedAt: new Date().toISOString()
        };
        await this.state.upsert(job);
        await this.logger.write("info", "Commit submitted and saved.", {
            bountyId: job.bountyId,
            bountyCode: job.bountyCode,
            submissionId: job.submissionId,
            commitTxHash: job.commitTxHash,
            solutionKind: resultKindLabel(job.solutionKind, job.proofFormat)
        });
        await this.tryReveal(job);
    }
    async assertPreparedContext(bountyManager) {
        if (bountyManager.toLowerCase() !== this.config.bountyManager.toLowerCase()) {
            throw new Error(`API prepared BountyManager ${bountyManager}, but config has ${this.config.bountyManager}.`);
        }
    }
    async assertBondAndAllowance(bondToken, solverBond, transactions) {
        const token = this.tokenInfo(bondToken);
        const [balance, allowance] = await Promise.all([
            this.publicClient.readContract({
                address: bondToken,
                abi: erc20Abi,
                functionName: "balanceOf",
                args: [this.account.address]
            }),
            this.publicClient.readContract({
                address: bondToken,
                abi: erc20Abi,
                functionName: "allowance",
                args: [this.account.address, this.config.bountyManager]
            })
        ]);
        if (balance < solverBond) {
            throw new Error(`Insufficient ${token.symbol} for solver bond. Balance ${tokenTextFor(balance, token)}, bond ${tokenTextFor(solverBond, token)}.`);
        }
        if (allowance >= solverBond) {
            return;
        }
        const approveTx = transactions.find((tx) => tx.functionName === "approve");
        if (!approveTx) {
            throw new Error("Prepare commit response did not include an approve transaction.");
        }
        await this.logger.write("info", "Approving solver bond.", {
            bondToken,
            allowance: tokenTextFor(allowance, token),
            solverBond: tokenTextFor(solverBond, token)
        });
        await this.sendPreparedTransaction(approveTx);
    }
    async sendPreparedTransaction(tx) {
        await this.logger.write("info", `Sending transaction: ${tx.label}.`, {
            to: tx.to,
            functionName: tx.functionName
        });
        const hash = await this.walletClient.sendTransaction({
            to: tx.to,
            data: tx.data,
            value: BigInt(tx.value)
        });
        const receipt = await this.publicClient.waitForTransactionReceipt({ hash });
        if (receipt.status !== "success") {
            throw new Error(`${tx.label} transaction failed: ${hash}`);
        }
        await this.logger.write("info", `Transaction confirmed: ${tx.label}.`, { hash });
        return receipt;
    }
    submissionIdFromCommitReceipt(receipt, expectedBountyId) {
        const events = parseEventLogs({
            abi: bountyManagerAbi,
            eventName: "SolutionCommitted",
            logs: receipt.logs
        });
        for (const event of events) {
            if (event.address.toLowerCase() !== this.config.bountyManager.toLowerCase()) {
                continue;
            }
            const args = event.args;
            if (args.bountyId?.toString() === expectedBountyId &&
                args.solver?.toLowerCase() === this.account.address.toLowerCase() &&
                args.submissionId) {
                return args.submissionId.toString();
            }
        }
        throw new Error("Could not find SolutionCommitted event in commit receipt.");
    }
    async processExistingJobs() {
        for (const job of this.state.allJobs()) {
            if (!(await this.shouldProcessJob(job))) {
                continue;
            }
            if (job.phase === "committed") {
                await this.tryReveal(job);
            }
            else if (job.phase === "revealed" && this.config.waitForFinalization) {
                await this.trackFinalization(job);
            }
        }
    }
    isCurrentWalletJob(job) {
        return job.solver.toLowerCase() === this.account.address.toLowerCase();
    }
    isLiveJob(job) {
        return !["finalized", "expired", "dry-run", "skipped-unsat"].includes(job.phase);
    }
    activeJobCount() {
        return this.state.allJobs().filter((job) => this.isCurrentWalletJob(job) && this.isLiveJob(job)).length;
    }
    hasLiveJobForCurrentWallet(bountyId) {
        const job = this.state.jobForBounty(bountyId);
        return Boolean(job && this.isCurrentWalletJob(job) && this.isLiveJob(job));
    }
    async shouldProcessJob(job) {
        if (this.isCurrentWalletJob(job)) {
            return true;
        }
        const warningKey = `${job.bountyId}:${job.submissionId || ""}:${job.solver}`;
        if (!this.foreignJobWarnings.has(warningKey)) {
            this.foreignJobWarnings.add(warningKey);
            await this.logger.write("warn", "Skipping job created by a different solver wallet.", {
                bountyId: job.bountyId,
                bountyCode: job.bountyCode,
                submissionId: job.submissionId,
                currentSolver: this.account.address,
                jobSolver: job.solver
            });
        }
        return false;
    }
    async tryReveal(job) {
        if (!job.submissionId) {
            await this.logger.write("warn", "Committed job has no submissionId; cannot reveal automatically.", {
                bountyId: job.bountyId,
                bountyCode: job.bountyCode
            });
            return;
        }
        const detail = await this.api.bounty(job.bountyCode || job.bountyId);
        const now = nowSeconds();
        const revealDeadline = seconds(detail.bounty.revealDeadline);
        const submission = await this.readSubmission(job);
        if (submission.state === 6) {
            await this.state.upsert({ ...job, phase: "finalized" });
            return;
        }
        if (submission.state === 5) {
            await this.state.upsert({ ...job, phase: "expired" });
            await this.logger.write("warn", "Submission is invalid on-chain.", {
                bountyId: job.bountyId,
                submissionId: job.submissionId
            });
            return;
        }
        if (submission.state >= 2) {
            await this.state.upsert({ ...job, phase: "revealed" });
            return;
        }
        if (now > revealDeadline) {
            await this.state.upsert({ ...job, phase: "expired" });
            await this.logger.write("error", "Reveal window expired before reveal transaction was sent.", {
                bountyId: job.bountyId,
                submissionId: job.submissionId
            });
            return;
        }
        const reveal = await this.api.prepareReveal({
            bounty: job.bountyCode,
            submissionId: job.submissionId,
            solutionKind: job.solutionKind,
            proofFormat: job.proofFormat,
            solutionRef: job.solutionRef,
            solutionDigest: job.solutionDigest,
            salt: job.salt
        });
        await this.assertPreparedContext(reveal.bountyManager);
        const revealTx = reveal.transactions[0];
        if (!revealTx) {
            throw new Error("Prepare reveal response did not include a reveal transaction.");
        }
        const receipt = await this.sendPreparedTransaction(revealTx);
        await this.state.upsert({
            ...job,
            phase: "revealed",
            revealTxHash: receipt.transactionHash
        });
        await this.logger.write("info", "Reveal submitted and saved.", {
            bountyId: job.bountyId,
            bountyCode: job.bountyCode,
            submissionId: job.submissionId,
            revealTxHash: receipt.transactionHash
        });
    }
    async readSubmission(job) {
        if (!job.submissionId) {
            throw new Error("submissionId is missing.");
        }
        const submission = (await this.publicClient.readContract({
            address: this.config.bountyManager,
            abi: bountyManagerAbi,
            functionName: "getSubmission",
            args: [BigInt(job.bountyId), BigInt(job.submissionId)]
        }));
        if (submission.solver.toLowerCase() === zeroAddress) {
            throw new Error("Submission does not exist on-chain.");
        }
        return submission;
    }
    async readBounty(job) {
        const bounty = (await this.publicClient.readContract({
            address: this.config.bountyManager,
            abi: bountyManagerAbi,
            functionName: "getBounty",
            args: [BigInt(job.bountyId)]
        }));
        if (bounty.issuer.toLowerCase() === zeroAddress) {
            throw new Error("Bounty does not exist on-chain.");
        }
        return bounty;
    }
    async settleSolverBond(job, submission) {
        if (!job.submissionId || submission.bondSettled) {
            return;
        }
        if (this.config.dryRun) {
            await this.logger.write("info", "Dry run enabled; solver bond settlement was not submitted.", {
                bountyId: job.bountyId,
                bountyCode: job.bountyCode,
                submissionId: job.submissionId,
                state: stateLabel(submission.state)
            });
            return;
        }
        await this.logger.write("info", "Sending transaction: Claim solver bond.", {
            bountyId: job.bountyId,
            bountyCode: job.bountyCode,
            submissionId: job.submissionId,
            state: stateLabel(submission.state)
        });
        const hash = await this.walletClient.writeContract({
            address: this.config.bountyManager,
            abi: bountyManagerAbi,
            functionName: "claimSolverBond",
            args: [BigInt(job.bountyId), BigInt(job.submissionId)]
        });
        await this.publicClient.waitForTransactionReceipt({ hash });
        const updated = await this.readSubmission(job);
        await this.state.upsert({
            ...job,
            phase: updated.bondSlashed ? "expired" : "finalized",
            finalizedTxHash: hash,
            solverWon: false
        });
        await this.logger.write("info", updated.bondSlashed ? "Solver bond slashed." : "Solver bond refunded.", {
            bountyId: job.bountyId,
            bountyCode: job.bountyCode,
            submissionId: job.submissionId,
            txHash: hash
        });
    }
    async trackFinalization(job) {
        if (!job.submissionId) {
            return;
        }
        const submission = await this.readSubmission(job);
        if (submission.state === 6) {
            await this.state.upsert({
                ...job,
                phase: "finalized",
                solverWon: !submission.bondSlashed
            });
            await this.logger.write("info", "Submission finalized.", {
                bountyId: job.bountyId,
                bountyCode: job.bountyCode,
                submissionId: job.submissionId,
                solverWon: !submission.bondSlashed
            });
            return;
        }
        const bounty = await this.readBounty(job);
        if (bounty.finalized) {
            if (!submission.bondSettled) {
                await this.settleSolverBond(job, submission);
                return;
            }
            await this.state.upsert({
                ...job,
                phase: submission.bondSlashed ? "expired" : "finalized",
                solverWon: false
            });
            await this.logger.write("info", "Bounty finalized without this submission winning.", {
                bountyId: job.bountyId,
                bountyCode: job.bountyCode,
                submissionId: job.submissionId,
                state: stateLabel(submission.state),
                bondSettled: submission.bondSettled,
                bondSlashed: submission.bondSlashed
            });
            return;
        }
        const finalizable = (await this.publicClient.readContract({
            address: this.config.bountyManager,
            abi: bountyManagerAbi,
            functionName: "isFinalizable",
            args: [BigInt(job.bountyId), BigInt(job.submissionId)]
        }));
        await this.logger.write("info", "Waiting for verifier/keeper finalization.", {
            bountyId: job.bountyId,
            bountyCode: job.bountyCode,
            submissionId: job.submissionId,
            state: stateLabel(submission.state),
            forVotes: submission.forVotes,
            againstVotes: submission.againstVotes,
            finalizable
        });
    }
}
export async function runSolverClient() {
    await new SolverClient().start();
}
