Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(examples): Implement GNS (Gno Name Service) #2

Closed
wants to merge 15 commits into from
215 changes: 215 additions & 0 deletions examples/gno.land/p/demo/gns/domain_registry.gno
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
package gns

import (
"std"
"time"

"gno.land/p/demo/avl"
"gno.land/p/demo/grc/grc721"
)

// domainRegistry represents a registry for domain names with metadata
type domainRegistry struct {
domains grc721.IGRC721 // Interface for basic NFT functionality
metadata *avl.Tree // AVL tree for storing domain metadata
}

// DomainRegistry defines the methods for managing domain names and metadata
type DomainRegistry interface {
BalanceOf(owner std.Address) (uint64, error)
OwnerOf(domainName string) (std.Address, error)
SafeTransferFrom(from, to std.Address, domainName string) error
TransferFrom(from, to std.Address, domainName string) error
Approve(approved std.Address, domainName string) error
SetApprovalForAll(operator std.Address, approved bool) error
GetApproved(domainName string) (std.Address, error)
IsApprovedForAll(owner, operator std.Address) bool
Mint(to std.Address, domainName string) error

RegisterDomain(owner std.Address, domainName string, metadata Metadata) error
SetDomainData(domainName string, metadata Metadata) error
GetDomainData(domainName string, field MetadataField) (Metadata, error)
GetDomainFields(domainName string, fields []MetadataField) (Metadata, error)
RenewDomain(domainName string, additionalDuration time.Duration) error
}

// NewDomainRegistry creates a new domain registry with metadata extensions
func NewDomainRegistry(name, symbol string) *domainRegistry {
registry := grc721.NewBasicNFT(name, symbol)

return &domainRegistry{
domains: registry,
metadata: avl.NewTree(),
}
}

func (d *domainRegistry) RegisterDomain(owner std.Address, domainName string, metadata Metadata) error {
err := d.domains.Mint(owner, grc721.TokenID(domainName))
if err != nil {
return err
}

d.metadata.Set(domainName, metadata)

return nil
}

// RenewDomain extends the expiration time of a domain name
func (d *domainRegistry) RenewDomain(domainName string, additionalDuration time.Duration) error {
data, found := d.metadata.Get(domainName)
if !found {
return ErrInvalidDomainName
}

metadata := data.(Metadata)

owner, err := d.domains.OwnerOf(grc721.TokenID(domainName))
if err != nil {
return err
}

caller := std.PrevRealm().Addr()
if caller != owner {
return ErrUnauthorized
}

// Todo : apply deduction renewal fee here

metadata.ExpirationTime.Add(additionalDuration)
d.metadata.Set(domainName, metadata)

return nil
}

// SetDomainData sets the metadata for a given domain name
func (d *domainRegistry) SetDomainData(domainName string, metadata Metadata) error {
owner, err := d.domains.OwnerOf(grc721.TokenID(domainName))
if err != nil {
return err
}

caller := std.PrevRealm().Addr()
if caller != owner {
return ErrUnauthorized
}

d.metadata.Set(domainName, metadata)
return nil
}

// GetDomainFields retrieves multiple fields of metadata for a given domain
func (d *domainRegistry) GetDomainFields(domainName string, fields []MetadataField) (Metadata, error) {
data, found := d.metadata.Get(domainName)
if !found {
return Metadata{}, ErrInvalidDomainName
}

metadata := data.(Metadata)

if len(fields) == 0 {
return metadata, nil
}

var result Metadata
for _, field := range fields {
switch field {
case FieldAvatar:
result.Avatar = metadata.Avatar
case FieldRegistrationTime:
result.RegistrationTime = metadata.RegistrationTime
case FieldExpirationTime:
result.ExpirationTime = metadata.ExpirationTime
case FieldRenewalFee:
result.RenewalFee = metadata.RenewalFee
case FieldAttributes:
result.Attributes = metadata.Attributes
case FieldDescription:
result.Description = metadata.Description
case FieldContactInfo:
result.ContactInfo = metadata.ContactInfo
default:
return Metadata{}, ErrInvalidMetadataField
}
}

return result, nil
}

