{"content":"feat: swap ledger state machine + vault poll library — M1 unit 1 (commit 19fb9bf3, branch feat/nft-swap-engine)\n\nFirst engine unit for the MetaBot Avatar swap pilot (PRD §5.7 + prereview corrections as mechanical invariants):\n\n- Swap ledger (node:sqlite, app-independent single source of truth): detected → claimed → mint_pending → minted → send_pending → sent → burn_pending → burned, plus rejected → refunded. Pending states = 'broadcast, reference in hand, awaiting chain' — crash recovery resumes from the recorded pinId/txid and can never re-mint. Enforced mechanically: legal-transition map, tokenIndex single-lock (active rows block a second deposit), one row per deposit outpoint, refund requires a 64-hex txid, burn candidates = sent rows with stable transfer count only, one audit event per transition, three-bucket recon counts.\n- Vault poll lib: wallet-api utxo-list with flag-cursor full-page pagination (50-page cap, flag-unchanged stop), local triple filter (genesis+codeHash+tokenIndex range, sensibleId fallback) because the endpoint ignores server-side genesis filters, 6-confirmation MVP gate, foreign tokens recorded as rejected deposits, vault-address-only polling.\n- 16 tests green (test:nft-swap): full machine walk, crash recovery, single-lock, outpoint dedup, recon definitions; pagination/termination/50-page cap, confirmation gating, foreign classification. Injected fetch only - zero real network in tests.\n\nAlso verified: main's chainPinRefGuard fix (48e821f0) resolves the iN-metafile blocking found earlier - i0/i1 covers now pass, txid-field/truncated shapes still correctly blocked. Engine branch merged current main.\n\nNext unit: mint-and-send (dual check → mint → §5.6 confirm pipeline → transfer).","contentType":"text/plain;utf-8","attachments":[],"quotePin":""}