From dbd4f40f2927ada1d22d475c251f993c406cf4d5 Mon Sep 17 00:00:00 2001 From: moul <94029+moul@users.noreply.github.com> Date: Fri, 7 Feb 2025 10:38:29 +0100 Subject: [PATCH] feat(examples): add p/moul/admin Signed-off-by: moul <94029+moul@users.noreply.github.com> --- examples/gno.land/p/moul/admin/admin.gno | 172 +++++++++++++ examples/gno.land/p/moul/admin/admin_test.gno | 229 ++++++++++++++++++ examples/gno.land/p/moul/admin/gno.mod | 1 + 3 files changed, 402 insertions(+) create mode 100644 examples/gno.land/p/moul/admin/admin.gno create mode 100644 examples/gno.land/p/moul/admin/admin_test.gno create mode 100644 examples/gno.land/p/moul/admin/gno.mod diff --git a/examples/gno.land/p/moul/admin/admin.gno b/examples/gno.land/p/moul/admin/admin.gno new file mode 100644 index 00000000000..7f2f258fe07 --- /dev/null +++ b/examples/gno.land/p/moul/admin/admin.gno @@ -0,0 +1,172 @@ +package admin + +import ( + "errors" + "std" +) + +const ( + AdminTransferEvent = "AdminTransfer" + AdminTypeWallet = "wallet" + AdminTypeContract = "contract" +) + +var ( + ErrUnauthorized = errors.New("unauthorized") + ErrInvalidAddress = errors.New("invalid address") +) + +// PrivilegedAction defines a function that performs a privileged action. +type PrivilegedAction func() + +// ContractAdminHandler defines the interface that contract-based admins must implement. +// This allows privileged actions to be handled asynchronously (or synchronously via a DAO or similar governance system). +type ContractAdminHandler interface { + HandlePrivilegedAction(action PrivilegedAction) +} + +// Admin encapsulates an owner and its type. It supports both wallet-based ownership and contract-based (e.g. DAO) ownership. +type Admin struct { + owner std.Address + ownerType string // valid values are "wallet" or "contract" + contractHandler ContractAdminHandler +} + +// New creates a new Admin instance with the initial owner set to the current caller (from std.PrevRealm()) +// and defaults to a wallet-based admin. +func New() *Admin { + return &Admin{ + owner: std.PrevRealm().Addr(), + ownerType: AdminTypeWallet, + } +} + +// NewWithAddress creates a new Admin instance with the specified address as the owner. +// The admin type defaults to a wallet. +func NewWithAddress(addr std.Address) *Admin { + return &Admin{ + owner: addr, + ownerType: AdminTypeWallet, + } +} + +// Owner returns the current admin owner. +func (a *Admin) Owner() std.Address { + if a == nil { + return std.Address("") + } + return a.owner +} + +// CallerIsAdmin checks if the caller is the current admin. +func (a *Admin) CallerIsAdmin() bool { + if a == nil { + return false + } + return std.PrevRealm().Addr() == a.owner +} + +// AssertCallerIsAdmin panics if the caller is not the admin. +func (a *Admin) AssertCallerIsAdmin() { + if a == nil { + panic(ErrUnauthorized) + } + caller := std.PrevRealm().Addr() + if caller != a.owner { + panic(ErrUnauthorized) + } +} + +// TransferToWallet transfers admin rights to a new wallet address. +// It resets any existing contract handler. +func (a *Admin) TransferToWallet(walletAddr std.Address) error { + if !a.CallerIsAdmin() { + return ErrUnauthorized + } + if !walletAddr.IsValid() { + return ErrInvalidAddress + } + prevOwner := a.owner + a.owner = walletAddr + a.ownerType = AdminTypeWallet + a.contractHandler = nil + + std.Emit( + AdminTransferEvent, + "from", prevOwner.String(), + "to", walletAddr.String(), + "type", AdminTypeWallet, + ) + + return nil +} + +// TransferToContract transfers admin rights to a contract address that implements ContractAdminHandler. +// This is useful when switching to a DAO-based governance model. +func (a *Admin) TransferToContract(contractAddr std.Address, handler ContractAdminHandler) error { + if !a.CallerIsAdmin() { + return ErrUnauthorized + } + if !contractAddr.IsValid() { + return ErrInvalidAddress + } + prevOwner := a.owner + a.owner = contractAddr + a.ownerType = AdminTypeContract + a.contractHandler = handler + + std.Emit( + AdminTransferEvent, + "from", prevOwner.String(), + "to", contractAddr.String(), + "type", AdminTypeContract, + ) + + return nil +} + +// AdminToDo executes a privileged action if the caller is authorized. +// When the admin is a wallet, the action is executed immediately. +// When the admin is a contract (e.g. DAO), the action is passed to the contract's handler +// for asynchronous processing (such as through proposal enqueuing). +func (a *Admin) AdminToDo(action PrivilegedAction) error { + if a == nil { + return ErrUnauthorized + } + caller := std.PrevRealm().Addr() + if caller != a.owner { + return ErrUnauthorized + } + + switch a.ownerType { + case AdminTypeWallet: + action() + case AdminTypeContract: + if a.contractHandler == nil { + return ErrUnauthorized + } + a.contractHandler.HandlePrivilegedAction(action) + default: + return ErrUnauthorized + } + return nil +} + +// DropAdmin revokes admin rights by setting the owner to an empty address. +// This can be used to disable any admin-specific actions. +func (a *Admin) DropAdmin() error { + if !a.CallerIsAdmin() { + return ErrUnauthorized + } + prevOwner := a.owner + a.owner = "" + a.ownerType = "" + a.contractHandler = nil + + std.Emit( + AdminTransferEvent, + "from", prevOwner.String(), + "to", "", + ) + return nil +} diff --git a/examples/gno.land/p/moul/admin/admin_test.gno b/examples/gno.land/p/moul/admin/admin_test.gno new file mode 100644 index 00000000000..80dabd7877f --- /dev/null +++ b/examples/gno.land/p/moul/admin/admin_test.gno @@ -0,0 +1,229 @@ +package admin + +import ( + "std" + "testing" + + "gno.land/p/demo/testutils" +) + +var ( + alice = testutils.TestAddress("alice") + bob = testutils.TestAddress("bob") +) + +// MockContractHandler implements ContractAdminHandler for testing +type MockContractHandler struct { + lastAction PrivilegedAction + called bool +} + +func (m *MockContractHandler) HandlePrivilegedAction(action PrivilegedAction) { + m.lastAction = action + m.called = true +} + +func TestNew(t *testing.T) { + std.TestSetRealm(std.NewUserRealm(alice)) + + a := New() + if got := a.Owner(); got != alice { + t.Errorf("New() owner = %v, want %v", got, alice) + } + if a.ownerType != AdminTypeWallet { + t.Errorf("New() ownerType = %v, want %v", a.ownerType, AdminTypeWallet) + } +} + +func TestNewWithAddress(t *testing.T) { + a := NewWithAddress(alice) + if got := a.Owner(); got != alice { + t.Errorf("NewWithAddress() owner = %v, want %v", got, alice) + } + if a.ownerType != AdminTypeWallet { + t.Errorf("NewWithAddress() ownerType = %v, want %v", a.ownerType, AdminTypeWallet) + } +} + +func TestCallerIsAdmin(t *testing.T) { + a := NewWithAddress(alice) + + // Test with correct admin + std.TestSetRealm(std.NewUserRealm(alice)) + if !a.CallerIsAdmin() { + t.Error("CallerIsAdmin() = false, want true for correct admin") + } + + // Test with incorrect admin + std.TestSetRealm(std.NewUserRealm(bob)) + if a.CallerIsAdmin() { + t.Error("CallerIsAdmin() = true, want false for incorrect admin") + } + + // Test with nil receiver + var nilAdmin *Admin + if nilAdmin.CallerIsAdmin() { + t.Error("CallerIsAdmin() = true, want false for nil receiver") + } +} + +func TestAssertCallerIsAdmin(t *testing.T) { + a := NewWithAddress(alice) + + // Test with correct admin + std.TestSetRealm(std.NewUserRealm(alice)) + a.AssertCallerIsAdmin() // Should not panic + + // Test with incorrect admin + std.TestSetRealm(std.NewUserRealm(bob)) + defer func() { + if r := recover(); r != ErrUnauthorized { + t.Errorf("AssertCallerIsAdmin() panic = %v, want %v", r, ErrUnauthorized) + } + }() + a.AssertCallerIsAdmin() // Should panic +} + +func TestTransferToWallet(t *testing.T) { + a := NewWithAddress(alice) + std.TestSetRealm(std.NewUserRealm(alice)) + + // Test successful transfer + if err := a.TransferToWallet(bob); err != nil { + t.Errorf("TransferToWallet() error = %v, want nil", err) + } + if got := a.Owner(); got != bob { + t.Errorf("Owner after transfer = %v, want %v", got, bob) + } + + // Test unauthorized transfer + if err := a.TransferToWallet(alice); err != ErrUnauthorized { + t.Errorf("TransferToWallet() error = %v, want %v", err, ErrUnauthorized) + } + + // Test invalid address + std.TestSetRealm(std.NewUserRealm(bob)) + if err := a.TransferToWallet(""); err != ErrInvalidAddress { + t.Errorf("TransferToWallet() error = %v, want %v", err, ErrInvalidAddress) + } +} + +func TestTransferToContract(t *testing.T) { + a := NewWithAddress(alice) + std.TestSetRealm(std.NewUserRealm(alice)) + handler := &MockContractHandler{} + + // Test successful transfer + if err := a.TransferToContract(bob, handler); err != nil { + t.Errorf("TransferToContract() error = %v, want nil", err) + } + if got := a.Owner(); got != bob { + t.Errorf("Owner after transfer = %v, want %v", got, bob) + } + if a.ownerType != AdminTypeContract { + t.Errorf("ownerType after transfer = %v, want %v", a.ownerType, AdminTypeContract) + } + if a.contractHandler != handler { + t.Error("contractHandler not set correctly") + } + + // Test unauthorized transfer + if err := a.TransferToContract(alice, handler); err != ErrUnauthorized { + t.Errorf("TransferToContract() error = %v, want %v", err, ErrUnauthorized) + } + + // Test invalid address + std.TestSetRealm(std.NewUserRealm(bob)) + if err := a.TransferToContract("", handler); err != ErrInvalidAddress { + t.Errorf("TransferToContract() error = %v, want %v", err, ErrInvalidAddress) + } +} + +func TestAdminToDo(t *testing.T) { + a := NewWithAddress(alice) + std.TestSetRealm(std.NewUserRealm(alice)) + + // Test wallet-based admin + var executed bool + action := func() { executed = true } + + if err := a.AdminToDo(action); err != nil { + t.Errorf("AdminToDo() error = %v, want nil", err) + } + if !executed { + t.Error("AdminToDo() did not execute action for wallet-based admin") + } + + // Test contract-based admin + handler := &MockContractHandler{} + if err := a.TransferToContract(bob, handler); err != nil { + t.Errorf("TransferToContract() error = %v, want nil", err) + } + + std.TestSetRealm(std.NewUserRealm(bob)) + executed = false + if err := a.AdminToDo(action); err != nil { + t.Errorf("AdminToDo() error = %v, want nil", err) + } + if !handler.called { + t.Error("AdminToDo() did not call handler for contract-based admin") + } + + // Test unauthorized + std.TestSetRealm(std.NewUserRealm(alice)) + if err := a.AdminToDo(action); err != ErrUnauthorized { + t.Errorf("AdminToDo() error = %v, want %v", err, ErrUnauthorized) + } +} + +func TestDropAdmin(t *testing.T) { + a := NewWithAddress(alice) + std.TestSetRealm(std.NewUserRealm(alice)) + + // Test successful drop + if err := a.DropAdmin(); err != nil { + t.Errorf("DropAdmin() error = %v, want nil", err) + } + if got := a.Owner(); got != "" { + t.Errorf("Owner after drop = %v, want empty", got) + } + if a.ownerType != "" { + t.Errorf("ownerType after drop = %v, want empty", a.ownerType) + } + if a.contractHandler != nil { + t.Error("contractHandler after drop should be nil") + } + + // Test unauthorized drop + std.TestSetRealm(std.NewUserRealm(bob)) + if err := a.DropAdmin(); err != ErrUnauthorized { + t.Errorf("DropAdmin() error = %v, want %v", err, ErrUnauthorized) + } +} + +func TestNilReceiver(t *testing.T) { + var a *Admin + + // Test Owner() + if got := a.Owner(); got != "" { + t.Errorf("Owner() = %v, want empty for nil receiver", got) + } + + // Test CallerIsAdmin() + if a.CallerIsAdmin() { + t.Error("CallerIsAdmin() = true, want false for nil receiver") + } + + // Test AdminToDo() + if err := a.AdminToDo(func() {}); err != ErrUnauthorized { + t.Errorf("AdminToDo() error = %v, want %v for nil receiver", err, ErrUnauthorized) + } + + // Test AssertCallerIsAdmin() + defer func() { + if r := recover(); r != ErrUnauthorized { + t.Errorf("AssertCallerIsAdmin() panic = %v, want %v for nil receiver", r, ErrUnauthorized) + } + }() + a.AssertCallerIsAdmin() +} diff --git a/examples/gno.land/p/moul/admin/gno.mod b/examples/gno.land/p/moul/admin/gno.mod new file mode 100644 index 00000000000..5885bce01b6 --- /dev/null +++ b/examples/gno.land/p/moul/admin/gno.mod @@ -0,0 +1 @@ +module gno.land/p/moul/admin \ No newline at end of file