diff --git a/examples/gno.land/p/demo/accesscontrol/accesscontrol.gno b/examples/gno.land/p/demo/accesscontrol/accesscontrol.gno new file mode 100644 index 00000000000..5785464d511 --- /dev/null +++ b/examples/gno.land/p/demo/accesscontrol/accesscontrol.gno @@ -0,0 +1,204 @@ +package accesscontrol + +import ( + "std" + + "gno.land/p/demo/avl" + "gno.land/p/demo/ownable" +) + +const ( + RoleCreatedEvent = "RoleCreated" + RoleGrantedEvent = "RoleGranted" + RoleRevokedEvent = "RoleRevoked" + RoleRenouncedEvent = "RoleRenounced" + RoleSetEvent = "RoleSet" +) + +// Role struct to store role information +type Role struct { + Name string + Holders *avl.Tree // std.Address -> struct{} + Ownable *ownable.Ownable +} + +// Roles struct to store all Roles information +type Roles struct { + Roles []*Role + UserToRoles avl.Tree // std.Address -> []*Role + Ownable *ownable.Ownable +} + +func validRoleName(name string) error { + if len(name) > 30 || name == "" { + return ErrNameRole + } + return nil +} + +// NewRole creates a new instance of Role +func NewRole(name string, admin std.Address) (*Role, error) { + if err := validRoleName(name); err != nil { + return nil, ErrNameRole + } + + return &Role{ + Name: name, + Holders: avl.NewTree(), + Ownable: ownable.NewWithAddress(admin), + }, nil +} + +// CreateRole create a new role within the realm +func (rs *Roles) CreateRole(name string) (*Role, error) { + if err := validRoleName(name); err != nil { + return nil, ErrNameRole + } + + if !rs.Ownable.CallerIsOwner() { + return nil, ErrNotOwner + } + + for _, role := range rs.Roles { + if role.Name == name { + return nil, ErrRoleSameName + } + } + + role, err := NewRole(name, rs.Ownable.Owner()) + if err != nil { + return nil, err + } + + rs.Roles = append(rs.Roles, role) + + std.Emit( + RoleCreatedEvent, + "roleName", name, + "sender", rs.Ownable.Owner().String(), + ) + + return role, nil +} + +// HasAccount check if an account has a specific role +func (r *Role) HasAccount(account std.Address) bool { + return r.Holders.Has(account.String()) +} + +// FindRole searches for a role by its name +func (rs *Roles) FindRole(name string) (*Role, error) { + for _, role := range rs.Roles { + if role.Name == name { + return role, nil + } + } + + return nil, ErrRoleNotFound +} + +// GrantRole grants a role to an account +func (rs *Roles) GrantRole(name string, account std.Address) error { + r, err := rs.FindRole(name) + if err != nil { + return ErrRoleNotFound + } + + if !r.Ownable.CallerIsOwner() { + return ErrNotOwner + } + + r.Holders.Set(account.String(), struct{}{}) + + // Add in UserToRoles + roles, found := rs.UserToRoles.Get(account.String()) + if !found { + roles = []*Role{} + } + roles = append(roles.([]*Role), r) + rs.UserToRoles.Set(account.String(), roles) + + std.Emit( + RoleGrantedEvent, + "roleName", r.Name, + "account", account.String(), + "sender", std.PreviousRealm().Address().String(), + ) + + return nil +} + +// RevokeRole revokes a role from an account +func (rs *Roles) RevokeRole(name string, account std.Address) error { + r, err := rs.FindRole(name) + if err != nil { + return ErrRoleNotFound + } + + if !r.Ownable.CallerIsOwner() { + return ErrNotOwner + } + + r.Holders.Remove(account.String()) + + // Remove in UserToRoles + roles, found := rs.UserToRoles.Get(account.String()) + if found { + updatedRoles := []*Role{} + for _, role := range roles.([]*Role) { + if role != r { + updatedRoles = append(updatedRoles, role) + } + } + rs.UserToRoles.Set(account.String(), updatedRoles) + } + + std.Emit( + RoleRevokedEvent, + "roleName", r.Name, + "account", account.String(), + "sender", std.PreviousRealm().Address().String(), + ) + + return nil +} + +// RenounceRole allows an account to renounce a role it holds +func (rs *Roles) RenounceRole(name string) error { + r, err := rs.FindRole(name) + if err != nil { + return ErrRoleNotFound + } + + caller := std.OriginCaller() + + if !r.HasAccount(caller) { + return ErrAccountNotRole + } + + r.Holders.Remove(caller.String()) + + std.Emit( + RoleRenouncedEvent, + "roleName", r.Name, + "account", caller.String(), + "sender", caller.String(), + ) + + return nil +} + +// SetRoleAdmin transfers the ownership of the role to a new administrator +func (r *Role) SetRoleAdmin(admin std.Address) error { + if err := r.Ownable.TransferOwnership(admin); err != nil { + return err + } + + std.Emit( + RoleSetEvent, + "roleName", r.Name, + "newAdminRole", r.Ownable.Owner().String(), + ) + + return nil +} diff --git a/examples/gno.land/p/demo/accesscontrol/accesscontrol_test.gno b/examples/gno.land/p/demo/accesscontrol/accesscontrol_test.gno new file mode 100644 index 00000000000..447d2f2ae10 --- /dev/null +++ b/examples/gno.land/p/demo/accesscontrol/accesscontrol_test.gno @@ -0,0 +1,244 @@ +package accesscontrol + +import ( + "std" + "testing" + + "gno.land/p/demo/ownable" + "gno.land/p/demo/testutils" + "gno.land/p/demo/uassert" +) + +var ( + admin = testutils.TestAddress("admin1") + newAdmin = testutils.TestAddress("admin2") + user1 = testutils.TestAddress("user1") + user2 = testutils.TestAddress("user2") + + roleName = "TestRole" +) + +func initSetup(admin std.Address) *Roles { + return &Roles{ + Roles: []*Role{}, + Ownable: ownable.NewWithAddress(admin), + } +} + +func TestCreateRole(t *testing.T) { + roles := initSetup(admin) + + std.TestSetOriginCaller(admin) + + role, err := roles.CreateRole(roleName) + uassert.NoError(t, err) + uassert.True(t, role != nil, "role should not be nil") + uassert.Equal(t, role.Name, roleName) + + _, err = roles.CreateRole(roleName) + uassert.Error(t, err, "should fail on duplicate role creation") +} + +func TestGrantRole(t *testing.T) { + roles := initSetup(admin) + + std.TestSetOriginCaller(admin) + + _, err := roles.CreateRole(roleName) + uassert.NoError(t, err) + + err = roles.GrantRole(roleName, user1) + uassert.NoError(t, err) + + role, err := roles.FindRole(roleName) + uassert.NoError(t, err) + uassert.True(t, role.HasAccount(user1), "user1 should have the TestRole") + + rolesList, found := roles.UserToRoles.Get(user1.String()) + uassert.True(t, found, "user1 should be in UserToRoles") + uassert.True(t, containsRole(rolesList.([]*Role), role), "UserToRoles should contain TestRole for user1") +} + +func TestGrantRoleByNonOwner(t *testing.T) { + roles := initSetup(admin) + + std.TestSetOriginCaller(admin) + _, err := roles.CreateRole(roleName) + uassert.NoError(t, err) + + std.TestSetOriginCaller(user2) + roles.Ownable.TransferOwnership(user2) + err = roles.GrantRole(roleName, user1) + uassert.Error(t, err, "non-owner should not be able to grant roles") + + roles.Ownable.TransferOwnership(admin) +} + +func TestRevokeRole(t *testing.T) { + roles := initSetup(admin) + + std.TestSetOriginCaller(admin) + + _, err := roles.CreateRole(roleName) + uassert.NoError(t, err) + err = roles.GrantRole(roleName, user1) + uassert.NoError(t, err) + + err = roles.RevokeRole(roleName, user1) + uassert.NoError(t, err) + + role, err := roles.FindRole(roleName) + uassert.NoError(t, err) + uassert.False(t, role.HasAccount(user1), "user1 should no longer have the TestRole") + + rolesList, found := roles.UserToRoles.Get(user1.String()) + if found { + uassert.False(t, containsRole(rolesList.([]*Role), role), "UserToRoles should not contain TestRole for user1 after revocation") + } +} + +func TestRenounceRole(t *testing.T) { + roles := initSetup(admin) + + std.TestSetOriginCaller(admin) + + role, err := roles.CreateRole(roleName) + uassert.NoError(t, err) + err = roles.GrantRole(roleName, user1) + uassert.NoError(t, err) + + // Pas besoin de transférer la propriété pour renoncer à un rôle + std.TestSetOriginCaller(user1) + err = roles.RenounceRole(roleName) + uassert.NoError(t, err) + + role, err = roles.FindRole(roleName) + uassert.NoError(t, err) + uassert.False(t, role.HasAccount(user1), "user1 should have renounced the TestRole") +} + +func TestSetRoleAdmin(t *testing.T) { + roles := initSetup(admin) + + std.TestSetOriginCaller(admin) + + role, err := roles.CreateRole(roleName) + uassert.NoError(t, err) + + err = role.SetRoleAdmin(newAdmin) + uassert.NoError(t, err, "admin change should succeed") + + std.TestSetOriginCaller(newAdmin) + uassert.Equal(t, role.Ownable.Owner(), newAdmin, "the new admin should be newAdmin") + + std.TestSetOriginCaller(admin) + uassert.NotEqual(t, role.Ownable.Owner(), admin, "the old admin should no longer be the owner") +} + +func TestCreateRoleInvalidName(t *testing.T) { + roles := initSetup(admin) + + std.TestSetOriginCaller(admin) + + _, err := roles.CreateRole("") + uassert.Error(t, err, "should fail on empty role name") + + longRoleName := "thisisaverylongrolenamethatexceedsthenormallimitfortestingpurposes" + _, err = roles.CreateRole(longRoleName) + uassert.Error(t, err, "should fail on very long role name") +} + +func TestRevokeRoleByNonOwner(t *testing.T) { + roles := initSetup(admin) + + std.TestSetOriginCaller(admin) + + _, err := roles.CreateRole(roleName) + uassert.NoError(t, err) + err = roles.GrantRole(roleName, user1) + uassert.NoError(t, err) + + std.TestSetOriginCaller(user2) + err = roles.RevokeRole(roleName, user1) + uassert.Error(t, err, "non-owner should not be able to revoke roles") +} + +func TestGrantRoleToNonExistentRole(t *testing.T) { + roles := initSetup(admin) + + std.TestSetOriginCaller(admin) + + err := roles.GrantRole("NonExistentRole", user1) + uassert.Error(t, err, "should fail when granting non-existent role") +} + +func TestRevokeRoleFromNonExistentRole(t *testing.T) { + roles := initSetup(admin) + + std.TestSetOriginCaller(admin) + + err := roles.RevokeRole("NonExistentRole", user1) + uassert.Error(t, err, "should fail when revoking non-existent role") +} + +func TestRenounceNonExistentRole(t *testing.T) { + roles := initSetup(admin) + + std.TestSetOriginCaller(user1) + + err := roles.RenounceRole("NonExistentRole") + uassert.Error(t, err, "should fail when renouncing non-existent role") +} + +func TestDeleteRole(t *testing.T) { + roles := initSetup(admin) + + std.TestSetOriginCaller(admin) + + role, err := roles.CreateRole(roleName) + uassert.NoError(t, err) + uassert.True(t, role != nil, "role should not be nil") + + roles.Roles = []*Role{} // Clear roles for testing purpose + _, err = roles.FindRole(roleName) + uassert.Error(t, err, "should fail when trying to find deleted role") +} + +func TestUserToRolesWithMultipleRoles(t *testing.T) { + roles := initSetup(admin) + + std.TestSetOriginCaller(admin) + + roleName1 := "Role1" + roleName2 := "Role2" + + // Create two roles + _, err := roles.CreateRole(roleName1) + uassert.NoError(t, err) + _, err = roles.CreateRole(roleName2) + uassert.NoError(t, err) + + // Grant both roles to user1 + err = roles.GrantRole(roleName1, user1) + uassert.NoError(t, err) + err = roles.GrantRole(roleName2, user1) + uassert.NoError(t, err) + + // Check if user1 has both roles + rolesList, found := roles.UserToRoles.Get(user1.String()) + uassert.True(t, found, "user1 should be in UserToRoles") + role1, _ := roles.FindRole(roleName1) + role2, _ := roles.FindRole(roleName2) + uassert.True(t, containsRole(rolesList.([]*Role), role1), "UserToRoles should contain Role1 for user1") + uassert.True(t, containsRole(rolesList.([]*Role), role2), "UserToRoles should contain Role2 for user1") +} + +// func test for check if a role is in a list of roles +func containsRole(roles []*Role, target *Role) bool { + for _, role := range roles { + if role == target { + return true + } + } + return false +} diff --git a/examples/gno.land/p/demo/accesscontrol/doc.gno b/examples/gno.land/p/demo/accesscontrol/doc.gno new file mode 100644 index 00000000000..c6ba2f79f88 --- /dev/null +++ b/examples/gno.land/p/demo/accesscontrol/doc.gno @@ -0,0 +1,59 @@ +// Package accesscontrol implements a role-based access control (RBAC) system for Gno applications. +// It provides functionality to create, assign, revoke, and transfer roles. +// +// # Usage +// +// Import the `gno.land/p/demo/accesscontrol` package to manage roles within your Gno realm. You can create roles, +// assign them to users, revoke them, and transfer role ownership. +// +// Roles can be created by the contract owner using `CreateRole`. Each role is uniquely identified by its name. +// +// CreateRole("editor") +// +// Use `GrantRole` to assign a role to a user, and `RevokeRole` to remove a role from a user. +// +// GrantRole("editor", userAddress) +// +// RevokeRole("editor", userAddress) +// +// Users can renounce their roles using `RenounceRole`, voluntarily removing themselves from a role. +// +// RenounceRole("editor") +// +// You can look up a role by name with `FindRole`. +// +// FindRole("editor") +// +// Role ownership can be transferred using `SetRoleAdmin`. +// +// SetRoleAdmin(newAdminAddress) +// +// Key events +// - `RoleCreatedEvent`: Triggered when a new role is created +// Key includes: +// `roleName` (name of the role) +// `sender` (address of the sender) +// +// - `RoleGrantedEvent`: Triggered when a role is granted to an account +// Key includes: +// `roleName` (name of the role) +// `account` (address of the account) +// `sender` (address of the sender) +// +// - `RoleRevokedEvent`: Triggered when a role is revoked from an account +// Key includes: +// `roleName` (name of the role) +// `account` (address of the account) +// `sender` (address of the sender) +// +// - `RoleRenouncedEvent`: Triggered when a role is renounced by an account +// Key includes: +// `roleName` (name of the role) +// `account` (address of the account) +// +// - `RoleSetEvent`: Triggered when a role's administrator is set or changed +// Key includes: +// `roleName` (name of the role) +// `newAdmin` (address of the new administrator) +// `sender` (address of the sender) +package accesscontrol diff --git a/examples/gno.land/p/demo/accesscontrol/errors.gno b/examples/gno.land/p/demo/accesscontrol/errors.gno new file mode 100644 index 00000000000..7f4261345eb --- /dev/null +++ b/examples/gno.land/p/demo/accesscontrol/errors.gno @@ -0,0 +1,12 @@ +package accesscontrol + +import "errors" + +var ( + ErrNotMatchAccount = errors.New("accesscontrol: caller confirmation does not match account") + ErrRoleSameName = errors.New("accesscontrol: role already exists with the same name") + ErrRoleNotFound = errors.New("accesscontrol: role not found") + ErrAccountNotRole = errors.New("accesscontrol: this account does not have the role") + ErrNameRole = errors.New("accesscontrol: role name cannot be empty or exceed 30 characters") + ErrNotOwner = errors.New("accesscontrol: caller is not the owner of the role") +) diff --git a/examples/gno.land/p/demo/accesscontrol/gno.mod b/examples/gno.land/p/demo/accesscontrol/gno.mod new file mode 100644 index 00000000000..92b519f5df5 --- /dev/null +++ b/examples/gno.land/p/demo/accesscontrol/gno.mod @@ -0,0 +1 @@ +module gno.land/p/demo/accesscontrol diff --git a/examples/gno.land/p/demo/timelock/doc.gno b/examples/gno.land/p/demo/timelock/doc.gno new file mode 100644 index 00000000000..025d93ecc17 --- /dev/null +++ b/examples/gno.land/p/demo/timelock/doc.gno @@ -0,0 +1,32 @@ +// Package timelock provides a library for scheduling, cancelling, and +// executing time-locked operations in Gno. It ensures that +// operations are only carried out after a specified delay and offers +// mechanisms for managing and verifying the status of these operations. +// This package leverages an AVL tree for efficient management of timestamps +// and integrates role-based access control for administrative tasks. +// +// # Usage: +// +// import "gno.land/p/demo/timelock" +// import "gno.land/p/demo/accesscontrol" +// +// Initialize timelock utility with an AVL tree and access control. +// timestamps := avl.NewTree() +// adminRole := accesscontrol.NewRole("admin", std.Address("admin-address")) +// timeLockUtil := timelock.NewTimeLockUtil(timestamps, adminRole, 30) +// +// Schedule an operation with a delay of 60 seconds. +// id := seqid.ID() +// timeLockUtil.Schedule(id, 60) +// +// Check if an operation is pending. +// isPending := timeLockUtil.IsPending(id) +// +// Execute the operation when it is pending. +// if timeLockUtil.IsPending(id) { +// timeLockUtil.Execute(id) +// } +// +// Update the minimum delay for future operations. +// timeLockUtil.UpdateDelay(45) +package timelock diff --git a/examples/gno.land/p/demo/timelock/errors.gno b/examples/gno.land/p/demo/timelock/errors.gno new file mode 100644 index 00000000000..4027dab5890 --- /dev/null +++ b/examples/gno.land/p/demo/timelock/errors.gno @@ -0,0 +1,14 @@ +package timelock + +import "errors" + +var ( + ErrNilTimestampsOrAccessControl = errors.New("timelock: timestamps and accesscontrol values must be different from nil") + ErrInsufficientDelay = errors.New("timelockutil: Schedule: insufficient delay") + ErrOperationAlreadyScheduled = errors.New("timelockutil: Schedule: operation already scheduled") + ErrOperationNotPending = errors.New("timelock: operation not pending") + ErrUnexpectedType = errors.New("timelockutil: GetTimestamp: unexpected type") + ErrUpadateDelay = errors.New("timelock: UpdateDelay: only admin can update delay") + ErrOperationCancelNotPending = errors.New("timelock: Cancel: operation not pending") + ErrOperationExecuteNotPending = errors.New("timelock: Execute: operation not pending") +) diff --git a/examples/gno.land/p/demo/timelock/gno.mod b/examples/gno.land/p/demo/timelock/gno.mod new file mode 100644 index 00000000000..adac60b1b92 --- /dev/null +++ b/examples/gno.land/p/demo/timelock/gno.mod @@ -0,0 +1 @@ +module gno.land/p/demo/timelock diff --git a/examples/gno.land/p/demo/timelock/timelock.gno b/examples/gno.land/p/demo/timelock/timelock.gno new file mode 100644 index 00000000000..fcb6d1c7c40 --- /dev/null +++ b/examples/gno.land/p/demo/timelock/timelock.gno @@ -0,0 +1,205 @@ +package timelock + +import ( + "std" + "strconv" + "time" + + "gno.land/p/demo/accesscontrol" + "gno.land/p/demo/avl" + "gno.land/p/demo/seqid" + "gno.land/p/demo/ufmt" +) + +// Represents the status of a planned operation +type OperationState int + +const ( + Unset OperationState = iota + Pending + Ready + Done +) + +func (os OperationState) StateToString() string { + switch os { + case Unset: + return "Unset" + case Pending: + return "Pending" + case Ready: + return "Ready" + case Done: + return "Done" + default: + return "Unknown" + } +} + +// OperationStatus represents the status of an operation +type OperationStatus struct { + sheduleTime int64 + isDone bool +} + +// TimeLock stores the necessary parameters for the timelock operations +type TimeLock struct { + timestamps *avl.Tree // id -> time.Time + accessControl *accesscontrol.Role + minDelay uint64 +} + +// New instance of TimeLock +func NewTimeLock(timestamps *avl.Tree, accessControl *accesscontrol.Role, minDelay uint64) (*TimeLock, error) { + if timestamps == nil || accessControl == nil { + return nil, ErrNilTimestampsOrAccessControl + } + + return &TimeLock{ + timestamps: timestamps, + accessControl: accessControl, + minDelay: minDelay, + }, nil +} + +// Schedules an operation to be carried out after a minimum delay +func (tl *TimeLock) Schedule(id seqid.ID, delay uint64) error { + if delay < tl.minDelay { + return ErrInsufficientDelay + } + + if tl.timestamps.Has(id.Binary()) { + return ErrOperationAlreadyScheduled + } + + timestamp := time.Now().Unix() + int64(delay) + status := OperationStatus{sheduleTime: timestamp, isDone: false} + tl.timestamps.Set(id.Binary(), status) + + std.Emit( + "TimeLockScheduled", + "id", id.String(), + "delay", strconv.FormatInt(int64(delay), 10), + ) + + return nil +} + +// Remove operation +func (tl *TimeLock) Remove(id seqid.ID) { + tl.timestamps.Remove(id.Binary()) + + std.Emit( + "TimeLockRemoved", + "id", id.String(), + ) +} + +// Cancels a planned operation +func (tl *TimeLock) Cancel(id seqid.ID) error { + if !tl.IsPending(id) { + return ErrOperationCancelNotPending + } + + tl.timestamps.Remove(id.Binary()) + + std.Emit( + "TimeLockCancelled", + "id", id.String(), + ) + return nil +} + +// Executes a pending operation +func (tl *TimeLock) Execute(id seqid.ID) error { + if !tl.IsPending(id) { + return ErrOperationExecuteNotPending + } + + status, err := tl.GetOperationStatus(id) + if err != nil { + return err + } + status.isDone = true + tl.timestamps.Set(id.Binary(), status) + + std.Emit( + "TimeLockExecuted", + "id", id.String(), + ) + + return nil +} + +// Update the minimum lead time for future operations +func (tl *TimeLock) UpdateDelay(newDelay uint64) error { + if std.PreviousRealm().Address() != tl.accessControl.Ownable.Owner() { + return ErrUpadateDelay + } + + std.Emit( + "TimeLockMinDelayChanged", + "oldDelay", strconv.FormatInt(int64(tl.minDelay), 10), + "newDelay", strconv.FormatInt(int64(newDelay), 10), + ) + + tl.minDelay = newDelay + + return nil +} + +// Checks if an operation is pending +func (tl *TimeLock) IsPending(id seqid.ID) bool { + state, err := tl.GetOperationState(id) + if err != nil { + // Handle the error appropriately; for now, we assume the operation is not pending if there's an error + ufmt.Errorf("Error retrieving operation state: %v", err) + return false + } + + return state == Pending +} + +// Checks if an operation is ready +func (tl *TimeLock) IsReady(id seqid.ID) bool { + state, err := tl.GetOperationState(id) + if err != nil { + // Handle the error appropriately; for now, we assume the operation is not pending if there's an error + ufmt.Errorf("Error retrieving operation state: %v", err) + return false + } + + return state == Ready +} + +// Returns the status of an operation +func (tl *TimeLock) GetOperationState(id seqid.ID) (OperationState, error) { + status, err := tl.GetOperationStatus(id) + if err != nil { + return Unset, err + } + if status.isDone { + return Done, nil + } + if status.sheduleTime == 0 { + return Unset, nil + } + if status.sheduleTime > time.Now().Unix() { + return Pending, nil + } + return Ready, nil +} + +// Returns the status of an operation +func (tl *TimeLock) GetOperationStatus(id seqid.ID) (OperationStatus, error) { + value, ok := tl.timestamps.Get(id.Binary()) + + if !ok { + return OperationStatus{}, nil // Return an empty status if the operation is not found + } + if status, ok := value.(OperationStatus); ok { + return status, nil + } else { + return OperationStatus{}, ErrUnexpectedType + } +} diff --git a/examples/gno.land/p/demo/timelock/timelock_test.gno b/examples/gno.land/p/demo/timelock/timelock_test.gno new file mode 100644 index 00000000000..f8607409c86 --- /dev/null +++ b/examples/gno.land/p/demo/timelock/timelock_test.gno @@ -0,0 +1,89 @@ +package timelock + +import ( + "std" + "testing" + "time" + + "gno.land/p/demo/accesscontrol" + "gno.land/p/demo/avl" + "gno.land/p/demo/seqid" + "gno.land/p/demo/uassert" +) + +func TestTimelock(t *testing.T) { + // Initialization + timestamps := avl.NewTree() + minDelay := uint64(2) // 2 seconds to simplify testing + accessControl, _ := accesscontrol.NewRole("admin", std.OriginCaller()) + timelockUtil, err := NewTimeLock(timestamps, accessControl, minDelay) + + // Generate a new ID from time.Now().UnixNano() with seconds added to guarantee uniqueness + newID := func(offset int64) seqid.ID { + return seqid.ID(time.Now().UnixNano() + offset) + } + + uassert.NoError(t, err, "Failed to create TimeLock instance") + + // Test Schedule + t.Run("Schedule", func(t *testing.T) { + id := newID(0) + delay := uint64(3) // 3 seconds + + err := timelockUtil.Schedule(id, delay) + + uassert.NoError(t, err, "Schedule failed") + + status, err := timelockUtil.GetOperationStatus(id) + + uassert.NoError(t, err, "failed to get operation status") + uassert.NotEmpty(t, status.sheduleTime, "operation status not set or invalid") + }) + + // Test Cancel + t.Run("Cancel", func(t *testing.T) { + id := newID(1) + + // Plan a new operation to ensure it is unique + err := timelockUtil.Schedule(id, uint64(3)) + uassert.NoError(t, err, "Failed to schedule operation for cancellation") + + err = timelockUtil.Cancel(id) + uassert.NoError(t, err, "Cancel failed") + + status, err := timelockUtil.GetOperationStatus(id) + uassert.NoError(t, err, "failed to get operation status") + uassert.Empty(t, status.sheduleTime, "operation not cancelled") + }) + + // Test Execute + t.Run("Execute", func(t *testing.T) { + id := newID(2) + delay := uint64(3) // 3 seconds + futureTime := time.Now().Unix() + int64(delay) + + // Schedule the operation with a future timestamp + err := timelockUtil.Schedule(id, delay) + uassert.NoError(t, err, "Failed to schedule operation for execution") + + // Simulates the passage of time by setting the timestamp to a future time + timestamps.Set(id.Binary(), OperationStatus{sheduleTime: futureTime, isDone: false}) + + err = timelockUtil.Execute(id) + uassert.NoError(t, err, "Execute failed") + + state, err := timelockUtil.GetOperationState(id) + uassert.NoError(t, err, "failed to get operation state") + uassert.Equal(t, Done.StateToString(), state.StateToString(), "operation not executed") + }) + + // Test UpdateDelay + t.Run("UpdateDelay", func(t *testing.T) { + newDelay := uint64(4) // 4 seconds + + err := timelockUtil.UpdateDelay(newDelay) + uassert.NoError(t, err, "UpdateDelay failed") + + uassert.Equal(t, newDelay, timelockUtil.minDelay, "minDelay not updated") + }) +}