github.com/gnolang/gno@v0.0.0-20240520182011-228e9d0192ce/docs/how-to-guides/write-simple-dapp.md (about) 1 --- 2 id: write-simple-dapp 3 --- 4 5 # How to write a simple dApp on Gno.land 6 7 ## Overview 8 9 This guide will show you how to write a complete dApp that combines both a package and a realm. 10 Our app will allow any user to create a poll, and subsequently vote 11 YAY or NAY for any poll that has not exceeded the voting deadline. 12 13 ## Defining dApp functionality 14 15 Our dApp will consist of a Poll package, which will handle all things related to the Poll struct, 16 and a Poll Factory realm, which will handle the user-facing functionality and rendering. 17 18 For simplicity, we will define the functionality in plain text, and leave comments explaining the code. 19 20 ### Poll Package 21 22 - Defines a `Poll` struct 23 - Defines a `NewPoll` constructor 24 - Defines `Poll` field getters 25 - Defines a `Vote` function 26 - Defines a `HasVoted` check method 27 - Defines a `VoteCount` getter method 28 29 [embedmd]:# (../assets/how-to-guides/write-simple-dapp/poll-1.gno go) 30 ```go 31 package poll 32 33 import ( 34 "std" 35 36 "gno.land/p/demo/avl" 37 ) 38 39 // Main struct 40 type Poll struct { 41 title string 42 description string 43 deadline int64 // block height 44 voters *avl.Tree // addr -> yes / no (bool) 45 } 46 47 // Getters 48 func (p Poll) Title() string { 49 return p.title 50 } 51 52 func (p Poll) Description() string { 53 return p.description 54 } 55 56 func (p Poll) Deadline() int64 { 57 return p.deadline 58 } 59 60 func (p Poll) Voters() *avl.Tree { 61 return p.voters 62 } 63 64 // Poll instance constructor 65 func NewPoll(title, description string, deadline int64) *Poll { 66 return &Poll{ 67 title: title, 68 description: description, 69 deadline: deadline, 70 voters: avl.NewTree(), 71 } 72 } 73 74 // Vote Votes for a user 75 func (p *Poll) Vote(voter std.Address, vote bool) { 76 p.Voters().Set(voter.String(), vote) 77 } 78 79 // HasVoted vote: yes - true, no - false 80 func (p *Poll) HasVoted(address std.Address) (bool, bool) { 81 vote, exists := p.Voters().Get(address.String()) 82 if exists { 83 return true, vote.(bool) 84 } 85 return false, false 86 } 87 88 // VoteCount Returns the number of yay & nay votes 89 func (p Poll) VoteCount() (int, int) { 90 var yay int 91 92 p.Voters().Iterate("", "", func(key string, value interface{}) bool { 93 vote := value.(bool) 94 if vote == true { 95 yay = yay + 1 96 } 97 }) 98 return yay, p.Voters().Size() - yay 99 } 100 ``` 101 102 View this code in the Playground [here](https://play.gno.land/p/dwARIIq0meB). 103 104 A few remarks: 105 106 - We are using the `std` library for accessing blockchain-related functionality 107 and types, such as `std.Address`. 108 - Since the `map` data type is not deterministic in Go, we need to use the AVL 109 tree structure, defined 110 under `gno.land/p/demo/avl`. It behaves similarly to a map; it maps a key of 111 type `string` onto a value of any type - `interface{}`. 112 - We are importing the `gno.land/p/demo/avl` package directly from on-chain storage. 113 You can find predeployed packages & libraries which provide additional Gno 114 functionality in the [Gno monorepo](https://github.com/gnolang/gno), under the `examples/` folder. 115 116 :::info 117 After testing the `Poll` package, we need to deploy it in order to use it in our realm. 118 Check out the [deployment](deploy.md) guide to learn how to do this. 119 ::: 120 121 ### Poll Factory Realm 122 123 Moving on, we can create the Poll Factory realm. 124 125 The realm will contain the following functionality: 126 127 - An exported `NewPoll` method, to allow users to create polls 128 - An exported `Vote` method, to allow users to pledge votes for any active poll 129 - A `Render` function to display the realm state 130 131 [embedmd]:# (../assets/how-to-guides/write-simple-dapp/poll-2.gno go) 132 ```go 133 package poll 134 135 import ( 136 "bytes" 137 "std" 138 139 "gno.land/p/demo/avl" 140 "gno.land/p/demo/poll" 141 "gno.land/p/demo/seqid" 142 "gno.land/p/demo/ufmt" 143 ) 144 145 // state variables 146 var ( 147 polls *avl.Tree // id -> Poll 148 pollIDCounter seqid.ID 149 ) 150 151 func init() { 152 polls = avl.NewTree() 153 } 154 155 // NewPoll - Creates a new Poll instance 156 func NewPoll(title, description string, deadline int64) string { 157 // get block height 158 if deadline <= std.GetHeight() { 159 panic("deadline has to be in the future") 160 } 161 162 // Generate int 163 id := pollIDCounter.Next().String() 164 p := poll.NewPoll(title, description, deadline) 165 166 // add new poll in avl tree 167 polls.Set(id, p) 168 169 return ufmt.Sprintf("Successfully created poll #%s!", id) 170 } 171 172 // Vote - vote for a specific Poll 173 // yes - true, no - false 174 func Vote(id string, vote bool) string { 175 // get txSender 176 txSender := std.GetOrigCaller() 177 178 // get specific Poll from AVL tree 179 pollRaw, exists := polls.Get(id) 180 181 if !exists { 182 panic("poll with specified doesn't exist") 183 } 184 185 // cast Poll into proper format 186 poll, _ := pollRaw.(*poll.Poll) 187 188 voted, _ := poll.HasVoted(txSender) 189 if voted { 190 panic("you've already voted!") 191 } 192 193 if poll.Deadline() <= std.GetHeight() { 194 panic("voting for this poll is closed") 195 } 196 197 // record vote 198 poll.Vote(txSender, vote) 199 200 // update Poll in tree 201 polls.Set(id, poll) 202 203 if vote == true { 204 return ufmt.Sprintf("Successfully voted YAY for poll #%s!", id) 205 } 206 return ufmt.Sprintf("Successfully voted NAY for poll #%s!", id) 207 } 208 ``` 209 210 :::info 211 Depending on where you deployed your `Poll` package, you will have to change its 212 import path in the realm code. 213 ::: 214 215 With that we have written the core functionality of the realm, and all that is left is 216 the [Render function](../concepts/realms.md). 217 Its purpose is to help us display the state of the realm in Markdown, by formatting the state into a string buffer: 218 219 220 [embedmd]:# (../assets/how-to-guides/write-simple-dapp/poll-3.gno go) 221 ```go 222 func Render(path string) string { 223 var b bytes.Buffer 224 225 b.WriteString("# Polls!\n\n") 226 227 if polls.Size() == 0 { 228 b.WriteString("### No active polls currently!") 229 return b.String() 230 } 231 polls.Iterate("", "", func(key string, value interface{}) bool { 232 233 // cast raw data from tree into Poll struct 234 p := value.(*poll.Poll) 235 ddl := p.Deadline() 236 237 yay, nay := p.VoteCount() 238 yayPercent := 0 239 nayPercent := 0 240 241 if yay+nay != 0 { 242 yayPercent = yay * 100 / (yay + nay) 243 nayPercent = nay * 100 / (yay + nay) 244 } 245 246 b.WriteString( 247 ufmt.Sprintf( 248 "## Poll #%s: %s\n", 249 key, // poll ID 250 p.Title(), 251 ), 252 ) 253 254 dropdown := "<details>\n<summary>Poll details</summary><br>" 255 256 b.WriteString(dropdown + "Description: " + p.Description()) 257 258 b.WriteString( 259 ufmt.Sprintf("<br>Voting until block: %d<br>Current vote count: %d", 260 p.Deadline(), 261 p.Voters().Size()), 262 ) 263 264 b.WriteString( 265 ufmt.Sprintf("<br>YAY votes: %d (%d%%)", yay, yayPercent), 266 ) 267 b.WriteString( 268 ufmt.Sprintf("<br>NAY votes: %d (%d%%)</details>", nay, nayPercent), 269 ) 270 271 dropdown = "<br><details>\n<summary>Vote details</summary>" 272 b.WriteString(dropdown) 273 274 p.Voters().Iterate("", "", func(key string, value interface{}) bool { 275 276 voter := key 277 vote := value.(bool) 278 279 if vote == true { 280 b.WriteString( 281 ufmt.Sprintf("<br>%s voted YAY!", voter), 282 ) 283 } else { 284 b.WriteString( 285 ufmt.Sprintf("<br>%s voted NAY!", voter), 286 ) 287 } 288 return false 289 }) 290 291 b.WriteString("</details>\n\n") 292 return false 293 }) 294 return b.String() 295 } 296 ``` 297 298 View this code in the Playground [here](https://play.gno.land/p/5jgHw29sGq4). 299 300 To see how to deploy this app, visit the [Deployment guide](./deploy.md). 301 302 ## Conclusion 303 304 That's it 🎉 305 306 You have successfully built a simple but fully-fledged dApp using Gno! 307 Now you're ready to conquer new, more complex dApps in Gno.