From 3e6002780943a8fbcb8ba78d26bdbbe6f948c07e Mon Sep 17 00:00:00 2001 From: macbotmini-eng <209834998+macbotmini-eng@users.noreply.github.com> Date: Tue, 5 May 2026 03:20:24 -0600 Subject: [PATCH 1/6] feat(bitflow-funding-coordinator): add funding coordinator skill --- skills/bitflow-funding-coordinator/AGENT.md | 40 ++ skills/bitflow-funding-coordinator/SKILL.md | 130 ++++ .../bitflow-funding-coordinator.ts | 579 ++++++++++++++++++ 3 files changed, 749 insertions(+) create mode 100644 skills/bitflow-funding-coordinator/AGENT.md create mode 100644 skills/bitflow-funding-coordinator/SKILL.md create mode 100644 skills/bitflow-funding-coordinator/bitflow-funding-coordinator.ts diff --git a/skills/bitflow-funding-coordinator/AGENT.md b/skills/bitflow-funding-coordinator/AGENT.md new file mode 100644 index 00000000..00ff5ab5 --- /dev/null +++ b/skills/bitflow-funding-coordinator/AGENT.md @@ -0,0 +1,40 @@ +--- +name: bitflow-funding-coordinator-agent +skill: bitflow-funding-coordinator +description: "Coordinates route-ready Bitflow funding swaps only after checkpoint, confirmation, and Hiro verification checks pass." +--- + +# Agent Behavior - Bitflow Funding Coordinator + +## Decision order + +1. Run `doctor` first. If it fails, stop and surface the blocker. +2. Run `status` to detect an unresolved funding checkpoint. +3. Run `plan` with explicit wallet, source token, target token, amount, slippage, and handoff label. +4. Confirm that the user wants the funding leg, not a downstream HODLMM or Zest placement. +5. Execute `run --confirm=FUND` only after reviewing the plan. +6. If a txid is already known, use `resume --txid` instead of rebroadcasting. +7. Parse JSON output and pass the handoff payload to the next strategy only after `routeReady` is true. + +## Guardrails + +- Never broadcast without `--confirm=FUND`. +- Never treat funding as yield deployment. This skill stops after the target token is route-ready. +- Never call HODLMM, Zest, borrow, repay, or unwind write paths. +- Never retry a write silently after interruption. +- Never start a new route while an unresolved checkpoint exists. +- Never expose wallet passwords, private keys, mnemonics, wallet IDs, or raw signer material. +- Always surface txid, Hiro status, route-ready token, handoff label, and next action. + +## On error + +- Log the JSON error payload. +- Do not retry silently. +- If a txid exists, instruct the operator to use `resume --txid`. +- If no txid exists, instruct the operator to inspect `status` and rerun `plan`. + +## On success + +- Confirm the tx hash, Hiro status, wallet, source token, target token, and handoff label. +- Report that downstream routing can start only with the emitted target token. +- Do not claim that #559, HODLMM, or Zest placement has executed. diff --git a/skills/bitflow-funding-coordinator/SKILL.md b/skills/bitflow-funding-coordinator/SKILL.md new file mode 100644 index 00000000..d8cfa31e --- /dev/null +++ b/skills/bitflow-funding-coordinator/SKILL.md @@ -0,0 +1,130 @@ +--- +name: bitflow-funding-coordinator +description: "Coordinates Bitflow funding swaps into route-ready target tokens for downstream strategy skills." +metadata: + author: "macbotmini-eng" + author-agent: "Hex Stallion" + user-invocable: "false" + arguments: "doctor | status | plan | run | resume | cancel" + entry: "bitflow-funding-coordinator/bitflow-funding-coordinator.ts" + requires: "wallet, signing, settings, bitflow-swap-aggregator, nonce-manager" + tags: "defi, write, mainnet-only, requires-funds, l2" +--- + +# Bitflow Funding Coordinator + +## What it does + +`bitflow-funding-coordinator` is the funding leg for Bitflow-routed strategies. It turns a caller-selected source token into a caller-selected target token through the accepted `bitflow-swap-aggregator`, records a route checkpoint, confirms the swap on Hiro, and emits a handoff payload for downstream strategy skills. + +The required v1 acceptance path is STX to sBTC so wallets that start with idle STX can produce route-ready sBTC for `bitflow-hodlmm-zest-yield-loop`. + +## Why agents need it + +Downstream strategy routers should not quietly perform funding swaps while they are deciding where to place capital. This skill keeps the funding leg explicit: quote, plan, confirm, swap, verify, and hand off the resulting target token without performing HODLMM, Zest, borrow, repay, or unwind actions. + +## Safety notes + +- This is a write skill and can move wallet funds. +- Mainnet only. +- `run` refuses without `--confirm=FUND`. +- The delegated swap primitive still requires its own `--confirm=SWAP` internally. +- The skill refuses overlapping local checkpoints unless the operator resumes or cancels the prior route. +- The skill records any returned txid before its own Hiro confirmation loop. +- It does not deposit into HODLMM, supply to Zest, borrow, repay, or choose a yield venue. + +## Commands + +### doctor + +Checks wallet, Bitflow swap aggregator availability, nonce-manager availability signal, Hiro reachability, and any pending funding checkpoint. + +```bash +bun run skills/bitflow-funding-coordinator/bitflow-funding-coordinator.ts doctor --wallet +``` + +### status + +Reads the local funding checkpoint and the delegated swap primitive's wallet readiness checks. + +```bash +bun run skills/bitflow-funding-coordinator/bitflow-funding-coordinator.ts status --wallet +``` + +### plan + +Produces a quote-backed funding plan through `bitflow-swap-aggregator` without broadcasting. + +```bash +bun run skills/bitflow-funding-coordinator/bitflow-funding-coordinator.ts plan --wallet --token-in token-stx --token-out token-sbtc --amount-in 1 --handoff-label bitflow-hodlmm-zest-yield-loop +``` + +### run + +Executes the funding swap after explicit confirmation, checkpoints the route, verifies Hiro success, and emits a route-ready handoff. + +```bash +bun run skills/bitflow-funding-coordinator/bitflow-funding-coordinator.ts run --wallet --token-in token-stx --token-out token-sbtc --amount-in 1 --confirm=FUND --handoff-label bitflow-hodlmm-zest-yield-loop +``` + +### resume + +Confirms an already-broadcast funding txid and completes the local checkpoint without rebroadcasting. + +```bash +bun run skills/bitflow-funding-coordinator/bitflow-funding-coordinator.ts resume --wallet --txid +``` + +### cancel + +Marks an unresolved local checkpoint as operator-cancelled. + +```bash +bun run skills/bitflow-funding-coordinator/bitflow-funding-coordinator.ts cancel --wallet +``` + +## Output contract + +All commands emit exactly one JSON object to stdout. + +Success: + +```json +{ + "status": "success", + "action": "plan", + "data": { + "fundingRoute": "token-stx-to-token-sbtc", + "mode": "one-shot", + "routeReady": false, + "handoff": { + "label": "bitflow-hodlmm-zest-yield-loop", + "readyToken": "token-sbtc", + "readyAmount": null + } + }, + "error": null +} +``` + +Blocked: + +```json +{ + "status": "blocked", + "action": "run", + "data": {}, + "error": { + "code": "CONFIRMATION_REQUIRED", + "message": "This write skill requires --confirm=FUND.", + "next": "Review plan output and rerun with --confirm=FUND." + } +} +``` + +## Known constraints + +- V1 proves STX to sBTC funding for the #471 / #559 route. +- Token selection is delegated to the live Bitflow swap aggregator token registry. +- DCA-style repeated chunks are represented as `--mode dca-chunk`, but autonomous scheduling is intentionally out of scope for this submission. +- This skill does not replace `bitflow-swap-aggregator`; it adds funding checkpoint, resume/cancel, handoff, and downstream-boundary semantics around that primitive. diff --git a/skills/bitflow-funding-coordinator/bitflow-funding-coordinator.ts b/skills/bitflow-funding-coordinator/bitflow-funding-coordinator.ts new file mode 100644 index 00000000..3673ce31 --- /dev/null +++ b/skills/bitflow-funding-coordinator/bitflow-funding-coordinator.ts @@ -0,0 +1,579 @@ +#!/usr/bin/env bun + +import { Command } from "commander"; +import { spawn } from "child_process"; +import * as fs from "fs/promises"; +import * as os from "os"; +import * as path from "path"; +import * as crypto from "crypto"; + +type Json = string | number | boolean | null | Json[] | { [key: string]: Json }; +type JsonMap = { [key: string]: Json }; +type Status = "success" | "blocked" | "error"; +type CheckpointStep = "planned" | "broadcast" | "complete" | "cancelled"; + +interface SharedOptions { + wallet?: string; + tokenIn?: string; + tokenOut?: string; + amountIn?: string; + targetOut?: string; + maxSlippageBps?: string; + slippageBps?: string; + feeUstx?: string; + minGasReserveUstx?: string; + mempoolDepthLimit?: string; + waitSeconds?: string; + mode?: string; + handoffLabel?: string; + txid?: string; +} + +interface RunOptions extends SharedOptions { + confirm?: string; +} + +interface Checkpoint { + version: number; + routeId: string; + wallet: string; + mode: string; + tokenIn: string; + tokenOut: string; + amountIn: string | null; + targetOut: string | null; + expectedAmountOut: Json | null; + txid: string | null; + step: CheckpointStep; + hiroStatus: string | null; + handoffLabel: string; + createdAt: string; + updatedAt: string; + nextRequiredAction: string; +} + +class BlockedError extends Error { + constructor( + public code: string, + message: string, + public next: string, + public data: JsonMap = {} + ) { + super(message); + } +} + +const NETWORK = process.env.NETWORK || "mainnet"; +const HIRO_API = process.env.STACKS_API_HOST || "https://api.hiro.so"; +const EXPLORER = "https://explorer.hiro.so/txid"; +const CONFIRM_TOKEN = "FUND"; +const DEFAULT_WAIT_SECONDS = 300; +const DEFAULT_MEMPOOL_DEPTH_LIMIT = 0; +const DEFAULT_HANDOFF_LABEL = "bitflow-hodlmm-zest-yield-loop"; +const STATE_ROOT = path.join(os.homedir(), ".aibtc", "state", "bitflow-funding-coordinator"); +const SWAP_SKILL = path.join("skills", "bitflow-swap-aggregator", "bitflow-swap-aggregator.ts"); + +function stringify(value: unknown): Json { + if (typeof value === "bigint") return value.toString(); + if (Array.isArray(value)) return value.map(stringify); + if (value && typeof value === "object") { + return Object.fromEntries(Object.entries(value).map(([key, val]) => [key, stringify(val)])) as JsonMap; + } + if (value === undefined) return null; + return value as Json; +} + +function emit(status: Status, action: string, data: JsonMap, error: JsonMap | null): void { + process.stdout.write(`${JSON.stringify({ status, action, data: stringify(data), error: stringify(error) }, null, 2)}\n`); +} + +function success(action: string, data: JsonMap): void { + emit("success", action, data, null); +} + +function blocked(action: string, code: string, message: string, next: string, data: JsonMap = {}): void { + emit("blocked", action, data, { code, message, next }); +} + +function fail(action: string, error: unknown): void { + if (error instanceof BlockedError) { + blocked(action, error.code, error.message, error.next, error.data); + process.exit(0); + return; + } + const message = error instanceof Error ? error.message : String(error); + emit("error", action, {}, { code: "ERROR", message, next: "Run doctor/status and inspect the failing check before retrying." }); + process.exitCode = 1; +} + +function parseInteger(value: string | undefined, fallback: number, label: string): number { + if (value === undefined) return fallback; + const parsed = Number(value); + if (!Number.isInteger(parsed) || parsed < 0) throw new Error(`${label} must be a non-negative integer`); + return parsed; +} + +function requireWallet(opts: SharedOptions): string { + if (!opts.wallet) throw new Error("--wallet is required"); + return opts.wallet; +} + +function requireFundingArgs(opts: SharedOptions): { wallet: string; tokenIn: string; tokenOut: string; amountIn: string } { + const wallet = requireWallet(opts); + if (!opts.tokenIn) throw new Error("--token-in is required"); + if (!opts.tokenOut) throw new Error("--token-out is required"); + if (!opts.amountIn) throw new Error("--amount-in is required for v1 funding"); + return { wallet, tokenIn: opts.tokenIn, tokenOut: opts.tokenOut, amountIn: opts.amountIn }; +} + +function checkpointPath(wallet: string): string { + return path.join(STATE_ROOT, `${wallet}.json`); +} + +async function readCheckpoint(wallet: string): Promise { + try { + return JSON.parse(await fs.readFile(checkpointPath(wallet), "utf8")) as Checkpoint; + } catch (error) { + if ((error as NodeJS.ErrnoException).code === "ENOENT") return null; + throw error; + } +} + +async function writeCheckpoint(checkpoint: Checkpoint): Promise { + await fs.mkdir(STATE_ROOT, { recursive: true, mode: 0o700 }); + const next = { ...checkpoint, updatedAt: new Date().toISOString() }; + const finalPath = checkpointPath(checkpoint.wallet); + const tempPath = `${finalPath}.${process.pid}.${Date.now()}.tmp`; + await fs.writeFile(tempPath, `${JSON.stringify(next, null, 2)}\n`, { mode: 0o600 }); + await fs.rename(tempPath, finalPath); + return next; +} + +function newCheckpoint(opts: SharedOptions, plan: JsonMap): Checkpoint { + const { wallet, tokenIn, tokenOut, amountIn } = requireFundingArgs(opts); + const createdAt = new Date().toISOString(); + return { + version: 1, + routeId: crypto.createHash("sha256").update(`${wallet}:${tokenIn}:${tokenOut}:${amountIn}:${createdAt}`).digest("hex").slice(0, 16), + wallet, + mode: opts.mode ?? "one-shot", + tokenIn, + tokenOut, + amountIn, + targetOut: opts.targetOut ?? null, + expectedAmountOut: extractExpectedOutput(plan), + txid: null, + step: "planned", + hiroStatus: null, + handoffLabel: opts.handoffLabel ?? DEFAULT_HANDOFF_LABEL, + createdAt, + updatedAt: createdAt, + nextRequiredAction: "Run funding swap with --confirm=FUND", + }; +} + +function isUnresolved(checkpoint: Checkpoint | null): boolean { + return !!checkpoint && checkpoint.step !== "complete" && checkpoint.step !== "cancelled"; +} + +function fundingRoute(opts: SharedOptions): string { + return `${opts.tokenIn ?? "unknown"}-to-${opts.tokenOut ?? "unknown"}`; +} + +function baseHandoff(opts: SharedOptions, readyAmount: Json | null, routeReady: boolean): JsonMap { + return { + label: opts.handoffLabel ?? DEFAULT_HANDOFF_LABEL, + readyToken: opts.tokenOut ?? null, + readyAmount: routeReady ? readyAmount : null, + routeReady, + }; +} + +function fundingEnvelope(opts: SharedOptions, primitive: JsonMap, extra: JsonMap = {}): JsonMap { + const readyAmount = extractOutputBalance(primitive); + return { + fundingRoute: fundingRoute(opts), + mode: opts.mode ?? "one-shot", + wallet: opts.wallet ?? null, + tokenIn: opts.tokenIn ?? null, + tokenOut: opts.tokenOut ?? null, + amountIn: opts.amountIn ?? null, + expectedAmountOut: extractExpectedOutput(primitive), + routeReady: extra.routeReady ?? false, + handoff: baseHandoff(opts, readyAmount, Boolean(extra.routeReady)), + primitive, + boundaries: { + downstreamWritesPerformed: false, + hodlmmWritePerformed: false, + zestWritePerformed: false, + borrowOrLeveragePerformed: false, + }, + ...extra, + }; +} + +function toCliArgs(opts: SharedOptions, command: "doctor" | "plan" | "run"): string[] { + const args = [command]; + const push = (flag: string, value: string | undefined) => { + if (value !== undefined) args.push(flag, value); + }; + push("--wallet", opts.wallet); + if (command !== "doctor") { + push("--token-in", opts.tokenIn); + push("--token-out", opts.tokenOut); + push("--amount-in", opts.amountIn); + push("--slippage-bps", opts.maxSlippageBps ?? opts.slippageBps); + push("--fee-ustx", opts.feeUstx); + } + push("--min-gas-reserve-ustx", opts.minGasReserveUstx); + push("--mempool-depth-limit", opts.mempoolDepthLimit ?? String(DEFAULT_MEMPOOL_DEPTH_LIMIT)); + push("--wait-seconds", opts.waitSeconds); + if (command === "run") args.push("--confirm", "SWAP"); + return args; +} + +async function runPrimitive(args: string[]): Promise { + const fullArgs = ["run", SWAP_SKILL, ...args]; + const child = spawn("bun", fullArgs, { + cwd: process.cwd(), + env: process.env, + stdio: ["ignore", "pipe", "pipe"], + }); + const stdout: Buffer[] = []; + const stderr: Buffer[] = []; + child.stdout.on("data", (chunk) => stdout.push(Buffer.from(chunk))); + child.stderr.on("data", (chunk) => stderr.push(Buffer.from(chunk))); + const code = await new Promise((resolve) => child.on("close", resolve)); + const out = Buffer.concat(stdout).toString("utf8").trim(); + const err = Buffer.concat(stderr).toString("utf8").trim(); + let parsed: JsonMap | null = null; + if (out) { + try { + parsed = JSON.parse(out) as JsonMap; + } catch { + throw new Error(`bitflow-swap-aggregator returned non-JSON output: ${out.slice(0, 240)}`); + } + } + if (code !== 0 && !parsed) { + throw new Error(`bitflow-swap-aggregator failed with exit ${code}${err ? `: ${err.slice(0, 240)}` : ""}`); + } + if (!parsed) throw new Error("bitflow-swap-aggregator returned empty output"); + return parsed; +} + +async function fetchJson(url: string): Promise { + const response = await fetch(url); + if (!response.ok) { + const body = await response.text().catch(() => ""); + throw new Error(`HTTP ${response.status} from ${url}${body ? `: ${body.slice(0, 180)}` : ""}`); + } + return response.json() as Promise; +} + +async function waitForTx(txid: string, waitSeconds: number): Promise { + const deadline = Date.now() + waitSeconds * 1000; + let last: JsonMap | null = null; + while (Date.now() <= deadline) { + try { + const tx = await fetchJson(`${HIRO_API}/extended/v1/tx/${txid}`); + last = tx; + const status = String(tx.tx_status ?? ""); + if (status === "success" || status === "failed" || status.startsWith("abort")) return tx; + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + if (!message.startsWith("HTTP 404 ")) throw error; + last = { tx_status: "not_indexed", tx_id: txid }; + } + await new Promise((resolve) => setTimeout(resolve, 5_000)); + } + return last; +} + +function walk(value: unknown, predicate: (key: string, value: unknown) => string | null): string | null { + if (!value || typeof value !== "object") return null; + for (const [key, child] of Object.entries(value)) { + const found = predicate(key, child); + if (found) return found; + const nested = walk(child, predicate); + if (nested) return nested; + } + return null; +} + +function extractTxid(value: unknown): string | null { + return walk(value, (key, child) => { + if ((key === "txid" || key === "tx_id" || key === "txId") && typeof child === "string" && /^0x[0-9a-fA-F]{64}$/.test(child)) { + return child; + } + return null; + }); +} + +function extractExpectedOutput(value: unknown): Json | null { + const quote = walk(value, (key, child) => { + if ((key === "quote" || key === "expectedAmountOut") && (typeof child === "string" || typeof child === "number")) return String(child); + return null; + }); + return quote; +} + +function extractOutputBalance(value: unknown): Json | null { + const outputBalance = walk(value, (key, child) => { + if (key === "outputBalance" && (typeof child === "string" || typeof child === "number")) return String(child); + return null; + }); + return outputBalance; +} + +function txProof(txid: string, tx: JsonMap | null): JsonMap { + return { + txid, + explorer: `${EXPLORER}/${txid}?chain=mainnet`, + status: tx?.tx_status ?? "unknown", + sender: tx?.sender_address ?? null, + contract: (tx?.contract_call as JsonMap | undefined)?.contract_id ?? null, + function: (tx?.contract_call as JsonMap | undefined)?.function_name ?? null, + result: (tx?.tx_result as JsonMap | undefined)?.repr ?? null, + postConditionMode: tx?.post_condition_mode ?? null, + postConditionCount: Array.isArray(tx?.post_conditions) ? (tx?.post_conditions as Json[]).length : null, + }; +} + +async function checkHiro(): Promise { + try { + const info = await fetchJson(`${HIRO_API}/v2/info`); + return { ok: true, chainId: info.network_id ?? null }; + } catch (error) { + return { ok: false, error: error instanceof Error ? error.message : String(error) }; + } +} + +async function fileExists(filePath: string): Promise { + return fs.access(filePath).then(() => true, () => false); +} + +async function dependencySignals(): Promise { + return { + bitflowSwapAggregator: await fileExists(SWAP_SKILL), + nonceManagerLocal: await fileExists(path.join("skills", "nonce-manager", "nonce-manager.ts")), + nonceManagerDeclared: true, + noncePolicy: "serialize funding writes with nonce-manager when available; never run overlapping local checkpoints", + }; +} + +async function runDoctor(opts: SharedOptions): Promise { + try { + if (NETWORK !== "mainnet") throw new BlockedError("MAINNET_ONLY", "bitflow-funding-coordinator is mainnet-only.", "Set NETWORK=mainnet."); + const wallet = opts.wallet; + const [hiro, dependencies, checkpoint] = await Promise.all([ + checkHiro(), + dependencySignals(), + wallet ? readCheckpoint(wallet) : Promise.resolve(null), + ]); + let primitive: JsonMap | null = null; + if (wallet && dependencies.bitflowSwapAggregator) { + primitive = await runPrimitive(toCliArgs(opts, "doctor")); + } + success("doctor", { + network: NETWORK, + wallet: wallet ?? null, + hiro, + dependencies, + checkpoint: checkpoint ?? null, + unresolvedCheckpoint: isUnresolved(checkpoint), + primitive, + }); + } catch (error) { + fail("doctor", error); + } +} + +async function runStatus(opts: SharedOptions): Promise { + try { + const wallet = requireWallet(opts); + const [checkpoint, dependencies, primitive] = await Promise.all([ + readCheckpoint(wallet), + dependencySignals(), + runPrimitive(toCliArgs(opts, "doctor")), + ]); + success("status", { + wallet, + checkpoint: checkpoint ?? null, + unresolvedCheckpoint: isUnresolved(checkpoint), + dependencies, + primitive, + }); + } catch (error) { + fail("status", error); + } +} + +async function runPlan(opts: SharedOptions): Promise { + try { + requireFundingArgs(opts); + const primitive = await runPrimitive(toCliArgs(opts, "plan")); + success("plan", fundingEnvelope(opts, primitive, { routeReady: false })); + } catch (error) { + fail("plan", error); + } +} + +async function runFunding(opts: RunOptions): Promise { + try { + const { wallet } = requireFundingArgs(opts); + if (opts.confirm !== CONFIRM_TOKEN) { + throw new BlockedError("CONFIRMATION_REQUIRED", "This write skill requires --confirm=FUND.", "Review plan output and rerun with --confirm=FUND."); + } + const existing = await readCheckpoint(wallet); + if (isUnresolved(existing)) { + throw new BlockedError("UNRESOLVED_CHECKPOINT", "A previous funding checkpoint is unresolved.", "Use resume --txid if a transaction was broadcast, or cancel if the operator has verified no write should continue.", { checkpoint: existing as unknown as JsonMap }); + } + const plan = await runPrimitive(toCliArgs(opts, "plan")); + let checkpoint = await writeCheckpoint(newCheckpoint(opts, plan)); + + const runOpts = { ...opts, waitSeconds: "0" }; + const primitive = await runPrimitive(toCliArgs(runOpts, "run")); + const txid = extractTxid(primitive); + if (!txid) { + throw new BlockedError("PRIMITIVE_TXID_MISSING", "bitflow-swap-aggregator did not return a txid.", "Inspect primitive output and do not retry until broadcast state is understood.", { primitive }); + } + checkpoint = await writeCheckpoint({ + ...checkpoint, + txid, + step: "broadcast", + hiroStatus: "pending", + nextRequiredAction: "Await Hiro tx_status=success", + }); + + const immediateStatus = String((primitive.data as JsonMap | undefined)?.proof && ((primitive.data as JsonMap).proof as JsonMap).status || ""); + const waitSeconds = parseInteger(opts.waitSeconds, DEFAULT_WAIT_SECONDS, "--wait-seconds"); + const mined = immediateStatus === "success" ? null : await waitForTx(txid, waitSeconds); + const proof = immediateStatus === "success" ? ((primitive.data as JsonMap).proof as JsonMap) : txProof(txid, mined); + const status = String(proof.status ?? "unknown"); + + if (status !== "success") { + checkpoint = await writeCheckpoint({ + ...checkpoint, + hiroStatus: status, + nextRequiredAction: "Run resume --txid after Hiro reports tx_status=success", + }); + throw new BlockedError("TX_NOT_CONFIRMED", "Funding txid is recorded but Hiro has not confirmed success.", "Use resume --txid after the transaction confirms; do not rebroadcast blindly.", { checkpoint: checkpoint as unknown as JsonMap, proof }); + } + + checkpoint = await writeCheckpoint({ + ...checkpoint, + step: "complete", + hiroStatus: "success", + nextRequiredAction: "Funding complete; downstream strategy can consume handoff.", + }); + success("run", fundingEnvelope(opts, primitive, { routeReady: true, checkpoint: checkpoint as unknown as JsonMap, proof })); + } catch (error) { + fail("run", error); + } +} + +async function runResume(opts: SharedOptions): Promise { + try { + const wallet = requireWallet(opts); + const checkpoint = await readCheckpoint(wallet); + const txid = opts.txid ?? checkpoint?.txid; + if (!txid) throw new Error("--txid is required when no checkpoint txid exists"); + const mined = await waitForTx(txid, parseInteger(opts.waitSeconds, DEFAULT_WAIT_SECONDS, "--wait-seconds")); + const proof = txProof(txid, mined); + const status = String(proof.status ?? "unknown"); + if (status !== "success") { + throw new BlockedError("TX_NOT_CONFIRMED", "Hiro has not confirmed this funding txid as success.", "Wait for confirmation and rerun resume --txid; do not rebroadcast.", { checkpoint: checkpoint as unknown as JsonMap | null, proof }); + } + const completed = await writeCheckpoint({ + ...(checkpoint ?? { + version: 1, + routeId: crypto.createHash("sha256").update(`${wallet}:${txid}`).digest("hex").slice(0, 16), + wallet, + mode: opts.mode ?? "one-shot", + tokenIn: opts.tokenIn ?? "unknown", + tokenOut: opts.tokenOut ?? "unknown", + amountIn: opts.amountIn ?? null, + targetOut: opts.targetOut ?? null, + expectedAmountOut: null, + createdAt: new Date().toISOString(), + handoffLabel: opts.handoffLabel ?? DEFAULT_HANDOFF_LABEL, + }), + txid, + step: "complete", + hiroStatus: "success", + nextRequiredAction: "Funding complete; downstream strategy can consume handoff.", + }); + success("resume", { + fundingRoute: fundingRoute({ ...opts, tokenIn: completed.tokenIn, tokenOut: completed.tokenOut }), + wallet, + txid, + routeReady: true, + checkpoint: completed as unknown as JsonMap, + proof, + handoff: { + label: completed.handoffLabel, + readyToken: completed.tokenOut, + readyAmount: null, + routeReady: true, + }, + }); + } catch (error) { + fail("resume", error); + } +} + +async function runCancel(opts: SharedOptions): Promise { + try { + const wallet = requireWallet(opts); + const checkpoint = await readCheckpoint(wallet); + if (!checkpoint) { + success("cancel", { wallet, cancelled: false, reason: "No checkpoint exists." }); + return; + } + const cancelled = await writeCheckpoint({ + ...checkpoint, + step: "cancelled", + nextRequiredAction: "Operator cancelled local funding checkpoint after external verification.", + }); + success("cancel", { wallet, cancelled: true, checkpoint: cancelled as unknown as JsonMap }); + } catch (error) { + fail("cancel", error); + } +} + +function addSharedOptions(command: Command): Command { + return command + .option("--wallet ", "wallet that owns the funding source token") + .option("--token-in ", "source token symbol, token ID, or contract ID") + .option("--token-out ", "target token symbol, token ID, or contract ID") + .option("--amount-in ", "human-readable source token amount") + .option("--target-out ", "minimum desired target token output, recorded for handoff") + .option("--max-slippage-bps ", "maximum slippage tolerance in basis points") + .option("--slippage-bps ", "alias for --max-slippage-bps") + .option("--fee-ustx ", "delegated swap transaction fee in micro-STX") + .option("--min-gas-reserve-ustx ", "minimum residual STX after write") + .option("--mempool-depth-limit ", "maximum pending sender transactions; default is 0", String(DEFAULT_MEMPOOL_DEPTH_LIMIT)) + .option("--wait-seconds ", "Hiro confirmation wait window", String(DEFAULT_WAIT_SECONDS)) + .option("--mode ", "funding mode", "one-shot") + .option("--handoff-label