@seedprotocol/publish
v0.6.2
Published
Publish UI components and helpers for Seed Protocol
Downloads
459
Readme
@seedprotocol/publish
Publish UI components and helpers for Seed Protocol.
The publish flow (ConnectButton, etc.) runs ensureEasSchemasForItem before getPublishPayload, which registers EAS schemas and adds naming attestations so EASSCAN displays them. If you build a custom publish flow that calls item.getPublishPayload() directly, you must run schema setup first or use this package's flow.
initPublish() or PublishProvider registers the revocation executor, so item.unpublish() works when the publish package is configured. See docs/ATTESTATION_REVOCATION.md for permanence and UX guidance.
Badges / on-chain vs pending: use @seedprotocol/sdk getSeedPublishState for “any on-chain anchor for this seed”, getPublishPendingDiff for per-property heads missing attestation UIDs, and getItemsData (includeEas, publishedVersionUid vs latestVersionUid) for list rows. When a publish run finishes, observe usePublishProcess(seedLocalId) or the publishProcesses table instead of ad hoc window events.
Setup
uploadApiBaseUrl is required. Provide either rpcUrl or thirdwebClientId (RPC falls back to Thirdweb’s edge when only the client id is set).
Thirdweb is an optional peer. Core publish uses SeedSigner + SeedTxSender (PublishWallet). Import ConnectButton / in-app wallets from @seedprotocol/publish/thirdweb.
Recipe 1: EIP-1193 + EOA (no Thirdweb)
User pays gas. Works with MetaMask / injected wallets.
import {
PublishProvider,
useSeedWallet,
ensureWalletThenPublish,
initPublish,
} from '@seedprotocol/publish'
import { optimismSepolia } from 'viem/chains'
initPublish({
uploadApiBaseUrl: import.meta.env.VITE_UPLOAD_API_BASE_URL,
rpcUrl: import.meta.env.VITE_RPC_URL,
chain: optimismSepolia,
accountMode: 'eoa',
})
function Connect() {
const { status, connect, address } = useSeedWallet()
return (
<button
onClick={() => connect((window as any).ethereum)}
disabled={status === 'connecting'}
>
{address ?? 'Connect wallet'}
</button>
)
}
function App() {
return (
<PublishProvider>
<Connect />
</PublishProvider>
)
}
// Later:
// await ensureWalletThenPublish(item, { dataItemSigner: /* from useSeedWallet().signer */ })Recipe 2: EIP-1193 + permissionless EIP-7702 (gasless, no Thirdweb)
initPublish({
uploadApiBaseUrl: import.meta.env.VITE_UPLOAD_API_BASE_URL,
rpcUrl: import.meta.env.VITE_RPC_URL,
chain: optimismSepolia,
accountMode: 'eip7702',
bundlerUrl: import.meta.env.VITE_BUNDLER_URL,
paymasterUrl: import.meta.env.VITE_PAYMASTER_URL, // optional; defaults to bundlerUrl
})useSeedWallet().connect(provider) upgrades the EOA sender to a permissionless EIP-7702 SeedTxSender when accountMode is eip7702 and bundlerUrl is set. ModularCore / ManagedAccount session-key flows remain Thirdweb-only.
Recipe 3: Thirdweb ConnectButton (batteries-included)
import { initPublish } from '@seedprotocol/publish'
import {
PublishProvider,
ConnectButton,
} from '@seedprotocol/publish/thirdweb'
initPublish({
thirdwebClientId: import.meta.env.VITE_THIRDWEB_CLIENT_ID,
uploadApiBaseUrl: import.meta.env.VITE_UPLOAD_API_BASE_URL,
})
function App() {
return (
<PublishProvider
config={{
thirdwebClientId: import.meta.env.VITE_THIRDWEB_CLIENT_ID,
uploadApiBaseUrl: import.meta.env.VITE_UPLOAD_API_BASE_URL,
}}
>
<ConnectButton />
</PublishProvider>
)
}You can optionally pass queryClient or queryClientRef to customize the Seed QueryClient.
useIntegerLocalIds
When using the new contract that expects uint256 localIdIndex/publishLocalIdIndex instead of string localId/publishLocalId (gas-efficient), set useIntegerLocalIds: true:
<PublishProvider
config={{
thirdwebClientId: '...',
uploadApiBaseUrl: '...',
useIntegerLocalIds: true, // Use integer-based payload for new contract
}}
>
<App />
</PublishProvider>To revert to the old contract (string-based), set useIntegerLocalIds: false or omit the flag. No code changes required beyond config.
Experimental: Arweave bundler (instant uploads)
When using your own gateway with an Arweave bundler, you can enable instant uploads instead of the default reimbursement + chunk upload flow. This is experimental and not yet validated for production.
Memory: Large publishes hold signed DataItems and a single packed batch body in memory briefly. Electron and other Chromium renderers often cap near ~4 GB JS heap. See docs/PUBLISH_MEMORY.md for scaling, publishMode, and path differences (signDataItems vs dataItemSigner).
Set useArweaveBundler: true. The bundler uses the same uploadApiBaseUrl (e.g. ${uploadApiBaseUrl}/upload/batch).
<PublishProvider
config={{
thirdwebClientId: import.meta.env.VITE_THIRDWEB_CLIENT_ID,
uploadApiBaseUrl: import.meta.env.VITE_UPLOAD_API_BASE_URL,
useArweaveBundler: true,
}}
>
<App />
</PublishProvider>When using the bundler, you must provide a signer at publish time via PublishManager.createPublish options:
// Signer passed at publish time (recommended for apps where signer isn't available at startup)
PublishManager.createPublish(item, address, account, {
signDataItems: async (uploads) => {
// Sign with wallet (ArConnect, MetaMask, etc.)
return uploads.map((u) => ({ transaction: { id: '...' }, versionId: u.versionLocalId, modelName: u.itemPropertyName }))
},
})
// Or for backend/scripts with a private key:
PublishManager.createPublish(item, address, account, {
dataItemSigner: myArweaveSigner,
})You can also provide signDataItems or dataItemSigner in the PublishProvider config as a fallback when the signer is available at startup.
Html properties with embedded data:image/...;base64,... (materialization): When useArweaveBundler: true, the publish machine runs the same two-phase flow as L1: phase 1 uploads non-deferred payloads (including materialized Image DataItems), rewrites Html files on disk with Arweave URLs, then phase 2 builds and uploads Html-only DataItems. signDataItems is invoked twice per publish in that scenario (once per phase)—implementations should sign/upload the uploads array they receive each time. The in-process dataItemSigner path performs two HTTP batch uploads to your bundler API. Per-property htmlEmbeddedDataUriPolicy: 'preserve' skips materialization and keeps a single phase.
Arweave upload tags
Add optional tags (e.g. App-Name) on PublishProvider / initPublish config as arweaveUploadTags, and/or per publish via createPublish options. Resolved order: [...configTags, ...perPublishTags], appended after Content-SHA-256 / Content-Type on each upload.
When implementing signDataItems, use upload.tags as the tag list for each DataItem. Avoid rebuilding tags from contentHash / contentType only, or you will drop configured tags.
Publish process history
Local publish runs are stored in SQLite (publish_processes). Useful exports from @seedprotocol/publish:
| Need | API |
|------|-----|
| All runs, newest first (global activity) | usePublishProcesses(), or usePublishProcessesState() if you also need a non-in_progress count in one subscription |
| Runs for one seed only | usePublishProcessesForSeed(seedLocalId), or usePublishProcessesStateForSeed(seedLocalId) |
| Non-active count only | usePublishProcessesNonActiveCount() or usePublishProcessesNonActiveCountForSeed(seedLocalId) |
| Clear finished runs (keep in_progress) app-wide | clearCompletedPublishProcesses() |
| Clear finished runs for one seed only | clearCompletedPublishProcessesForSeed(seedLocalId) |
| Remove one run by row id | deletePublishProcessById(id) |
| Remove many runs by row ids | deletePublishProcessesByIds(ids) |
| Wipe all history for a seed (including in-progress) | deletePublishProcessesForSeed(seedLocalId) — deletes every row for that seed, not a single run |
import {
usePublishProcessesStateForSeed,
clearCompletedPublishProcessesForSeed,
deletePublishProcessById,
} from '@seedprotocol/publish'
// Per-seed history + “clear finished for this seed” without scanning the full table
const { records } = usePublishProcessesStateForSeed(item.seedLocalId)
await clearCompletedPublishProcessesForSeed(item.seedLocalId)
await deletePublishProcessById(runId) // numeric row id from `records`Publish automation (ManagedAccount grants)
Apps can enroll a server-held session key to publish/revoke on a user’s ManagedAccount without taking the user’s root key. Requires modularAccountModuleContract (executor-module-only approvedTargets). See docs/PUBLISH_AUTOMATION.md.
APIs live on @seedprotocol/publish (sidecar attest/revoke) and @seedprotocol/publish/thirdweb (enrollPublishAutomation, revokePublishAutomation, session-key helpers, assertStorageBoundToIdentity).
Modular executor (useModularExecutor) and EIP-7702
When useModularExecutor is enabled, multiPublish is sent from the user’s Thirdweb in-app modular wallet (EIP-7702 execution mode) against their ManagedAccount contract. Before the first on-chain publish, createAttestations runs ensureEip7702ModularAccountReady(), which checks Optimism Sepolia bytecode at the modular wallet address (EIP-7702 delegation / minimal account). If bytecode is still empty and autoDeployEip7702ModularAccount is true (the default when useModularExecutor is on), it calls Thirdweb’s deploySmartAccount bootstrap (no-op if already upgraded). Set autoDeployEip7702ModularAccount: false to surface Eip7702ModularAccountPublishError instead of auto-deploying.
ensureSmartWalletThenPublish: With useModularExecutor, the publish machine’s account and default dataItemSigner come from getConnectedModularAccount() (the modular EIP-7702 in-app wallet), not from resolveSmartWalletForPublish or the activeAccount argument (that parameter is ignored on this path for API compatibility). ensureEip7702ModularAccountReady() runs once before createPublish so EIP-7702 readiness failures surface before the publish actor starts; createAttestations still calls it again (no-op when already deployed).
Routing (important): multiPublish calldata uses the ABI generated from the reference deployment MULTI_PUBLISH_ABI_REFERENCE_ADDRESS_OP_SEPOLIA (0xcd8c… — same hex as the deprecated SEED_PROTOCOL_CONTRACT_ADDRESS_OP_SEPOLIA alias). The transaction to / getContract address is the user’s on-chain publisher: managed account for interactive modular publish (runModularExecutorPublishPrep().managedAddress), or the executor module when an automation session key is detected (routeToExecutorModule). Non-modular publish targets the deployed publisher contract. EOAs (no contract at address) never use multiPublish; the publish machine routes them to direct EAS (createAttestationsDirectToEas). Set useDirectEas: true to force that path even when the publisher is a deployed contract. Receipt parsing uses modularAccountModuleContract when configured, otherwise the managed / publisher address.
Managed account: runModularExecutorPublishPrep() still ensures the EIP-4337 managed publishing contract exists on Optimism Sepolia (and optionally installs the executor module). That is separate from the modular wallet’s EIP-7702 upgrade.
Before the first multiPublish on that path, createAttestations runs ensureModularPublishBootstrap, which:
- Provisions the modular wallet as a session signer on the ManagedAccount (
ensureManagedSignerSessionKey—addSessionKeysigned by the managed EIP-4337 wallet when needed). - Ensures the ManagedAccount EAS pointer matches config (
ensureManagedAccountEasConfigured). - Falls back to
ensureEip7702ModularAccountReadyonly when session-key setup fails andautoDeployEip7702ModularAccountis enabled.
Diagnostic helper: defaultApprovedTargetsForModularPublish(managedAddress) remains exported for apps that build custom permission flows.
Resolved config: Use getPublishConfig() after initPublish / PublishProvider for autoDeployEip7702ModularAccount and other resolved defaults—not only usePublishConfig(), which returns the raw PublishConfig object.
Development
bun install
bun run build