gnØland

Disclaimer

Realms Development

State

The main difference between packages /p and realms /r is that realms can manage a state. This state is a set of variables which values can be modified whenever a transaction executes a function of the Realm.

The state is simply defined as global variables inside the Realm:

package lastmessage var ( last_author string = "" last_message string = "" ) // Anyone can call CreateNewMessage in a transaction to modify this Realm state func CreateNewMessage(author, message string) { last_author = author last_message = message }

Realm init

Each Realm can specify an init() function that will be executed during the first call to the Realm. This can be seen as a constructor for the Realm, where you can set initial values and initial logic for your Realm.

Renderability standard

Each Realm can expose a public Render(path string) string function which is expected to return valid markdown. This function is expected to render the current state of the Realm and must not attempt to modify it.

The Render function can be called with an RPC call and is executed by the RPC node directly without performing a transaction, which means that any state modification performed in this function will not actually be saved on-chain.

Realm-side

The path parameter represents the query options of the Render function. There are different type of render parameters:

  1. No parameter

    If path is equal to "" , it means that the Render function must returns the overview of the Realm state (ie its homepage).

  2. Query parameters (/r/realm:xxx)

    /r/hello:gno the Realm must render the gno resource of the hello realm

  3. Sub-paths (/r/realm/sub)

    A Realm can render any amount of nested subpaths, each subpath must support query parameters if relevant (ex: /r/hello/en:gno)

Frontend-side

If you are building a frontend for a Realm, it is important to support some render parameters that can be specified in the URL.

Example of helper URL parameters (to redirect to reply function of the board realm): /r/demo/boards?help&__func=CreateReply&bid=1&threadid=2&postid=2&body.type=textarea

The help parameter specified here indicates that the frontend is expected to render a function helper for the user with the following parameters:

  • __func=: the name of the function
  • bid, threadid etc. here are the values to pre-fill in the parameters of the CreateReply function
  • each parameters name can be suffixed with .type to specify the input type that is expected (only textarea can be specified for now)

Example: A Meetup Realm

Let’s create a meetup platform where people can register to an event.

package meetup import ( "std" "time" "strings" "strconv" ) //////////////////////////////////////////////////////////// // 1. Let's define our Realm state //////////////////////////////////////////////////////////// // Meetup represents a meetup event, with metadata and attendees list type Meetup struct { Id uint64 Title string Description string Registered []std.Address Date uint64 } // Meetups are stored in the Realm state var meetups []Meetup // Admin is the admin address that will have special rights on this Realm const admin std.Address = "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5" //////////////////////////////////////////////////////////// // 2. Now, let’s enable the admin to create a new meetup //////////////////////////////////////////////////////////// // Utility function to make sure the current caller is the registered admin func assertAdmin() { if std.GetOrigCaller() != admin { // panic in Gno makes the transaction revert panic("Can only be called by an admin") } } // Public function to let the admin create a new event // Note that public functions can be called from a frontend or CLI, // it is thus recommended to have simple parameters (and avoid arrays for example) func CreateMeetup(title, description string, timestamp uint64) { // we make sure this function is called by the admin assertAdmin() meetups = append(meetups, Meetup{ Id: uint64(len(meetups)), // id just an incremented value Title: title, Description: description, Registered: []std.Address{}, Date: timestamp, }) } //////////////////////////////////////////////////////////// // 3. Lets now make sure that anyone can register to an event //////////////////////////////////////////////////////////// // Register to the event {id} func Register(id uint64) { caller := std.GetOrigCaller() if time.Now().Unix() > int64(meetups[id].Date) { panic("Meetup is finished, you cannot register to it.") } meetups[id].Registered = append(meetups[id].Registered, caller) } //////////////////////////////////////////////////////////// // 4. We can now create a Render function, which will render 2 paths: // - the homepage of the realm, that will render upcoming and past events lists // - meetup pages, which render the meetup specified as parameter //////////////////////////////////////////////////////////// func Render(path string) string { // home page (ie /r/meetup) if len(path) == 0 { s := "# Upcoming events\n" for _, m := range meetups { if time.Now().Unix() < int64(m.Date) { date := time.Unix(int64(m.Date), 0).String() s += "* " + strconv.FormatUint(m.Id, 10) + " " + m.Title + " " + date + "\n" } } s += "\n" s += "# Past events\n" for _, m := range meetups { if time.Now().Unix() > int64(m.Date) { date := time.Unix(int64(m.Date), 0).String() s += "* " + strconv.FormatUint(m.Id, 10) + " " + m.Title + " - " + date + "\n" } } return s } else { // meetup page (ie /r/meetup:1) rawId, _ := strconv.Atoi(path) id := uint64(rawId) if id >= uint64(len(meetups)) { return "Meetup does not exist" } date := time.Unix(int64(meetups[id].Date), 0).String() s := "# " + strconv.FormatUint(meetups[id].Id, 10) + " - " + meetups[id].Title + "\nOn " + date + "\n" + meetups[id].Description return s } }
  1. Let’s create a meetup_file.gno and use the testing library to unit test some of our Realm functions
package meetup import ( "std" "testing" "time" ) // Utility to verify if a Realm function panics func assertPanic(t *testing.T, f func()) { defer func() { if r := recover(); r == nil { t.Errorf("The code did not panic") } }() f() } func TestCreateMeetupNonAdmin(t *testing.T) { // We set the caller of the call to non-admin address std.TestSetOrigCaller("g1rel7980x4y257yh30umy3jx223efwakvnbbbbb") // We expect this call to panic as only the admin can call CreateMeetup assertPanic(t, func () { CreateMeetup("Blockchain Golang Meetup", "Learn how to use Golang for blockchains", 1701280800) }) } func TestCreateMeetupAdmin(t *testing.T) { // We set the caller to the proper admin value std.TestSetOrigCaller("g1rel7980x4y257yh30umy3jx223efwakvnabcde") title := "Blockchain Golang Meetup" CreateMeetup(title, "Learn how to use Golang for blockchains", 1701280800) // The test fails if event was not added to meetups list (Realm state) if len(meetups) != 1 && meetups[0].Title == title { t.Errorf("Invalid meetup creation") } }

Previous doc: Local Setup

Next doc: Community Resources