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.
+
+
+
+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
+)