diff --git a/agent-node/src/chainSubmitter.js b/agent-node/src/chainSubmitter.js index 7c7c45c..61234ca 100644 --- a/agent-node/src/chainSubmitter.js +++ b/agent-node/src/chainSubmitter.js @@ -69,6 +69,12 @@ function getConnectedWallet(chainId) { // ─── Public API ────────────────────────────────────────────────────────────── +// Serializes all on-chain submissions from the agent signer. Concurrent +// extensions (e.g. one PR fanning out to multiple streams on the same repo) +// otherwise grab the same account nonce and one tx fails with "nonce too low". +// Submissions are infrequent, so a single global queue is simplest and safe. +let _submitChain = Promise.resolve(); + /** * Submit an ExtensionVoucher on-chain. * @@ -82,7 +88,15 @@ function getConnectedWallet(chainId) { * * @returns {Promise<{ txHash: string, blockNumber: number, gasUsed: string, chainId: number }>} */ -export async function submitExtension({ +export async function submitExtension(params) { + // Queue behind any in-flight submission so account nonces never collide. + const run = _submitChain.then(() => _submitExtension(params)); + // Keep the queue alive whether this one succeeds or fails. + _submitChain = run.then(() => {}, () => {}); + return run; +} + +async function _submitExtension({ streamId, extensionDurationSeconds, nonce, diff --git a/frontend/src/hooks/useProfile.js b/frontend/src/hooks/useProfile.js index 9778db6..86cf4ce 100644 --- a/frontend/src/hooks/useProfile.js +++ b/frontend/src/hooks/useProfile.js @@ -3,6 +3,18 @@ import { useState, useEffect, useRef, useCallback } from 'react'; const AGENT_URL = import.meta.env.VITE_AGENT_URL ?? 'http://localhost:3000'; const CACHE_KEY = addr => `cronstream_profile_${addr?.toLowerCase()}`; +/** + * A profile is "complete" only when every required field is present. A partial + * save (e.g. the onboarding 401 bug) can leave a profile with a role but no + * username, which has no Settings field to fix — so we route such profiles back + * through Setup to collect what's missing. + */ +export function isProfileComplete(p) { + if (!p || !p.role || !p.username || !p.name) return false; + if (p.role === 'contractor' && !p.github) return false; + return true; +} + // ─── Module-level deduplication ────────────────────────────────────────────── // Multiple components (AppShell, Dashboard, CreateStreamModal, etc.) all call // useProfile(address) independently. Without deduplication each mount fires its @@ -170,6 +182,9 @@ export function useProfile(address) { }; setProfile(optimistic); localStorage.setItem(CACHE_KEY(address), JSON.stringify(optimistic)); + // Notify other mounted components (AppShell, dashboard) immediately so the + // role/username reflect without waiting for the server round-trip or a refresh. + notifyListeners(address, optimistic); // Bust the memory cache so next mount re-fetches fresh data invalidateCache(address); @@ -205,7 +220,7 @@ export function useProfile(address) { if (address) { invalidateCache(address); fetchProfile(address); } } - return { profile, saveProfile, loading, synced, hasProfile: !!profile, refreshProfile }; + return { profile, saveProfile, loading, synced, hasProfile: !!profile, profileComplete: isProfileComplete(profile), refreshProfile }; } function _shortAddr(addr) { diff --git a/frontend/src/pages/Connect.jsx b/frontend/src/pages/Connect.jsx index 0425169..c77a0a9 100644 --- a/frontend/src/pages/Connect.jsx +++ b/frontend/src/pages/Connect.jsx @@ -7,13 +7,13 @@ import { useProfile } from '../hooks/useProfile'; export default function Connect() { const navigate = useNavigate(); const { address, isConnected } = useAccount(); - const { hasProfile } = useProfile(address); + const { profileComplete } = useProfile(address); useEffect(() => { if (isConnected) { - navigate(hasProfile ? '/app/dashboard' : '/app/setup', { replace: true }); + navigate(profileComplete ? '/app/dashboard' : '/app/setup', { replace: true }); } - }, [isConnected, hasProfile, navigate]); + }, [isConnected, profileComplete, navigate]); return (
Tell us how you'll use CronStream.
++ {completing ? 'A required detail is missing. Add it to finish.' : "Tell us how you'll use CronStream."} +
- {/* Role picker */} + {/* Role picker — hidden once a role exists (role is immutable) */} + {!completing && (