Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 77 additions & 1 deletion provider/provider/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,7 @@ pub struct ParameterDefinition {
}

// --- New Provider Struct ---
#[derive(PartialEq, Clone, Debug, Serialize, Deserialize)]
#[derive(PartialEq, Clone, Debug, Serialize)]
pub struct RegisteredProvider {
pub provider_name: String,
// Provide Node Identity (HNS entry (Node Identity) of the the process serving as the provider)
Expand All @@ -203,6 +203,82 @@ pub struct RegisteredProvider {
// Price per call in USDC, should be clear in HNS entry
pub price: f64,
pub endpoint: EndpointDefinition,
// Whether the provider is currently live/active (None = legacy, Some(false) = offline, Some(true) = online)
pub is_live: Option<bool>,
}

// Old version of RegisteredProvider for migration purposes
#[derive(Deserialize)]
struct OldRegisteredProvider {
pub provider_name: String,
pub provider_id: String,
pub description: String,
pub instructions: String,
pub registered_provider_wallet: String,
pub price: f64,
pub endpoint: EndpointDefinition,
}

// New version with is_live field for deserialization (avoids recursion)
#[derive(Deserialize)]
struct NewRegisteredProvider {
pub provider_name: String,
pub provider_id: String,
pub description: String,
pub instructions: String,
pub registered_provider_wallet: String,
pub price: f64,
pub endpoint: EndpointDefinition,
pub is_live: Option<bool>,
}

// Custom Deserialize implementation for RegisteredProvider to handle migration
impl<'de> Deserialize<'de> for RegisteredProvider {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
// Use an untagged enum to try different deserialization strategies
#[derive(Deserialize)]
#[serde(untagged)]
enum RegisteredProviderVariant {
New(NewRegisteredProvider),
Old(OldRegisteredProvider),
}

match RegisteredProviderVariant::deserialize(deserializer) {
Ok(RegisteredProviderVariant::New(new_provider)) => {
Ok(RegisteredProvider {
provider_name: new_provider.provider_name,
provider_id: new_provider.provider_id,
description: new_provider.description,
instructions: new_provider.instructions,
registered_provider_wallet: new_provider.registered_provider_wallet,
price: new_provider.price,
endpoint: new_provider.endpoint,
is_live: new_provider.is_live,
})
},
Ok(RegisteredProviderVariant::Old(old_provider)) => {
info!("Migrating old RegisteredProvider to new structure - setting is_live to None (legacy)");
// Migrate old provider to new structure with is_live set to None (legacy)
Ok(RegisteredProvider {
provider_name: old_provider.provider_name,
provider_id: old_provider.provider_id,
description: old_provider.description,
instructions: old_provider.instructions,
registered_provider_wallet: old_provider.registered_provider_wallet,
price: old_provider.price,
endpoint: old_provider.endpoint,
is_live: None, // None = legacy provider, no explicit state set
})
},
Err(_) => {
// If both fail, return the error
Err(serde::de::Error::custom("Failed to deserialize RegisteredProvider"))
}
}
}
}

