"use strict";
/**
 * J41Agent — Main agent class.
 * Combines identity, client, signing, and job handling into one interface.
 *
 * @example
 * ```typescript
 * const agent = new J41Agent({
 *   apiUrl: 'https://api.junction41.io',
 *   wif: process.env.J41_AGENT_WIF!,
 * });
 *
 * await agent.register('myagent');
 * await agent.start();
 * ```
 */
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
    var ownKeys = function(o) {
        ownKeys = Object.getOwnPropertyNames || function (o) {
            var ar = [];
            for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
            return ar;
        };
        return ownKeys(o);
    };
    return function (mod) {
        if (mod && mod.__esModule) return mod;
        var result = {};
        if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
        __setModuleDefault(result, mod);
        return result;
    };
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.J41Agent = exports.RegistrationTimeoutError = void 0;
const node_events_1 = require("node:events");
const index_js_1 = require("./client/index.js");
const keypair_js_1 = require("./identity/keypair.js");
const signer_js_1 = require("./identity/signer.js");
const messages_js_1 = require("./signing/messages.js");
const client_js_1 = require("./chat/client.js");
const vdxf_js_1 = require("./onboarding/vdxf.js");
const attestation_js_1 = require("./privacy/attestation.js");
const calculator_js_1 = require("./pricing/calculator.js");
const canary_js_1 = require("./safety/canary.js");
const node_crypto_1 = require("node:crypto");
const json_canonicalize_1 = require("json-canonicalize");
const update_js_1 = require("./identity/update.js");
const vdxf_js_2 = require("./onboarding/vdxf.js");
const index_js_2 = require("./workspace/index.js");
/**
 * Thrown when identity registration is broadcast on-chain but block
 * confirmation times out. Carries partial context so callers can save
 * state and recover later.
 */
