Skip to content

feat: implement escrow delegate management with permissions#36

Merged
aledefra merged 8 commits intodevelopfrom
delegates
Dec 15, 2025
Merged

feat: implement escrow delegate management with permissions#36
aledefra merged 8 commits intodevelopfrom
delegates

Conversation

@aledefra
Copy link
Collaborator

@aledefra aledefra commented Dec 5, 2025

No description provided.

@aledefra aledefra marked this pull request as ready for review December 6, 2025 17:37
Copilot AI review requested due to automatic review settings December 6, 2025 17:37
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This pull request implements a comprehensive escrow delegate management system with granular permissions, allowing escrow owners to grant specific operational permissions to trusted addresses without transferring full ownership.

Key Changes:

  • Added a bitwise permission system with four permission types: createJobs, extendDuration, extendNodes, and redeemUnused
  • Implemented delegate management UI in the Account page with CRUD operations for delegates
  • Updated authentication flow to fetch and store user permissions during login
  • Added permission checks to protected pages (ExtendJob, EditJob, Project, Monitor)

Reviewed changes

Copilot reviewed 12 out of 12 changed files in this pull request and generated 9 comments.

Show a summary per file
File Description
src/lib/permissions/delegates.ts Defines permission types and bitwise operations for delegate access control
src/lib/contexts/deployment/context.ts Adds EscrowAccess type and permission-related context properties
src/lib/contexts/deployment/deployment-provider.tsx Implements fetchEscrowAccess and hasEscrowPermission functions
src/components/account/delegates/EscrowDelegates.tsx New component for managing delegate addresses and permissions with full CRUD operations
src/pages/Account.tsx Adds "Delegates" tab to account management interface
src/pages/deeploys/job/ExtendJob.tsx Enforces extendDuration permission with early return pattern
src/pages/deeploys/job/EditJob.tsx Enforces extendNodes permission with early return pattern
src/pages/deeploys/Project.tsx Enforces createJobs permission with early return pattern
src/pages/deeploys/Monitor.tsx Enforces redeemUnused permission with early return pattern
src/pages/Login.tsx Updates authentication to fetch escrow access and permissions, allowing delegate login
src/blockchain/PoAIManager.ts Adds ABI definitions for getAddressRegistration and delegate-related functions
src/blockchain/CspEscrow.ts Adds ABI definitions for getDelegatePermissions, getDelegatedAddresses, setDelegatePermissions, and removeDelegate

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +304 to +352
<div
key={delegate.address}
className="row w-full flex-wrap items-center justify-between gap-3 rounded-xl border border-slate-100 bg-white px-4 py-3 hover:border-slate-200"
onClick={() => openEditModal(delegate)}
>
<div className="col gap-1">
<CopyableValue value={delegate.address}>
<div className="font-roboto-mono text-sm font-medium">
{getShortAddressOrHash(delegate.address, 6, true)}
</div>
</CopyableValue>
</div>

<div className="row flex-wrap gap-1.5">
{activePermissions.length ? (
activePermissions.map((permission) => (
<SmallTag key={permission.key}>{permission.label}</SmallTag>
))
) : (
<SmallTag>No permissions</SmallTag>
)}
</div>

<div className="row gap-2">
<Button
size="sm"
variant="flat"
startContent={<RiPencilLine />}
onPress={() => {
openEditModal(delegate);
}}
>
Edit
</Button>
<Button
size="sm"
color="danger"
variant="flat"
startContent={<RiDeleteBinLine />}
onPress={() => {
removeDelegate(delegate.address);
}}
isDisabled={!canManageDelegates || removingAddress === delegate.address}
isLoading={removingAddress === delegate.address}
>
Remove
</Button>
</div>
</div>
Copy link

Copilot AI Dec 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The entire delegate row has an onClick handler that opens the edit modal, which may create confusing UX since there are also explicit "Edit" and "Remove" buttons within the row. Clicking the Remove button will trigger both the row's onClick and the button's onPress, potentially opening the edit modal while removing.

Consider removing the onClick from the row and relying only on the explicit button actions, or add event.stopPropagation() to the button handlers.

Copilot uses AI. Check for mistakes.
Comment on lines +145 to +148
if (!trimmedAddress || !trimmedAddress.startsWith('0x') || trimmedAddress.length !== 42) {
toast.error('Enter a valid delegate address.');
return;
}
Copy link

Copilot AI Dec 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The address validation only checks for basic format (starts with '0x' and length is 42). Consider using viem's isAddress() function for more robust validation, including checksum validation. This would prevent issues with invalid addresses being submitted to the smart contract.

Example:

import { isAddress } from 'viem';

if (!isAddress(trimmedAddress)) {
    toast.error('Enter a valid delegate address.');
    return;
}

Copilot uses AI. Check for mistakes.
placeholder="0x..."
value={formAddress}
onValueChange={(value) => setFormAddress(value)}
isDisabled={!canManageDelegates || isSaving}
Copy link