// GetDomainData retrieves metadata for a given domain
func (d *domainRegistry) GetDomainData(domainName string, field MetadataField) (Metadata, error) {
data, found := d.metadata.Get(domainName)
if !found {
return Metadata{}, ErrInvalidDomainName
}

metadata := data.(Metadata)

switch field {
case FieldAvatar:
return Metadata{
Avatar: metadata.Avatar,
}, nil
case FieldRegistrationTime:
return Metadata{
RegistrationTime: metadata.RegistrationTime,
}, nil
case FieldExpirationTime:
return Metadata{
ExpirationTime: metadata.ExpirationTime,
}, nil
case FieldRenewalFee:
return Metadata{
RenewalFee: metadata.RenewalFee,
}, nil
case FieldAttributes:
return Metadata{
Attributes: metadata.Attributes,
}, nil
case FieldDescription:
return Metadata{
Description: metadata.Description,
}, nil
case FieldContactInfo:
return Metadata{
ContactInfo: metadata.ContactInfo,
}, nil
default:
return Metadata{}, ErrInvalidMetadataField
}
}

func (d *domainRegistry) BalanceOf(owner std.Address) (uint64, error) {
return d.domains.BalanceOf(owner)
}

func (d *domainRegistry) OwnerOf(domainName string) (std.Address, error) {
return d.domains.OwnerOf(grc721.TokenID(domainName))
}

func (d *domainRegistry) SafeTransferFrom(from, to std.Address, domainName string) error {
return d.domains.SafeTransferFrom(from, to, grc721.TokenID(domainName))
}

func (d *domainRegistry) TransferFrom(from, to std.Address, domainName string) error {
return d.domains.TransferFrom(from, to, grc721.TokenID(domainName))
}

func (d *domainRegistry) Approve(approved std.Address, domainName string) error {
return d.domains.Approve(approved, grc721.TokenID(domainName))
}

func (d *domainRegistry) SetApprovalForAll(operator std.Address, approved bool) error {
return d.domains.SetApprovalForAll(operator, approved)
}

func (d *domainRegistry) GetApproved(domainName string) (std.Address, error) {
return d.domains.GetApproved(grc721.TokenID(domainName))
}

func (d *domainRegistry) IsApprovedForAll(owner, operator std.Address) bool {
return d.domains.IsApprovedForAll(owner, operator)
}

func (d *domainRegistry) Mint(to std.Address, domainName string) error {
return d.domains.Mint(to, grc721.TokenID(domainName))
}
134 changes: 134 additions & 0 deletions examples/gno.land/p/demo/gns/domain_registry_test.gno
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
package gns

import (
"std"
"testing"
"time"

"gno.land/p/demo/grc/grc721"
"gno.land/p/demo/testutils"
"gno.land/p/demo/urequire"
)

var (
addr1 = testutils.TestAddress("bob")
addr2 = testutils.TestAddress("alice")
)

func TestSetAndGetMetadata(t *testing.T) {
registry := NewDomainRegistry("Test Registry", "TST")

// Create a mock owner address
owner := addr1

std.TestSetRealm(std.NewUserRealm(owner))
std.TestSetOrigCaller(owner)

metadata := Metadata{
Avatar: "avatar_url",
RegistrationTime: time.Now(),
ExpirationTime: time.Now().Add(365 * 24 * time.Hour),
Attributes: []Trait{
{DisplayType: "string", TraitType: "type", Value: "example"},
},
Description: "Test domain",
ContactInfo: "contact@example.com",
}

// Mint a new domain for the owner
domainName := "test.gno"
err := registry.domains.Mint(owner, grc721.TokenID(domainName))
urequire.NoError(t, err)

// Set metadata for the domain
err = registry.SetDomainData(domainName, metadata)
urequire.NoError(t, err)

// Retrieve the metadata
retrievedMetadata, err := registry.GetDomainData(domainName, FieldAvatar)
urequire.NoError(t, err)
urequire.Equal(t, metadata.Avatar, retrievedMetadata.Avatar)
}