class RegistrationTimeoutError extends Error {
    onboardId;
    lastStatus;
    identityName;
    constructor(onboardId, lastStatus, identityName) {
        super(`Registration timed out waiting for block confirmation. ` +
            `The identity "${identityName}" may already exist on-chain. ` +
            `Use the recover command to check and resume.`);
        this.name = 'RegistrationTimeoutError';
        this.onboardId = onboardId;
        this.lastStatus = lastStatus;
        this.identityName = identityName;
    }
}
exports.RegistrationTimeoutError = RegistrationTimeoutError;
/** Default request timeout for raw fetch calls (ms) */
const FETCH_TIMEOUT = 30_000;
/** Minimum allowed polling interval (ms) */
const MIN_POLL_INTERVAL = 5_000;
class J41Agent extends node_events_1.EventEmitter {
    _client;
    keypair = null;
    identityName;
    iAddress;
    wif;
    signer;
    handler;
    jobConfig;
    networkType = 'verustest';
    pollTimer = null;
    running = false;
    chatClient = null;
    _workspace = null;
    chatHandler = null;
    apiUrl;
    canaryConfig = null;
    polling = false;
    seenJobIds = new Set();
    loginPromise = null;
    constructor(config) {
        super();
        if (config.apiUrl.startsWith('http://')) {
            if (process.env.J41_ALLOW_INSECURE === '1') {
                console.warn('[J41] WARNING: Using insecure HTTP — credentials sent in cleartext');
            }
            else {
                throw new Error('Refusing to connect over insecure HTTP. ' +
                    'Set J41_ALLOW_INSECURE=1 to override (dev only).');
            }
        }
        this.apiUrl = config.apiUrl;
        this._client = new index_js_1.J41Client({ apiUrl: config.apiUrl });
        // Wire auto re-auth: on 401/403, J41Client calls login() to refresh session (S2)
        this._client.setOnSessionExpired(async () => {
            console.log('[J41] Session expired, re-authenticating...');
            await this.login();
        });
        this.wif = config.wif || null;
        this.signer = config.signer || null;
        this.identityName = config.identityName || null;
        this.iAddress = config.iAddress || null;
        this.handler = config.handler || null;
        this.jobConfig = config.jobConfig || { pollInterval: 30_000 };
        this.networkType = config.network || 'verustest';
        // Prevent uncaught 'error' events from crashing the process.
        // Only log if no user-provided listener is registered.
        this.on('error', (err) => {
            if (this.listenerCount('error') <= 1) {
                console.error('[J41] Unhandled error:', err instanceof Error ? err.message : err);
            }
        });
    }
    /**
     * True if this agent is configured to delegate signing to a remote signer
     * (i.e. the dispatcher's host-side broker). Exposed so callers can take
     * different paths without poking at internals.
     */
    get usesRemoteSigner() {
        return this.signer !== null;
    }
    /**
     * Sign an arbitrary protocol message. Prefers the configured remote
     * signer when present; falls back to the local WIF. Used for non-broker-
     * gated paths (auth challenge, registerWithJ41 payload, status changes,
     * bounty/job/review payloads, attestations).
     *
     * Throws if neither a signer nor a WIF is configured.
     */
    /**
     * Sign an opaque (non-protocol-shaped) message. Audit 2026-06-02 H1/H4/H10:
     * the protocol-shape guard is enforced here so any future callsite that
     * forgets to validate is blocked rather than silently turned into a
     * platform-signing-oracle.
     *
     * Callers that legitimately need to sign a `J41-*|...` string MUST:
     *   1. Build the canonical message LOCALLY with one of the `build*Message`
     *      helpers in signing/messages.ts (never sign bytes the platform
     *      handed back), AND
     *   2. Call `_signMessageBuilt` instead of this method.
     *
     * Both rules apply jointly — the local builder is what makes the bytes
     * trustworthy, and `_signMessageBuilt` is the path that bypasses the guard
     * for those audited callsites.
     */
    async _signMessage(message) {
        (0, messages_js_1.assertNotProtocolMessage)(message);
        return this._signMessageBuilt(message);
    }
    /**
     * Sign bytes that the SDK assembled itself via a local builder (or that a
     * caller has shape-validated against the expected protocol prefix and
     * fields). Adding a new callsite REQUIRES security review — every callsite
     * must be auditable as "the platform cannot influence the resulting J41-*
     * shape." See `_signMessage` doc above.
     */
    async _signMessageBuilt(message) {
        if (this.signer)
            return this.signer.signMessage(message);
        if (!this.wif)
            throw new Error('WIF key required (no remote signer configured)');
        return (0, signer_js_1.signMessage)(this.wif, message, this.networkType);
    }
    /**
     * Sign a structured broker-gated request (accept, deliver, dispute_respond).
     * Prefers the configured remote signer when present; falls back to building
     * the canonical message locally + signing with the WIF.
     *
     * The fallback is what the existing in-process agent has always done; the
     * broker-backed path differs in that the dispatcher RECONSTRUCTS the
     * message from its authoritative job record so a compromised container
     * cannot inflate amounts or sign for the wrong job.
     *
     * `jobCtx` is required for the WIF-fallback path (the agent already has
     * the fetched job on hand at the call site) and IGNORED by the remote
     * signer (the broker re-fetches the authoritative job itself — it never
     * trusts caller-supplied amount/buyer/jobHash for protocol-gated ops).
     *
     * Throws if neither a signer nor a WIF is configured.
     */
    async _signBrokered(req, jobCtx) {
        if (this.signer)
            return this.signer.signBrokered(req);
        if (!this.wif)
            throw new Error('WIF key required (no remote signer configured)');
        return this._signBrokeredLocally(req, jobCtx);
    }
    /**
     * Local fallback for `_signBrokered` — used only when no `RemoteSigner`
     * was supplied. Replicates the message construction that lived inline at
     * each call site before this refactor (so behaviour is byte-identical on
     * the legacy path).
     *
     * Requires `this.wif` and, for `accept`, an authoritative `job` object the
     * caller fetches from the platform.
     */
    async _signBrokeredLocally(req, job) {
        if (!this.wif)
            throw new Error('WIF key required');
        const timestamp = Math.floor(Date.now() / 1000);
        let message;
        switch (req.type) {
            case 'accept': {
                if (!job?.jobHash || !job.buyerVerusId || job.amount == null || !job.currency) {
                    throw new Error('_signBrokeredLocally(accept) requires job{jobHash,buyerVerusId,amount,currency}');
                }
                message = `J41-ACCEPT|Job:${job.jobHash}|Buyer:${job.buyerVerusId}|Amt:${job.amount} ${job.currency}|Ts:${timestamp}|I accept this job and commit to delivering the work.`;
                break;
            }
            case 'deliver': {
                if (!job?.jobHash)
                    throw new Error('_signBrokeredLocally(deliver) requires job.jobHash');
                message = `J41-DELIVER|Job:${job.jobHash}|Delivery:${req.deliveryHash}|Ts:${timestamp}|I have delivered the work for this job.`;
                break;
            }
            case 'dispute_respond': {
                if (!job?.jobHash)
                    throw new Error('_signBrokeredLocally(dispute_respond) requires job.jobHash');
                message = (0, messages_js_1.buildDisputeRespondMessage)({ jobHash: job.jobHash, action: req.action, timestamp });
                break;
            }
            default: {
                const _exhaustive = req;
                throw new Error(`Unsupported brokered request type: ${JSON.stringify(_exhaustive)}`);
            }
        }
        const signature = (0, signer_js_1.signMessage)(this.wif, message, this.networkType);
        return { signature, timestamp, message };
    }
    /**
     * Read-only access to the underlying J41Client.
     * Use J41Agent methods for operations that require canary checking or authentication.
     */
    get client() {
        return this._client;
    }
    /** Lazy workspace client — created on first access */
    get workspace() {
        if (!this._workspace) {
            this._workspace = new index_js_2.WorkspaceClient({
                apiUrl: this._client.getBaseUrl(),
                getSessionToken: () => this._client.getSessionToken(),
            });
        }
        return this._workspace;
    }
    /**
     * Generate a new keypair for this agent.
     * Call this before register() if no WIF was provided.
     */
    generateKeys(network) {
        const net = network || this.networkType;
        this.keypair = (0, keypair_js_1.generateKeypair)(net);
        this.wif = this.keypair.wif;
        return this.keypair;
    }
    /**
     * Authenticate with the J41 platform and return the session cookie.
     * Also sets the session token on the underlying J41Client for subsequent requests.
     * Shared by registerWithJ41(), registerService(), and enableCanaryProtection().
     */
    async login() {
        // Mutex: deduplicate concurrent login calls to prevent session token races
        if (this.loginPromise)
            return this.loginPromise;
        this.loginPromise = this._loginImpl();
        try {
            return await this.loginPromise;
        }
        finally {
            this.loginPromise = null;
        }
    }
    async _loginImpl() {
        if (!this.wif && !this.signer)
            throw new Error('WIF key or remote signer required');
        if (!this.identityName)
            throw new Error('Identity name required');
        const challengeRes = await this._client.getAuthChallenge();
        if (challengeRes.expiresAt) {
            const expiryMs = new Date(challengeRes.expiresAt).getTime();
            if (Number.isNaN(expiryMs)) {
                throw new Error('Auth challenge has unparseable expiresAt timestamp');
            }
            if (expiryMs < Date.now()) {
                throw new Error('Auth challenge already expired — clock skew or stale response');
            }
        }
        // Domain guard: never let a (MITM-able) server challenge be a J41-protocol
        // message we'd sign with our identity key — that would be a signing oracle.
        // The remote signer's own policy should also enforce this; we defend in depth.
        (0, messages_js_1.assertNotProtocolMessage)(challengeRes.challenge);
        const signature = await this._signMessage(challengeRes.challenge);
        const controller = new AbortController();
        const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT);
        let loginRes;
        try {
            loginRes = await fetch(`${this.apiUrl}/auth/login`, {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                signal: controller.signal,
                body: JSON.stringify({
                    challengeId: challengeRes.challengeId,
                    verusId: this.identityName,
                    signature,
                }),
            });
        }
        catch (err) {
            if (err.name === 'AbortError') {
                throw new Error(`Login request timed out after ${FETCH_TIMEOUT}ms`);
            }
            throw err;
        }
        finally {
            clearTimeout(timer);
        }
        if (!loginRes.ok) {
            let errMsg = loginRes.statusText;
            try {
                const err = await loginRes.json();
                errMsg = err.error?.message || errMsg;
            }
            catch { /* non-JSON response */ }
            throw new Error(`Login failed: ${errMsg}`);
        }
        const cookies = loginRes.headers.get('set-cookie');
        if (!cookies) {
            throw new Error('Login succeeded but no session cookie was returned');
        }
        // Extract session token and set on J41Client for subsequent API calls
        const match = cookies.match(/verus_session=([^;]+)/);
        if (!match) {
            throw new Error('Login succeeded but session cookie did not contain verus_session token');
        }
        this._client.setSessionToken(match[1]);
        // Return only the clean cookie name=value pair, not the full set-cookie with attributes
        return `verus_session=${match[1]}`;
    }
    /**
     * Authenticate with the J41 platform (public method).
     * Use this when resuming an agent that already has an on-chain identity
     * and just needs a session token to start polling/chatting.
     */
    async authenticate() {
        await this.login();
        console.log('[J41] ✅ Authenticated');
        this.emit('authenticated');
    }
    /**
     * Register a new identity on the Junction41.
     * J41 creates a subID under agentplatform@ with your R-address.
     *
     * @param name - Desired agent name (e.g. "myagent")
     * @returns Identity info once registered
     */
    async register(name, network = 'verustest') {
        const previousNetwork = this.networkType;
        this.networkType = network;
        try {
            if (!this.keypair && this.wif) {
                this.keypair = (0, keypair_js_1.keypairFromWIF)(this.wif, network);
            }
            else if (!this.keypair) {
                this.generateKeys(network);
            }
            const kp = this.keypair;
            console.log(`[J41] Registering "${name}.agentplatform@"...`);
            // Step 1: Request challenge
            console.log(`[J41] Requesting challenge...`);
            const challengeResp = await this._client.onboard(name, kp.address, kp.pubkey);
            if (challengeResp.status !== 'challenge') {
                throw new Error(`Unexpected response: ${JSON.stringify(challengeResp)}`);
            }
            // Step 2: Sign the challenge with our private key
            if (!challengeResp.challenge || !challengeResp.token) {
                throw new Error('Challenge response missing challenge or token');
            }
            const challenge = challengeResp.challenge;
            const token = challengeResp.token;
            // Onboarding: Use IdentitySignature format with R-address as identity
            // (the local verification expects this format, not legacy signMessage)
            // Onboarding challenge uses R-address message verification path on server.
            // Use legacy signMessage here; keep signChallenge for identity/i-address flows.
            (0, messages_js_1.assertNotProtocolMessage)(challenge);
            const signature = (0, signer_js_1.signMessage)(this.wif, challenge, network);
            console.log(`[J41] Challenge signed. Submitting registration...`);
            // Step 3: Submit with signature
            const result = await this._client.onboardWithSignature(name, kp.address, kp.pubkey, challenge, token, signature);
            // Poll for completion (blocks can take 1-15 minutes depending on network)
            console.log(`[J41] Waiting for block confirmation (this can take several minutes)...`);
            let status = await this._client.onboardStatus(result.onboardId);
            let attempts = 0;
            const maxAttempts = 120; // ~20 minutes at 10s intervals
            while (status.status !== 'registered' && status.status !== 'failed' && attempts < maxAttempts) {
                await new Promise(r => setTimeout(r, 10_000));
                status = await this._client.onboardStatus(result.onboardId);
                attempts++;
                if (attempts % 6 === 0) {
                    console.log(`[J41] Still waiting... (${Math.round(attempts * 10 / 60)}min elapsed, status: ${status.status})`);
                }
            }
            if (status.status === 'failed') {
                throw new Error(`Registration failed: ${status.error}`);
            }
            if (status.status !== 'registered') {
                throw new RegistrationTimeoutError(result.onboardId, status.status, `${name}.agentplatform@`);
            }
            // Wait for real i-address (not 'pending-lookup')
            if (!status.iAddress || status.iAddress === 'pending-lookup') {
                console.log(`[J41] Waiting for i-address from J41...`);
                let iAddressAttempts = 0;
                const maxIAddressAttempts = 30; // 5 more minutes
                while ((!status.iAddress || status.iAddress === 'pending-lookup') && iAddressAttempts < maxIAddressAttempts) {
                    await new Promise(r => setTimeout(r, 10_000));
                    status = await this._client.onboardStatus(result.onboardId);
                    iAddressAttempts++;
                    if (iAddressAttempts % 6 === 0) {
                        console.log(`[J41] Still waiting for i-address... (${Math.round(iAddressAttempts * 10 / 60)}min elapsed)`);
                    }
                }
                if (!status.iAddress || status.iAddress === 'pending-lookup') {
                    throw new Error('J41 did not return i-address — contact platform admin');
                }
            }
            if (!status.identity) {
                throw new Error('Server returned registered status without identity name');
            }
            if (!status.iAddress) {
                throw new Error('Server returned registered status without i-address');
            }
            this.identityName = status.identity;
            this.iAddress = status.iAddress;
            console.log(`[J41] ✅ Registered: ${this.identityName} (${this.iAddress})`);
            this.emit('registered', { identity: this.identityName, iAddress: this.iAddress });
            return { identity: this.identityName, iAddress: this.iAddress };
        }
        catch (err) {
            // Rollback networkType on failure to prevent corrupted state
            this.networkType = previousNetwork;
            throw err;
        }
    }
    /**
     * Register the agent with the J41 platform (after on-chain identity exists).
     * This creates the agent profile and enables receiving jobs.
     *
     * @param agentData - Agent profile data
     * @returns Registration result
     */
    async registerWithJ41(agentData) {
        if (!this.wif) {
            throw new Error('WIF key required for registration');
        }
        // Ensure keypair is derived when agent is instantiated from existing WIF
        if (!this.keypair) {
            this.keypair = (0, keypair_js_1.keypairFromWIF)(this.wif, this.networkType);
        }
        if (!this.identityName) {
            throw new Error('Identity name required (call register() first or set identityName)');
        }
        console.log(`[J41] Registering with J41 platform...`);
        // Step 1: Login (sets session token on J41Client for subsequent requests)
        console.log(`[J41] Logging in...`);
        await this.login();
        console.log(`[J41] ✅ Logged in`);
        // Step 2: Register agent with signed payload (S3: use J41Client.request() for retry+error handling)
        console.log(`[J41] Submitting registration...`);
        const payload = {
            verusId: this.identityName,
            timestamp: Math.floor(Date.now() / 1000),
            nonce: (0, node_crypto_1.randomUUID)(),
            action: 'register',
            data: agentData,
        };
        // Canonical JSON of locally-built payload — not a J41-* protocol message.
        const message = (0, json_canonicalize_1.canonicalize)(payload);
        const regSignature = await this._signMessageBuilt(message);
        let isAlreadyRegistered = false;
        let agentIdResult = '';
        try {
            const result = await this._client.registerAgent({ ...payload, signature: regSignature });
            agentIdResult = result.agentId;
            console.log(`[J41] ✅ Registered with J41 platform`);
            this.emit('registeredWithJ41', { agentId: agentIdResult });
        }
        catch (regErr) {
            // 409 = already registered — continue (non-fatal)
            if (regErr instanceof Error && 'statusCode' in regErr && regErr.statusCode === 409) {
                isAlreadyRegistered = true;
                console.log(`[J41] Agent already registered`);
            }
            else {
                throw regErr;
            }
        }
        // Auto-register canary token (non-fatal on failure, runs even on 409 re-registration)
        if (agentData.canary !== false) {
            await this.registerCanaryToken();
        }
        // Build VDXF contentmultimap for on-chain publishing
        const profile = {
            name: agentData.name,
            type: agentData.type,
            description: agentData.description,
            payAddress: agentData.payAddress,
            network: agentData.network,
            profile: agentData.profile,
            session: agentData.session,
            platformConfig: agentData.platformConfig,
            workspaceCapability: agentData.workspaceCapability,
            models: agentData.models,
            markup: agentData.markup,
        };
        const contentmultimap = (0, vdxf_js_1.buildAgentContentMultimap)(profile);
        const identityName = this.identityName;
        const updatePayload = (0, vdxf_js_1.buildUpdateIdentityPayload)(identityName, contentmultimap);
        this.emit('vdxf:payload', { contentmultimap, updatePayload });
        return { agentId: isAlreadyRegistered ? 'existing' : agentIdResult };
    }
    /**
     * Register a canary token with SovGuard (non-fatal on failure).
     * Uses J41Client.registerCanary() for retry + error handling (S3).
     */
    async registerCanaryToken() {
        try {
            const canary = (0, canary_js_1.generateCanary)();
            await this._client.registerCanary(canary.registration);
            this.canaryConfig = canary;
            this.emit('canary:registered', { active: true });
            console.log('[J41] Canary token registered with SovGuard');
        }
        catch (e) {
            console.warn('[J41] Canary registration failed (non-fatal):', e.message);
        }
    }
    /**
     * Register a service offering.
     * Must be called after registerWithJ41().
     */
    async registerService(serviceData) {
        if (!this.identityName) {
            throw new Error('Identity name required');
        }
        console.log(`[J41] Registering service: ${serviceData.name}...`);
        await this.login();
        // S3: Map convenience fields (currency → priceCurrency) to API-expected shape.
        // Price may arrive as a string from CLI tools — coerce to number.
        const apiData = {
            name: serviceData.name,
            description: serviceData.description,
            category: serviceData.category,
            price: typeof serviceData.price === 'string'
                ? parseFloat(serviceData.price)
                : serviceData.price,
            priceCurrency: serviceData.currency || 'VRSC',
            turnaround: serviceData.turnaround,
            paymentTerms: serviceData.paymentTerms,
            privateMode: serviceData.privateMode,
            sovguard: serviceData.sovguard,
            acceptedCurrencies: serviceData.acceptedCurrencies,
            resolutionWindow: serviceData.resolutionWindow,
            refundPolicy: serviceData.refundPolicy,
            serviceType: serviceData.serviceType,
            endpointUrl: serviceData.endpointUrl,
            modelPricing: serviceData.modelPricing,
            rateLimits: serviceData.rateLimits,
        };
        const result = await this._client.registerService(apiData);
        console.log(`[J41] ✅ Service registered: ${serviceData.name}`);
        return result;
    }
    // ------------------------------------------
    // File sharing
    // ------------------------------------------
    /**
     * Upload a file to a job.
     * @param jobId - Job ID
     * @param filePath - Local file path to upload
     * @returns Uploaded file metadata
     */
    async uploadFile(jobId, filePath) {
        const fs = await Promise.resolve().then(() => __importStar(require('fs')));
        const path = await Promise.resolve().then(() => __importStar(require('path')));
        const data = fs.readFileSync(filePath);
        const filename = path.basename(filePath);
        return this._client.uploadFile(jobId, new Blob([data]), filename);
    }
    /**
     * Upload raw data as a file to a job.
     * @param jobId - Job ID
     * @param data - File content as string or Buffer
     * @param filename - Filename to use
     * @param mimeType - Optional MIME type
     * @returns Uploaded file metadata
     */
    async uploadFileData(jobId, data, filename, mimeType) {
        const buf = typeof data === 'string' ? Buffer.from(data) : Buffer.from(data);
        const blob = new Blob([buf], mimeType ? { type: mimeType } : undefined);
        return this._client.uploadFile(jobId, blob, filename, mimeType);
    }
    /**
     * Download a file from a job.
     * @param jobId - Job ID
     * @param fileId - File ID
     * @returns File data, filename, mimeType, and checksum
     */
    async downloadFile(jobId, fileId) {
        return this._client.downloadFile(jobId, fileId);
    }
    /**
     * Download a file and save it to a local path.
     * @param jobId - Job ID
     * @param fileId - File ID
     * @param outputDir - Directory to save to (defaults to cwd)
     * @returns Local file path
     */
    async downloadFileTo(jobId, fileId, outputDir) {
        const fs = await Promise.resolve().then(() => __importStar(require('fs')));
        const path = await Promise.resolve().then(() => __importStar(require('path')));
        const result = await this._client.downloadFile(jobId, fileId);
        const dir = outputDir || process.cwd();
        const filePath = path.join(dir, result.filename);
        fs.writeFileSync(filePath, Buffer.from(result.data));
        return filePath;
    }
    /**
     * List files attached to a job.
     */
    async listFiles(jobId) {
        return this._client.getJobFiles(jobId);
    }
    /**
     * Delete a file from a job (uploader only).
     */
    async deleteFile(jobId, fileId) {
        return this._client.deleteFile(jobId, fileId);
    }
    /**
     * Push a VDXF status update on-chain (active/inactive).
     * Builds and broadcasts a signed identity update tx.
     */
    async _updateOnChainStatus(status) {
        if (!this.wif || !this.iAddress)
            return null;
        try {
            await this.login();
            const vdxfAdditions = {};
            vdxfAdditions[vdxf_js_2.VDXF_KEYS.agent.status] = [(0, vdxf_js_2.makeSubDD)(vdxf_js_2.VDXF_KEYS.agent.status, status)];
            const { data: identityData } = await this._client.getIdentityRaw();
            const utxoResp = await this._client.getUtxos();
            const utxos = utxoResp.utxos || utxoResp;
            if (!utxos.length) {
                console.warn(`[J41]   ⚠️  No UTXOs — cannot update on-chain status`);
                return null;
            }
            const rawhex = (0, update_js_1.buildIdentityUpdateTx)({
                wif: this.wif,
                identityData,
                utxos,
                vdxfAdditions,
                network: this.networkType,
            });
            const txResult = await this._client.broadcast(rawhex);
            const txid = txResult.txid || txResult;
            console.log(`[J41]   On-chain status → ${status} (txid: ${txid})`);
            return txid;
        }
        catch (e) {
            const msg = e instanceof Error ? e.message : String(e);
            console.warn(`[J41]   ⚠️  On-chain status update failed: ${msg}`);
            return null;
        }
    }
    /**
     * Deactivate the agent: update on-chain status, toggle platform status,
     * and optionally remove service listings.
     *
     * @param options.removeServices - Also delete all service listings (default: true)
     * @param options.onChain - Also update VDXF status to inactive on-chain (default: true)
     * @returns Deactivation result with status
     */
    async deactivate(options = {}) {
        const removeServices = options.removeServices ?? true;
        const onChain = options.onChain ?? true;
        if (!this.identityName || !this.iAddress) {
            throw new Error('Identity name and iAddress required for deactivation');
        }
        console.log(`[J41] Deactivating agent ${this.identityName}...`);
        await this.login();
        let servicesRemoved = 0;
        let onChainTxid = null;
        // 1. Update on-chain VDXF status
        if (onChain) {
            onChainTxid = await this._updateOnChainStatus('inactive');
        }
        // 2. Remove services
        if (removeServices) {
            try {
                const svcResp = await this._client.getMyServices();
                const services = svcResp.data || [];
                for (const svc of services) {
                    try {
                        await this._client.deleteService(svc.id);
                        console.log(`[J41]   Removed service: ${svc.name}`);
                        servicesRemoved++;
                    }
                    catch (e) {
                        const msg = e instanceof Error ? e.message : String(e);
                        console.warn(`[J41]   ⚠️  Failed to remove service ${svc.name}: ${msg}`);
                    }
                }
            }
            catch (e) {
                const msg = e instanceof Error ? e.message : String(e);
                console.warn(`[J41]   ⚠️  Could not list services: ${msg}`);
            }
        }
        // 3. Toggle platform status
        if (!this.wif && !this.signer) {
            throw new Error('[J41] deactivate() requires a WIF key or remote signer — agent was not initialized with signing capability');
        }
        const verusId = this.iAddress || this.identityName;
        const timestamp = Math.floor(Date.now() / 1000);
        const nonce = (0, node_crypto_1.randomUUID)();
        const message = `J41-STATUS|Agent:${verusId}|Status:inactive|Ts:${timestamp}|Nonce:${nonce}`;
        const signature = await this._signMessageBuilt(message);
        const result = await this._client.setAgentStatus(verusId, 'inactive', signature, timestamp, nonce);
        console.log(`[J41] ✅ Agent deactivated: ${result.status}`);
        return { status: result.status, servicesRemoved, onChainTxid };
    }
    /**
     * Reactivate a previously deactivated agent on the platform
     * and update on-chain VDXF status to active.
     *
     * @param options.onChain - Also update VDXF status to active on-chain (default: true)
     */
    async activate(options = {}) {
        const onChain = options.onChain ?? true;
        if (!this.identityName) {
            throw new Error('Identity name required for activation');
        }
        console.log(`[J41] Activating agent ${this.identityName}...`);
        await this.login();
        let onChainTxid = null;
        // 1. Update on-chain VDXF status
        if (onChain) {
            onChainTxid = await this._updateOnChainStatus('active');
        }
        // 2. Toggle platform status
        const verusId = this.iAddress || this.identityName;
        const timestamp = Math.floor(Date.now() / 1000);
        const nonce = (0, node_crypto_1.randomUUID)();
        const message = `J41-STATUS|Agent:${verusId}|Status:active|Ts:${timestamp}|Nonce:${nonce}`;
        const signature = await this._signMessageBuilt(message);
        const result = await this._client.setAgentStatus(verusId, 'active', signature, timestamp, nonce);
        console.log(`[J41] ✅ Agent activated: ${result.status}`);
        return { status: result.status, onChainTxid };
    }
    /**
     * Set the job handler (how your agent responds to jobs).
     */
    setHandler(handler) {
        this.handler = handler;
    }
    /**
     * Start listening for jobs.
     * Uses polling by default — webhook and websocket support coming.
     */
    async start() {
        if (this.running)
            return;
        if (!this._client.getSessionToken()) {
            throw new Error('Agent must be authenticated before starting. Call registerWithJ41() or login first.');
        }
        // Prevent double-start race: set running before any async work
        this.running = true;
        const interval = Math.max(this.jobConfig.pollInterval || 30_000, MIN_POLL_INTERVAL);
        console.log(`[J41] Listening for jobs (polling every ${interval / 1000}s)...`);
        // Initial check (non-fatal — still start polling even if first check fails)
        try {
            await this.checkForJobs();
        }
        catch (err) {
            this.emit('error', err instanceof Error ? err : new Error(String(err)));
        }
        this.pollTimer = setInterval(() => {
            if (!this.running)
                return;
            this.checkForJobs().catch((err) => {
                this.emit('error', err instanceof Error ? err : new Error(String(err)));
            });
        }, interval);
        // Guard against stop() being called during the initial checkForJobs() await
        if (!this.running) {
            clearInterval(this.pollTimer);
            this.pollTimer = null;
            return;
        }
        this.emit('started');
    }
    /**
     * Stop listening for jobs.
     */
    stop() {
        if (this.pollTimer) {
            clearInterval(this.pollTimer);
            this.pollTimer = null;
        }
        this.running = false;
        // Don't clear seenJobIds — persist across stop/start to avoid re-processing jobs
        // Clear UTXO tracking to prevent unbounded growth in long-running agents
        this._spentUtxos.clear();
        this._pendingChange = [];
        this.emit('stopped');
        console.log('[J41] Stopped.');
        this.chatClient?.disconnect();
        this.chatClient = null;
        this._workspace?.disconnect();
        this._workspace = null;
    }
    /**
     * Connect to SovGuard WebSocket for real-time messaging.
     * Call after login (needs session token on client).
     */
    async connectChat() {
        if (!this._client.getSessionToken()) {
            throw new Error('Must be logged in before connecting to chat');
        }
        // Clean up existing chat client to prevent socket leaks
        if (this.chatClient) {
            this.chatClient.disconnect();
            this.chatClient = null;
        }
        this.chatClient = new client_js_1.ChatClient({
            apiUrl: this._client.getBaseUrl(),
            sessionToken: this._client.getSessionToken(),
        });
        // S4: Surface chat reconnect failures so callers can handle silent death
        this.chatClient.onReconnectFailed = (err) => {
            this.emit('chat:reconnectFailed', err);
            this.emit('error', new Error(`Chat permanently disconnected: ${err.message}`));
        };
        this.chatClient.onMessage((msg) => {
            // Don't handle our own messages — check both iAddress and identityName independently
            if (msg.senderVerusId === this.iAddress || msg.senderVerusId === this.identityName)
                return;
            this.emit('chat:message', msg);
            if (this.chatHandler) {
                const result = this.chatHandler(msg.jobId, msg);
                if (result && typeof result.catch === 'function') {
                    result.catch((e) => {
                        this.emit('error', e instanceof Error ? e : new Error(String(e)));
                    });
                }
            }
        });
        // Wire up session/job lifecycle events
        this.chatClient.onSessionEnding(async (event) => {
            this.emit('session:ending', event);
            if (this.handler?.onSessionEnding) {
                try {
                    const job = await this._client.getJob(event.jobId);
                    await this.handler.onSessionEnding(job, event.reason, event.requestedBy);
                }
                catch (err) {
                    this.emit('error', err instanceof Error ? err : new Error(String(err)));
                }
            }
            else {
                // Default: auto-deliver when no custom handler is provided
                console.log(`[J41] Session ending for job ${event.jobId} — auto-delivering...`);
                try {
                    await this.autoDeliver(event.jobId);
                }
                catch (err) {
                    this.emit('error', err instanceof Error ? err : new Error(String(err)));
                }
            }
        });
        this.chatClient.onSessionExpiring((event) => {
            this.emit('session:expiring', event);
        });
        this.chatClient.onJobStatusChanged(async (event) => {
            this.emit('job:statusChanged', event);
            // Dispatch to relevant JobHandler hooks based on new status
            if (!this.handler)
                return;
            try {
                const job = await this._client.getJob(event.jobId);
                if (event.status === 'in_progress' && this.handler.onJobStarted) {
                    await this.handler.onJobStarted(job);
                }
                else if (event.status === 'completed' && this.handler.onJobCompleted) {
                    await this.handler.onJobCompleted(job);
                }
                else if (event.status === 'disputed' && this.handler.onJobDisputed) {
                    await this.handler.onJobDisputed(job, 'Dispute raised');
                }
                else if (event.status === 'cancelled' && this.handler.onJobCancelled) {
                    await this.handler.onJobCancelled(job, event.reason);
                }
            }
            catch (err) {
                this.emit('error', err instanceof Error ? err : new Error(String(err)));
            }
        });
        // Budget request events
        this.chatClient.on('budget_approved', (data) => {
            this.emit('budget:approved', data);
        });
        this.chatClient.on('budget_declined', (data) => {
            this.emit('budget:declined', data);
        });
        this.chatClient.on('budget_added', (data) => {
            this.emit('budget:added', data);
        });
        // Handle review notifications — auto-accept and update identity on-chain
        this.chatClient.onReviewReceived(async (event) => {
            this.emit('review:received', event);
            console.log(`[J41] Review received for job ${event.jobHash} (rating: ${event.rating}) — processing...`);
            try {
                await this.acceptReview(event.inboxId);
            }
            catch (err) {
                this.emit('error', err instanceof Error ? err : new Error(String(err)));
            }
        });
        await this.chatClient.connect();
        console.log('[CHAT] ✅ Connected to SovGuard');
        // Join rooms for any active jobs we're the seller on
        try {
            const accepted = await this._client.getMyJobs({ status: 'accepted', role: 'seller' });
            const inProgress = await this._client.getMyJobs({ status: 'in_progress', role: 'seller' });
            const allJobs = [...(accepted.data || []), ...(inProgress.data || [])];
            for (const job of allJobs) {
                this.chatClient.joinJob(job.id);
            }
            if (allJobs.length > 0) {
                console.log(`[CHAT] Joined ${allJobs.length} active job room(s)`);
            }
        }
        catch (e) {
            console.error('[CHAT] Failed to join existing job rooms:', e);
        }
    }
    /**
     * Set a handler for incoming chat messages.
     * Handler receives (jobId, message) and can call sendChatMessage() to reply.
     */
    onChatMessage(handler) {
        this.chatHandler = handler;
    }
    /**
     * Send a chat message in a job room.
     */
    sendChatMessage(jobId, content) {
        if (!this.chatClient?.isConnected) {
            throw new Error('Chat not connected');
        }
        if (this.canaryConfig && (0, canary_js_1.checkForCanaryLeak)(content, this.canaryConfig.token)) {
            throw new Error('Canary token detected in outbound message — potential prompt injection leak. Message blocked.');
        }
        this.chatClient.sendMessage(jobId, content);
    }
    /**
     * Auto-deliver a job (used as default when session ends and no custom handler is set).
     * Signs a delivery message and submits it to the platform.
     *
     * `deliveryHash` is the SHA-256 of `deliveryMessage` — historically this
     * was the sentinel string `"session-ended"`, but the broker's HEX64
     * validator requires a real 64-char hex SHA-256, and hashing the message
     * is also more honest (anyone can recompute it). Behaviour from the
     * backend's perspective is unchanged; `deliveryHash` is an opaque
     * commitment to whatever the agent considers "delivered".
     */
    async autoDeliver(jobId) {
        if (!this.iAddress) {
            console.error(`[J41] Cannot auto-deliver job ${jobId}: i-address required`);
            return;
        }
        if (!this.wif && !this.signer) {
            console.error(`[J41] Cannot auto-deliver job ${jobId}: WIF key or remote signer required`);
            return;
        }
        try {
            const job = await this._client.getJob(jobId);
            // Only deliver if job is in a deliverable state
            if (job.status !== 'accepted' && job.status !== 'in_progress') {
                console.log(`[J41] Job ${jobId} is in status '${job.status}', skipping auto-deliver`);
                return;
            }
            const deliveryMessage = 'Session ended — work delivered automatically.';
            const { createHash } = await Promise.resolve().then(() => __importStar(require('node:crypto')));
            const deliveryHash = createHash('sha256').update(deliveryMessage, 'utf8').digest('hex');
            const brokered = await this._signBrokered({ type: 'deliver', jobId, deliveryHash }, { jobHash: job.jobHash });
            await this._client.deliverJob(jobId, deliveryHash, brokered.signature, brokered.timestamp, deliveryMessage);
            console.log(`[J41] ✅ Auto-delivered job ${jobId}`);
            this.emit('job:delivered', { jobId });
        }
        catch (err) {
            console.error(`[J41] Auto-deliver failed for job ${jobId}:`, err instanceof Error ? err.message : String(err));
            throw err;
        }
    }
    /**
     * Set revocation and recovery authorities for this agent's identity.
     * Builds and broadcasts a signed updateidentity transaction.
     *
     * IMPORTANT: Once set, only the revocation authority can revoke this identity,
     * and only the recovery authority can recover it. Choose trusted i-addresses.
     *
     * @param revokeAddress - i-address of the revocation authority
     * @param recoverAddress - i-address of the recovery authority
     * @returns Transaction ID of the broadcast update
     */
    async setRevokeRecoverAuthorities(revokeAddress, recoverAddress) {
        if (!this.wif || !this.iAddress) {
            throw new Error('WIF key and iAddress are required to update identity authorities');
        }
        // Validate i-address format
        if (!revokeAddress.startsWith('i') || revokeAddress.length < 30) {
            throw new Error(`Invalid revocation authority address: ${revokeAddress}`);
        }
        if (!recoverAddress.startsWith('i') || recoverAddress.length < 30) {
            throw new Error(`Invalid recovery authority address: ${recoverAddress}`);
        }
        console.log(`[J41] Setting revocation authority: ${revokeAddress}`);
        console.log(`[J41] Setting recovery authority: ${recoverAddress}`);
        // Fetch current identity data + UTXOs
        const [{ data: identityData }, utxoData] = await Promise.all([
            this._client.getIdentityRaw(),
            this._client.getUtxos(),
        ]);
        if (!identityData.prevOutput) {
            throw new Error('Identity previous output not found — identity may not be confirmed on-chain');
        }
        // Check if authorities are already set correctly
        if (identityData.identity.revocationauthority === revokeAddress &&
            identityData.identity.recoveryauthority === recoverAddress) {
            console.log(`[J41] Authorities already set correctly, no update needed`);
            return 'already-set';
        }
        // Build identity update transaction with new authorities (no VDXF changes)
        const signedTxHex = (0, update_js_1.buildIdentityUpdateTx)({
            wif: this.wif,
            identityData,
            utxos: utxoData.utxos,
            vdxfAdditions: {}, // no content changes
            network: this.networkType,
            revocationauthority: revokeAddress,
            recoveryauthority: recoverAddress,
        });
        // Broadcast
        const result = await this._client.broadcast(signedTxHex);
        console.log(`[J41] ✅ Authorities updated on-chain: ${result.txid}`);
        return result.txid;
    }
    /**
     * Check the current revocation and recovery authorities for this identity.
     * Warns if they are self-referential (identity is its own authority = weaker security).
     *
     * @returns Object with authority addresses and warning flags
     */
    async checkAuthorities() {
        const { data: identityData } = await this._client.getIdentityRaw();
        const id = identityData.identity;
        const selfRevoke = id.revocationauthority === id.identityaddress;
        const selfRecover = id.recoveryauthority === id.identityaddress;
        if (selfRevoke) {
            console.warn(`[J41] ⚠️  Revocation authority is self-referential — if WIF is compromised, attacker can revoke your identity`);
        }
        if (selfRecover) {
            console.warn(`[J41] ⚠️  Recovery authority is self-referential — if WIF is compromised, attacker controls recovery`);
        }
        return {
            revocationauthority: id.revocationauthority,
            recoveryauthority: id.recoveryauthority,
            identityaddress: id.identityaddress,
            selfRevoke,
            selfRecover,
        };
    }
    /**
     * Update the on-chain agent.status VDXF key ('active' or 'inactive').
     * Builds and broadcasts an identity update TX.
     */
    async setOnChainStatus(status) {
        if (!this.wif || !this.iAddress)
            throw new Error('WIF and i-address required');
        await this.login();
        const [{ data: identityData }, utxoData] = await Promise.all([
            this._client.getIdentityRaw(),
            this._client.getUtxos(),
        ]);
        if (!identityData.prevOutput)
            throw new Error('Identity not found on-chain');
        const utxos = (utxoData.utxos || []).filter((u) => u.satoshis > 0);
        if (utxos.length === 0)
            throw new Error('No UTXOs available for TX fee');
        const vdxfAdditions = {
            [vdxf_js_2.VDXF_KEYS.agent.status]: [(0, vdxf_js_2.makeSubDD)(vdxf_js_2.VDXF_KEYS.agent.status, status)],
        };
        const signedTxHex = (0, update_js_1.buildIdentityUpdateTx)({
            wif: this.wif,
            identityData,
            utxos,
            vdxfAdditions,
            network: this.networkType,
        });
        const result = await this._client.broadcast(signedTxHex);
        console.log(`[J41] On-chain status → ${status} (txid: ${result.txid})`);
        this.emit('status:changed', { status, txid: result.txid });
        return result.txid;
    }
    /**
     * Accept a review from the inbox and update identity on-chain.
     * Builds a signed updateidentity transaction, broadcasts it, and marks the inbox item as accepted.
     */
    async acceptReview(inboxId) {
        if (!this.wif || !this.iAddress) {
            throw new Error(`Cannot accept review ${inboxId}: WIF key and i-address required`);
        }
        try {
            // 1. Fetch the inbox item to get VDXF data
            const { data: inboxItem } = await this._client.getInboxItem(inboxId);
            if (inboxItem.status !== 'pending') {
                console.log(`[J41] Inbox item ${inboxId} already ${inboxItem.status}, skipping`);
                return;
            }
            // 2. Get current identity data + UTXOs from platform
            const [{ data: identityData }, utxoData] = await Promise.all([
                this._client.getIdentityRaw(),
                this._client.getUtxos(),
            ]);
            if (!identityData.prevOutput) {
                throw new Error(`Cannot accept review ${inboxId}: identity previous output not found — identity may not be on-chain yet`);
            }
            if (!utxoData.utxos || utxoData.utxos.length === 0) {
                throw new Error(`Cannot accept review ${inboxId}: no UTXOs available for TX fee — fund the agent wallet`);
            }
            // 3. Build nested DD VDXF additions from the inbox item's review data
            const reviewKeys = vdxf_js_2.VDXF_KEYS.review;
            const vdxfAdditions = {};
            // Audit 2026-06-02 H8: restrict allowed VDXF keys to the review.*
            // namespace. Without this, a compromised platform inbox could pass any
            // VDXF key in inboxItem.vdxfData — including agent.payAddress — and
            // we'd write attacker-controlled data to our on-chain identity. The
            // allowlist is the set of i-addresses under VDXF_KEYS.review.
            const reviewAllowedIaddrs = new Set(Object.values(reviewKeys));
            // Check if vdxfData is already formatted (flat or legacy)
            if (inboxItem.vdxfData && Object.keys(inboxItem.vdxfData).length > 0) {
                // Use pre-computed VDXF data from the inbox item, dropping any key
                // outside the review namespace.
                for (const [key, value] of Object.entries(inboxItem.vdxfData)) {
                    if (value != null) {
                        if (!reviewAllowedIaddrs.has(key)) {
                            console.error(`[J41] acceptReview ${inboxId}: dropping unexpected VDXF key ${key} ` +
                                `(not in review.* namespace) — possible platform tampering`);
                            continue;
                        }
                        vdxfAdditions[key] = Array.isArray(value) ? value : [value];
                    }
                }
                if (Object.keys(vdxfAdditions).length === 0) {
                    throw new Error(`acceptReview ${inboxId}: inbox vdxfData contained no review.* keys after whitelist`);
                }
            }
            else {
                // Build review.record as flat key
                const reviewRecord = {
                    timestamp: Math.floor(Date.now() / 1000),
                };
                if (inboxItem.senderVerusId)
                    reviewRecord.buyer = inboxItem.senderVerusId;
                if (inboxItem.jobHash)
                    reviewRecord.jobHash = inboxItem.jobHash;
                if (inboxItem.signature)
                    reviewRecord.signature = inboxItem.signature;
                if (inboxItem.message)
                    reviewRecord.message = inboxItem.message;
                if (inboxItem.rating != null)
                    reviewRecord.rating = inboxItem.rating;
                vdxfAdditions[reviewKeys.record] = [(0, vdxf_js_2.makeSubDD)(reviewKeys.record, JSON.stringify(reviewRecord))];
            }
            // 4. Build and sign the identity update transaction
            console.log(`[J41] Building identity update transaction...`);
            const signedTxHex = (0, update_js_1.buildIdentityUpdateTx)({
                wif: this.wif,
                identityData,
                utxos: utxoData.utxos,
                vdxfAdditions,
                network: this.networkType,
            });
            // 5. Broadcast the signed transaction
            console.log(`[J41] Broadcasting identity update transaction...`);
            const broadcastResult = await this._client.broadcast(signedTxHex);
            console.log(`[J41] ✅ Identity updated on-chain: ${broadcastResult.txid}`);
            // 6. Mark inbox item as accepted
            await this._client.acceptInboxItem(inboxId, broadcastResult.txid);
            console.log(`[J41] ✅ Review accepted and identity updated`);
            this.emit('review:accepted', {
                inboxId,
                txid: broadcastResult.txid,
            });
        }
        catch (err) {
            console.error(`[J41] Failed to accept review ${inboxId}:`, err instanceof Error ? err.message : String(err));
            throw err;
        }
    }
    /**
     * Accept a job record from the inbox and update identity on-chain.
     * Same pattern as acceptReview — fetches inbox item, builds VDXF update tx, broadcasts.
     */
    async acceptJobRecord(inboxId) {
        if (!this.wif || !this.iAddress) {
            throw new Error(`Cannot accept job record ${inboxId}: WIF key and i-address required`);
        }
        try {
            const { data: inboxItem } = await this._client.getInboxItem(inboxId);
            if (inboxItem.status !== 'pending') {
                console.log(`[J41] Inbox item ${inboxId} already ${inboxItem.status}, skipping`);
                return;
            }
            const [{ data: identityData }, utxoData] = await Promise.all([
                this._client.getIdentityRaw(),
                this._client.getUtxos(),
            ]);
            if (!identityData.prevOutput) {
                throw new Error(`Cannot accept job record ${inboxId}: identity not found on-chain`);
            }
            if (!utxoData.utxos || utxoData.utxos.length === 0) {
                throw new Error(`Cannot accept job record ${inboxId}: no UTXOs — fund the agent wallet`);
            }
            const vdxfAdditions = {};
            // Audit 2026-06-02 H8 mirror: same whitelist for acceptJobRecord,
            // restricted to job.* namespace.
            const jobAllowedIaddrs = new Set(Object.values(vdxf_js_2.VDXF_KEYS.job));
            if (inboxItem.vdxfData && Object.keys(inboxItem.vdxfData).length > 0) {
                for (const [key, value] of Object.entries(inboxItem.vdxfData)) {
                    if (value != null) {
                        if (!jobAllowedIaddrs.has(key)) {
                            console.error(`[J41] acceptJobRecord ${inboxId}: dropping unexpected VDXF key ${key} ` +
                                `(not in job.* namespace) — possible platform tampering`);
                            continue;
                        }
                        vdxfAdditions[key] = Array.isArray(value) ? value : [value];
                    }
                }
                if (Object.keys(vdxfAdditions).length === 0) {
                    throw new Error(`acceptJobRecord ${inboxId}: inbox vdxfData contained no job.* keys after whitelist`);
                }
            }
            else {
                const jobRecord = {
                    timestamp: Math.floor(Date.now() / 1000),
                };
                if (inboxItem.senderVerusId)
                    jobRecord.buyer = inboxItem.senderVerusId;
                if (inboxItem.jobHash)
                    jobRecord.jobHash = inboxItem.jobHash;
                if (inboxItem.amount != null)
                    jobRecord.amount = inboxItem.amount;
                if (inboxItem.currency)
                    jobRecord.currency = inboxItem.currency;
                if (inboxItem.completedAt)
                    jobRecord.completedAt = inboxItem.completedAt;
                vdxfAdditions[vdxf_js_2.VDXF_KEYS.job.record] = [(0, vdxf_js_2.makeSubDD)(vdxf_js_2.VDXF_KEYS.job.record, JSON.stringify(jobRecord))];
            }
            const signedTxHex = (0, update_js_1.buildIdentityUpdateTx)({
                wif: this.wif,
                identityData,
                utxos: utxoData.utxos,
                vdxfAdditions,
                network: this.networkType,
            });
            const broadcastResult = await this._client.broadcast(signedTxHex);
            console.log(`[J41] ✅ Job record written on-chain: ${broadcastResult.txid}`);
            await this._client.acceptInboxItem(inboxId, broadcastResult.txid);
            console.log(`[J41] ✅ Job record accepted`);
            this.emit('job_record:accepted', { inboxId, txid: broadcastResult.txid });
        }
        catch (err) {
            console.error(`[J41] Failed to accept job record ${inboxId}:`, err instanceof Error ? err.message : String(err));
            throw err;
        }
    }
    /**
     * Respond to a dispute (seller side). Auto-signs the response.
     */
    async respondToDispute(jobId, options) {
        if (!this.wif && !this.signer)
            throw new Error('Agent not initialized with WIF or remote signer');
        if (!this.iAddress)
            throw new Error('Agent not initialized with i-address');
        const job = await this._client.getJob(jobId);
        if (!job.jobHash)
            throw new Error(`Job ${jobId} is missing jobHash — cannot sign dispute response`);
        const brokered = await this._signBrokered({ type: 'dispute_respond', jobId, action: options.action }, { jobHash: job.jobHash });
        const result = await this._client.respondToDispute(jobId, {
            ...options,
            timestamp: brokered.timestamp,
            signature: brokered.signature,
        });
        this.emit('dispute:responded', { jobId, action: options.action });
        return result;
    }
    /**
     * Accept a rework offer (buyer side). Auto-signs the acceptance.
     */
    async acceptRework(jobId) {
        if (!this.wif && !this.signer)
            throw new Error('Agent not initialized with WIF or remote signer');
        const job = await this._client.getJob(jobId);
        if (!job.jobHash)
            throw new Error(`Job ${jobId} is missing jobHash — cannot sign rework acceptance`);
        const jobHash = job.jobHash;
        const timestamp = Math.floor(Date.now() / 1000);
        // Rework-accept is not in the broker's gated set today; it's a buyer-side
        // action and the message is rebuildable from public job state. Sign as a
        // generic message — the signer's policy (if any) decides whether to allow.
        const msg = (0, messages_js_1.buildReworkAcceptMessage)({ jobHash, timestamp });
        const signature = await this._signMessageBuilt(msg);
        const result = await this._client.acceptRework(jobId, { timestamp, signature });
        this.emit('rework:accepted', { jobId });
        return result;
    }
    /**
     * Request additional budget for a job.
     * The other party receives a budget_request event and can approve/decline.
     */
    async requestBudget(jobId, params) {
        return this._client.requestBudget(jobId, params);
    }
    /**
     * Join a specific job's chat room.
     */
    joinJobChat(jobId) {
        this.chatClient?.joinJob(jobId);
    }
    /**
     * Check for new job requests and process them.
     */
    /** Maximum number of job IDs to track for deduplication */
    static MAX_SEEN_JOBS = 10_000;
    async checkForJobs() {
        if (!this.handler || this.polling)
            return;
        this.polling = true;
        try {
            const res = await this._client.getMyJobs({ status: 'requested', role: 'seller' });
            // Audit 2026-06-02 H5: cap the per-poll job count. A compromised or
            // buggy platform could otherwise return an arbitrarily large response
            // and force unbounded per-job signer round-trips. The cap is well above
            // any realistic backlog; oversize responses log + truncate rather than
            // refuse outright (we still want to make progress on the first N).
            const MAX_PER_POLL = Number(process.env.J41_MAX_JOBS_PER_POLL ?? 50);
            const allJobs = res.data || [];
            if (allJobs.length > MAX_PER_POLL) {
                console.error(`[J41] checkForJobs: response had ${allJobs.length} jobs; truncating to ` +
                    `${MAX_PER_POLL} (J41_MAX_JOBS_PER_POLL). Excess will appear on next poll.`);
            }
            const jobs = allJobs.slice(0, MAX_PER_POLL);
            for (const job of jobs) {
                // Stop processing if agent was stopped mid-poll
                if (!this.running)
                    break;
                // Skip jobs without an id (malformed response) or already processed
                if (!job.id)
                    continue;
                if (this.seenJobIds.has(job.id))
                    continue;
                try {
                    this.emit('job:requested', job);
                    if (this.handler.onJobRequested) {
                        const decision = await this.handler.onJobRequested(job);
                        if (decision === 'accept') {
                            if ((!this.wif && !this.signer) || !this.iAddress) {
                                this.emit('error', new Error(`Cannot accept job ${job.id}: WIF/remote-signer and i-address required`));
                                continue;
                            }
                            try {
                                if (!job.jobHash) {
                                    this.emit('error', new Error(`Cannot accept job ${job.id}: missing jobHash`));
                                    continue;
                                }
                                const brokered = await this._signBrokered({ type: 'accept', jobId: job.id }, {
                                    jobHash: job.jobHash,
                                    buyerVerusId: job.buyerVerusId,
                                    amount: job.amount,
                                    currency: job.currency,
                                });
                                await this._client.acceptJob(job.id, brokered.signature, brokered.timestamp);
                                this.seenJobIds.add(job.id);
                                this.emit('job:accepted', job);
                                // Auto-join chat room if chat is connected
                                if (this.chatClient?.isConnected) {
                                    this.chatClient.joinJob(job.id);
                                }
                            }
                            catch (err) {
                                // Don't mark as seen on failure — allow retry on next poll
                                this.emit('error', new Error(`Failed to accept job ${job.id}: ${err instanceof Error ? err.message : String(err)}`));
                            }
                        }
                        else if (decision === 'reject') {
                            this.seenJobIds.add(job.id);
                            this.emit('job:rejected', job);
                        }
                        // 'hold' = do nothing; NOT added to seenJobIds so it's re-evaluated next poll
                    }
                    else {
                        // No onJobRequested handler — mark as seen to avoid repeated job:requested events
                        this.seenJobIds.add(job.id);
                    }
                }
                catch (jobErr) {
                    // Per-job error: don't skip remaining jobs in batch
                    this.emit('error', jobErr instanceof Error ? jobErr : new Error(String(jobErr)));
                }
            }
            // Evict oldest entries if the set grows too large
            while (this.seenJobIds.size > J41Agent.MAX_SEEN_JOBS) {
                const first = this.seenJobIds.values().next().value;
                if (first !== undefined)
                    this.seenJobIds.delete(first);
                else
                    break;
            }
        }
        catch (error) {
            this.emit('error', error instanceof Error ? error : new Error(String(error)));
        }
        finally {
            this.polling = false;
        }
    }
    /** Get the agent's identity name */
    get identity() {
        return this.identityName;
    }
    /** Get the agent's i-address */
    get address() {
        return this.iAddress;
    }
    /** Check if agent is currently listening for jobs */
    get isRunning() {
        return this.running;
    }
    // ------------------------------------------
    // Canary Protection
    // ------------------------------------------
    /**
     * Enable canary protection standalone (after initial registration, or to re-enable with a new token).
     * Generates a new canary token and registers it with SovGuard.
     * Returns the systemPromptInsert for embedding; the raw token is kept internal.
     */
    async enableCanaryProtection() {
        await this.login();
        const canary = (0, canary_js_1.generateCanary)();
        // S3: Use J41Client.registerCanary() for retry + error handling
        await this._client.registerCanary(canary.registration);
        this.canaryConfig = canary;
        this.emit('canary:registered', { active: true });
        return { active: true, systemPromptInsert: canary.systemPromptInsert };
    }
    /**
     * Wrap a system prompt with the agent's canary token.
     * The canary must be initialized first (via registerWithJ41() or enableCanaryProtection()).
     */
    getProtectedSystemPrompt(systemPrompt) {
        if (!this.canaryConfig) {
            throw new Error('Canary not initialized — call registerWithJ41() or enableCanaryProtection() first');
        }
        return systemPrompt + '\n' + this.canaryConfig.systemPromptInsert;
    }
    /** Whether canary protection is currently active */
    get canaryActive() {
        return this.canaryConfig !== null;
    }
    // ------------------------------------------
    // Privacy Tier
    // ------------------------------------------
    privacyTier = 'standard';
    /**
     * Set the agent's privacy tier.
     * Stores locally and updates the platform profile.
     */
    async setPrivacyTier(tier) {
        this.privacyTier = tier;
        await this._client.updateAgentProfile({ privacyTier: tier });
        this.emit('privacy:updated', tier);
    }
    /** Get the current privacy tier */
    getPrivacyTier() {
        return this.privacyTier;
    }
    // ------------------------------------------
    // Deletion Attestation
    // ------------------------------------------
    /**
     * Generate, sign, and submit a deletion attestation.
     * Call this after destroying a job's container and data.
     *
     * @param jobId - The job ID
     * @param containerId - Docker/OCI container ID that was destroyed
     * @param dataVolumes - List of volume paths that were deleted
     * @param deletionMethod - Method used (default: 'container-destroy+volume-rm')
     * @returns The signed attestation
     */
    async attestDeletion(jobId, containerId, options, network) {
        if (!this.wif && !this.signer) {
            throw new Error('WIF key or remote signer required for signing attestations');
        }
        if (!this.identityName) {
            throw new Error('Agent must be registered before attesting deletions');
        }
        // `network` is honored only when falling back to the local WIF path.
        // Remote signers are constructed knowing their own network.
        const net = network ?? this.networkType;
        const now = new Date().toISOString();
        const payload = (0, attestation_js_1.generateAttestationPayload)({
            jobId,
            containerId,
            createdAt: options?.createdAt || now,
            destroyedAt: options?.destroyedAt || now,
            dataVolumes: options?.dataVolumes,
            deletionMethod: options?.deletionMethod,
            attestedBy: this.identityName,
        });
        let attestation;
        if (this.signer) {
            const { signAttestationWith } = await Promise.resolve().then(() => __importStar(require('./privacy/attestation.js')));
            attestation = await signAttestationWith(payload, (msg) => this.signer.signMessage(msg));
        }
        else {
            attestation = (0, attestation_js_1.signAttestation)(payload, this.wif, net);
        }
        // Submit to platform
        await this._client.submitAttestation(attestation);
        this.emit('attestation:submitted', attestation);
        return attestation;
    }
    // ------------------------------------------
    // Pricing
    // ------------------------------------------
    /**
     * Estimate pricing for a job based on model, category, and token usage.
     * Pure local calculation — no API call needed.
     *
     * @param model - LLM model name
     * @param category - Job category (trivial, simple, medium, complex, premium)
     * @param inputTokens - Estimated input tokens (default: 2000)
     * @param outputTokens - Estimated output tokens (default: 1000)
     * @returns Price recommendation with min/recommended/premium/ceiling
     */
    estimatePrice(model, category, inputTokens = 2000, outputTokens = 1000) {
        return (0, calculator_js_1.recommendPrice)({
            model,
            inputTokens,
            outputTokens,
            category,
            privacyTier: this.privacyTier,
        });
    }
    // ------------------------------------------
    // Bounty convenience methods
    // ------------------------------------------
    /**
     * Post a new bounty with automatic signing.
     * Signs the bounty commitment message and submits via the client.
     */
    async postBounty(data) {
        if (!this.wif && !this.signer)
            throw new Error('Agent not initialized with WIF or remote signer');
        const currency = data.currency || 'VRSCTEST';
        const timestamp = Math.floor(Date.now() / 1000);
        const msg = (0, messages_js_1.buildPostBountyMessage)(data.title, data.amount, currency, timestamp);
        const signature = await this._signMessageBuilt(msg);
        const result = await this._client.postBounty({
            title: data.title,
            description: data.description,
            amount: data.amount,
            currency,
            category: data.category,
            maxClaimants: data.maxClaimants,
            applicationDeadline: data.applicationDeadline,
            signature,
            timestamp,
        });
        this.emit('bounty:posted', { id: result.id, title: data.title });
        return result;
    }
    /**
     * Apply to a bounty with automatic signing.
     * Signs the application message and submits via the client.
     */
    async applyToBounty(bountyId, message) {
        if (!this.wif && !this.signer)
            throw new Error('Agent not initialized with WIF or remote signer');
        const timestamp = Math.floor(Date.now() / 1000);
        const msg = (0, messages_js_1.buildApplyBountyMessage)(bountyId, timestamp);
        const signature = await this._signMessageBuilt(msg);
        const result = await this._client.applyToBounty(bountyId, {
            message,
            signature,
            timestamp,
        });
        this.emit('bounty:applied', { id: result.id, bountyId });
        return result;
    }
    /**
     * Cancel a bounty (poster only). No signing needed.
     */
    async cancelBounty(bountyId) {
        const result = await this._client.cancelBounty(bountyId);
        this.emit('bounty:cancelled', { id: result.id, bountyId });
        return result;
    }
    // ------------------------------------------
    // Hiring (agent-to-agent or agent-as-buyer)
    // ------------------------------------------
    /**
     * Hire another agent by creating a job.
     * Fetches the canonical signing message from the platform, signs it, and submits.
     *
     * @param data - Job details (sellerVerusId, description, amount, etc.)
     * @returns The created Job object
     */
    async createJob(data) {
        if (!this.wif && !this.signer)
            throw new Error('Agent not initialized with WIF or remote signer');
        await this.login();
        // Auto-resolve seller's payment address if not provided
        if (!data.paymentAddress) {
            try {
                const payInfo = await this._client.getAgentPaymentAddress(data.sellerVerusId);
                if (payInfo.address)
                    data.paymentAddress = payInfo.address;
            }
            catch {
                // Non-fatal — backend will try to derive it
            }
        }
        const timestamp = Math.floor(Date.now() / 1000);
        // Audit 2026-06-02 H1/H10: previously signed whatever bytes the platform
        // returned from getJobRequestMessage — letting a compromised/MITM'd
        // platform substitute a J41-COMPLETE / J41-DEPOSIT-REPORT etc. and harvest
        // a fund-loss signature from a benign createJob call. Refuse to sign
        // unless the platform-returned bytes start with the expected job-request
        // prefix AND embed our seller/amount/timestamp.
        const { message } = await this._client.getJobRequestMessage({
            sellerVerusId: data.sellerVerusId,
            description: data.description,
            amount: data.amount,
            currency: data.currency,
            deadline: data.deadline,
            timestamp,
            sovguardEnabled: data.sovguardEnabled,
        });
        // The platform's exact J41-* prefix for the job-request canonical message
        // varies (we've seen J41-JOB-REQUEST, J41-CREATE-JOB, etc. across backend
        // revisions). The defensive value is in BINDING the fields the caller
        // supplied — an attacker who substitutes a different J41-* shape (e.g.,
        // J41-COMPLETE) cannot also fit our seller/amount/timestamp into that
        // shape's field layout, so the bound-field check is sufficient.
        if (typeof message !== 'string' || !/^J41-/.test(message)) {
            throw new Error('Refusing to sign platform-supplied bytes that do not start with J41-');
        }
        if (!message.includes(data.sellerVerusId) ||
            !message.includes(String(data.amount)) ||
            !message.includes(String(timestamp))) {
            throw new Error('Refusing to sign job-request bytes that do not bind our seller/amount/timestamp');
        }
        const signature = await this._signMessageBuilt(message);
        const job = await this._client.createJob({
            ...data,
            currency: data.currency || 'VRSCTEST',
            timestamp,
            signature,
        });
        this.emit('job:created', { id: job.id, seller: data.sellerVerusId });
        return job;
    }
    /**
     * Mark a job as completed (buyer confirms work is satisfactory).
     * Signs the completion message automatically.
     */
    async completeJob(jobId) {
        if (!this.wif && !this.signer)
            throw new Error('Agent not initialized with WIF or remote signer');
        await this.login();
        const jobData = await this._client.getJob(jobId);
        const timestamp = Math.floor(Date.now() / 1000);
        const message = `J41-COMPLETE|Job:${jobData.jobHash}|Ts:${timestamp}|I confirm the work has been delivered satisfactorily.`;
        const signature = await this._signMessageBuilt(message);
        return this._client.completeJob(jobId, signature, timestamp);
    }
    /**
     * Submit a review for an agent after a completed job.
     * Gets the canonical message from the backend, signs it, and submits.
     */
    async submitReview(params) {
        if (!this.wif && !this.signer)
            throw new Error('Agent not initialized with WIF or remote signer');
        await this.login();
        const timestamp = Math.floor(Date.now() / 1000);
        // Get canonical message from backend
        const msgResult = await this._client.getReviewMessage({
            agentVerusId: params.agentVerusId,
            jobHash: params.jobHash,
            message: params.message || '',
            rating: params.rating,
            timestamp,
        });
        // Same confused-deputy defense as createJob — bound-field check is the
        // load-bearing defense; the exact J41-* prefix may vary across backend
        // revisions, so we accept any J41-* prefix and rely on field binding.
        if (typeof msgResult.message !== 'string' || !/^J41-/.test(msgResult.message)) {
            throw new Error('Refusing to sign platform-supplied bytes that do not start with J41-');
        }
        if (!msgResult.message.includes(params.jobHash) ||
            !msgResult.message.includes(String(params.rating))) {
            throw new Error('Refusing to sign review bytes that do not bind our jobHash + rating');
        }
        const signature = await this._signMessageBuilt(msgResult.message);
        const buyerVerusId = this.identityName
            ? (this.identityName.endsWith('@') ? this.identityName : this.identityName + '@')
            : this.iAddress || '';
        const result = await this._client.submitReview({
            agentVerusId: params.agentVerusId,
            buyerVerusId,
            jobHash: params.jobHash,
            rating: params.rating,
            message: params.message || '',
            timestamp: msgResult.timestamp,
            signature,
        });
        return result;
    }
    // ------------------------------------------
    // Payments (on-chain VRSC transfers)
    // ------------------------------------------
    /**
     * Send VRSC/VRSCTEST to an address or VerusID.
     * Builds, signs, and broadcasts the transaction.
     *
     * @param to - Destination R-address, i-address, or VerusID (e.g., "alice@")
     * @param amount - Amount in VRSC (not satoshis)
     * @returns Transaction ID
     */
    async sendCurrency(to, amount, opts) {
        if (!this.wif)
            throw new Error('Agent not initialized with WIF');
        const wif = this.wif;
        return this.serializeSend(async () => {
            await this.login();
            // Resolve VerusID to i-address if needed
            let toAddress = to;
            if (to.includes('@') || to.includes('.')) {
                const trust = opts?.trustPlatformResolution
                    ?? (process.env.J41_TRUST_PLATFORM_RESOLUTION === '1');
                if (!trust) {
                    throw new Error(`sendCurrency refused: '${to}' is a VerusID; platform-resolved addresses ` +
                        `are not trusted by default (audit 2026-06-02 H2). Pass an R-address or ` +
                        `i-address, OR set trustPlatformResolution: true (or env ` +
                        `J41_TRUST_PLATFORM_RESOLUTION=1) to opt in to the legacy behavior.`);
                }
                try {
                    const payInfo = await this._client.getAgentPaymentAddress(to);
                    toAddress = payInfo.address || to;
                }
                catch {
                    toAddress = to;
                }
            }
            // Get UTXOs — merge confirmed + our unconfirmed change, exclude already-spent, dedupe.
            const utxoResp = await this._client.getUtxos();
            let apiUtxos = (utxoResp.utxos || utxoResp);
            if (!Array.isArray(apiUtxos))
                apiUtxos = [];
            apiUtxos = apiUtxos.filter((u) => u.satoshis > 0);
            let utxos = this.mergeUtxos(apiUtxos);
            if (utxos.length === 0) {
                throw new Error('No UTXOs available — wallet is empty');
            }
            // If sourceAddress specified, only use UTXOs from that address
            if (opts?.sourceAddress) {
                utxos = utxos.filter((u) => u.address === opts.sourceAddress);
                if (utxos.length === 0) {
                    throw new Error(`No spendable UTXOs on ${opts.sourceAddress}`);
                }
            }
            // Determine change address
            const { buildPayment, wifToAddress, isIAddress } = await Promise.resolve().then(() => __importStar(require('./tx/payment.js')));
            let changeAddress = opts?.changeAddress;
            if (!changeAddress) {
                // Default: change goes back to the source address type
                // If spending from i-address, change stays on i-address
                // If spending from R-address (or mixed), change goes to R-address
                const allFromI = utxos.every((u) => u.address && isIAddress(u.address));
                if (allFromI && this.iAddress) {
                    changeAddress = this.iAddress;
                }
                else {
                    changeAddress = wifToAddress(wif, this.networkType);
                }
            }
            // SECURITY: change MUST return to one of the agent's own addresses. A
            // caller-supplied changeAddress pointing elsewhere would divert wallet
            // change — frequently the bulk of the balance — to a third party. This
            // protects every caller (MCP tools and SDK-direct) at the construction point.
            const ownRAddress = wifToAddress(wif, this.networkType);
            const selfAddrs = new Set([ownRAddress, this.iAddress].filter(Boolean));
            if (!selfAddrs.has(changeAddress)) {
                throw new Error(`Refusing to send change to a non-self address (${changeAddress}); change must go to the agent's own R-address or i-address.`);
            }
            const built = buildPayment({
                wif,
                toAddress,
                amount,
                utxos,
                changeAddress,
                network: this.networkType,
                returnDetails: true,
            });
            // Mark inputs spent BEFORE broadcast so a following send can't reselect them.
            for (const spent of built.spentUtxos)
                this._spentUtxos.add(`${spent.txid}:${spent.vout}`);
            // Audit L-SDK-ddos-1: evict oldest entries when cap exceeded.
            while (this._spentUtxos.size > J41Agent.MAX_SPENT_UTXOS) {
                const oldest = this._spentUtxos.values().next().value;
                if (oldest === undefined)
                    break;
                this._spentUtxos.delete(oldest);
            }
            const result = await this._client.broadcast(built.rawhex);
            const txid = typeof result === 'string' ? result : result.txid || JSON.stringify(result);
            // Chain the change UTXO for the next send (uses the real broadcast txid).
            if (built.changeUtxo) {
                built.changeUtxo.txid = txid;
                this._pendingChange.push(built.changeUtxo);
                while (this._pendingChange.length > J41Agent.MAX_PENDING_CHANGE) {
                    this._pendingChange.shift();
                }
            }
            this.emit('payment:sent', { txid, to: toAddress, amount });
            return txid;
        });
    }
    /**
     * Send to multiple addresses in a single TX (e.g. agent payment + platform fee).
     * @returns txid
     */
    // Track spent UTXOs and unconfirmed change for UTXO chaining (multiple TXs per block)
    // Audit 2026-06-02 L-SDK-ddos-1: both sets grow until stop() — long-running
    // operators bleed memory. Bound them and evict insertion-order oldest when
    // cap is exceeded. Set iteration order is insertion-order in JS so this
    // works as a cheap LRU; the trade-off is that a UTXO chained > N sends ago
    // can be re-selected, but the per-tx serializeSend() + apiUtxos.filter()
    // already prevent same-block double-spend, so the only consequence is
    // re-confirming an already-spent UTXO at apiUtxos time (which apiUtxos
    // strips before we get here).
    static MAX_SPENT_UTXOS = 10_000;
    static MAX_PENDING_CHANGE = 1_000;
    _spentUtxos = new Set(); // "txid:vout" keys
    _pendingChange = []; // unconfirmed change UTXOs from our own TXs
    _txChain = Promise.resolve(); // serializes sends
    /**
     * Serialize fund-moving operations so two concurrent sends can't select the
     * same UTXO (double-spend → one tx rejected / payment silently fails).
     */
    serializeSend(fn) {
        const run = this._txChain.then(fn, fn);
        this._txChain = run.then(() => undefined, () => undefined);
        return run;
    }
    /**
     * Merge confirmed API UTXOs with our unconfirmed change, excluding
     * already-spent outpoints and de-duplicating by txid:vout. A change UTXO that
     * has since confirmed appears in BOTH lists — selecting it twice would build
     * an invalid (duplicate-input) tx. Also prunes pending-change entries that are
     * now spent or confirmed so the list can't grow unbounded.
     */
    mergeUtxos(apiUtxos) {
        const apiKeys = new Set(apiUtxos.map((u) => `${u.txid}:${u.vout}`));
        this._pendingChange = this._pendingChange.filter((u) => {
            const k = `${u.txid}:${u.vout}`;
            return !this._spentUtxos.has(k) && !apiKeys.has(k);
        });
        const seen = new Set();
        const merged = [];
        for (const u of [...apiUtxos, ...this._pendingChange]) {
            const k = `${u.txid}:${u.vout}`;
            if (this._spentUtxos.has(k) || seen.has(k))
                continue;
            seen.add(k);
            merged.push(u);
        }
        return merged;
    }
    async sendMultiPayment(outputs) {
        if (!this.wif)
            throw new Error('Agent not initialized with WIF');
        const wif = this.wif;
        return this.serializeSend(async () => {
            await this.login();
            const utxoResp = await this._client.getUtxos();
            let apiUtxos = (utxoResp.utxos || utxoResp);
            if (!Array.isArray(apiUtxos))
                apiUtxos = [];
            apiUtxos = apiUtxos.filter((u) => u.satoshis > 0);
            // Merge confirmed + unconfirmed change, exclude spent, dedupe by txid:vout.
            const allUtxos = this.mergeUtxos(apiUtxos);
            if (allUtxos.length === 0) {
                throw new Error('No UTXOs available — wallet is empty or all outputs are unconfirmed');
            }
            const { buildMultiPayment, wifToAddress } = await Promise.resolve().then(() => __importStar(require('./tx/payment.js')));
            const changeAddress = wifToAddress(wif, this.networkType);
            const result = buildMultiPayment({
                wif,
                outputs,
                utxos: allUtxos,
                changeAddress,
                network: this.networkType,
                returnDetails: true,
            });
            // Mark inputs spent BEFORE broadcast so a following send can't reselect them.
            for (const spent of result.spentUtxos) {
                this._spentUtxos.add(`${spent.txid}:${spent.vout}`);
            }
            const broadcastResult = await this._client.broadcast(result.rawhex);
            const txid = typeof broadcastResult === 'string' ? broadcastResult : broadcastResult.txid || JSON.stringify(broadcastResult);
            // Track change output so it can be spent in the next TX (UTXO chaining)
            if (result.changeUtxo) {
                // Use the real broadcast txid (may differ from computed txid for some networks)
                result.changeUtxo.txid = txid;
                this._pendingChange.push(result.changeUtxo);
            }
            const totalAmount = outputs.reduce((s, o) => s + o.amount, 0);
            this.emit('payment:sent', { txid, outputs, totalAmount });
            return txid;
        });
    }
    /**
     * Transfer funds between the agent's R-address and i-address.
     * @param direction - 'to-identity' moves R→i, 'to-r-address' moves i→R
     * @param amount - Amount in VRSC
     */
    async transferFunds(direction, amount) {
        if (!this.wif)
            throw new Error('Agent not initialized with WIF');
        if (!this.iAddress)
            throw new Error('Agent has no i-address');
        const { wifToAddress } = await Promise.resolve().then(() => __importStar(require('./tx/payment.js')));
        const rAddress = wifToAddress(this.wif, this.networkType);
        if (direction === 'to-identity') {
            // R-address → i-address
            return this.sendCurrency(this.iAddress, amount, {
                sourceAddress: rAddress,
                changeAddress: rAddress,
            });
        }
        else {
            // i-address → R-address
            return this.sendCurrency(rAddress, amount, {
                sourceAddress: this.iAddress,
                changeAddress: this.iAddress,
            });
        }
    }
}
exports.J41Agent = J41Agent;
//# sourceMappingURL=agent.js.map