Seed Protocol
@seedprotocol/react

Publishing and EAS sync

React hooks for publishing an item (usePublishItem) and for watching the SDK's EAS sync runs (useEasSyncProcesses and related hooks).

usePublishItem wraps item.publish(), and the useEasSyncProcesses* hooks watch the SDK's EAS sync runs. Publishing itself is done by the separate @seedprotocol/publish package, on the OP Sepolia testnet: item.publish(), and so usePublishItem, works only once that package's initPublish() has run.

import {
  usePublishItem,
  useEasSyncProcesses,
  useEasSyncProcessesState,
  useEasSyncProcessesNonActiveCount,
  useEasSyncProcessById,
} from '@seedprotocol/react'
import type { EasSyncProcessRecord } from '@seedprotocol/react'

usePublishItem

Calls item.publish(options) and reports whether the publish is running and how it failed.

function usePublishItem(): {
  publishItem: (item: Item<any> | undefined, options?: ItemPublishOptions) => void
  isLoading: boolean
  error: Error | null
  resetError: () => void
}

The return type isn't exported. ItemPublishOptions is exported from @seedprotocol/sdk.

item.publish() hands the item to the publish executor that @seedprotocol/publish registers in initPublish() (or <PublishProvider config>). That executor runs a full publish, Arweave uploads and then EAS attestations, with the publish wallet. item.publish() lists what it rejects with before the publish starts, and item.publish() and publishItem how a publish fails.

Returns

FieldTypeDescription
publishItem(item: Item<any> | undefined, options?: ItemPublishOptions) => voidStarts item.publish(options) and returns at once. Does nothing for undefined. With @seedprotocol/publish, options are its CreatePublishOptions (signers, publishMode, tags, onPublished, maxUploadBytes, confirmUpload).
isLoadingbooleantrue while the item's state machine is in its publishing state, which lasts until the publish has finished or failed. It stays false when item.publish() rejects before the publish starts.
errorError | nullWhat item.publish() rejected with. Cleared when a new publish starts.
resetError() => voidSets error back to null.

error keeps the class and code of what item.publish() rejected with, so match with instanceof or code, not the message. A ManagedAccountPublishError with useModularExecutor arrives as itself, and so do UploadTooLargeError and UploadNeedsConfirmError from @seedprotocol/publish, with the upload figures in estimate. When the user agrees to the size in an UploadNeedsConfirmError, call publishItem(item, { confirmUpload: true }). Any other publish that fails after it started rejects with an Error whose message names the step, Publish of <id> failed at <step>: …, and whose cause is that step's own error, such as a PublishNotAuthorizedError at authorizing.

publishItem returns nothing to await. Follow isLoading and error, or call item.publish() yourself and await it. For the step-by-step status of a running publish, use publish's useItemPublishStatus.

Unless options has a dataItemSigner, the publish signs its data items with the publish wallet's data item signer, and ConnectButton registers one in every setup: the in-app wallet with useModularExecutor, and otherwise the smart wallet's admin EOA, or a plain in-app or injected wallet's own key. A wallet you register with setPublishWallet uses its dataItemSigner, or its signer when it isn't kind: 'smart'; a smart account with neither falls back to the config's dataItemSigner, and without that an item with Html, Image or File values fails at creatingArweaveDataItems. See Who signs.

publishItem takes the Item class, while useItem returns IItem. Cast with item as Item<any>.

Example

import type { Item } from '@seedprotocol/sdk'
import { useItem, usePublishItem } from '@seedprotocol/react'

function PublishButton({ seedLocalId }: { seedLocalId: string }) {
  const { item } = useItem({ modelName: 'Post', seedLocalId })
  const { publishItem, isLoading, error, resetError } = usePublishItem()

  return (
    <>
      <button
        disabled={!item || isLoading}
        onClick={() => {
          resetError()
          publishItem(item as Item<any> | undefined)
        }}
      >
        {isLoading ? 'Publishing…' : 'Publish'}
      </button>
      {error && <p role="alert">{error.message}</p>}
    </>
  )
}

EAS sync processes

The SDK pulls attestations from EAS into the local database in sync runs. A run starts when you call client.syncFromEas(), after the user's addresses change (when the client is configured to sync on address change), at startup once the client has found its models' schemas on EAS, and when an automatic sync that failed is retried after the device comes back online. Requests that arrive during a run are merged into one next run. Each run writes a row to the eas_sync_processes table when it starts and updates it when it finishes. The hooks below watch that table, so you can show sync progress, a history, or a "last sync failed" banner.

