Skip to content

Commit

Permalink
feat: add btree_dao to demo (#3388)
Browse files Browse the repository at this point in the history
## Description

BTree DAO is a little organisation of people who used BTree by
@wyhaines. It serves the purpose of demonstrating some of the
functionalities like iterating through the list from start and the end,
adding nodes and general implementation of the BTree. Besides that, it
encourages developers to use BTree and join the DAO.

### How it works:
Currently the realm has 2 ways of members joining: 
- Either by submiting the BTree instance used in some other realm
- Or by sending a direct transaction using gnokey or Studio Connect
having string as argument

Both ways allow members to become a part of the DAO but at different
levels: the idea is that if a user decides to submit his own BTree he
becomes a true member, and if a user supports the cause and just want to
hang around until he gets the hang of the BTree implementation he can do
so by joining thorugh a 'seed'. In the realm joining functions are
PlantTree and PlantSeed for different roles.
When a member joins he gets minted an NFT made using basic_nft.gno from
GRC721 which is like his little proof of membership.

### Concerns 
To become a 'tree' member developer needs to submit his whole BTree
which might be a little unsafe as that BTree might contain some
sensitive information. I would like to hear an opinion on this of
someone more experienced, as it is right now I have been extra careful
not to expose any of the information (besides size) from the submitted
BTrees.

### Contributors checklist:
- [x] Create a BTree to store all the member info
- [x] Implement Record interface, make nodes' creation times be compared
- [x] Implement DAO joining process for 2 types of members
- [x] Make a Render() function to display member addresses
- [x] Demonstrate more of BTree functionalities
- [x] Add tests

---------

Co-authored-by: Leon Hudak <[email protected]>
  • Loading branch information
matijamarjanovic and leohhhn authored Jan 9, 2025
1 parent b92a6a4 commit 384d2be
Show file tree
Hide file tree
Showing 3 changed files with 307 additions and 0 deletions.
209 changes: 209 additions & 0 deletions examples/gno.land/r/demo/btree_dao/btree_dao.gno
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
package btree_dao

import (
"errors"
"std"
"strings"
"time"

"gno.land/p/demo/btree"
"gno.land/p/demo/grc/grc721"
"gno.land/p/demo/ufmt"
"gno.land/p/moul/md"
)

// RegistrationDetails holds the details of a user's registration in the BTree DAO.
// It stores the user's address, registration time, their B-Tree if they planted one,
// and their NFT ID.
type RegistrationDetails struct {
Address std.Address
RegTime time.Time
UserBTree *btree.BTree
NFTID string
}

// Less implements the btree.Record interface for RegistrationDetails.
// It compares two RegistrationDetails based on their registration time.
// Returns true if the current registration time is before the other registration time.
func (rd *RegistrationDetails) Less(than btree.Record) bool {
other := than.(*RegistrationDetails)
return rd.RegTime.Before(other.RegTime)
}

var (
dao = grc721.NewBasicNFT("BTree DAO", "BTDAO")
tokenID = 0
members = btree.New()
)

// PlantTree allows a user to plant their B-Tree in the DAO forest.
// It mints an NFT to the user and registers their tree in the DAO.
// Returns an error if the tree is already planted, empty, or if NFT minting fails.
func PlantTree(userBTree *btree.BTree) error {
return plantImpl(userBTree, "")
}

// PlantSeed allows a user to register as a seed in the DAO with a message.
// It mints an NFT to the user and registers them as a seed member.
// Returns an error if the message is empty or if NFT minting fails.
func PlantSeed(message string) error {
return plantImpl(nil, message)
}

// plantImpl is the internal implementation that handles both tree planting and seed registration.
// For tree planting (userBTree != nil), it verifies the tree isn't already planted and isn't empty.
// For seed planting (userBTree == nil), it verifies the seed message isn't empty.
// In both cases, it mints an NFT to the user and adds their registration details to the members tree.
// Returns an error if any validation fails or if NFT minting fails.
func plantImpl(userBTree *btree.BTree, seedMessage string) error {
// Get the caller's address
userAddress := std.GetOrigCaller()

var nftID string
var regDetails *RegistrationDetails

if userBTree != nil {
// Handle tree planting
var treeExists bool
members.Ascend(func(record btree.Record) bool {
regDetails := record.(*RegistrationDetails)
if regDetails.UserBTree == userBTree {
treeExists = true
return false
}
return true
})
if treeExists {
return errors.New("tree is already planted in the forest")
}

if userBTree.Len() == 0 {
return errors.New("cannot plant an empty tree")
}

nftID = ufmt.Sprintf("%d", tokenID)
regDetails = &RegistrationDetails{
Address: userAddress,
RegTime: time.Now(),
UserBTree: userBTree,
NFTID: nftID,
}
} else {
// Handle seed planting
if seedMessage == "" {
return errors.New("seed message cannot be empty")
}
nftID = "seed_" + ufmt.Sprintf("%d", tokenID)
regDetails = &RegistrationDetails{
Address: userAddress,
RegTime: time.Now(),
UserBTree: nil,
NFTID: nftID,
}
}

// Mint an NFT to the user
err := dao.Mint(userAddress, grc721.TokenID(nftID))
if err != nil {
return err
}

members.Insert(regDetails)
tokenID++
return nil
}

// Render generates a Markdown representation of the DAO members.
// It displays:
// - Total number of NFTs minted
// - Total number of members
// - Size of the biggest planted tree
// - The first 3 members (OGs)
// - The latest 10 members
// Each member entry includes their address and owned NFTs (🌳 for trees, 🌱 for seeds).
// The path parameter is currently unused.
// Returns a formatted Markdown string.
func Render(path string) string {
var latestMembers []string
var ogMembers []string

// Get total size and first member
totalSize := members.Len()
biggestTree := 0
if maxMember := members.Max(); maxMember != nil {
if userBTree := maxMember.(*RegistrationDetails).UserBTree; userBTree != nil {
biggestTree = userBTree.Len()
}
}

// Collect the latest 10 members
members.Descend(func(record btree.Record) bool {
if len(latestMembers) < 10 {
regDetails := record.(*RegistrationDetails)
addr := regDetails.Address
nftList := ""
balance, err := dao.BalanceOf(addr)
if err == nil && balance > 0 {
nftList = " (NFTs: "
for i := uint64(0); i < balance; i++ {
if i > 0 {
nftList += ", "
}
if regDetails.UserBTree == nil {
nftList += "🌱#" + regDetails.NFTID
} else {
nftList += "🌳#" + regDetails.NFTID
}
}
nftList += ")"
}
latestMembers = append(latestMembers, string(addr)+nftList)
return true
}
return false
})

// Collect the first 3 members (OGs)
members.Ascend(func(record btree.Record) bool {
if len(ogMembers) < 3 {
regDetails := record.(*RegistrationDetails)
addr := regDetails.Address
nftList := ""
balance, err := dao.BalanceOf(addr)
if err == nil && balance > 0 {
nftList = " (NFTs: "
for i := uint64(0); i < balance; i++ {
if i > 0 {
nftList += ", "
}
if regDetails.UserBTree == nil {
nftList += "🌱#" + regDetails.NFTID
} else {
nftList += "🌳#" + regDetails.NFTID
}
}
nftList += ")"
}
ogMembers = append(ogMembers, string(addr)+nftList)
return true
}
return false
})

var sb strings.Builder

sb.WriteString(md.H1("B-Tree DAO Members"))
sb.WriteString(md.H2("Total NFTs Minted"))
sb.WriteString(ufmt.Sprintf("Total NFTs minted: %d\n\n", dao.TokenCount()))
sb.WriteString(md.H2("Member Stats"))
sb.WriteString(ufmt.Sprintf("Total members: %d\n", totalSize))
if biggestTree > 0 {
sb.WriteString(ufmt.Sprintf("Biggest tree size: %d\n", biggestTree))
}
sb.WriteString(md.H2("OG Members"))
sb.WriteString(md.BulletList(ogMembers))
sb.WriteString(md.H2("Latest Members"))
sb.WriteString(md.BulletList(latestMembers))

return sb.String()
}
97 changes: 97 additions & 0 deletions examples/gno.land/r/demo/btree_dao/btree_dao_test.gno
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package btree_dao

import (
"std"
"strings"
"testing"
"time"

"gno.land/p/demo/btree"
"gno.land/p/demo/uassert"
"gno.land/p/demo/urequire"
)

func setupTest() {
std.TestSetOrigCaller(std.Address("g1ej0qca5ptsw9kfr64ey8jvfy9eacga6mpj2z0y"))
members = btree.New()
}

type TestElement struct {
value int
}

func (te *TestElement) Less(than btree.Record) bool {
return te.value < than.(*TestElement).value
}

func TestPlantTree(t *testing.T) {
setupTest()

tree := btree.New()
elements := []int{30, 10, 50, 20, 40}
for _, val := range elements {
tree.Insert(&TestElement{value: val})
}

err := PlantTree(tree)
urequire.NoError(t, err)

found := false
members.Ascend(func(record btree.Record) bool {
regDetails := record.(*RegistrationDetails)
if regDetails.UserBTree == tree {
found = true
return false
}
return true
})
uassert.True(t, found)

err = PlantTree(tree)
uassert.Error(t, err)

emptyTree := btree.New()
err = PlantTree(emptyTree)
uassert.Error(t, err)
}

func TestPlantSeed(t *testing.T) {
setupTest()

err := PlantSeed("Hello DAO!")
urequire.NoError(t, err)

found := false
members.Ascend(func(record btree.Record) bool {
regDetails := record.(*RegistrationDetails)
if regDetails.UserBTree == nil {
found = true
uassert.NotEmpty(t, regDetails.NFTID)
uassert.True(t, strings.Contains(regDetails.NFTID, "seed_"))
return false
}
return true
})
uassert.True(t, found)

err = PlantSeed("")
uassert.Error(t, err)
}

func TestRegistrationDetailsOrdering(t *testing.T) {
setupTest()

rd1 := &RegistrationDetails{
Address: std.Address("test1"),
RegTime: time.Now(),
NFTID: "0",
}
rd2 := &RegistrationDetails{
Address: std.Address("test2"),
RegTime: time.Now().Add(time.Hour),
NFTID: "1",
}

uassert.True(t, rd1.Less(rd2))
uassert.False(t, rd2.Less(rd1))
}
1 change: 1 addition & 0 deletions examples/gno.land/r/demo/btree_dao/gno.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module gno.land/r/demo/btree_dao

0 comments on commit 384d2be

Please sign in to comment.