Feat/intuition onchain integration#12
Open
Wieedze wants to merge 47 commits into
Open
Conversation
- CLAUDE.md: layering rules, typing rules, commit rules, phase gates - .claude/agents/: architecture / typing / intuition / docs-writer reviewers - .claude/settings.json: bun-only permissions, deny npm/npx/yarn/pnpm - docs/: architecture, ADRs (ADR-001 layering, ADR-002 bun), env inventory, plan - .env.example: testnet defaults (chain 13579, MultiVault, GraphQL, RPC)
- wagmi + viem: wallet + contract interactions - @rainbow-me/rainbowkit: wallet UI - @tanstack/react-query: GraphQL caching - graphql + graphql-request: Intuition indexer client - remove package-lock.json (bun is the canonical package manager per ADR-002)
- docs/architecture: layering rules (pages -> components -> hooks -> services -> lib) - docs/decisions: ADRs (ADR-001 layering, ADR-002 bun-only) - docs/env: env var inventory - docs/plan: bounty phase plan with commit-level breakdown - .env.example: testnet defaults (chain 13579, MultiVault, GraphQL, RPC) - .gitignore: env files and personal tooling
Single source of truth for environment variables. Validates at module load with developer-friendly diagnostics so a misconfigured .env crashes at boot rather than producing late opaque failures during contract calls or queries. - supports chain 1155 (mainnet) and 13579 (testnet) - enforces 0x-prefixed 40-char hex on VITE_MULTIVAULT_ADDRESS - exposes derived networkName for UI use - src/config/env.ts is the only file allowed to read import.meta.env
The Intuition L3 (mainnet 1155, testnet 13579) is not in viem's built-in chain list, so each chain is declared via defineChain. Canonical RPC and explorer URLs are baked in as defaults; the wagmi config will override the RPC at provider construction using the env-loaded URL. - intuitionMainnet / intuitionTestnet exports - getIntuitionChain(chainId) helper - IntuitionChainId / IntuitionChain types
Sourced from the canonical Intuition V2 contracts. The L3 is not indexed by Etherscan, so the ABI must be declared explicitly via parseAbi. Split into read- and write-only subsets so query-only services don't inadvertently hold a payable function reference. A merged multivaultAbi is exported for the rare consumer that needs both directions on the same contract instance. Covers: cost queries, existence checks, atom/triple data + ID derivation, vault state, preview functions (mandatory before any write), fee queries, config getters, atom-wallet helper, plus the full write surface (createAtoms, createTriples, deposit/redeem with batch variants, approve, claimAtomWalletDepositFees).
Atom IDs and triple IDs share the same EVM representation (bytes32) but are not interchangeable — depositing into a triple vault expecting an atom vault, or vice versa, corrupts protocol state silently. The branded types make the mismatch a compile-time error; the asAtomId / asTripleId constructors validate the bytes32 shape at the I/O boundary. CurveId is branded to prevent mixing with other bigint values (assets, shares, fees). The active curve is fetched once per session via getBondingCurveConfig() and cached. assertNever is the exhaustiveness helper for switches on discriminated unions — adding a new variant without a matching case becomes a compile-time error at the call site.
Stateless typed read layer over the Intuition indexer. The client is built lazily on first call and kept as a module-level singleton for connection reuse. Caching, retries, and React state belong to the consumer hooks (TanStack Query); the service surfaces raw network errors and trusts the indexer schema contract. Initial query surface: - findPredicateAtomsByLabel: locate canonical predicate atoms ordered by usage count (skip TextObject legacy entries per skill convention) - getAtomByTermId: read a single atom by its bytes32 ID, returns null on miss - listTriplesByPredicate: enumerate triples for a predicate, paginated by the caller Additional queries (positions by user, triples by subject, etc.) will land in subsequent commits as the consuming hooks come online.
Wraps the configured pinning endpoint (VITE_IPFS_PIN_ENDPOINT) in a
generic POST/JSON-LD adapter. The deployment platform supplies the
endpoint URL — Pinata, Web3.Storage, self-hosted Helia, or the
Intuition team's service all conform to the same { uri } response
contract.
Per the skill convention, every atom except CAIP-10 blockchain
addresses is pinned first; the resulting ipfs:// URI becomes the
atom's bytes payload via stringToHex(). Plain-string fallbacks
produce legacy TextObject atoms and are intentionally unsupported
here — the on-chain write path must treat a pinning failure as
fatal and abort before issuing any contract call.
Initial schema set covers the three most common atom-type categories:
- pinThing for generic concepts
- pinPerson for people
- pinOrganization for companies / DAOs / projects
Additional schemas (CreativeWork, SoftwareApplication, etc.) land in
follow-up commits as the atom-types map grows.
Reads atomCost, tripleCost, and defaultCurveId from the MultiVault in a single multicall via wagmi's useReadContracts. These constants change only on governance actions, so we cache them indefinitely with staleTime: Infinity and refetch only on explicit invalidation. The return shape is a discriminated union over loading / error / success — consumers must handle each variant before submitting, otherwise the write would either revert (MultiVault_InsufficientAssets) or hit a stale curve. The defaultCurveId is wrapped in the branded CurveId at the I/O boundary so downstream services and hooks cannot accidentally mix it with assets or shares. The hook itself is part of the foundation module: it's defined here but consumed only after the bounty PR introduces the WagmiProvider in App.tsx.
The Intuition indexer exposes pinning as GraphQL mutations on the same
endpoint as the read queries — there is no separate IPFS REST endpoint
to configure. Verified against the canonical sofia-core implementation
and the indexer's generated GraphQL schema (PinThingInput, PinPersonInput,
PinOrganizationInput all returning PinOutput { uri }).
Removes VITE_IPFS_PIN_ENDPOINT from the env loader and .env.example —
deployments now only need the GraphQL endpoint, which is already
configured for queries.
The service surface stays the same (pinThing / pinPerson / pinOrganization
returning a branded ipfs:// URI), so downstream callers are unaffected.
Both mainnet and testnet are registered so the user can switch chains via the wallet UI. The http transport for the active chain (per env) uses the env-configured RPC URL; the inactive chain falls back to its built-in default. No connectors are declared in this config — the RainbowKit provider added by the bounty PR wraps the config and supplies its own connector list, keeping the foundation module agnostic to wallet-UI choices.
The Intuition indexer's Hasura request-transformation template references every pin-mutation field; sending an undefined optional field triggers a transformation error at the gateway. Coerce undefined to '' before issuing the mutation so the wire payload always carries the full shape — same pattern as the canonical sofia-core AtomService. Affects pinThing, pinPerson, pinOrganization.
…tion Per the Intuition skill, TextObject atoms are legacy plain-string predicates and must not be reused — when only TextObject candidates exist, callers should pin a structured replacement instead. The raw findPredicateAtomsByLabel stays for debug/analytics access, but the new pickCanonicalPredicate helper enforces the rule, and resolveCanonicalPredicateByLabel wraps both calls so consumer hooks cannot fall through to a TextObject atom by accident.
The Number(value) as 1155 | 13579 cast is safe because the preceding z.enum guarantees the runtime value is one of the two literals, but the project rule requires every type assertion outside parsing/boundary code to carry a // boundary: comment. Add it inline.
…aultConfig The bounty integration requires a wallet-selection modal. RainbowKit's getDefaultConfig is the canonical wagmi v2 setup that supplies a curated connector list for injected, browser-extension, and mobile wallets out of the box. It requires a WalletConnect Cloud project ID — registered free at https://cloud.walletconnect.com — added to env.ts as a required var and surfaced through env.walletConnectProjectId. The transport policy is unchanged: env-configured RPC for the active chain, default RPC for the inactive chain. ssr: false because Ontology is a client-only Vite SPA.
main.tsx wraps the app with WagmiProvider > QueryClientProvider so wagmi hooks resolve at any depth. RainbowKit's stylesheet is imported once at the entry to keep modal styles loaded before any consumer renders. The RainbowKitProvider itself stays inside App.tsx because the modal theme tracks the user's dark/light preference — kept close to the theme state.
…hetic Replaces the default RainbowKit ConnectButton with a custom wallet manager modeled on intuition-fee-proxy-template's pattern, adapted to Ontology's CSS tokens and h-8 header rhythm. States: - disconnected: outlined Connect pill with wallet icon - wrong network: destructive-tinted pill - connected: round avatar (deterministic HSL gradient from address, or ENS image) opening an inline dropdown with wallet identity, network info, and copy / explorer / disconnect actions App.tsx wraps the existing tree with RainbowKitProvider so the modal theme tracks dark/light mode, and surfaces WalletButton in the header next to the tutorial and theme toggles.
Pure function that classifies an Ontology atom-type ID into the on-chain materialization strategy: - caip10: Ethereum accounts/contracts/tokens (no IPFS pinning; the CAIP-10 URI is the atom data itself) - person: Person, AIAgent (pinned via pinPerson) - organization: Organization, LocalBusiness, Brand (pinned via pinOrganization) - thing: catch-all for everything else (pinned via pinThing) Atom-type IDs not in the explicit sets fall through to 'thing' by default — surface this file in code review when adding new types to src/data/atom-types.ts to opt into a more specific classification. Also exposes encodeAtomData(uri) which centralizes the stringToHex encoding so consumer hooks never reach into viem primitives directly.
Full on-chain submission flow for a single claim, exposed as a hook that returns a discriminated state machine: idle -> preparing -> creating-atoms -> creating-triple -> confirmed (or -> error from any phase). Steps: 1. Pin subject + object atoms in parallel via the IPFS service (or encode CAIP-10 URI directly for blockchain atom types). 2. Resolve the canonical predicate atom by label via resolveCanonicalPredicateByLabel; pin a Thing if no canonical exists yet. 3. Compute atom IDs with calculateAtomId and check existence with isTermCreated. Skip already-created atoms. 4. Batch-create missing atoms in a single createAtoms tx; await confirmation. 5. Create the triple with createTriples (single-element arrays); await confirmation. 6. Compute and surface the resulting tripleId via calculateTripleId so the UI can link to the explorer or seed local history. isReady gates the submit button on (wallet connected) AND (Intuition session loaded), so consumers never submit before atomCost / tripleCost are known.
Convert the service layer to injectable classes so business logic is fully decoupled from React and from infrastructure clients. Module-level mutable singletons (`let cachedClient`) are gone — each service now receives its dependencies through the constructor and is testable in isolation without monkey-patching globals. New surface: - IndexerService wraps the GraphQL read queries (predicate resolution, atom lookup, triple listing). Canonical predicate resolution is the only public path to predicate IDs, enforcing the no-TextObject rule at the API boundary instead of by convention. - PinningService wraps the pinThing/Person/Organization mutations on the same GraphQL endpoint, sharing the underlying client with the indexer. - MultiVaultReadService and MultiVaultWriteService split the contract surface so a read-only consumer never accidentally holds a wallet- bound instance. Both services own their respective ABI subset, and the write service awaits transaction receipts internally — callers treat a resolved promise as "mined". - ClaimSubmissionService is the new orchestrator: it owns the seven- step submission pipeline (pin atoms, resolve canonical predicate, compute IDs, existence-check, batch-create missing atoms, create triple, derive triple ID). All the business decisions that used to leak into the hook now live here behind a single `submit(...)` call that emits phase events for UI progress and returns a typed result. - factory.ts is the composition root: the only place in the app that knows how to wire the service tree from raw clients. Tests construct services directly with mocks; production code calls the factory. The codebase's `erasableSyntaxOnly: true` setting forbids parameter properties, so each class declares its fields explicitly and assigns them in the body of the constructor. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
The previous implementation owned the full submission pipeline inline: GraphQL queries, IPFS pinning, contract reads/writes, ID derivation, existence checks, batch math. That made the hook 280 lines, mixed business logic with React state, and forced anyone writing a related flow (batch builder, retry, edit) to duplicate the same plumbing. Move all of that into ClaimSubmissionService and have the hook simply forward to it. The hook now: - Reads connection state and protocol session from wagmi/TanStack. - Memoizes a service tree built from the current public/wallet client. - Calls services.claimSubmission.submit(draft, context, onPhase) and maps the phase callbacks + terminal result onto the existing SubmissionState discriminated union. The public API (submit, reset, state, isReady) is unchanged, so ClaimBuilder and any future consumer keep working without edits. No more `walletClient.writeContract` or contract ABI imports in the hook file — those moved to MultiVaultWriteService. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
… shape
wagmi v2's useBalance returns { decimals, symbol, value: bigint } and
no longer exposes a pre-formatted `formatted` string. The dropdown
relied on `balance.formatted`, which crashes the type-check and would
render `undefined` at runtime. Format on the fly with viem's
formatUnits before applying the four-decimal display rule.
Bundles a small UX addition that surfaces the chain switcher inside
the dropdown via openChainModal, so a user on the wrong network can
recover without leaving the connected state.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
…onfig The original chain definitions used a /http suffix on RPC URLs and 'tTRUST' as the testnet symbol — taken from an older skill revision. This caused two issues: - wagmi useBalance returned an unparseable balance for users on either network (NaN display in the wallet UI) - the wallet-installed chain disagreed with our config (wallets canonically show TRUST as the symbol on both networks) Cross-checked against intuition-fee-proxy-template/packages/sdk/src/chains.ts and aligned: - Drop /http suffix on RPC URLs (mainnet and testnet) - Use TRUST as native-currency symbol on both networks - Register Multicall3 deployment (0xcA11...CA11) so wagmi can batch read calls efficiently - Add public RPC URL alongside default for viem fallback Wagmi config simplified accordingly: no manual transports override — getDefaultConfig now uses each chain's rpcUrls.default.http[0] which matches the env value. Wallets that already added the chain with the old config need to delete-and-re-add for the symbol/RPC to refresh.
The session-snapshot shape (atomCost, tripleCost, defaultCurveId) is consumed by both the hook (which fetches it via wagmi) and the ClaimSubmissionService (which uses the values to compute fees). Moving the interface to types.ts lets the service layer reference the canonical shape without depending on a hook — keeps services React-free and unblocks the orchestrator's constructor injection. The hook re-exports the type so existing imports are not broken.
Adds the read-side primitives the visualization views need to start
consuming on-chain data with a clean fallback path:
- IndexerService.listAtoms({ limit, offset }) — recent atoms paginated
- IndexerService.listRecentTriples({ limit, offset }) — recent triples
with subject/predicate/object atoms joined inline (single GraphQL
round-trip yields labeled edges)
- JoinedTripleRecord type for the joined triple shape
- useIndexer — lightweight read-only React-side wiring (no wallet
client; useable in any context that just needs to query the indexer)
- useLiveAtoms / useLiveTriples — TanStack Query wrappers exposing
the raw query handle so consumers can branch on isLoading/error and
fall back to static seed data when the indexer is unreachable or
returns an empty page
These hooks are the foundation for B3b/c/d (graph/tree/matrix live
data); each consuming view will merge the live records with its
existing static fallback so the UI stays usable offline.
- useLiveTriples fetches the 200 most recent triples from the indexer - Counts aggregated by (subject.type, object.type) pair, with a null-guard skipping triples whose joined atom rows have been pruned - Edges with live triples get an accent-tinted stroke and a 2x thickness boost so they stand out in the dense schema graph - Status pill in the header surfaces loading / indexer error / live count, hidden behind the type-filter highlight when active JoinedTripleRecord updated to mark subject/predicate/object as nullable, matching the indexer schema.
- useLiveAtoms fetches the 200 most recent atoms from the indexer - Counts aggregated per atom type, surfaced as a 'Type · N' suffix on the radial tree labels — preserves the schema view while making on-chain volume visible per type at a glance - Status pill in the header shows loading / indexer error / total live atoms count
A new D3 force-directed graph that complements the schema relationship graph by visualizing actual on-chain atoms as nodes and triples as directed edges: - Nodes: each unique atom from the 100 most recent triples, colored by its atom-type category (Person/Thing/Organization/etc.) with a neutral fallback for types outside the static palette (e.g. legacy TextObject atoms) - Edges: each triple as a directed link from subject to object with an accent-tinted stroke and arrowhead; hovering an edge surfaces the predicate label as a tooltip so the canvas stays uncluttered - Same UX primitives as the schema graph: force simulation, zoom, pan, fullscreen, drag-to-reposition, hover-to-highlight neighbors Self-contained: no props, pulls live data via TanStack Query and gracefully shows an empty-state hint when the indexer has no triples to render. Mounted on the Explorer page below the schema views so reviewers see the instance-level knowledge graph without leaving the landing route.
ClaimSubmissionService.submitBatch() handles N drafts in exactly two transactions regardless of size — the design createAtoms and createTriples were built for. Atoms are deduplicated across drafts so a label appearing in three claims gets pinned and registered once. Flow: 1. Dedupe (value, type) atom specs across all drafts 2. Pin all unique atoms in parallel (or build CAIP-10 URI) 3. Resolve unique predicates (canonical from indexer, or pin a Thing) 4. Compute atom IDs and run existence checks in parallel 5. ONE createAtoms tx for every previously-uncreated atom 6. ONE createTriples tx for every triple via parallel arrays useSubmitBatch React binding mirrors useSubmitClaim's state machine so the BatchBuilder UI reuses the same status-panel pattern. The BatchBuilder gains a 'Publish all onchain (N)' outlined accent button with a tooltip explaining the 2-tx contract when disabled. Both submission hooks invalidate the 'intuition' query family on confirmed so the live graph and tree views pick up the freshly published atoms/triples without waiting for staleTime to expire. Verified onchain testnet tx: - Atoms: 0xb3d427...463740 - Triples: 0x617272...82b171 (2 claims: sofia employs sam + sofia employs max)
Splits the live data section into a 2/3 + 1/3 grid with both cards at a fixed height so they match exactly and the graph never inherits runaway height from the recent-claims sibling. - LiveInstanceGraph accepts selectedAtomId / onSelectAtom props and surfaces a persistent highlight (enlarged node, accent ring, dimmed neighborhood) driven externally - RecentLiveClaimsCard mirrors the selection: each row's subject and object labels are buttons that toggle selection, with an accent background on the active row - Selection state lifted to home.tsx so clicking an atom node in the graph highlights its row in the list, and tapping a label in the list focuses its node in the graph Stacks vertically below the lg breakpoint.
Surfaces the most recent on-chain claims at the top of the matrix
listed by their resolved atom labels (subject — predicate — object)
so users see published claims instance-level rather than only the
schema combinations beneath.
- useLiveTriples({ limit: 50 }) + null-guard on the joined atom
relations (the indexer marks them nullable when an atom row has
been pruned)
- Recent claims section: top 5 triples with type-colored labels,
hidden when the indexer is empty so the schema layout stays clean
- LiveTriplesPill header status mirrors the graph and tree patterns
- liveAtomColor falls back to a neutral shade for types outside our
static palette (e.g. legacy TextObject rows)
When a sibling component (the recent-claims card) drives a selection, the graph now animates an affine transform that lands the highlighted node at the center of the canvas at 1.6x zoom — drives the click-from-recent-claims pivot UX. Node positions are read from a ref so the recentre effect doesn't re-fire on every simulation tick. The 'data-edge-id' attribute is kept as to satisfy noUnusedParameters.
Surfaces real on-chain claims for the selected subject type as inspiration in the claim-builder examples list. Replaces direct imports of the static EXAMPLE_CLAIMS seed with a useLiveExamples hook that: - Queries IndexerService.listTriplesBySubjectType for recent triples matching the selected atom type - Filters out triples whose predicate label maps to no known static predicate (so the form's validation rules and selector still apply on click) - Deduplicates by (subject, predicate, object) tuple and caps at the caller-supplied limit (3 by default) - Falls back to the curated EXAMPLE_CLAIMS seed when live data is unavailable, still loading, or empty for the selected type — keeps the UI usable offline and meaningful for atom types without on-chain activity yet The static seed stays in /src/data/example-claims.ts as the offline fallback; consumers no longer reach into it directly.
…ergence Every nodes update used to bring up to a few seconds of visible 'collision dance' as the layout settled — newly-spawned nodes start at the origin and the force simulation needs ~300 ticks to converge. A 'See on the graph' pivot landing during that window read stale coordinates, so the camera focused on the wrong position. Switch the simulation to a stop+manual-tick model: build the forces, then run 300 ticks in memory before letting D3 attach to the DOM. Nodes appear at their converged coordinates on first render — no animated settling. A small alpha=0.05 restart at the end keeps drag and hover interactions feeling alive without re-energizing the whole graph. The zoom-to-selected effect can now apply immediately (no settle delay) because target coordinates are stable as soon as the node shows up in the nodes array.
…data refreshes The simulation rebuilds on every nodes change (data refetch), and each rebuild places nodes at slightly shifted converged coordinates once a freshly-published triple enters the dataset. The previously saved camera transform pointed at the old coordinates, so the selected atom drifted off-center after each invalidation cycle. The recenter effect now re-applies the zoom on every (selection, nodes) change while a selection is active. The first focus animates so the user sees the camera pivot; subsequent re-applies snap silently to the new converged coordinates, so the lock feels perfectly stable across refetches. The camera releases as soon as the user clears the selection (clicking the active row toggles it off).
After a successful publish, the claim preview was lingering with its three action buttons (Save / Add to Batch / Publish onchain), visually inviting a duplicate publish. The submission status panel below already carries the success message + tx links + 'See on the graph' affordance, so the form values are no longer useful — clear them once onchain.state.status flips to 'confirmed' so the next claim starts on a fresh canvas.
…imit ceiling The status pills on the four schema/live cards (Entity Hierarchy, Entity Relationship, Live Knowledge Graph, Recent claims, Entity Matrix) advertised numbers like '200 ONCHAIN' that were really just our hardcoded query window — not the actual on-chain total. They suggested authoritative counts the app couldn't deliver, so they read as noise. - Remove LiveAtomsPill, LiveStatusPill, LiveTriplesPill, and the two StatusBadge variants. The card titles already carry the context; further numbers belong only where they're accurate. - Drop the now-dead total/count helper variables that fed those badges. - Raise every useLive* limit (atoms, triples, triple counts) from 50/100/200 to 5000 — high enough that the indexer's current data fits unpaginated, so the visualizations reflect everything it has rather than an arbitrary head.
Backs the matrix's per-row live badge with a single GraphQL fetch over listRecentTriples, aggregated client-side by the (subjectType, objectType) key. Matches the consumer wiring already merged in claim-matrix.tsx so the build no longer references a missing module. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
… panel
The single 'Examples' list mixed authored prompts (Billy, Alice Johnson)
with on-chain triples, which was confusing and made it easy to publish a
duplicate by clicking what looked like a template. Restructure into:
- 'Examples' — always the curated static seed, clickable to fill the form.
- 'Related onchain' — read-only list of real triples for the selected
subject type, surfaced so the user can see what already exists and
author a complementary claim instead of duplicating one.
Hardens the indexer query at the same time: switching from the unreliable
nested atom_type filter to a wider listRecentTriples + client-side filter
so the live list isn't silently empty for atom types the indexer's nested
filter rejects.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
…sult Re-publishing a claim that was already onchain reverts with MultiVault_TripleExists, which MetaMask renders as 'unknown gas fees' because gas estimation fails before signing — confusing for both the single-claim and batch flows. Compute the triple ID up-front and skip createTriples for any triple that already exists. For batches, filter the parallel arrays so the single createTriples call only carries brand-new triples (no-op when every entry duplicates an existing one). Surface subjectAtomId / predicateAtomId / objectAtomId on the submission result so the UI can drive a graph selection or pivot once a claim confirms — used by the upcoming 'See on graph' affordance. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
A successful batch confirmation showed two opaque tx hashes and nothing about which claims were actually published. Render a numbered list of the submitted drafts (subject — predicate — object, with the same color coding as the staging list) and a 'See on graph' button per row that selects the subject in the live graph and scrolls the section into view. Snapshot the batch entries at submit time so the labels persist even if the user empties or edits the staging list afterwards. On Dismiss after a confirmed batch — and on Clear batch — also wipe the submission state machine and the snapshot so the next composition starts from a clean slate instead of inheriting a stale confirmation card. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
A batch with two identical (subject, predicate, object) tuples writes to the same onchain triple slot twice — the second createTriples call reverts. Reject duplicates upstream in addToBatch (returns false on match) and surface a 1.8s inline notice in the claim builder so the user understands why their click had no effect. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Replace the '0x' sentinel for skipped createTriples with `Hex | undefined` on `ClaimSubmissionResult.tripleTxHash` so consumers can distinguish a real hash from a no-op. Fix `submitBatch` to populate `subjectAtomId`, `predicateAtomId`, and `objectAtomId` per draft (was missing required fields, hidden by the length-1 shortcut workaround which is now removed). Drop the dead defensive guard in `useSubmitBatch` and propagate the nullable `tripleTxHash` through the UI status panels. Remove unused exports: `multivaultAbi` (merged), `getIntuitionChain` / `intuitionChains` / `IntuitionChain` / `IntuitionChainId`, indexer methods `getAtomByTermId` / `listTriplesByPredicate` / `listTriplesBySubjectType`, the public `findPredicateAtomsByLabel` (inlined into its sole caller), and `assertNever`. Simplify the null-guard ladders in `useLiveExamples` and `useLiveTripleCounts`, and move the orphan Hasura-coercion comment in `PinningService` into the class JSDoc.
feat(data-migration): live indexer-driven views + post-publish UX
|
The preview deployment for Ontology is ready. 🟢 Open Preview | Open Build Logs | Open Application Logs Last updated at: 2026-04-30 13:02:01 CET |
When the pre-flight isTermCreated check finds the triple already lives
on-chain, the service short-circuits without calling createTriples — but
the UI still rendered the generic 'published onchain' confirmation with
no way to tell that nothing actually happened in this transaction. Make
the case explicit:
- Single submit: panel title flips to 'Claim already onchain' and a
short note explains no new transaction was needed. The 'See on the
graph' affordance still works because the triple is genuinely
on-chain.
- Batch: the per-draft result list marks each pre-existing triple
with an amber 'already onchain' badge alongside its 'See on graph'
button. When the entire batch was duplicates the panel title flips
to 'All claims were already onchain'; when only some drafts were
duplicates a '(some were already onchain)' suffix is appended.
The service now returns a per-draft tripleTxHash on the batch path
(undefined for already-existed entries, the shared createTriples hash
otherwise) so the UI can render that distinction without a second
existence query. The hook picks the shared hash via the first
non-undefined entry rather than results[0], which would have been
undefined whenever the first draft happened to be a duplicate.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Mission
Integrate the Intuition Protocol into the Ontology app so users can
publish their claims onchain (atoms + triples) and explore real
indexer-driven data
1155, MultiVault0x6E35cF57A41fA15eA0EaE9C33e751b01A784Fe7e), tested on chain13579.What this PR ships
1. Wallet connection
(mainnet + testnet).
WalletButtonmatching the Ontology aesthetic (deterministicgradient avatar, balance via
useBalance, clickable network row).config/env.ts.2. Onchain claim submission
ClaimSubmissionService.submitand.submitBatchorchestrate the fullflow: pin (IPFS via GraphQL
pinThing/pinPerson/pinOrganization)→ resolve canonical predicate → existence check → ONE
createAtoms→ONE
createTriples.isTermCreatedfor both atoms and triples to avoid the"unknown gas fees" revert when a term already exists onchain.
3. Live indexer-driven views
LiveInstanceGraph: D3 force-directed instance graph from the indexer,pre-warmed for instant convergence, camera locked on selected atom.
RecentLiveClaimsCard: side-by-side with the graph, click-syncselection in both directions.
claim-matrix: per-row live triple count for every(subject, object)pair.
relationship-graph,atom-tree: counts + accent edges from live data.claim-builderexamples: split static templates from a read-onlyRelated onchainpanel so users see what already exists and craft acomplementary claim.
4. Post-publish UX
See on graphbuttons that pivot the live graph to the published subject.
links + the "See on the graph" affordance.
addToBatchrejects identical(subject, predicate, object)triples with an inline feedback (1.8s).
DismissandClear batchclean both the staging list and theconfirmation card.
Architecture
services/intuition/: pure TS, no React. Class-based with constructorDI (
IndexerService,PinningService,MultiVaultReadService,MultiVaultWriteService,ClaimSubmissionService).hooks/intuition/: pure orchestration over services(
useSubmitClaim,useSubmitBatch,useIntuitionSession,useLiveExamples,useLiveTripleCounts,useLiveAtoms,useLiveTriples).AtomId,TripleId,CurveId) so opaque bytes32 IDscan't be confused at call sites.
SubmissionState,BatchSubmissionState).config/env.ts) with zod validation; noimport.meta.envoutside it.See
docs/architecture.md,docs/decisions.md,docs/env.md,docs/plan.md.Onchain proofs (testnet, chain 13579)
<0x…><0x…><0x…><0x…><tripleId 0x…>Explorer: https://testnet.explorer.intuition.systems
Test plan
bun install && bun run build && bun run lint— clean..envfrom.env.example(chain id, RPC, GraphQL, MultiVaultaddress, WalletConnect project id).
and the live graph picks up the new triple within ~12s.
popups, per-triple summary appears, "See on graph" focuses the
subject atom.
with "This claim is already in the batch."
popup.
RecentLiveClaimsCard— graph centers on it.What's intentionally out of scope
Glossarypage is left untouched (different intent: educationalcontent, not onchain claims).