Rows live in the local database, so they survive reloads and are shared by every tab of the app.

The hooks can mount before the client is ready, as children of SeedClientGate do. Until then useEasSyncProcesses, useEasSyncProcessesNonActiveCount and both fields of useEasSyncProcessesState are undefined, and useEasSyncProcessById with an id reports isLoading: true. Syncing across devices has an example.

EasSyncProcessRecord

One row of eas_sync_processes. The type is the SDK's EasSyncProcessType.

FieldTypeDescription
idnumberAuto-incrementing row ID.
statusstring'in_progress', 'completed' or 'failed'. A row left in_progress by a closed tab stays so until the next run starts, which marks it failed with Interrupted: the sync stopped before it finished (tab closed?).
startedAtnumberWhen the run started (ms since epoch).
completedAtnumber | nullWhen it finished.
requestPayloadstringJSON: what asked for the run (source: client_api, address_change, models_init or retry; older rows can also hold event_bus, which the SDK no longer writes), the correlation IDs of the requests it serves, and its options.
errorMessagestring | nullThe failure message, cut to 500 characters.
errorDetailsstring | nullThe failure's stack trace, cut to 2,000 characters.
persistedSnapshotstringJSON: the run's last recorded phase.
createdAtnumber | nullRow creation time.
updatedAtnumber | nullLast update time.

useEasSyncProcesses

Every sync run, newest first.

function useEasSyncProcesses(): EasSyncProcessRecord[] | undefined

Returns the rows as a bare array, or undefined until the first result. It re-renders whenever a row is added or changes.

import { useEasSyncProcesses } from '@seedprotocol/react'

function SyncHistory() {
  const runs = useEasSyncProcesses()
  if (!runs) return null

  return (
    <ul>
      {runs.slice(0, 5).map((run) => (
        <li key={run.id}>
          {new Date(run.startedAt).toLocaleString()}: {run.status}
          {run.errorMessage ? ` (${run.errorMessage})` : ''}
        </li>
      ))}
    </ul>
  )
}

useEasSyncProcessesState

The same rows plus the number of runs that aren't in progress, from one subscription. Use it when a component needs both.

function useEasSyncProcessesState(): {
  records: EasSyncProcessRecord[] | undefined
  nonActiveCount: number | undefined
}
FieldTypeDescription
recordsEasSyncProcessRecord[] | undefinedAs useEasSyncProcesses.
nonActiveCountnumber | undefinedRows whose status isn't 'in_progress' (completed plus failed). undefined until the first result.
import { useEasSyncProcessesState } from '@seedprotocol/react'

function SyncBadge() {
  const { records } = useEasSyncProcessesState()
  const running = records?.some((r) => r.status === 'in_progress') ?? false
  const lastFailed = records?.[0]?.status === 'failed'

  if (running) return <span>Syncing…</span>
  if (lastFailed) return <span role="alert">Last sync failed</span>
  return null
}

useEasSyncProcessesNonActiveCount

The nonActiveCount of useEasSyncProcessesState on its own, for a "clear history" button's label, for example.

function useEasSyncProcessesNonActiveCount(): number | undefined

It still subscribes to every row, so it re-renders whenever any row changes.

useEasSyncProcessById

One sync run by its id.

function useEasSyncProcessById(id: number | undefined): {
  record: EasSyncProcessRecord | null
  isLoading: boolean
}
FieldTypeDescription
recordEasSyncProcessRecord | nullThe row, or null while loading or when no row has that id.
isLoadingbooleantrue until the first result, including while the client isn't ready. false when id is undefined.

When id is undefined, the hook runs no query and returns { record: null, isLoading: false }, so check id before showing "not found".

import { useEasSyncProcessById } from '@seedprotocol/react'

function SyncRunDetail({ id }: { id?: number }) {
  const { record, isLoading } = useEasSyncProcessById(id)

  if (id === undefined) return <p>Select a sync run</p>
  if (isLoading) return <p>Loading…</p>
  if (!record) return <p>No sync run with ID {id}</p>

  return (
    <dl>
      <dt>Status</dt>
      <dd>{record.status}</dd>
      {record.errorMessage && (
        <>
          <dt>Error</dt>
          <dd>{record.errorMessage}</dd>
        </>
      )}
    </dl>
  )
}

On this page