#[derive(Clone, Debug, Serialize, Deserialize)]
Expand Down
50 changes: 49 additions & 1 deletion provider/ui/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,7 @@ function AppContent() {
setProvidersError(null);
try {
const providers = await fetchRegisteredProvidersApi();

setRegisteredProviders(providers);
console.log("Fetched registered providers:", providers);
} catch (error) {
Expand Down Expand Up @@ -288,6 +289,52 @@ function AppContent() {
setShowForm(true);
}, []);

const handleToggleProviderLive = useCallback(async (provider: RegisteredProvider) => {
if (!isWalletConnected) {
alert('Please connect your wallet to update provider status on the hypergrid.');
return;
}

// Determine the new live status
const newLiveStatus = provider.is_live === false ? true : false;

try {
// Look up the actual TBA address for this provider from backend
const tbaAddress = await lookupProviderTbaAddressFromBackend(provider.provider_name, publicClient);

if (!tbaAddress) {
alert(`No blockchain entry found for provider "${provider.provider_name}". Please register on the hypergrid first.`);
return;
}

console.log(`Toggling provider ${provider.provider_name} to ${newLiveStatus ? 'live' : 'offline'}`);

// Create a simple update plan for just the is_live toggle
const toggleUpdatePlan = {
needsOnChainUpdate: true,
needsOffChainUpdate: true,
onChainNotes: [{ key: '~is-live', value: newLiveStatus.toString() }],
updatedProvider: {
...provider,
is_live: newLiveStatus
}
};

// Store the update plan for the callback
(window as any).pendingUpdatePlan = toggleUpdatePlan;

// Update just the ~is-live note using the existing update flow
await providerUpdate.updateProviderNotes(tbaAddress, [
{ key: '~is-live', value: newLiveStatus.toString() }
]);

// The success will be handled by the providerUpdate.onUpdateComplete callback
} catch (error) {
console.error('Error toggling provider live status:', error);
alert(`Failed to update provider status: ${(error as Error).message}`);
}
}, [isWalletConnected, publicClient, providerUpdate]);

const handleProviderRegistration = useCallback(async (provider: RegisteredProvider) => {
console.log("Starting registration for provider:", provider);

Expand Down Expand Up @@ -462,9 +509,10 @@ function AppContent() {
<div className="flex flex-col gap-2">
{registeredProviders.map((provider) => (
<RegisteredProviderView
key={provider.provider_id || provider.provider_name}
key={`${provider.provider_name}-${provider.provider_id}`}
provider={provider}
onEdit={handleEditProvider}
onToggleLive={handleToggleProviderLive}
/>
))}
</div>
Expand Down
4 changes: 0 additions & 4 deletions provider/ui/src/components/CurlJsonViewer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,6 @@ const CurlJsonViewer: React.FC<CurlJsonViewerProps> = ({
}) => {


const isFieldModifiable = useCallback((jsonPointer: string) => {
return modifiableFields.some(f => f.jsonPointer === jsonPointer);
}, [modifiableFields]);

const getFieldByPointer = useCallback((pointer: string) => {
return potentialFields.find(f => f.jsonPointer === pointer);
}, [potentialFields]);
Expand Down
17 changes: 0 additions & 17 deletions provider/ui/src/components/ModifiableFieldsList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,23 +8,6 @@ interface ModifiableFieldsListProps {
onFieldNameChange: (field: ModifiableField, newName: string) => void;
}

// Helper to group fields by their parent path
function groupFieldsByParent(fields: ModifiableField[]) {
const groups: Record<string, ModifiableField[]> = {};

fields.forEach(field => {
// Find the parent path (everything before the last segment)
const parts = field.jsonPointer.split('/');
const parentPath = parts.slice(0, -1).join('/');

if (!groups[parentPath]) {
groups[parentPath] = [];
}
groups[parentPath].push(field);
});

return groups;
}

const ModifiableFieldsList: React.FC<ModifiableFieldsListProps> = ({
modifiableFields,
Expand Down
8 changes: 2 additions & 6 deletions provider/ui/src/components/ProviderConfigModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,11 +52,6 @@ const ProviderConfigModal: React.FC<ProviderConfigModalProps> = ({
onProviderUpdate,
providerRegistration,
providerUpdate,
publicClient,
handleProviderUpdated,
processUpdateResponse,
resetEditState,
handleCloseAddNewModal
}) => {
const { address: connectedWalletAddress } = useAccount();
const [showValidation, setShowValidation] = useState(false);
Expand Down Expand Up @@ -422,7 +417,8 @@ const ProviderConfigModal: React.FC<ProviderConfigModalProps> = ({
providerDescription,
instructions,
registeredProviderWallet,
price: parseFloat(price) || 0
price: parseFloat(price) || 0,
isLive: isEditMode && editingProvider ? editingProvider.is_live : true
}}
onValidationSuccess={handleValidationSuccess}
onValidationError={handleValidationError}
Expand Down
45 changes: 37 additions & 8 deletions provider/ui/src/components/RegisteredProviderView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@ import { RegisteredProvider } from '../types/hypergrid_provider';
export interface RegisteredProviderViewProps {
provider: RegisteredProvider;
onEdit?: (provider: RegisteredProvider) => void;
onToggleLive?: (provider: RegisteredProvider) => void;
}

const RegisteredProviderView: React.FC<RegisteredProviderViewProps> = ({ provider, onEdit }) => {
const RegisteredProviderView: React.FC<RegisteredProviderViewProps> = ({ provider, onEdit, onToggleLive }) => {

const formatPrice = (price: number) => {
if (typeof price !== 'number' || isNaN(price)) return 'N/A';
Expand Down Expand Up @@ -37,19 +38,25 @@ const RegisteredProviderView: React.FC<RegisteredProviderViewProps> = ({ provide
};

const handleClick = (e: React.MouseEvent) => {
// Only trigger edit if not clicking the button itself
if ((e.target as HTMLElement).tagName !== 'BUTTON') {
// Only trigger edit if not clicking a button
const target = e.target as HTMLElement;
if (target.tagName !== 'BUTTON' && !target.closest('button')) {
onEdit?.(provider);
}
};

const handleToggleLive = (e: React.MouseEvent) => {
e.stopPropagation();
onToggleLive?.(provider);
};

return (
<div
className="bg-white dark:bg-black p-5 rounded-xl shadow-sm border border-gray-200 dark:border-white hover:shadow-md transition-all cursor-pointer"
onClick={handleClick}
>
<div className="flex flex-col gap-3">
{/* Top row with icon, name, and edit button */}
{/* Top row with icon, name, status, and edit button */}
<div className="flex items-start justify-between gap-3">
<div className="flex items-start gap-3 flex-1 min-w-0">
<span className="text-xl mt-0.5">🔌</span>
Expand All @@ -75,10 +82,32 @@ const RegisteredProviderView: React.FC<RegisteredProviderViewProps> = ({ provide
</button>
</div>

{/* Bottom row with price */}
<div className="text-sm text-gray-700 dark:text-gray-300">
<span>💰 Price: </span>
<span className="font-semibold text-gray-900 dark:text-gray-100">{formatPrice(provider.price)}</span>
{/* Bottom row with price and toggle */}
<div className="flex items-center justify-between">
<div className="text-sm text-gray-700 dark:text-gray-300">
<span>💰 Price: </span>
<span className="font-semibold text-gray-900 dark:text-gray-100">{formatPrice(provider.price)}</span>
</div>

{/* Toggle switch - legacy providers default to "on" */}
<div className="flex items-center gap-2">
<span className="text-xs text-gray-500 dark:text-gray-400">
{provider.is_live === false ? 'Off' : 'On'}
</span>
<button
onClick={handleToggleLive}
className={`relative inline-flex h-5 w-9 rounded-full transition-colors focus:outline-none focus:ring-2 focus:ring-gray-500 focus:ring-offset-2 ${
provider.is_live === false
? 'bg-gray-300 dark:bg-gray-600 justify-start'
: 'bg-green-500 dark:bg-green-600 justify-end'
}`}
aria-label={provider.is_live === false ? 'Turn provider on' : 'Turn provider off'}
>
<span className={`h-3 w-3 m-1 bg-white transition-all ${
provider.is_live === false ? 'rounded-sm' : 'rounded-full'
}`} />
</button>
</div>
</div>
</div>
</div>
Expand Down
5 changes: 4 additions & 1 deletion provider/ui/src/components/ValidationPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ interface ValidationPanelProps {
instructions: string;
registeredProviderWallet: string;
price: number;
isLive?: boolean; // For preserving is_live state in edit mode
};
onValidationSuccess: (validatedProvider: any) => void;
onValidationError: (error: string) => void;
Expand Down Expand Up @@ -108,7 +109,9 @@ const ValidationPanel: React.FC<ValidationPanelProps> = ({
instructions: providerMetadata.instructions,
registered_provider_wallet: providerMetadata.registeredProviderWallet,
price: providerMetadata.price,
endpoint: curlTemplate // The curlTemplate IS the new EndpointDefinition
endpoint: curlTemplate, // The curlTemplate IS the new EndpointDefinition
// For new registrations, set is_live to true. For edits, preserve existing value
is_live: isEditMode ? providerMetadata.isLive : true
};

// Send provider object and arguments for validation
Expand Down
Loading