Copilot AI Dec 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When editing an existing delegate, the address field should be disabled to prevent confusion. The smart contract's setDelegatePermissions function updates permissions for a given address, so the address cannot be changed. Users should only be able to modify permissions when editing.

Add isDisabled={!canManageDelegates || isSaving || !!editingAddress} to make it clear the address is immutable when editing.

Suggested change
isDisabled={!canManageDelegates || isSaving}
isDisabled={!canManageDelegates || isSaving || !!editingAddress}

Copilot uses AI. Check for mistakes.
Comment on lines +145 to +153
if (!trimmedAddress || !trimmedAddress.startsWith('0x') || trimmedAddress.length !== 42) {
toast.error('Enter a valid delegate address.');
return;
}

if (permissionsValue === 0n) {
toast.error('Select at least one permission.');
return;
}
Copy link

Copilot AI Dec 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider adding validation to prevent adding the owner's own address as a delegate, as the owner already has full permissions. Also, add validation to prevent the zero address (0x0000000000000000000000000000000000000000).

Example:

if (trimmedAddress.toLowerCase() === ownerAddress?.toLowerCase()) {
    toast.error('Cannot add owner address as delegate.');
    return;
}

if (isZeroAddress(trimmedAddress)) {
    toast.error('Cannot add zero address as delegate.');
    return;
}

Copilot uses AI. Check for mistakes.
Comment on lines +32 to +44
if (!hasEscrowPermission('extendDuration')) {
return (
<div className="center-all flex-1">
<DetailedAlert
variant="red"
icon={<RiAlertLine />}
title="Permission required"
description={<div>You do not have permission to extend job duration.</div>}
isCompact
/>
</div>
);
}
Copy link

Copilot AI Dec 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The permission check may show a false negative if the component renders before fetchEscrowAccess completes during login. When currentUserPermissions is undefined, hasEscrowPermission returns false, potentially showing "Permission required" briefly even to authorized users.

Consider showing a loading state when currentUserPermissions === undefined to distinguish between "loading permissions" and "lacks permission":

if (currentUserPermissions === undefined) {
    return <ExtendJobPageLoading />;
}

if (!hasEscrowPermission('extendDuration')) {
    return (
        <div className="center-all flex-1">
            <DetailedAlert ... />
        </div>
    );
}

Copilot uses AI. Check for mistakes.
Comment on lines +326 to +338
if (!hasEscrowPermission('extendNodes')) {
return (
<div className="center-all flex-1">
<DetailedAlert
variant="red"
icon={<RiAlertLine />}
title="Permission required"
description={<div>You do not have permission to extend job nodes.</div>}
isCompact
/>
</div>
);
}
Copy link

Copilot AI Dec 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The permission check may show a false negative if the component renders before fetchEscrowAccess completes during login. When currentUserPermissions is undefined, hasEscrowPermission returns false, potentially showing "Permission required" briefly even to authorized users.

Consider showing a loading state when currentUserPermissions === undefined to distinguish between "loading permissions" and "lacks permission":

if (currentUserPermissions === undefined) {
    return <EditJobPageLoading />;
}

if (!hasEscrowPermission('extendNodes')) {
    return (
        <div className="center-all flex-1">
            <DetailedAlert ... />
        </div>
    );
}

Copilot uses AI. Check for mistakes.
Comment on lines +95 to +107
if (!hasEscrowPermission('createJobs')) {
return (
<div className="center-all flex-1">
<DetailedAlert
variant="red"
icon={<RiAlertLine />}
title="Permission required"
description={<div>You do not have permission to create new jobs.</div>}
isCompact
/>
</div>
);
}
Copy link

Copilot AI Dec 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The permission check may show a false negative if the component renders before fetchEscrowAccess completes during login. When currentUserPermissions is undefined, hasEscrowPermission returns false, potentially showing "Permission required" briefly even to authorized users.

Consider showing a loading state when currentUserPermissions === undefined to distinguish between "loading permissions" and "lacks permission":

if (currentUserPermissions === undefined) {
    return <ProjectPageLoading />;
}

if (!hasEscrowPermission('createJobs')) {
    return (
        <div className="center-all flex-1">
            <DetailedAlert ... />
        </div>
    );
}

Copilot uses AI. Check for mistakes.
import {
ALL_DELEGATE_PERMISSIONS_MASK,
DelegatePermissionKey,
getPermissionValue,
Copy link

Copilot AI Dec 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unused import getPermissionValue.

Suggested change
getPermissionValue,

Copilot uses AI. Check for mistakes.
Copilot AI review requested due to automatic review settings December 15, 2025 14:10
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

@aledefra aledefra merged commit 61cbefe into develop Dec 15, 2025
1 check passed
@aledefra aledefra deleted the delegates branch December 15, 2025 18:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants