From 351d0293f4d166179c1038c2bb72d68f5abfa231 Mon Sep 17 00:00:00 2001 From: Morgan Bazalgette Date: Sat, 22 Jul 2023 19:39:05 +0200 Subject: [PATCH 1/8] feat: add 004-publishing-contracts --- .gitattributes | 1 + 004-publish-contracts/README.md | 0 004-publishing-contracts/README.md | 36 ++++++++++++++ 004-publishing-contracts/guestbook.gno | 68 ++++++++++++++++++++++++++ 4 files changed, 105 insertions(+) create mode 100644 .gitattributes delete mode 100644 004-publish-contracts/README.md create mode 100644 004-publishing-contracts/README.md create mode 100644 004-publishing-contracts/guestbook.gno 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/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..bd21f10 --- /dev/null +++ b/004-publishing-contracts/README.md @@ -0,0 +1,36 @@ +# 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 1000000 \ + --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 browser in your Gitpod, browse to . +You will see a markdown rendering of the website, by executing the Render() +function. 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`. diff --git a/004-publishing-contracts/guestbook.gno b/004-publishing-contracts/guestbook.gno new file mode 100644 index 0000000..b548027 --- /dev/null +++ b/004-publishing-contracts/guestbook.gno @@ -0,0 +1,68 @@ +// 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" + "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, nil), + } +} + +// 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 + 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 { + // We currently don't have a full fmt package; we have "ufmt" to do basic formatting. + // See `gno doc ufmt` for more information. + b.WriteString(ufmt.Sprintf( + "%s\n\n_written by %s at %s_\n\n----\n\n", + sig.Message, string(sig.Address), sig.Time.Format(""), + )) + + // TODO: resolve sig.Address to a username, by using the r/demo/users realm. + } + return b.String() +} + From 68a66c8d50b67414d2a05b9f64d866359d4ad3db Mon Sep 17 00:00:00 2001 From: Morgan Bazalgette Date: Sat, 22 Jul 2023 19:42:06 +0200 Subject: [PATCH 2/8] update ext --- .gitpod.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitpod.yml b/.gitpod.yml index 904aa72..80408cb 100644 --- a/.gitpod.yml +++ b/.gitpod.yml @@ -53,4 +53,4 @@ github: vscode: extensions: - - https://marketplace.visualstudio.com/_apis/public/gallery/publishers/harry-hov/vsextensions/gno/0.0.10/vspackage + - harry-hov.gno From 88677a4c626dc085b2a48831a8c71529b484096f Mon Sep 17 00:00:00 2001 From: Morgan Bazalgette Date: Sat, 22 Jul 2023 19:45:10 +0200 Subject: [PATCH 3/8] install gnokey --- .gitpod.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.gitpod.yml b/.gitpod.yml index 80408cb..42b2b49 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,12 @@ tasks: make install echo "Deps installed." + - name: Gno CLI + init: | + go mod download + go install github.com/gnolang/gno/gno.land/gnokey github.com/gnolang/gno/gnovm/cmd/gno + command: gno --help + #- name: faucet # ... From 7a4d3c6ac24033a526ae3daf27561ecc5e5d3172 Mon Sep 17 00:00:00 2001 From: Morgan Bazalgette Date: Sat, 22 Jul 2023 19:59:14 +0200 Subject: [PATCH 4/8] fixpod --- .gitpod.yml | 6 ++++-- 004-publishing-contracts/guestbook.gno | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/.gitpod.yml b/.gitpod.yml index 42b2b49..eb7f038 100644 --- a/.gitpod.yml +++ b/.gitpod.yml @@ -24,7 +24,9 @@ tasks: - name: Gno CLI init: | go mod download - go install github.com/gnolang/gno/gno.land/gnokey github.com/gnolang/gno/gnovm/cmd/gno + go install \ + github.com/gnolang/gno/gno.land/cmd/gnokey \ + github.com/gnolang/gno/gnovm/cmd/gno command: gno --help #- name: faucet @@ -53,4 +55,4 @@ github: vscode: extensions: - - harry-hov.gno + - https://marketplace.visualstudio.com/_apis/public/gallery/publishers/harry-hov/vsextensions/gno/0.0.10/vspackage diff --git a/004-publishing-contracts/guestbook.gno b/004-publishing-contracts/guestbook.gno index b548027..f816324 100644 --- a/004-publishing-contracts/guestbook.gno +++ b/004-publishing-contracts/guestbook.gno @@ -34,7 +34,7 @@ func Sign(message string) { // TODO: make sure caller hasn't signed the guestbook already - // append new signature + // append new signature -- at the top of the list signatures = append([]Signature{{ Message: message, Address: caller, From e3a5f862363b6c2f0ae58b6ad4627c635c6c7373 Mon Sep 17 00:00:00 2001 From: Morgan Bazalgette Date: Sat, 22 Jul 2023 20:46:06 +0200 Subject: [PATCH 5/8] more --- 004-publishing-contracts/README.md | 82 +++++++++++++++++++++++- 004-publishing-contracts/guestbook.gno | 18 ++++-- 004-publishing-contracts/screenshot.png | Bin 0 -> 14309 bytes gno.mod | 4 ++ 4 files changed, 95 insertions(+), 9 deletions(-) create mode 100644 004-publishing-contracts/screenshot.png diff --git a/004-publishing-contracts/README.md b/004-publishing-contracts/README.md index bd21f10..7b68306 100644 --- a/004-publishing-contracts/README.md +++ b/004-publishing-contracts/README.md @@ -29,8 +29,84 @@ gnokey maketx addpkg \ In the source code, there is a `Render` function. This enables us to browse the smart contract through the `gnoweb` HTTP frontend. -From the browser in your Gitpod, browse to . -You will see a markdown rendering of the website, by executing the Render() -function. If you click on the `[help]` link in the header, on the right, you +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 say 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. diff --git a/004-publishing-contracts/guestbook.gno b/004-publishing-contracts/guestbook.gno index f816324..cc97bae 100644 --- a/004-publishing-contracts/guestbook.gno +++ b/004-publishing-contracts/guestbook.gno @@ -5,6 +5,7 @@ package guestbook import ( "std" + "strings" "time" "gno.land/p/demo/ufmt" @@ -13,15 +14,15 @@ import ( type Signature struct { Message string Address std.Address - Time time.Time + 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, nil), - } + Time: time.Date(2023, time.January, 1, 12, 0, 0, 0, time.UTC), + }, } // Sign adds a new signature to the guestbook @@ -38,7 +39,7 @@ func Sign(message string) { signatures = append([]Signature{{ Message: message, Address: caller, - Time: time.Now(), + Time: time.Now(), }}, signatures...) } @@ -54,15 +55,20 @@ func Render(string) string { // pass to it as markdown. b.WriteString("# Guestbook\n\n") for _, sig := range signatures { + if sig.Address == "" { + sig.Address = "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 at %s_\n\n----\n\n", - sig.Message, string(sig.Address), sig.Time.Format(""), + sig.Message, string(sig.Address), 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 0000000000000000000000000000000000000000..f5c3dbd4bb87d6a9f219817214e9dc7e795bee56 GIT binary patch literal 14309 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 Ee|yXQLjV8( literal 0 HcmV?d00001 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 +) From 4e2dbebb6844b5a0e229f1af3c32fadacd1e6a68 Mon Sep 17 00:00:00 2001 From: Morgan Bazalgette Date: Sun, 23 Jul 2023 11:40:59 +0200 Subject: [PATCH 6/8] add even more tutorial! --- 004-publishing-contracts/README.md | 126 +++++++++++++++++++++++++++++ 1 file changed, 126 insertions(+) diff --git a/004-publishing-contracts/README.md b/004-publishing-contracts/README.md index 7b68306..df9df0a 100644 --- a/004-publishing-contracts/README.md +++ b/004-publishing-contracts/README.md @@ -110,3 +110,129 @@ 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... + +
+ 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 { + if sig.Address == "" { + sig.Address = "anonymous coward" + } else if u := users.GetUserByAddress(sig.Address); u != nil { + sig.Address = 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, string(sig.Address), 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. From dd4e2e94b7789cbe7a54bd24349ad3885f6a42f3 Mon Sep 17 00:00:00 2001 From: Morgan Bazalgette Date: Sun, 23 Jul 2023 11:43:31 +0200 Subject: [PATCH 7/8] typo --- 004-publishing-contracts/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/004-publishing-contracts/README.md b/004-publishing-contracts/README.md index df9df0a..8f1b9ce 100644 --- a/004-publishing-contracts/README.md +++ b/004-publishing-contracts/README.md @@ -45,7 +45,7 @@ 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 say whatever you want it to. +make it do whatever you want it to. ![A screenshot of a simple configuration of gnokey, that we will use later.](./screenshot.png) From 6e82916b41a4d9070110d4b256362db2b8224c32 Mon Sep 17 00:00:00 2001 From: Morgan Bazalgette Date: Sun, 23 Jul 2023 12:12:16 +0200 Subject: [PATCH 8/8] some adjustments after playtesting --- 004-publishing-contracts/README.md | 27 +++++++++++++++++++++----- 004-publishing-contracts/guestbook.gno | 9 +++++---- 2 files changed, 27 insertions(+), 9 deletions(-) diff --git a/004-publishing-contracts/README.md b/004-publishing-contracts/README.md index 8f1b9ce..511c49b 100644 --- a/004-publishing-contracts/README.md +++ b/004-publishing-contracts/README.md @@ -16,7 +16,7 @@ From this directory (use the `cd` command in the shell to navigate ``` gnokey maketx addpkg \ - --gas-wanted 1000000 \ + --gas-wanted 10000000 \ --gas-fee 1ugnot \ --pkgpath gno.land/r/demo/guestbook \ --pkgdir . \ @@ -173,6 +173,22 @@ 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 @@ -194,10 +210,11 @@ func Render(string) string { // pass to it as markdown. b.WriteString("# Guestbook\n\n") for _, sig := range signatures { - if sig.Address == "" { - sig.Address = "anonymous coward" + a := string(sig.Address) + if a == "" { + a = "anonymous coward" } else if u := users.GetUserByAddress(sig.Address); u != nil { - sig.Address = ufmt.Sprintf("[@%s](/r/demo/users:%s)", + 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. @@ -207,7 +224,7 @@ func Render(string) string { // 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, string(sig.Address), sig.Time.Format("2006-01-02"), + sig.Message, a, sig.Time.Format("2006-01-02"), )) } return b.String() diff --git a/004-publishing-contracts/guestbook.gno b/004-publishing-contracts/guestbook.gno index cc97bae..6466da7 100644 --- a/004-publishing-contracts/guestbook.gno +++ b/004-publishing-contracts/guestbook.gno @@ -55,8 +55,9 @@ func Render(string) string { // pass to it as markdown. b.WriteString("# Guestbook\n\n") for _, sig := range signatures { - if sig.Address == "" { - sig.Address = "anonymous coward" + 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. @@ -64,8 +65,8 @@ func Render(string) string { // 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, string(sig.Address), sig.Time.Format("2006-01-02"), + "%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.