func TestSetMetadata_NotOwner(t *testing.T) {
registry := NewDomainRegistry("Test Registry", "TST")

// Create a mock owner and non-owner address
owner := addr1
nonOwner := addr2

metadata := Metadata{
Avatar: "avatar_url",
RegistrationTime: time.Now(),
ExpirationTime: time.Now().Add(365 * 24 * time.Hour),
Attributes: []Trait{
{DisplayType: "string", TraitType: "type", Value: "example"},
},
Description: "Test domain",
ContactInfo: "contact@example.com",
}

// Mint a new domain for the owner
domainName := "test.gno"
err := registry.domains.Mint(owner, grc721.TokenID(domainName))
urequire.NoError(t, err)

// Try to set metadata for the domain as a non-owner
std.TestSetRealm(std.NewUserRealm(nonOwner))
std.TestSetOrigCaller(nonOwner)

err = registry.SetDomainData(domainName, metadata)
urequire.Error(t, err)
urequire.Equal(t, err.Error(), ErrUnauthorized.Error())
}

func TestGetMetadata_InvalidDomain(t *testing.T) {
registry := NewDomainRegistry("Test Registry", "TST")

// Try to get metadata for a domain that doesn't exist
domainName := "nonexistent.gno"
_, err := registry.GetDomainFields(domainName, []MetadataField{})
urequire.Error(t, err)
urequire.Equal(t, err.Error(), ErrInvalidDomainName.Error())
}

func TestRenewDomain_Success(t *testing.T) {
registry := NewDomainRegistry("Test Registry", "TST")

// Create a mock owner address
owner := addr1

std.TestSetRealm(std.NewUserRealm(owner))
std.TestSetOrigCaller(owner)

metadata := Metadata{
Avatar: "avatar_url",
RegistrationTime: time.Now(),
ExpirationTime: time.Now().Add(365 * 24 * time.Hour),
Attributes: []Trait{
{DisplayType: "string", TraitType: "type", Value: "example"},
},
Description: "Test domain",
ContactInfo: "contact@example.com",
}

// Mint a new domain for the owner
domainName := "renewable.gno"
err := registry.domains.Mint(owner, grc721.TokenID(domainName))
urequire.NoError(t, err)

// Set metadata for the domain
err = registry.SetDomainData(domainName, metadata)
urequire.NoError(t, err)

// Renew the domain
additionalDuration := 30 * 24 * time.Hour
err = registry.RenewDomain(domainName, additionalDuration)
urequire.NoError(t, err)

// Check that the expiration was extended
renewedMetadata, err := registry.GetDomainData(domainName, FieldExpirationTime)
urequire.NoError(t, err)
expectedExpirationTime := metadata.ExpirationTime
urequire.Equal(t, expectedExpirationTime.Unix(), renewedMetadata.ExpirationTime.Unix())
}
12 changes: 12 additions & 0 deletions examples/gno.land/p/demo/gns/errors.gno
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package gns

import (
"errors"
)

var (
ErrUnauthorized = errors.New("caller is not domain owner")
ErrInvalidDomainName = errors.New("invalid domain name")
ErrInvalidMetadataField = errors.New("invalid metadata field")
ErrInsufficientFunds = errors.New("insufficient funds for renewal")
)
7 changes: 7 additions & 0 deletions examples/gno.land/p/demo/gns/gno.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
module gno.land/r/demo/gns

require (
gno.land/p/demo/grc/grc721 v0.0.0-latest
gno.land/p/demo/ownable v0.0.0-latest
gno.land/p/demo/uassert v0.0.0-latest
)
Loading
Loading