From ec222ec8086bc07139bbdf9ef9e9448eaded5f52 Mon Sep 17 00:00:00 2001 From: Stefan Nikolic Date: Tue, 22 Oct 2024 15:35:43 +0200 Subject: [PATCH] feat(examples): add interactive realm `r/stefann/home` (#2918) # Description: This PR finalizes the core functionality of my home realm project, focusing on a dynamic and interactive home realm experience, driven by GNOT donations. The following key features and enhancements are introduced in this PR: ## Key Features: - **Dynamic Background Change:** - Implemented sequential background changes triggered by GNOT donations. Each donation updates the city background in a fixed order, cycling through a predefined set of cities. The background change is seamless, offering a "traveling" experience for donors. - **Sponsor Leaderboard:** - Added a sponsor leaderboard to showcase the top contributors based on their GNOT donations. The list displays the addresses of the top sponsors, formatted for readability (first and last characters with ellipses), and limits the displayed sponsors to the configurable `maxSponsors` setting. - **Donation Validation:** - Introduced a strict GNOT validation check. The system now rejects any donation attempts that don't include GNOT, ensuring only valid contributions update the state of the realm. - **Owner Withdrawal of Donations:** - Implemented a feature that allows the realm owner to withdraw accumulated GNOT donations, providing control over the funds contributed by supporters. - **Home Realm Configurations:** - **Cities:** Cities can be dynamically updated to refresh the possible backgrounds. - **Maximum Sponsors:** Admins can configure the number of sponsors shown on the leaderboard with the `maxSponsors` setting. - **Jar Link:** Admins can update the link to the donation jar. --- examples/gno.land/r/stefann/home/gno.mod | 9 + examples/gno.land/r/stefann/home/home.gno | 303 ++++++++++++++++++ .../gno.land/r/stefann/home/home_test.gno | 291 +++++++++++++++++ examples/gno.land/r/stefann/registry/gno.mod | 3 + .../gno.land/r/stefann/registry/registry.gno | 51 +++ 5 files changed, 657 insertions(+) create mode 100644 examples/gno.land/r/stefann/home/gno.mod create mode 100644 examples/gno.land/r/stefann/home/home.gno create mode 100644 examples/gno.land/r/stefann/home/home_test.gno create mode 100644 examples/gno.land/r/stefann/registry/gno.mod create mode 100644 examples/gno.land/r/stefann/registry/registry.gno diff --git a/examples/gno.land/r/stefann/home/gno.mod b/examples/gno.land/r/stefann/home/gno.mod new file mode 100644 index 00000000000..dd556e7f817 --- /dev/null +++ b/examples/gno.land/r/stefann/home/gno.mod @@ -0,0 +1,9 @@ +module gno.land/r/stefann/home + +require ( + gno.land/p/demo/avl v0.0.0-latest + gno.land/p/demo/ownable v0.0.0-latest + gno.land/p/demo/testutils v0.0.0-latest + gno.land/p/demo/ufmt v0.0.0-latest + gno.land/r/stefann/registry v0.0.0-latest +) diff --git a/examples/gno.land/r/stefann/home/home.gno b/examples/gno.land/r/stefann/home/home.gno new file mode 100644 index 00000000000..f40329ebf7e --- /dev/null +++ b/examples/gno.land/r/stefann/home/home.gno @@ -0,0 +1,303 @@ +package home + +import ( + "sort" + "std" + "strings" + + "gno.land/p/demo/avl" + "gno.land/p/demo/ownable" + "gno.land/p/demo/ufmt" + + "gno.land/r/stefann/registry" +) + +type City struct { + Name string + URL string +} + +type Sponsor struct { + Address std.Address + Amount std.Coins +} + +type Profile struct { + pfp string + aboutMe []string +} + +type Travel struct { + cities []City + currentCityIndex int + jarLink string +} + +type Sponsorship struct { + maxSponsors int + sponsors *avl.Tree + DonationsCount int + sponsorsCount int +} + +var ( + profile Profile + travel Travel + sponsorship Sponsorship + owner *ownable.Ownable +) + +func init() { + owner = ownable.NewWithAddress(registry.MainAddr()) + + profile = Profile{ + pfp: "https://i.ibb.co/Bc5YNCx/DSC-0095a.jpg", + aboutMe: []string{ + `### About Me`, + `Hey there! I’m Stefan, a student of Computer Science. I’m all about exploring and adventure — whether it’s diving into the latest tech or discovering a new city, I’m always up for the challenge!`, + + `### Contributions`, + `I'm just getting started, but you can follow my journey through Gno.land right [here](https://github.com/gnolang/hackerspace/issues/94) 🔗`, + }, + } + + travel = Travel{ + cities: []City{ + {Name: "Venice", URL: "https://i.ibb.co/1mcZ7b1/venice.jpg"}, + {Name: "Tokyo", URL: "https://i.ibb.co/wNDJv3H/tokyo.jpg"}, + {Name: "São Paulo", URL: "https://i.ibb.co/yWMq2Sn/sao-paulo.jpg"}, + {Name: "Toronto", URL: "https://i.ibb.co/pb95HJB/toronto.jpg"}, + {Name: "Bangkok", URL: "https://i.ibb.co/pQy3w2g/bangkok.jpg"}, + {Name: "New York", URL: "https://i.ibb.co/6JWLm0h/new-york.jpg"}, + {Name: "Paris", URL: "https://i.ibb.co/q9vf6Hs/paris.jpg"}, + {Name: "Kandersteg", URL: "https://i.ibb.co/60DzywD/kandersteg.jpg"}, + {Name: "Rothenburg", URL: "https://i.ibb.co/cr8d2rQ/rothenburg.jpg"}, + {Name: "Capetown", URL: "https://i.ibb.co/bPGn0v3/capetown.jpg"}, + {Name: "Sydney", URL: "https://i.ibb.co/TBNzqfy/sydney.jpg"}, + {Name: "Oeschinen Lake", URL: "https://i.ibb.co/QJQwp2y/oeschinen-lake.jpg"}, + {Name: "Barra Grande", URL: "https://i.ibb.co/z4RXKc1/barra-grande.jpg"}, + {Name: "London", URL: "https://i.ibb.co/CPGtvgr/london.jpg"}, + }, + currentCityIndex: 0, + jarLink: "https://TODO", // This value should be injected through UpdateJarLink after deployment. + } + + sponsorship = Sponsorship{ + maxSponsors: 5, + sponsors: avl.NewTree(), + DonationsCount: 0, + sponsorsCount: 0, + } +} + +func UpdateCities(newCities []City) { + owner.AssertCallerIsOwner() + travel.cities = newCities +} + +func AddCities(newCities ...City) { + owner.AssertCallerIsOwner() + + travel.cities = append(travel.cities, newCities...) +} + +func UpdateJarLink(newLink string) { + owner.AssertCallerIsOwner() + travel.jarLink = newLink +} + +func UpdatePFP(url string) { + owner.AssertCallerIsOwner() + profile.pfp = url +} + +func UpdateAboutMe(aboutMeStr string) { + owner.AssertCallerIsOwner() + profile.aboutMe = strings.Split(aboutMeStr, "|") +} + +func AddAboutMeRows(newRows ...string) { + owner.AssertCallerIsOwner() + + profile.aboutMe = append(profile.aboutMe, newRows...) +} + +func UpdateMaxSponsors(newMax int) { + owner.AssertCallerIsOwner() + if newMax <= 0 { + panic("maxSponsors must be greater than zero") + } + sponsorship.maxSponsors = newMax +} + +func Donate() { + address := std.GetOrigCaller() + amount := std.GetOrigSend() + + if amount.AmountOf("ugnot") == 0 { + panic("Donation must include GNOT") + } + + existingAmount, exists := sponsorship.sponsors.Get(address.String()) + if exists { + updatedAmount := existingAmount.(std.Coins).Add(amount) + sponsorship.sponsors.Set(address.String(), updatedAmount) + } else { + sponsorship.sponsors.Set(address.String(), amount) + sponsorship.sponsorsCount++ + } + + travel.currentCityIndex++ + sponsorship.DonationsCount++ + + banker := std.GetBanker(std.BankerTypeRealmSend) + ownerAddr := registry.MainAddr() + banker.SendCoins(std.CurrentRealm().Addr(), ownerAddr, banker.GetCoins(std.CurrentRealm().Addr())) +} + +type SponsorSlice []Sponsor + +func (s SponsorSlice) Len() int { + return len(s) +} + +func (s SponsorSlice) Less(i, j int) bool { + return s[i].Amount.AmountOf("ugnot") > s[j].Amount.AmountOf("ugnot") +} + +func (s SponsorSlice) Swap(i, j int) { + s[i], s[j] = s[j], s[i] +} + +func GetTopSponsors() []Sponsor { + var sponsorSlice SponsorSlice + + sponsorship.sponsors.Iterate("", "", func(key string, value interface{}) bool { + addr := std.Address(key) + amount := value.(std.Coins) + sponsorSlice = append(sponsorSlice, Sponsor{Address: addr, Amount: amount}) + return false + }) + + sort.Sort(sponsorSlice) + return sponsorSlice +} + +func GetTotalDonations() int { + total := 0 + sponsorship.sponsors.Iterate("", "", func(key string, value interface{}) bool { + total += int(value.(std.Coins).AmountOf("ugnot")) + return false + }) + return total +} + +func Render(path string) string { + out := ufmt.Sprintf("# Exploring %s!\n\n", travel.cities[travel.currentCityIndex].Name) + + out += renderAboutMe() + out += "\n\n" + out += renderTips() + + return out +} + +func renderAboutMe() string { + out := "
" + + out += "
\n\n" + + out += ufmt.Sprintf("
\n\n", travel.cities[travel.currentCityIndex%len(travel.cities)].URL) + + out += ufmt.Sprintf("my profile pic\n\n", profile.pfp) + + out += "
\n\n" + + for _, rows := range profile.aboutMe { + out += "
\n\n" + out += rows + "\n\n" + out += "
\n\n" + } + + out += "
\n\n" + + return out +} + +func renderTips() string { + out := `
` + "\n\n" + + out += `
` + "\n" + + out += `

Help Me Travel The World

` + "\n\n" + + out += renderTipsJar() + "\n" + + out += ufmt.Sprintf(`I am currently in %s,
tip the jar to send me somewhere else!
`, travel.cities[travel.currentCityIndex].Name) + + out += `
Click the jar, tip in GNOT coins, and watch my background change as I head to a new adventure!

` + "\n\n" + + out += renderSponsors() + + out += `
` + "\n\n" + + out += `
` + "\n" + + return out +} + +func formatAddress(address string) string { + if len(address) <= 8 { + return address + } + return address[:4] + "..." + address[len(address)-4:] +} + +func renderSponsors() string { + out := `

Sponsor Leaderboard

` + "\n" + + if sponsorship.sponsorsCount == 0 { + return out + `

No sponsors yet. Be the first to tip the jar!

` + "\n" + } + + topSponsors := GetTopSponsors() + numSponsors := len(topSponsors) + if numSponsors > sponsorship.maxSponsors { + numSponsors = sponsorship.maxSponsors + } + + out += `