diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..6635b0a --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +*.gno linguist-language=Go diff --git a/.gitpod.yml b/.gitpod.yml index 904aa72..eb7f038 100644 --- a/.gitpod.yml +++ b/.gitpod.yml @@ -5,12 +5,6 @@ additionalRepositories: checkoutLocation: gno tasks: - - name: Gno CLI - init: | - go mod download - go install github.com/gnolang/gno/gnovm/cmd/gno - command: gno --help - - name: Gnoland before: cd ../gno/gno.land/ init: go install ./cmd/gnoland @@ -27,6 +21,14 @@ tasks: make install echo "Deps installed." + - name: Gno CLI + init: | + go mod download + go install \ + github.com/gnolang/gno/gno.land/cmd/gnokey \ + github.com/gnolang/gno/gnovm/cmd/gno + command: gno --help + #- name: faucet # ... diff --git a/004-publish-contracts/README.md b/004-publish-contracts/README.md deleted file mode 100644 index e69de29..0000000 diff --git a/004-publishing-contracts/README.md b/004-publishing-contracts/README.md new file mode 100644 index 0000000..511c49b --- /dev/null +++ b/004-publishing-contracts/README.md @@ -0,0 +1,255 @@ +# Publishing contracts + +Now that you know how to write programs and debug gno code, let's get you started +publishing some smart contracts. + +In `guestbook.gno`, you can see a simple application for a guestbook. If you did +not already set up your test1 wallet, it's a good time to do so: [follow +the instructions from the second example](../002-gnokey/README.md). + +The advantage of using the `test1` key instead of your own key is that it has, +at genesis, a large number of tokens which can help us run transactions straight +off the bat. + +From this directory (use the `cd` command in the shell to navigate +`004-publish-contracts`), run the following command: + +``` +gnokey maketx addpkg \ + --gas-wanted 10000000 \ + --gas-fee 1ugnot \ + --pkgpath gno.land/r/demo/guestbook \ + --pkgdir . \ + --broadcast \ + test1 +``` + +## Browsing the contract + +In the source code, there is a `Render` function. This enables us to browse the +smart contract through the `gnoweb` HTTP frontend. + +From the simple browser in your Gitpod, browse to the path `/r/demo/guestbook` +(appending it to the Gitpod URL - so something like +`https://8888-gnolang-gettingstarted-lnw0x71frja.ws-eu102.gitpod.io/r/demo/guestbook`). +You will see a markdown rendering of the website, which works by executing the Render() +function. + +As expected, there will be one single signature on the guestbook -- the one +defined in `var signatures`. Let's fix that and add a new one! + +## Adding a new signature + +If you click on the `[help]` link in the header, on the right, you +will also be able to see a list of the exported functions of the realm, and +instructions on how to execute them using `gnokey`. + +By modifying the fields, you can interactively set up your `gnokey` command, and +make it do whatever you want it to. + +![A screenshot of a simple configuration of gnokey, that we will use +later.](./screenshot.png) + +Let's run that command: + +``` +gnokey maketx call \ + -pkgpath "gno.land/r/demo/guestbook" \ + -func "Sign" \ + -gas-fee 1000000ugnot \ + -gas-wanted 2000000 \ + -send "" \ + -broadcast \ + -chainid "dev" \ + -args "Hello world\! And hello, **bold**\!" \ + -remote "127.0.0.1:26657" \ + test1 +``` + +The transaction should succeed, and if you browse again to the homepage of the +realm you should see your new post with your address. Ta-da! :tada: + +## The magic of realms + +We have glossed over an important concept of Gno for the sake of the tutorial: +how global variables work in realms. You may have noticed that in the source +code we declared a global variable, which contained the first signature: + +```go +var signatures = []Signature{ + { + Message: "You've reached the end of the guestbook!", + Address: "", + Time: time.Date(2023, time.January, 1, 12, 0, 0, 0, time.UTC), + }, +} +``` + +This shows up when we browse the realm from the web, and all is good here. +However, after we call `Sign`, it stores the new signature in the global +variable, and suddenly -- poof! -- the signature appears in the guestbook. + +```go + // append new signature -- at the top of the list + signatures = append([]Signature{{ + Message: message, + Address: caller, + Time: time.Now(), + }}, signatures...) +``` + +This is the magic of Gno realms -- they are **stateful.** This means that when +in a transaction you change a global variable, or any data stored within it, the +new data is automagically stored and persisted on the blockchain. + +Because we're running in a blockchain context, as opposed to Go we can't have +things like network access, or access to the filesystem, or using +cryptographically random functions. These are all **non-deterministic,** meaning +that if you attempt to run the code twice, or on different machines, it may lead +to different results. Gno code, in contracts, like all other smart contracts, +must be **deterministic** and always have the same results if it's given the +same starting context -- which is why we provide global variables as a solution +for storing data. + +### Deterministic time + +The careful observer may note that `time.Now()` is not a deterministic value, as by +definition it is always changing and "should" be different each time the program +is executed. However, to enable the use of time.Now() in blockchain, this will +actually represent the _block time_. + +In a blockchain, a new "block" is created after a given amount of time. This +will roughly match the current time; but note the value does not change during +the execution of the program, and as such `time.Now()` cannot be used to +benchmark a program -- it is just a rough idea of what time the transaction is +executed. + +### The difference between realms and packages + +As you've learned, realms have the distinctive feature of being able to persist +their global variables. The underlying idea here is that realms are end-user +smart contracts; which is why they also support the `Render()` function for +viewing their data on the web. + +In `guestbook.gno`, however, we make an import of package +`gno.land/p/demo/ufmt`. This is an example of a _package_ (as opposed to a +_realm_) -- it is distinct from a realm because its import path starts with +`gno.land/p/` instead of `gno.land/r/`.[^1] + +Packages behave like normal Go packages, or similar concepts of other +programming languages: they are reusable pieces of code which are meant as +"building blocks" to build complex software. They don't persist any data, nor +their functions can be executed as smart contracts. + +## Keep going + +There are two more challenges for you in `guestbook.gno`: you can find them in +the `TODO` comments. + +The first one is to prevent a user from signing the guestbook more than once, +aborting the transaction entirely. In Gno, you can abort transactions by using +the `panic()` function. + +
+ Sample solution (only if you're stuck!) + +```go +for _, sig := range signatures { + if sig.Address == caller { + panic("you have already signed the guestbook!") + } +} +``` + +
+ +The second one is to use the `gno.land/r/demo/users` realm to render usernames. +The `users` realm is a "username registry" which is used in +other example realms, like [`microblog`](https://github.com/gnolang/gno/tree/master/examples/gno.land/p/demo/microblog), +[`boards`](https://github.com/gnolang/gno/tree/master/examples/gno.land/p/demo/boards) +and [`groups`](https://github.com/gnolang/gno/tree/master/examples/gno.land/p/demo/groups). +You can inspect it and register yourself as a user of the realm. + +We can make our guestbook nicer by referring to users using their username +instead of their address... + +Here's a sample command to register a user (note the `--send` argument -- we use +this to register without being "invited"): + +``` +gnokey maketx call \ + -pkgpath "gno.land/r/demo/users" \ + -func "Register" \ + -gas-fee 1000000ugnot \ + -gas-wanted 2000000 \ + -send "200000000ugnot" \ + -broadcast \ + -chainid "dev" \ + -args "" -args "torvalds" -args "https://github.com/torvalds" \ + -remote "127.0.0.1:26657" test1 +``` + +
+ Hint + +To call other realms in Gno, we simply have to import them at the top of the +file in the `import` statement. After doing that, you can use the function +`users.GetUserByAddress` to see if there is a matching User. + +
+ +
+ Sample solution (only if you're stuck!) + +```go +// add import: "gno.land/r/demo/users" + +func Render(string) string { + b := new(strings.Builder) + // gnoweb, which is the HTTP frontend we're using, will render the content we + // pass to it as markdown. + b.WriteString("# Guestbook\n\n") + for _, sig := range signatures { + a := string(sig.Address) + if a == "" { + a = "anonymous coward" + } else if u := users.GetUserByAddress(sig.Address); u != nil { + a = ufmt.Sprintf("[@%s](/r/demo/users:%s)", + u.Name(), u.Name()) + } + // We currently don't have a full fmt package; we have "ufmt" to do basic formatting. + // See `gno doc ufmt` for more information. + // + // If you are unfamiliar with Go time formatting, it is done by writing the way you'd + // format a reference time. See `gno doc time.Layout` for more information. + b.WriteString(ufmt.Sprintf( + "%s\n\n_written by %s at %s_\n\n----\n\n", + sig.Message, a, sig.Time.Format("2006-01-02"), + )) + } + return b.String() +} +``` + +
+ +## Recap + +1. By using the `gnokey maketx addpkg`, we can add a new package or realm to the + blockchain. +2. Packages are reusable "building blocks" of code. Realms are "special + packages" which: + - Can persist state in their global variables + - Can have their functions called as smart contracts, using `gnokey` + - Can be rendered through the web frontend, using the special `Render` function +3. Using the `[help]` page of gnoweb, we can construct transactions to smart + contracts we've created. +4. All Gno code must be deterministic and run exactly the same independent of + the machine. We are still allowed to use `time.Now()` -- however that will + actually return the block time instead of the machine's clock time. +5. We can make calls to other realms with their state by simply importing their + path in our Gno code. + +----- + +[^1]: at the moment, all code uploaded to the chain must have an import path starting with either of the two. diff --git a/004-publishing-contracts/guestbook.gno b/004-publishing-contracts/guestbook.gno new file mode 100644 index 0000000..6466da7 --- /dev/null +++ b/004-publishing-contracts/guestbook.gno @@ -0,0 +1,75 @@ +// Realm guestbook is a smart contract to register presences at a workshop. +// Participants to the workshop can add their own signature by calling the [Sign] +// contract. +package guestbook + +import ( + "std" + "strings" + "time" + + "gno.land/p/demo/ufmt" +) + +type Signature struct { + Message string + Address std.Address + Time time.Time +} + +var signatures = []Signature{ + { + Message: "You've reached the end of the guestbook!", + Address: "", + Time: time.Date(2023, time.January, 1, 12, 0, 0, 0, time.UTC), + }, +} + +// Sign adds a new signature to the guestbook +func Sign(message string) { + // AssertOriginCall makes it possible to call Sign only as a transaction - ie. + // it cannot be executed as a function, or called from other realms. + std.AssertOriginCall() + // caller, type std.Address, is the address of who has called this contract. + caller := std.GetOrigCaller() + + // TODO: make sure caller hasn't signed the guestbook already + + // append new signature -- at the top of the list + signatures = append([]Signature{{ + Message: message, + Address: caller, + Time: time.Now(), + }}, signatures...) +} + +// Render is called when running the realm through gnoweb, and allows to render +// the realm's internal data, without the possibility of changing it. +// +// Render accepts a string, which is a "request path" -- similar to an HTTP +// request. We will be further exploring this at a later time, for now the +// argument is ignored. +func Render(string) string { + b := new(strings.Builder) + // gnoweb, which is the HTTP frontend we're using, will render the content we + // pass to it as markdown. + b.WriteString("# Guestbook\n\n") + for _, sig := range signatures { + a := string(sig.Address) + if a == "" { + a = "anonymous coward" + } + // We currently don't have a full fmt package; we have "ufmt" to do basic formatting. + // See `gno doc ufmt` for more information. + // + // If you are unfamiliar with Go time formatting, it is done by writing the way you'd + // format a reference time. See `gno doc time.Layout` for more information. + b.WriteString(ufmt.Sprintf( + "%s\n\n_written by %s on %s_\n\n----\n\n", + sig.Message, a, sig.Time.Format("2006-01-02"), + )) + + // TODO: resolve sig.Address to a username, by using the r/demo/users realm. + } + return b.String() +} diff --git a/004-publishing-contracts/screenshot.png b/004-publishing-contracts/screenshot.png new file mode 100644 index 0000000..f5c3dbd Binary files /dev/null and b/004-publishing-contracts/screenshot.png differ diff --git a/gno.mod b/gno.mod index 2a7ca90..39fc97b 100644 --- a/gno.mod +++ b/gno.mod @@ -1 +1,5 @@ module gno.land/r/demo/getting-started + +require ( + gno.land/p/demo/ufmt v0.0.0-latest +)