diff --git a/docs/concepts/portal-loop.md b/docs/concepts/portal-loop.md index d96738bdddf..adc341e3ae4 100644 --- a/docs/concepts/portal-loop.md +++ b/docs/concepts/portal-loop.md @@ -67,3 +67,20 @@ has some drawbacks: Gno will fail to be replayed, meaning **data will be lost**. - Since transactions are archived and replayed during genesis, block height & timestamp cannot be relied upon. + +### Deploying to the Portal Loop + +There are two ways to deploy code to the Portal Loop: + +1. *automatic* - all packages in found in the `examples/gno.land/{p,r}/` directory in the [Gno monorepo](https://github.com/gnolang/gno) get added to the + new genesis each cycle, +2. *permissionless* - this includes replayed transactions with `addpkg`, and + new transactions you can issue with `gnokey maketx addpkg`. + +Since the packages in `examples/gno.land/{p,r}` are deployed first, +permissionless deployments get superseded when packages with identical `pkgpath` +get merged into `examples/`. + +The above mechanism is also how the `examples/` on the Portal Loop +get collaboratively iterated upon, which is its main mission. + diff --git a/examples/gno.land/r/demo/games/shifumi/gno.mod b/examples/gno.land/r/demo/games/shifumi/gno.mod new file mode 100644 index 00000000000..7a4fc173d3d --- /dev/null +++ b/examples/gno.land/r/demo/games/shifumi/gno.mod @@ -0,0 +1,7 @@ +module gno.land/r/demo/games/shifumi + +require ( + gno.land/p/demo/avl v0.0.0-latest + gno.land/p/demo/seqid v0.0.0-latest + gno.land/r/demo/users v0.0.0-latest +) diff --git a/examples/gno.land/r/demo/games/shifumi/shifumi.gno b/examples/gno.land/r/demo/games/shifumi/shifumi.gno new file mode 100644 index 00000000000..9094cb8fd69 --- /dev/null +++ b/examples/gno.land/r/demo/games/shifumi/shifumi.gno @@ -0,0 +1,120 @@ +package shifumi + +import ( + "errors" + "std" + "strconv" + + "gno.land/p/demo/avl" + "gno.land/p/demo/seqid" + + "gno.land/r/demo/users" +) + +const ( + empty = iota + rock + paper + scissors + last +) + +type game struct { + player1, player2 std.Address // shifumi is a 2 players game + move1, move2 int // can be empty, rock, paper, or scissors +} + +var games avl.Tree +var id seqid.ID + +func (g *game) play(player std.Address, move int) error { + if !(move > empty && move < last) { + return errors.New("invalid move") + } + if player != g.player1 && player != g.player2 { + return errors.New("invalid player") + } + if player == g.player1 && g.move1 == empty { + g.move1 = move + return nil + } + if player == g.player2 && g.move2 == empty { + g.move2 = move + return nil + } + return errors.New("already played") +} + +func (g *game) winner() int { + if g.move1 == empty || g.move2 == empty { + return -1 + } + if g.move1 == g.move2 { + return 0 + } + if g.move1 == rock && g.move2 == scissors || + g.move1 == paper && g.move2 == rock || + g.move1 == scissors && g.move2 == paper { + return 1 + } + return 2 +} + +// NewGame creates a new game where player1 is the caller and player2 the argument. +// A new game index is returned. +func NewGame(player std.Address) int { + games.Set(id.Next().String(), &game{player1: std.PrevRealm().Addr(), player2: player}) + return int(id) +} + +// Play executes a move for the game at index idx, where move can be: +// 1 (rock), 2 (paper), 3 (scissors). +func Play(idx, move int) { + v, ok := games.Get(seqid.ID(idx).String()) + if !ok { + panic("game not found") + } + if err := v.(*game).play(std.PrevRealm().Addr(), move); err != nil { + panic(err) + } +} + +func Render(path string) string { + mov1 := []string{"", " 🤜 ", " 🫱 ", " 👉 "} + mov2 := []string{"", " 🤛 ", " 🫲 ", " 👈 "} + win := []string{"pending", "draw", "player1", "player2"} + + output := `# 👊 ✋ ✌️ Shifumi +Actions: +* [NewGame](shifumi?help&__func=NewGame) opponentAddress +* [Play](shifumi?help&__func=Play) gameIndex move (1=rock, 2=paper, 3=scissors) + + game | player1 | | player2 | | win + --- | --- | --- | --- | --- | --- +` + // Output the 100 most recent games. + maxGames := 100 + for n := int(id); n > 0 && int(id)-n < maxGames; n-- { + v, ok := games.Get(seqid.ID(n).String()) + if !ok { + continue + } + g := v.(*game) + output += strconv.Itoa(n) + " | " + + shortName(g.player1) + " | " + mov1[g.move1] + " | " + + shortName(g.player2) + " | " + mov2[g.move2] + " | " + + win[g.winner()+1] + "\n" + } + return output +} + +func shortName(addr std.Address) string { + user := users.GetUserByAddress(addr) + if user != nil { + return user.Name + } + if len(addr) < 10 { + return string(addr) + } + return string(addr)[:10] + "..." +} diff --git a/examples/gno.land/r/leon/config/config.gno b/examples/gno.land/r/leon/config/config.gno new file mode 100644 index 00000000000..cbc1e537e3f --- /dev/null +++ b/examples/gno.land/r/leon/config/config.gno @@ -0,0 +1,65 @@ +package config + +import ( + "errors" + "std" +) + +var ( + main std.Address // leon's main address + backup std.Address // backup address +) + +func init() { + main = "g125em6arxsnj49vx35f0n0z34putv5ty3376fg5" +} + +func Address() std.Address { + return main +} + +func Backup() std.Address { + return backup +} + +func SetAddress(a std.Address) error { + if !a.IsValid() { + return errors.New("config: invalid address") + } + + if err := checkAuthorized(); err != nil { + return err + } + + main = a + return nil +} + +func SetBackup(a std.Address) error { + if !a.IsValid() { + return errors.New("config: invalid address") + } + + if err := checkAuthorized(); err != nil { + return err + } + + backup = a + return nil +} + +func checkAuthorized() error { + caller := std.PrevRealm().Addr() + if caller != main || caller != backup { + return errors.New("config: unauthorized") + } + + return nil +} + +func AssertAuthorized() { + caller := std.PrevRealm().Addr() + if caller != main || caller != backup { + panic("config: unauthorized") + } +} diff --git a/examples/gno.land/r/leon/config/gno.mod b/examples/gno.land/r/leon/config/gno.mod new file mode 100644 index 00000000000..e8cd5cd85b7 --- /dev/null +++ b/examples/gno.land/r/leon/config/gno.mod @@ -0,0 +1 @@ +module gno.land/r/leon/config diff --git a/examples/gno.land/r/leon/home/gno.mod b/examples/gno.land/r/leon/home/gno.mod new file mode 100644 index 00000000000..48cf64a9d0a --- /dev/null +++ b/examples/gno.land/r/leon/home/gno.mod @@ -0,0 +1,8 @@ +module gno.land/r/leon/home + +require ( + gno.land/p/demo/ufmt v0.0.0-latest + gno.land/r/demo/art/gnoface v0.0.0-latest + gno.land/r/demo/art/millipede v0.0.0-latest + gno.land/r/leon/config v0.0.0-latest +) diff --git a/examples/gno.land/r/leon/home/home.gno b/examples/gno.land/r/leon/home/home.gno new file mode 100644 index 00000000000..1f6a07e8959 --- /dev/null +++ b/examples/gno.land/r/leon/home/home.gno @@ -0,0 +1,121 @@ +package home + +import ( + "std" + "strconv" + + "gno.land/p/demo/ufmt" + + "gno.land/r/demo/art/gnoface" + "gno.land/r/demo/art/millipede" + "gno.land/r/leon/config" +) + +var ( + pfp string // link to profile picture + pfpCaption string // profile picture caption + abtMe [2]string +) + +func init() { + pfp = "https://i.imgflip.com/91vskx.jpg" + pfpCaption = "[My favourite painting & pfp](https://en.wikipedia.org/wiki/Wanderer_above_the_Sea_of_Fog)" + abtMe = [2]string{ + `### About me +Hi, I'm Leon, a DevRel Engineer at gno.land. I am a tech enthusiast, +life-long learner, and sharer of knowledge.`, + `### Contributions +My contributions to gno.land can mainly be found +[here](https://github.com/gnolang/gno/issues?q=sort:updated-desc+author:leohhhn). + +TODO import r/gh +`, + } +} + +func UpdatePFP(url, caption string) { + config.AssertAuthorized() + pfp = url + pfpCaption = caption +} + +func UpdateAboutMe(col1, col2 string) { + config.AssertAuthorized() + abtMe[0] = col1 + abtMe[1] = col2 +} + +func Render(path string) string { + out := "# Leon's Homepage\n\n" + + out += renderAboutMe() + out += renderBlogPosts() + out += "\n\n" + out += renderArt() + + return out +} + +func renderBlogPosts() string { + out := "" + //out += "## Leon's Blog Posts" + + // todo fetch blog posts authored by @leohhhn + // and render them + return out +} + +func renderAboutMe() string { + out := "
" + + out += "
\n\n" + out += ufmt.Sprintf("![my profile pic](%s)\n\n%s\n\n", pfp, pfpCaption) + out += "
\n\n" + + out += "
\n\n" + out += abtMe[0] + "\n\n" + out += "
\n\n" + + out += "
\n\n" + out += abtMe[1] + "\n\n" + out += "
\n\n" + + out += "
\n\n" + + return out +} + +func renderArt() string { + out := `
` + "\n\n" + out += "# Gno Art\n\n" + + out += "
" + + out += renderGnoFace() + out += renderMillipede() + out += "Empty spot :/" + + out += "
\n\n" + + out += "This art is dynamic; it will change with every new block.\n\n" + out += `
` + "\n" + + return out +} + +func renderGnoFace() string { + out := "
\n\n" + out += gnoface.Render(strconv.Itoa(int(std.GetHeight()))) + out += "
\n\n" + + return out +} + +func renderMillipede() string { + out := "
\n\n" + out += "Millipede\n\n" + out += "```\n" + millipede.Draw(int(std.GetHeight())%10+1) + "```\n" + out += "
\n\n" + + return out +} diff --git a/gnovm/pkg/gnolang/misc.go b/gnovm/pkg/gnolang/misc.go index a05de8c74aa..7f7ce0b3a87 100644 --- a/gnovm/pkg/gnolang/misc.go +++ b/gnovm/pkg/gnolang/misc.go @@ -150,6 +150,10 @@ func isReservedName(n Name) bool { // scans uverse static node for blocknames. (slow) func isUverseName(n Name) bool { + if n == "panic" { + // panic is not in uverse, as it is parsed as its own statement (PanicStmt) + return true + } uverseNames := UverseNode().GetBlockNames() for _, name := range uverseNames { if name == n { diff --git a/gnovm/pkg/gnolang/uverse.go b/gnovm/pkg/gnolang/uverse.go index 880a75396ca..38ccdddea85 100644 --- a/gnovm/pkg/gnolang/uverse.go +++ b/gnovm/pkg/gnolang/uverse.go @@ -928,17 +928,7 @@ func UverseNode() *PackageNode { return }, ) - defNative("panic", - Flds( // params - "err", AnyT(), // args[0] - ), - nil, // results - func(m *Machine) { - arg0 := m.LastBlock().GetParams1() - xv := arg0.Deref() - panic(xv.Sprint(m)) - }, - ) + // NOTE: panic is its own statement type, and is not defined as a function. defNative("print", Flds( // params "xs", Vrd(AnyT()), // args[0]