-
Notifications
You must be signed in to change notification settings - Fork 389
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
## 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
1 parent
b92a6a4
commit 384d2be
Showing
3 changed files
with
307 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
module gno.land/r/demo/btree_dao |