diff --git a/README.md b/README.md index 89bfd96d74f..8ad62cca820 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ If you haven't already, take a moment to check out our [website](https://gno.lan > can use it to check out > [some](https://gno.land/r/demo/boards) > [example](https://gno.land/r/gnoland/blog) -> [contracts](https://gno.land/r/demo/users). +> [contracts](https://gno.land/r/gnoland/users/v1). > > Use the `[source]` button in the header to inspect the program's source; use > the `[help]` button to view how you can use [`gnokey`](./gno.land/cmd/gnokey) diff --git a/docs/concepts/namespaces.md b/docs/concepts/namespaces.md index c7f03ec1f0a..c34f9b9e202 100644 --- a/docs/concepts/namespaces.md +++ b/docs/concepts/namespaces.md @@ -7,12 +7,6 @@ id: namespaces Namespaces provide users with the exclusive capability to publish contracts under their designated namespaces, similar to GitHub's user and organization model. -:::warning Not enabled - -This feature isn't enabled by default on the portal loop chain and is currently available only on test4.gno.land. - -::: - # Package Path A package path is a unique identifier for each package/realm. It specifies the location of the package source @@ -45,31 +39,28 @@ Examples: ## Registration Process The registration process is contract-based. The `AddPkg` command references -`sys/users` for filtering, which in turn is based on `r/demo/users`. +`r/sys/names` for filtering, which in turn is based on `r/sys/users`. -When `sys/users` is enabled, you need to register a name using `r/demo/users`. You can call the -`r/demo/users.Register` function to register the name for the caller's address. +To obtain a namespace, you need to register a name using `r/gnoland/users/v1`. +Visit [the realm](https://gno.land/r/gnoland/users/v1) for more information. -> ex: `test1` user registering as `patrick` +> ex: address `test1` registering as `patrick123` ```bash -$ gnokey maketx call -pkgpath gno.land/r/demo/users \ +$ gnokey maketx call -pkgpath gno.land/r/gnoland/users/v1 \ -func Register \ -gas-fee 1000000ugnot -gas-wanted 2000000 \ -broadcast \ - -chainid=test4 \ - -send=20000000ugnot \ - -args '' \ - -args 'patrick' \ - -args 'My Profile Quote' test1 + -chainid=portal-loop \ + -remote="https://gno.land/r/gnoland/users/v1" \ + -args 'patrick123' \ + test1 ``` :::note Chain-ID -Do not forget to update chain id, adequate to the network you're interacting with - +Depending on the network you're usiing, the ::: - After successful registration, you can add a package under the registered namespace. ## Anonymous Namespace diff --git a/docs/gno-tooling/cli/gnokey/querying-a-network.md b/docs/gno-tooling/cli/gnokey/querying-a-network.md index 1bb1bb8275f..248cfdf7f1c 100644 --- a/docs/gno-tooling/cli/gnokey/querying-a-network.md +++ b/docs/gno-tooling/cli/gnokey/querying-a-network.md @@ -152,28 +152,7 @@ gnokey query vm/qfile -data "gno.land/r/demo/wugnot/wugnot.gno" -remote https:// Output: ```bash height: 0 -data: package wugnot - -import ( - "std" - "strings" - - "gno.land/p/demo/grc/grc20" - "gno.land/p/demo/ufmt" - pusers "gno.land/p/demo/users" - "gno.land/r/demo/users" -) - -var ( - banker *grc20.Banker = grc20.NewBanker("wrapped GNOT", "wugnot", 0) - Token = banker.Token() -) - -const ( - ugnotMinDeposit uint64 = 1000 - wugnotMinDeposit uint64 = 1 -) -... +data: // package code ``` ## `vm/qeval` diff --git a/docs/reference/stdlibs/std/testing.md b/docs/reference/stdlibs/std/testing.md index 8a95ecf7827..cf0053445f7 100644 --- a/docs/reference/stdlibs/std/testing.md +++ b/docs/reference/stdlibs/std/testing.md @@ -108,7 +108,7 @@ Should be used in combination with [`NewUserRealm`](#newuserrealm) & addr := std.Address("g1ecely4gjy0yl6s9kt409ll330q9hk2lj9ls3ec") std.TestSetRealm(std.NewUserRealm(addr)) // or -std.TestSetRealm(std.NewCodeRealm("gno.land/r/demo/users")) +std.TestSetRealm(std.NewCodeRealm("gno.land/r/gnoland/users/v1")) ``` --- diff --git a/examples/gno.land/p/demo/pausable/pausable.gno b/examples/gno.land/p/demo/pausable/pausable.gno index e6a85771fa6..421f6e00b95 100644 --- a/examples/gno.land/p/demo/pausable/pausable.gno +++ b/examples/gno.land/p/demo/pausable/pausable.gno @@ -1,6 +1,10 @@ +// Package pausable provides a mechanism to programmatically pause and unpause +// functionality. This package allows an owner, defined via an Ownable object, +// to restrict operations or methods when the contract is in a "paused" state. package pausable import ( + "errors" "std" "gno.land/p/demo/ownable" @@ -11,6 +15,8 @@ type Pausable struct { paused bool } +var ErrPaused = errors.New("pausable: realm is currently paused") + // New returns a new Pausable struct with non-paused state as default func New() *Pausable { return &Pausable{ @@ -39,7 +45,7 @@ func (p *Pausable) Pause() error { } p.paused = true - std.Emit("Paused", "account", p.Owner().String()) + std.Emit("Paused", "by", p.Owner().String()) return nil } @@ -51,7 +57,7 @@ func (p *Pausable) Unpause() error { } p.paused = false - std.Emit("Unpaused", "account", p.Owner().String()) + std.Emit("Unpaused", "by", p.Owner().String()) return nil } diff --git a/examples/gno.land/p/demo/users/gno.mod b/examples/gno.land/p/demo/users/gno.mod deleted file mode 100644 index ad652803fb8..00000000000 --- a/examples/gno.land/p/demo/users/gno.mod +++ /dev/null @@ -1 +0,0 @@ -module gno.land/p/demo/users diff --git a/examples/gno.land/p/demo/users/types.gno b/examples/gno.land/p/demo/users/types.gno deleted file mode 100644 index d28b6a8ee42..00000000000 --- a/examples/gno.land/p/demo/users/types.gno +++ /dev/null @@ -1,14 +0,0 @@ -package users - -type AddressOrName string - -func (aon AddressOrName) IsName() bool { - return aon != "" && aon[0] == '@' -} - -func (aon AddressOrName) GetName() (string, bool) { - if len(aon) >= 2 && aon[0] == '@' { - return string(aon[1:]), true - } - return "", false -} diff --git a/examples/gno.land/p/demo/users/users.gno b/examples/gno.land/p/demo/users/users.gno deleted file mode 100644 index 204eaf19918..00000000000 --- a/examples/gno.land/p/demo/users/users.gno +++ /dev/null @@ -1,31 +0,0 @@ -package users - -import ( - "std" - "strconv" -) - -//---------------------------------------- -// Types - -type User struct { - Address std.Address - Name string - Profile string - Number int - Invites int - Inviter std.Address -} - -func (u *User) Render() string { - str := "## user " + u.Name + "\n" + - "\n" + - " * address = " + string(u.Address) + "\n" + - " * " + strconv.Itoa(u.Invites) + " invites\n" - if u.Inviter != "" { - str = str + " * invited by " + string(u.Inviter) + "\n" - } - str = str + "\n" + - u.Profile + "\n" - return str -} diff --git a/examples/gno.land/p/demo/users/users_test.gno b/examples/gno.land/p/demo/users/users_test.gno deleted file mode 100644 index 82abcb9fccb..00000000000 --- a/examples/gno.land/p/demo/users/users_test.gno +++ /dev/null @@ -1 +0,0 @@ -package users diff --git a/examples/gno.land/r/demo/boards/README.md b/examples/gno.land/r/demo/boards/README.md deleted file mode 100644 index 174e1c242fc..00000000000 --- a/examples/gno.land/r/demo/boards/README.md +++ /dev/null @@ -1,147 +0,0 @@ -This is a demo of Gno smart contract programming. This document was -constructed by Gno onto a smart contract hosted on the data Realm -name ["gno.land/r/demo/boards"](https://gno.land/r/demo/boards/) -([github](https://github.com/gnolang/gno/tree/master/examples/gno.land/r/demo/boards)). - - - -## Build `gnokey`, create your account, and interact with Gno. - -NOTE: Where you see `-remote localhost:26657` here, that flag can be replaced -with `-remote gno.land:26657` if you have $GNOT on the testnet. -(To use the testnet, also replace `-chainid dev` with `-chainid portal-loop` .) - -### Build `gnokey` (and other tools). - -```bash -git clone git@github.com:gnolang/gno.git -cd gno/gno.land -make build -``` - -### Generate a seed/mnemonic code. - -```bash -./build/gnokey generate -``` - -NOTE: You can generate 24 words with any good bip39 generator. - -### Create a new account using your mnemonic. - -```bash -./build/gnokey add -recover KEYNAME -``` - -NOTE: `KEYNAME` is your key identifier, and should be changed. - -### Verify that you can see your account locally. - -```bash -./build/gnokey list -``` - -Take note of your `addr` which looks something like `g17sphqax3kasjptdkmuqvn740u8dhtx4kxl6ljf` . -You will use this as your `ACCOUNT_ADDR`. - -## Interact with the blockchain. - -### Add $GNOT for your account. - -Before starting the `gnoland` node for the first time, your new account can be given $GNOT in the node genesis. -Edit the file `gno.land/genesis/genesis_balances.txt` and add the following line (simlar to the others), using -your `ACCOUNT_ADDR` and `KEYNAME` - -`ACCOUNT_ADDR=10000000000ugnot # @KEYNAME` - -### Alternative: Run a faucet to add $GNOT. - -Instead of editing `gno.land/genesis/genesis_balances.txt`, a more general solution (with more steps) -is to run a local "faucet" and use the web browser to add $GNOT. (This can be done at any time.) -See this page: https://github.com/gnolang/gno/blob/master/contribs/gnofaucet/README.md - - -### Start the `gnoland` node. - -```bash -./build/gnoland start -``` - -NOTE: The node already has the "boards" realm. - -Leave this running in the terminal. In a new terminal, cd to the same folder `gno/gno.land` . - -### Get your current balance, account number, and sequence number. - -```bash -./build/gnokey query auth/accounts/ACCOUNT_ADDR -remote localhost:26657 -``` - -### Register a board username with a smart contract call. - -The `USERNAME` for posting can different than your `KEYNAME`. It is internally linked to your `ACCOUNT_ADDR`. It must be at least 6 characters, lowercase alphanumeric with underscore. - -```bash -./build/gnokey maketx call -pkgpath "gno.land/r/demo/users" -func "Register" -args "" -args "USERNAME" -args "Profile description" -gas-fee "10000000ugnot" -gas-wanted "2000000" -send "200000000ugnot" -broadcast -chainid dev -remote 127.0.0.1:26657 KEYNAME -``` - -Interactive documentation: https://gno.land/r/demo/users$help&func=Register - -### Create a board with a smart contract call. - -```bash -./build/gnokey maketx call -pkgpath "gno.land/r/demo/boards" -func "CreateBoard" -args "BOARDNAME" -gas-fee "1000000ugnot" -gas-wanted "10000000" -broadcast -chainid dev -remote localhost:26657 KEYNAME -``` - -Interactive documentation: https://gno.land/r/demo/boards$help&func=CreateBoard - -Next, query for the permanent board ID by querying (you need this to create a new post): - -```bash -./build/gnokey query "vm/qeval" -data 'gno.land/r/demo/boards.GetBoardIDFromName("BOARDNAME")' -remote localhost:26657 -``` - -### Create a post of a board with a smart contract call. - -NOTE: If a board was created successfully, your SEQUENCE_NUMBER would have increased. - -```bash -./build/gnokey maketx call -pkgpath "gno.land/r/demo/boards" -func "CreateThread" -args BOARD_ID -args "Hello gno.land" -args "Text of the post" -gas-fee 1000000ugnot -gas-wanted 2000000 -broadcast -chainid dev -remote localhost:26657 KEYNAME -``` - -Interactive documentation: https://gno.land/r/demo/boards$help&func=CreateThread - -### Create a comment to a post. - -```bash -./build/gnokey maketx call -pkgpath "gno.land/r/demo/boards" -func "CreateReply" -args BOARD_ID -args "1" -args "1" -args "Nice to meet you too." -gas-fee 1000000ugnot -gas-wanted 2000000 -broadcast -chainid dev -remote localhost:26657 KEYNAME -``` - -Interactive documentation: https://gno.land/r/demo/boards$help&func=CreateReply - -```bash -./build/gnokey query "vm/qrender" -data "gno.land/r/demo/boards:BOARDNAME/1" -remote localhost:26657 -``` - -### Render page with optional path expression. - -The contents of `https://gno.land/r/demo/boards:` and `https://gno.land/r/demo/boards:gnolang` are rendered by calling -the `Render(path string)` function like so: - -```bash -./build/gnokey query "vm/qrender" -data "gno.land/r/demo/boards:gnolang" -``` -## View the board in the browser. - -### Start the web server. - -```bash -./build/gnoweb -``` - -This should print something like `Running on http://127.0.0.1:8888` . Leave this running in the terminal. - -### View in the browser - -In your browser, navigate to the printed address http://127.0.0.1:8888 . -To see you post, click on the package `/r/demo/boards` . diff --git a/examples/gno.land/r/demo/boards/misc.gno b/examples/gno.land/r/demo/boards/misc.gno index bc561ca7d22..01db5a16c5f 100644 --- a/examples/gno.land/r/demo/boards/misc.gno +++ b/examples/gno.land/r/demo/boards/misc.gno @@ -5,7 +5,7 @@ import ( "strconv" "strings" - "gno.land/r/demo/users" + "gno.land/r/sys/users" ) //---------------------------------------- @@ -78,18 +78,18 @@ func summaryOf(str string, length int) string { } func displayAddressMD(addr std.Address) string { - user := users.GetUserByAddress(addr) + user := users.ResolveAddress(addr) if user == nil { - return "[" + addr.String() + "](/r/demo/users:" + addr.String() + ")" + return "[" + addr.String() + "](/r/gnoland/users/v1:" + addr.String() + ")" } else { - return "[@" + user.Name + "](/r/demo/users:" + user.Name + ")" + return "[@" + user.Name() + "](/r/gnoland/users/v1:" + user.Name() + ")" } } func usernameOf(addr std.Address) string { - user := users.GetUserByAddress(addr) + user := users.ResolveAddress(addr) if user == nil { return "" } - return user.Name + return user.Name() } diff --git a/examples/gno.land/r/demo/boards/public.gno b/examples/gno.land/r/demo/boards/public.gno index 1d26126fcb2..a1bc8c14a4b 100644 --- a/examples/gno.land/r/demo/boards/public.gno +++ b/examples/gno.land/r/demo/boards/public.gno @@ -20,6 +20,7 @@ func CreateBoard(name string) BoardID { if !(std.IsOriginCall() || std.PrevRealm().IsUser()) { panic("invalid non-user call") } + bid := incGetBoardID() caller := std.GetOrigCaller() if usernameOf(caller) == "" { diff --git a/examples/gno.land/r/demo/boards/z_0_b_filetest.gno b/examples/gno.land/r/demo/boards/z_0_b_filetest.gno deleted file mode 100644 index 9bcbe9ffafa..00000000000 --- a/examples/gno.land/r/demo/boards/z_0_b_filetest.gno +++ /dev/null @@ -1,23 +0,0 @@ -// PKGPATH: gno.land/r/demo/boards_test -package boards_test - -// SEND: 19900000ugnot - -import ( - "gno.land/r/demo/boards" - "gno.land/r/demo/users" -) - -var bid boards.BoardID - -func init() { - users.Register("", "gnouser", "my profile") - bid = boards.CreateBoard("test_board") -} - -func main() { - println(boards.Render("test_board")) -} - -// Error: -// payment must not be less than 20000000 diff --git a/examples/gno.land/r/demo/boards/z_0_c_filetest.gno b/examples/gno.land/r/demo/boards/z_0_c_filetest.gno index 99fd339aed8..755359d15f0 100644 --- a/examples/gno.land/r/demo/boards/z_0_c_filetest.gno +++ b/examples/gno.land/r/demo/boards/z_0_c_filetest.gno @@ -4,14 +4,18 @@ package boards_test // SEND: 200000000ugnot import ( + "std" + "gno.land/r/demo/boards" - "gno.land/r/demo/users" + users "gno.land/r/gnoland/users/v1" ) var bid boards.BoardID func init() { - users.Register("", "gnouser", "my profile") + std.TestSetRealm(std.NewUserRealm(std.Address("g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm"))) // so that CurrentRealm.Addr() matches OrigCaller + std.TestSetRealm(std.NewUserRealm(std.Address("g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm"))) // so that CurrentRealm.Addr() matches OrigCaller + users.Register("gnouser123") boards.CreateThread(1, "First Post (title)", "Body of the first post. (body)") } diff --git a/examples/gno.land/r/demo/boards/z_0_d_filetest.gno b/examples/gno.land/r/demo/boards/z_0_d_filetest.gno index c77e60e3f3a..bc6cc6b62bf 100644 --- a/examples/gno.land/r/demo/boards/z_0_d_filetest.gno +++ b/examples/gno.land/r/demo/boards/z_0_d_filetest.gno @@ -4,14 +4,18 @@ package boards_test // SEND: 200000000ugnot import ( + "std" + "gno.land/r/demo/boards" - "gno.land/r/demo/users" + users "gno.land/r/gnoland/users/v1" ) var bid boards.BoardID func init() { - users.Register("", "gnouser", "my profile") + std.TestSetRealm(std.NewUserRealm(std.Address("g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm"))) // so that CurrentRealm.Addr() matches OrigCaller + std.TestSetRealm(std.NewUserRealm(std.Address("g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm"))) // so that CurrentRealm.Addr() matches OrigCaller + users.Register("gnouser123") bid = boards.CreateBoard("test_board") boards.CreateReply(bid, 0, 0, "Reply of the second post") } diff --git a/examples/gno.land/r/demo/boards/z_0_e_filetest.gno b/examples/gno.land/r/demo/boards/z_0_e_filetest.gno index 6db036e87ba..7c5aa4aad5e 100644 --- a/examples/gno.land/r/demo/boards/z_0_e_filetest.gno +++ b/examples/gno.land/r/demo/boards/z_0_e_filetest.gno @@ -4,14 +4,17 @@ package boards_test // SEND: 200000000ugnot import ( + "std" + "gno.land/r/demo/boards" - "gno.land/r/demo/users" + users "gno.land/r/gnoland/users/v1" ) var bid boards.BoardID func init() { - users.Register("", "gnouser", "my profile") + std.TestSetRealm(std.NewUserRealm(std.Address("g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm"))) // so that CurrentRealm.Addr() matches OrigCaller + users.Register("gnouser123") boards.CreateReply(bid, 0, 0, "Reply of the second post") } diff --git a/examples/gno.land/r/demo/boards/z_0_filetest.gno b/examples/gno.land/r/demo/boards/z_0_filetest.gno index a649895cb01..4a0df90fd41 100644 --- a/examples/gno.land/r/demo/boards/z_0_filetest.gno +++ b/examples/gno.land/r/demo/boards/z_0_filetest.gno @@ -1,17 +1,18 @@ // PKGPATH: gno.land/r/demo/boards_test package boards_test -// SEND: 20000000ugnot - import ( + "std" + "gno.land/r/demo/boards" - "gno.land/r/demo/users" + users "gno.land/r/gnoland/users/v1" ) var bid boards.BoardID func init() { - users.Register("", "gnouser", "my profile") + std.TestSetRealm(std.NewUserRealm(std.Address("g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm"))) // so that CurrentRealm.Addr() matches OrigCaller + err := users.Register("gnouser123") bid = boards.CreateBoard("test_board") boards.CreateThread(bid, "First Post (title)", "Body of the first post. (body)") @@ -30,12 +31,12 @@ func main() { // ## [First Post (title)](/r/demo/boards:test_board/1) // // Body of the first post. (body) -// \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm UTC](/r/demo/boards:test_board/1) \[[x](/r/demo/boards$help&func=DeletePost&bid=1&threadid=1&postid=1)] (0 replies) (0 reposts) +// \- [@gnouser123](/r/gnoland/users/v1:gnouser123), [2009-02-13 11:31pm UTC](/r/demo/boards:test_board/1) \[[x](/r/demo/boards$help&func=DeletePost&bid=1&threadid=1&postid=1)] (0 replies) (0 reposts) // // ---------------------------------------- // ## [Second Post (title)](/r/demo/boards:test_board/2) // // Body of the second post. (body) -// \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm UTC](/r/demo/boards:test_board/2) \[[x](/r/demo/boards$help&func=DeletePost&bid=1&threadid=2&postid=2)] (1 replies) (0 reposts) +// \- [@gnouser123](/r/gnoland/users/v1:gnouser123), [2009-02-13 11:31pm UTC](/r/demo/boards:test_board/2) \[[x](/r/demo/boards$help&func=DeletePost&bid=1&threadid=2&postid=2)] (1 replies) (0 reposts) // // diff --git a/examples/gno.land/r/demo/boards/z_10_a_filetest.gno b/examples/gno.land/r/demo/boards/z_10_a_filetest.gno index ad57283bfcf..9a763f5ad16 100644 --- a/examples/gno.land/r/demo/boards/z_10_a_filetest.gno +++ b/examples/gno.land/r/demo/boards/z_10_a_filetest.gno @@ -4,10 +4,11 @@ package boards_test // SEND: 200000000ugnot import ( + "std" "strconv" "gno.land/r/demo/boards" - "gno.land/r/demo/users" + users "gno.land/r/gnoland/users/v1" ) var ( @@ -16,7 +17,8 @@ var ( ) func init() { - users.Register("", "gnouser", "my profile") + std.TestSetRealm(std.NewUserRealm(std.Address("g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm"))) // so that CurrentRealm.Addr() matches OrigCaller + users.Register("gnouser123") bid = boards.CreateBoard("test_board") pid = boards.CreateThread(bid, "First Post in (title)", "Body of the first post. (body)") diff --git a/examples/gno.land/r/demo/boards/z_10_b_filetest.gno b/examples/gno.land/r/demo/boards/z_10_b_filetest.gno index cf8a332174f..0c56c63e5f5 100644 --- a/examples/gno.land/r/demo/boards/z_10_b_filetest.gno +++ b/examples/gno.land/r/demo/boards/z_10_b_filetest.gno @@ -4,10 +4,11 @@ package boards_test // SEND: 200000000ugnot import ( + "std" "strconv" "gno.land/r/demo/boards" - "gno.land/r/demo/users" + users "gno.land/r/gnoland/users/v1" ) var ( @@ -16,7 +17,8 @@ var ( ) func init() { - users.Register("", "gnouser", "my profile") + std.TestSetRealm(std.NewUserRealm(std.Address("g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm"))) // so that CurrentRealm.Addr() matches OrigCaller + users.Register("gnouser123") bid = boards.CreateBoard("test_board") pid = boards.CreateThread(bid, "First Post in (title)", "Body of the first post. (body)") diff --git a/examples/gno.land/r/demo/boards/z_10_c_filetest.gno b/examples/gno.land/r/demo/boards/z_10_c_filetest.gno index 7dd460500d6..1bece167a20 100644 --- a/examples/gno.land/r/demo/boards/z_10_c_filetest.gno +++ b/examples/gno.land/r/demo/boards/z_10_c_filetest.gno @@ -4,10 +4,11 @@ package boards_test // SEND: 200000000ugnot import ( + "std" "strconv" "gno.land/r/demo/boards" - "gno.land/r/demo/users" + users "gno.land/r/gnoland/users/v1" ) var ( @@ -17,7 +18,8 @@ var ( ) func init() { - users.Register("", "gnouser", "my profile") + std.TestSetRealm(std.NewUserRealm(std.Address("g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm"))) // so that CurrentRealm.Addr() matches OrigCaller + users.Register("gnouser123") bid = boards.CreateBoard("test_board") pid = boards.CreateThread(bid, "First Post in (title)", "Body of the first post. (body)") @@ -35,15 +37,15 @@ func main() { // # First Post in (title) // // Body of the first post. (body) -// \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/1) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&threadid=1&postid=1)] \[[repost](/r/demo/boards$help&func=CreateRepost&bid=1&postid=1)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&threadid=1&postid=1)] +// \- [@gnouser123](/r/gnoland/users/v1:gnouser123), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/1) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&threadid=1&postid=1)] \[[repost](/r/demo/boards$help&func=CreateRepost&bid=1&postid=1)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&threadid=1&postid=1)] // // > First reply of the First post // > -// > \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/1/2) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&threadid=1&postid=2)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&threadid=1&postid=2)] +// > \- [@gnouser123](/r/gnoland/users/v1:gnouser123), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/1/2) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&threadid=1&postid=2)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&threadid=1&postid=2)] // // ---------------------------------------------------- // # First Post in (title) // // Body of the first post. (body) -// \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/1) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&threadid=1&postid=1)] \[[repost](/r/demo/boards$help&func=CreateRepost&bid=1&postid=1)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&threadid=1&postid=1)] +// \- [@gnouser123](/r/gnoland/users/v1:gnouser123), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/1) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&threadid=1&postid=1)] \[[repost](/r/demo/boards$help&func=CreateRepost&bid=1&postid=1)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&threadid=1&postid=1)] // diff --git a/examples/gno.land/r/demo/boards/z_10_filetest.gno b/examples/gno.land/r/demo/boards/z_10_filetest.gno index 8a6d11c79cf..ece633fe355 100644 --- a/examples/gno.land/r/demo/boards/z_10_filetest.gno +++ b/examples/gno.land/r/demo/boards/z_10_filetest.gno @@ -4,10 +4,11 @@ package boards_test // SEND: 200000000ugnot import ( + "std" "strconv" "gno.land/r/demo/boards" - "gno.land/r/demo/users" + users "gno.land/r/gnoland/users/v1" ) var ( @@ -16,7 +17,8 @@ var ( ) func init() { - users.Register("", "gnouser", "my profile") + std.TestSetRealm(std.NewUserRealm(std.Address("g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm"))) // so that CurrentRealm.Addr() matches OrigCaller + users.Register("gnouser123") bid = boards.CreateBoard("test_board") pid = boards.CreateThread(bid, "First Post in (title)", "Body of the first post. (body)") @@ -33,7 +35,7 @@ func main() { // # First Post in (title) // // Body of the first post. (body) -// \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/1) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&threadid=1&postid=1)] \[[repost](/r/demo/boards$help&func=CreateRepost&bid=1&postid=1)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&threadid=1&postid=1)] +// \- [@gnouser123](/r/gnoland/users/v1:gnouser123), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/1) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&threadid=1&postid=1)] \[[repost](/r/demo/boards$help&func=CreateRepost&bid=1&postid=1)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&threadid=1&postid=1)] // // ---------------------------------------------------- // thread does not exist with id: 1 diff --git a/examples/gno.land/r/demo/boards/z_11_a_filetest.gno b/examples/gno.land/r/demo/boards/z_11_a_filetest.gno index d7dc7b90782..81e840c378e 100644 --- a/examples/gno.land/r/demo/boards/z_11_a_filetest.gno +++ b/examples/gno.land/r/demo/boards/z_11_a_filetest.gno @@ -4,10 +4,11 @@ package boards_test // SEND: 200000000ugnot import ( + "std" "strconv" "gno.land/r/demo/boards" - "gno.land/r/demo/users" + users "gno.land/r/gnoland/users/v1" ) var ( @@ -16,7 +17,8 @@ var ( ) func init() { - users.Register("", "gnouser", "my profile") + std.TestSetRealm(std.NewUserRealm(std.Address("g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm"))) // so that CurrentRealm.Addr() matches OrigCaller + users.Register("gnouser123") bid = boards.CreateBoard("test_board") pid = boards.CreateThread(bid, "First Post in (title)", "Body of the first post. (body)") diff --git a/examples/gno.land/r/demo/boards/z_11_b_filetest.gno b/examples/gno.land/r/demo/boards/z_11_b_filetest.gno index 3aa28095502..34b26b07b96 100644 --- a/examples/gno.land/r/demo/boards/z_11_b_filetest.gno +++ b/examples/gno.land/r/demo/boards/z_11_b_filetest.gno @@ -4,10 +4,11 @@ package boards_test // SEND: 200000000ugnot import ( + "std" "strconv" "gno.land/r/demo/boards" - "gno.land/r/demo/users" + users "gno.land/r/gnoland/users/v1" ) var ( @@ -16,7 +17,8 @@ var ( ) func init() { - users.Register("", "gnouser", "my profile") + std.TestSetRealm(std.NewUserRealm(std.Address("g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm"))) // so that CurrentRealm.Addr() matches OrigCaller + users.Register("gnouser123") bid = boards.CreateBoard("test_board") pid = boards.CreateThread(bid, "First Post in (title)", "Body of the first post. (body)") diff --git a/examples/gno.land/r/demo/boards/z_11_c_filetest.gno b/examples/gno.land/r/demo/boards/z_11_c_filetest.gno index df764303562..772c6758a8a 100644 --- a/examples/gno.land/r/demo/boards/z_11_c_filetest.gno +++ b/examples/gno.land/r/demo/boards/z_11_c_filetest.gno @@ -4,10 +4,11 @@ package boards_test // SEND: 200000000ugnot import ( + "std" "strconv" "gno.land/r/demo/boards" - "gno.land/r/demo/users" + users "gno.land/r/gnoland/users/v1" ) var ( @@ -16,7 +17,8 @@ var ( ) func init() { - users.Register("", "gnouser", "my profile") + std.TestSetRealm(std.NewUserRealm(std.Address("g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm"))) // so that CurrentRealm.Addr() matches OrigCaller + users.Register("gnouser123") bid = boards.CreateBoard("test_board") pid = boards.CreateThread(bid, "First Post in (title)", "Body of the first post. (body)") diff --git a/examples/gno.land/r/demo/boards/z_11_d_filetest.gno b/examples/gno.land/r/demo/boards/z_11_d_filetest.gno index f64b4c84bba..9958c493993 100644 --- a/examples/gno.land/r/demo/boards/z_11_d_filetest.gno +++ b/examples/gno.land/r/demo/boards/z_11_d_filetest.gno @@ -4,10 +4,11 @@ package boards_test // SEND: 200000000ugnot import ( + "std" "strconv" "gno.land/r/demo/boards" - "gno.land/r/demo/users" + users "gno.land/r/gnoland/users/v1" ) var ( @@ -17,7 +18,8 @@ var ( ) func init() { - users.Register("", "gnouser", "my profile") + std.TestSetRealm(std.NewUserRealm(std.Address("g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm"))) // so that CurrentRealm.Addr() matches OrigCaller + users.Register("gnouser123") bid = boards.CreateBoard("test_board") pid = boards.CreateThread(bid, "First Post in (title)", "Body of the first post. (body)") @@ -35,19 +37,19 @@ func main() { // # First Post in (title) // // Body of the first post. (body) -// \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/1) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&threadid=1&postid=1)] \[[repost](/r/demo/boards$help&func=CreateRepost&bid=1&postid=1)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&threadid=1&postid=1)] +// \- [@gnouser123](/r/gnoland/users/v1:gnouser123), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/1) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&threadid=1&postid=1)] \[[repost](/r/demo/boards$help&func=CreateRepost&bid=1&postid=1)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&threadid=1&postid=1)] // // > First reply of the First post // > -// > \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/1/2) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&threadid=1&postid=2)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&threadid=1&postid=2)] +// > \- [@gnouser123](/r/gnoland/users/v1:gnouser123), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/1/2) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&threadid=1&postid=2)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&threadid=1&postid=2)] // // ---------------------------------------------------- // # First Post in (title) // // Body of the first post. (body) -// \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/1) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&threadid=1&postid=1)] \[[repost](/r/demo/boards$help&func=CreateRepost&bid=1&postid=1)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&threadid=1&postid=1)] +// \- [@gnouser123](/r/gnoland/users/v1:gnouser123), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/1) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&threadid=1&postid=1)] \[[repost](/r/demo/boards$help&func=CreateRepost&bid=1&postid=1)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&threadid=1&postid=1)] // // > Edited: First reply of the First post // > -// > \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/1/2) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&threadid=1&postid=2)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&threadid=1&postid=2)] +// > \- [@gnouser123](/r/gnoland/users/v1:gnouser123), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/1/2) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&threadid=1&postid=2)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&threadid=1&postid=2)] // diff --git a/examples/gno.land/r/demo/boards/z_11_filetest.gno b/examples/gno.land/r/demo/boards/z_11_filetest.gno index 3f56293b3bd..2e8849830d4 100644 --- a/examples/gno.land/r/demo/boards/z_11_filetest.gno +++ b/examples/gno.land/r/demo/boards/z_11_filetest.gno @@ -4,10 +4,11 @@ package boards_test // SEND: 200000000ugnot import ( + "std" "strconv" "gno.land/r/demo/boards" - "gno.land/r/demo/users" + users "gno.land/r/gnoland/users/v1" ) var ( @@ -16,7 +17,8 @@ var ( ) func init() { - users.Register("", "gnouser", "my profile") + std.TestSetRealm(std.NewUserRealm(std.Address("g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm"))) // so that CurrentRealm.Addr() matches OrigCaller + users.Register("gnouser123") bid = boards.CreateBoard("test_board") pid = boards.CreateThread(bid, "First Post in (title)", "Body of the first post. (body)") @@ -33,11 +35,11 @@ func main() { // # First Post in (title) // // Body of the first post. (body) -// \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/1) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&threadid=1&postid=1)] \[[repost](/r/demo/boards$help&func=CreateRepost&bid=1&postid=1)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&threadid=1&postid=1)] +// \- [@gnouser123](/r/gnoland/users/v1:gnouser123), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/1) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&threadid=1&postid=1)] \[[repost](/r/demo/boards$help&func=CreateRepost&bid=1&postid=1)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&threadid=1&postid=1)] // // ---------------------------------------------------- // # Edited: First Post in (title) // // Edited: Body of the first post. (body) -// \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/1) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&threadid=1&postid=1)] \[[repost](/r/demo/boards$help&func=CreateRepost&bid=1&postid=1)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&threadid=1&postid=1)] +// \- [@gnouser123](/r/gnoland/users/v1:gnouser123), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/1) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&threadid=1&postid=1)] \[[repost](/r/demo/boards$help&func=CreateRepost&bid=1&postid=1)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&threadid=1&postid=1)] // diff --git a/examples/gno.land/r/demo/boards/z_12_a_filetest.gno b/examples/gno.land/r/demo/boards/z_12_a_filetest.gno index 909be880efa..fa714153c78 100644 --- a/examples/gno.land/r/demo/boards/z_12_a_filetest.gno +++ b/examples/gno.land/r/demo/boards/z_12_a_filetest.gno @@ -8,11 +8,12 @@ import ( "gno.land/p/demo/testutils" "gno.land/r/demo/boards" - "gno.land/r/demo/users" + users "gno.land/r/gnoland/users/v1" ) func main() { - users.Register("", "gnouser", "my profile") + std.TestSetRealm(std.NewUserRealm(std.Address("g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm"))) // so that CurrentRealm.Addr() matches OrigCaller + users.Register("gnouser123") // create a post via registered user bid1 := boards.CreateBoard("test_board1") pid := boards.CreateThread(bid1, "First Post (title)", "Body of the first post. (body)") diff --git a/examples/gno.land/r/demo/boards/z_12_b_filetest.gno b/examples/gno.land/r/demo/boards/z_12_b_filetest.gno index 6b2166895c0..6911f2bdd76 100644 --- a/examples/gno.land/r/demo/boards/z_12_b_filetest.gno +++ b/examples/gno.land/r/demo/boards/z_12_b_filetest.gno @@ -4,12 +4,15 @@ package boards_test // SEND: 200000000ugnot import ( + "std" + "gno.land/r/demo/boards" - "gno.land/r/demo/users" + users "gno.land/r/gnoland/users/v1" ) func main() { - users.Register("", "gnouser", "my profile") + std.TestSetRealm(std.NewUserRealm(std.Address("g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm"))) // so that CurrentRealm.Addr() matches OrigCaller + users.Register("gnouser123") bid1 := boards.CreateBoard("test_board1") pid := boards.CreateThread(bid1, "First Post (title)", "Body of the first post. (body)") bid2 := boards.CreateBoard("test_board2") diff --git a/examples/gno.land/r/demo/boards/z_12_c_filetest.gno b/examples/gno.land/r/demo/boards/z_12_c_filetest.gno index 7397c487d7d..d5d78b2a890 100644 --- a/examples/gno.land/r/demo/boards/z_12_c_filetest.gno +++ b/examples/gno.land/r/demo/boards/z_12_c_filetest.gno @@ -4,12 +4,15 @@ package boards_test // SEND: 200000000ugnot import ( + "std" + "gno.land/r/demo/boards" - "gno.land/r/demo/users" + users "gno.land/r/gnoland/users/v1" ) func main() { - users.Register("", "gnouser", "my profile") + std.TestSetRealm(std.NewUserRealm(std.Address("g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm"))) // so that CurrentRealm.Addr() matches OrigCaller + users.Register("gnouser123") bid1 := boards.CreateBoard("test_board1") boards.CreateThread(bid1, "First Post (title)", "Body of the first post. (body)") bid2 := boards.CreateBoard("test_board2") diff --git a/examples/gno.land/r/demo/boards/z_12_d_filetest.gno b/examples/gno.land/r/demo/boards/z_12_d_filetest.gno index 37b6473f7ac..74c936e1d05 100644 --- a/examples/gno.land/r/demo/boards/z_12_d_filetest.gno +++ b/examples/gno.land/r/demo/boards/z_12_d_filetest.gno @@ -4,12 +4,15 @@ package boards_test // SEND: 200000000ugnot import ( + "std" + "gno.land/r/demo/boards" - "gno.land/r/demo/users" + users "gno.land/r/gnoland/users/v1" ) func main() { - users.Register("", "gnouser", "my profile") + std.TestSetRealm(std.NewUserRealm(std.Address("g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm"))) // so that CurrentRealm.Addr() matches OrigCaller + users.Register("gnouser123") bid1 := boards.CreateBoard("test_board1") pid := boards.CreateThread(bid1, "First Post (title)", "Body of the first post. (body)") boards.CreateBoard("test_board2") diff --git a/examples/gno.land/r/demo/boards/z_12_filetest.gno b/examples/gno.land/r/demo/boards/z_12_filetest.gno index ac4adf6ee7b..a780874b7a1 100644 --- a/examples/gno.land/r/demo/boards/z_12_filetest.gno +++ b/examples/gno.land/r/demo/boards/z_12_filetest.gno @@ -4,8 +4,10 @@ package boards_test // SEND: 200000000ugnot import ( + "std" + "gno.land/r/demo/boards" - "gno.land/r/demo/users" + users "gno.land/r/gnoland/users/v1" ) var ( @@ -15,7 +17,8 @@ var ( ) func init() { - users.Register("", "gnouser", "my profile") + std.TestSetRealm(std.NewUserRealm(std.Address("g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm"))) // so that CurrentRealm.Addr() matches OrigCaller + users.Register("gnouser123") bid1 = boards.CreateBoard("test_board1") pid = boards.CreateThread(bid1, "First Post (title)", "Body of the first post. (body)") @@ -37,6 +40,6 @@ func main() { // ## [First Post (title)](/r/demo/boards:test_board1/1) // // Body of the first post. (body) -// \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm UTC](/r/demo/boards:test_board1/1) \[[x](/r/demo/boards$help&func=DeletePost&bid=1&threadid=1&postid=1)] (0 replies) (1 reposts) +// \- [@gnouser123](/r/gnoland/users/v1:gnouser123), [2009-02-13 11:31pm UTC](/r/demo/boards:test_board1/1) \[[x](/r/demo/boards$help&func=DeletePost&bid=1&threadid=1&postid=1)] (0 replies) (1 reposts) // // diff --git a/examples/gno.land/r/demo/boards/z_1_filetest.gno b/examples/gno.land/r/demo/boards/z_1_filetest.gno index 4d46c81b83d..07a978a38c1 100644 --- a/examples/gno.land/r/demo/boards/z_1_filetest.gno +++ b/examples/gno.land/r/demo/boards/z_1_filetest.gno @@ -4,14 +4,17 @@ package boards_test // SEND: 200000000ugnot import ( + "std" + "gno.land/r/demo/boards" - "gno.land/r/demo/users" + users "gno.land/r/gnoland/users/v1" ) var board *boards.Board func init() { - users.Register("", "gnouser", "my profile") + std.TestSetRealm(std.NewUserRealm(std.Address("g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm"))) // so that CurrentRealm.Addr() matches OrigCaller + users.Register("gnouser123") _ = boards.CreateBoard("test_board_1") _ = boards.CreateBoard("test_board_2") diff --git a/examples/gno.land/r/demo/boards/z_2_filetest.gno b/examples/gno.land/r/demo/boards/z_2_filetest.gno index 31b39644b24..4a4d3c94011 100644 --- a/examples/gno.land/r/demo/boards/z_2_filetest.gno +++ b/examples/gno.land/r/demo/boards/z_2_filetest.gno @@ -4,10 +4,11 @@ package boards_test // SEND: 200000000ugnot import ( + "std" "strconv" "gno.land/r/demo/boards" - "gno.land/r/demo/users" + users "gno.land/r/gnoland/users/v1" ) var ( @@ -16,7 +17,8 @@ var ( ) func init() { - users.Register("", "gnouser", "my profile") + std.TestSetRealm(std.NewUserRealm(std.Address("g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm"))) // so that CurrentRealm.Addr() matches OrigCaller + users.Register("gnouser123") bid = boards.CreateBoard("test_board") boards.CreateThread(bid, "First Post (title)", "Body of the first post. (body)") @@ -32,8 +34,8 @@ func main() { // # Second Post (title) // // Body of the second post. (body) -// \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/2) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&threadid=2&postid=2)] \[[repost](/r/demo/boards$help&func=CreateRepost&bid=1&postid=2)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&threadid=2&postid=2)] +// \- [@gnouser123](/r/gnoland/users/v1:gnouser123), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/2) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&threadid=2&postid=2)] \[[repost](/r/demo/boards$help&func=CreateRepost&bid=1&postid=2)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&threadid=2&postid=2)] // // > Reply of the second post -// > \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/2/3) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&threadid=2&postid=3)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&threadid=2&postid=3)] +// > \- [@gnouser123](/r/gnoland/users/v1:gnouser123), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/2/3) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&threadid=2&postid=3)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&threadid=2&postid=3)] // diff --git a/examples/gno.land/r/demo/boards/z_3_filetest.gno b/examples/gno.land/r/demo/boards/z_3_filetest.gno index 0b2a2df2f91..d60ea25c710 100644 --- a/examples/gno.land/r/demo/boards/z_3_filetest.gno +++ b/examples/gno.land/r/demo/boards/z_3_filetest.gno @@ -4,10 +4,11 @@ package boards_test // SEND: 200000000ugnot import ( + "std" "strconv" "gno.land/r/demo/boards" - "gno.land/r/demo/users" + users "gno.land/r/gnoland/users/v1" ) var ( @@ -16,7 +17,8 @@ var ( ) func init() { - users.Register("", "gnouser", "my profile") + std.TestSetRealm(std.NewUserRealm(std.Address("g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm"))) // so that CurrentRealm.Addr() matches OrigCaller + users.Register("gnouser123") bid = boards.CreateBoard("test_board") boards.CreateThread(bid, "First Post (title)", "Body of the first post. (body)") @@ -34,8 +36,8 @@ func main() { // # Second Post (title) // // Body of the second post. (body) -// \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/2) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&threadid=2&postid=2)] \[[repost](/r/demo/boards$help&func=CreateRepost&bid=1&postid=2)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&threadid=2&postid=2)] +// \- [@gnouser123](/r/gnoland/users/v1:gnouser123), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/2) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&threadid=2&postid=2)] \[[repost](/r/demo/boards$help&func=CreateRepost&bid=1&postid=2)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&threadid=2&postid=2)] // // > Reply of the second post -// > \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/2/3) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&threadid=2&postid=3)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&threadid=2&postid=3)] +// > \- [@gnouser123](/r/gnoland/users/v1:gnouser123), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/2/3) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&threadid=2&postid=3)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&threadid=2&postid=3)] // diff --git a/examples/gno.land/r/demo/boards/z_4_filetest.gno b/examples/gno.land/r/demo/boards/z_4_filetest.gno index b781e94e4db..e9d4a08f379 100644 --- a/examples/gno.land/r/demo/boards/z_4_filetest.gno +++ b/examples/gno.land/r/demo/boards/z_4_filetest.gno @@ -4,10 +4,11 @@ package boards_test // SEND: 200000000ugnot import ( + "std" "strconv" "gno.land/r/demo/boards" - "gno.land/r/demo/users" + users "gno.land/r/gnoland/users/v1" ) var ( @@ -16,7 +17,8 @@ var ( ) func init() { - users.Register("", "gnouser", "my profile") + std.TestSetRealm(std.NewUserRealm(std.Address("g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm"))) // so that CurrentRealm.Addr() matches OrigCaller + users.Register("gnouser123") bid = boards.CreateBoard("test_board") boards.CreateThread(bid, "First Post (title)", "Body of the first post. (body)") @@ -37,17 +39,18 @@ func main() { // # Second Post (title) // // Body of the second post. (body) -// \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/2) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&threadid=2&postid=2)] \[[repost](/r/demo/boards$help&func=CreateRepost&bid=1&postid=2)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&threadid=2&postid=2)] +// \- [@gnouser123](/r/gnoland/users/v1:gnouser123), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/2) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&threadid=2&postid=2)] \[[repost](/r/demo/boards$help&func=CreateRepost&bid=1&postid=2)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&threadid=2&postid=2)] // // > Reply of the second post -// > \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/2/3) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&threadid=2&postid=3)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&threadid=2&postid=3)] +// > \- [@gnouser123](/r/gnoland/users/v1:gnouser123), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/2/3) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&threadid=2&postid=3)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&threadid=2&postid=3)] // // > Second reply of the second post -// > \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/2/4) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&threadid=2&postid=4)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&threadid=2&postid=4)] +// > \- [@gnouser123](/r/gnoland/users/v1:gnouser123), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/2/4) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&threadid=2&postid=4)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&threadid=2&postid=4)] // // Realm: -// switchrealm["gno.land/r/demo/users"] +// switchrealm["gno.land/r/sys/users"] +// switchrealm["gno.land/r/sys/users"] // switchrealm["gno.land/r/demo/boards"] // u[f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:111]={ // "ObjectInfo": { @@ -905,8 +908,14 @@ func main() { // } // } // switchrealm["gno.land/r/demo/boards"] -// switchrealm["gno.land/r/demo/users"] -// switchrealm["gno.land/r/demo/users"] -// switchrealm["gno.land/r/demo/users"] +// switchrealm["gno.land/r/sys/users"] +// switchrealm["gno.land/r/sys/users"] +// switchrealm["gno.land/r/sys/users"] +// switchrealm["gno.land/r/sys/users"] +// switchrealm["gno.land/r/sys/users"] +// switchrealm["gno.land/r/sys/users"] +// switchrealm["gno.land/r/sys/users"] +// switchrealm["gno.land/r/sys/users"] +// switchrealm["gno.land/r/sys/users"] // switchrealm["gno.land/r/demo/boards"] // switchrealm["gno.land/r/demo/boards_test"] diff --git a/examples/gno.land/r/demo/boards/z_5_b_filetest.gno b/examples/gno.land/r/demo/boards/z_5_b_filetest.gno index e79da5c3677..148e598de3d 100644 --- a/examples/gno.land/r/demo/boards/z_5_b_filetest.gno +++ b/examples/gno.land/r/demo/boards/z_5_b_filetest.gno @@ -8,13 +8,14 @@ import ( "gno.land/p/demo/testutils" "gno.land/r/demo/boards" - "gno.land/r/demo/users" + users "gno.land/r/gnoland/users/v1" ) const admin = std.Address("g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj") func main() { - users.Register("", "gnouser", "my profile") + std.TestSetRealm(std.NewUserRealm(std.Address("g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm"))) // so that CurrentRealm.Addr() matches OrigCaller + users.Register("gnouser123") // create board via registered user bid := boards.CreateBoard("test_board") diff --git a/examples/gno.land/r/demo/boards/z_5_c_filetest.gno b/examples/gno.land/r/demo/boards/z_5_c_filetest.gno index 723e6a10204..2faaefeef44 100644 --- a/examples/gno.land/r/demo/boards/z_5_c_filetest.gno +++ b/examples/gno.land/r/demo/boards/z_5_c_filetest.gno @@ -8,13 +8,14 @@ import ( "gno.land/p/demo/testutils" "gno.land/r/demo/boards" - "gno.land/r/demo/users" + users "gno.land/r/gnoland/users/v1" ) const admin = std.Address("g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj") func main() { - users.Register("", "gnouser", "my profile") + std.TestSetRealm(std.NewUserRealm(std.Address("g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm"))) // so that CurrentRealm.Addr() matches OrigCaller + users.Register("gnouser123") // create board via registered user bid := boards.CreateBoard("test_board") @@ -33,8 +34,8 @@ func main() { // # First Post (title) // // Body of the first post. (body) -// \- [g1w3jhxapjta047h6lta047h6lta047h6laqcyu4](/r/demo/users:g1w3jhxapjta047h6lta047h6lta047h6laqcyu4), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/1) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&threadid=1&postid=1)] \[[repost](/r/demo/boards$help&func=CreateRepost&bid=1&postid=1)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&threadid=1&postid=1)] +// \- [g1w3jhxapjta047h6lta047h6lta047h6laqcyu4](/r/gnoland/users/v1:g1w3jhxapjta047h6lta047h6lta047h6laqcyu4), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/1) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&threadid=1&postid=1)] \[[repost](/r/demo/boards$help&func=CreateRepost&bid=1&postid=1)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&threadid=1&postid=1)] // // > Reply of the first post -// > \- [g1w3jhxapjta047h6lta047h6lta047h6laqcyu4](/r/demo/users:g1w3jhxapjta047h6lta047h6lta047h6laqcyu4), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/1/2) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&threadid=1&postid=2)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&threadid=1&postid=2)] +// > \- [g1w3jhxapjta047h6lta047h6lta047h6laqcyu4](/r/gnoland/users/v1:g1w3jhxapjta047h6lta047h6lta047h6laqcyu4), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/1/2) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&threadid=1&postid=2)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&threadid=1&postid=2)] // diff --git a/examples/gno.land/r/demo/boards/z_5_d_filetest.gno b/examples/gno.land/r/demo/boards/z_5_d_filetest.gno index 54cfe49eec6..9dc0ddb9665 100644 --- a/examples/gno.land/r/demo/boards/z_5_d_filetest.gno +++ b/examples/gno.land/r/demo/boards/z_5_d_filetest.gno @@ -8,13 +8,14 @@ import ( "gno.land/p/demo/testutils" "gno.land/r/demo/boards" - "gno.land/r/demo/users" + users "gno.land/r/gnoland/users/v1" ) const admin = std.Address("g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj") func main() { - users.Register("", "gnouser", "my profile") + std.TestSetRealm(std.NewUserRealm(std.Address("g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm"))) // so that CurrentRealm.Addr() matches OrigCaller + users.Register("gnouser123") // create board via registered user bid := boards.CreateBoard("test_board") pid := boards.CreateThread(bid, "First Post (title)", "Body of the first post. (body)") diff --git a/examples/gno.land/r/demo/boards/z_5_filetest.gno b/examples/gno.land/r/demo/boards/z_5_filetest.gno index 712af483891..f04ea5a0b7e 100644 --- a/examples/gno.land/r/demo/boards/z_5_filetest.gno +++ b/examples/gno.land/r/demo/boards/z_5_filetest.gno @@ -4,10 +4,11 @@ package boards_test // SEND: 200000000ugnot import ( + "std" "strconv" "gno.land/r/demo/boards" - "gno.land/r/demo/users" + users "gno.land/r/gnoland/users/v1" ) var ( @@ -16,7 +17,8 @@ var ( ) func init() { - users.Register("", "gnouser", "my profile") + std.TestSetRealm(std.NewUserRealm(std.Address("g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm"))) // so that CurrentRealm.Addr() matches OrigCaller + users.Register("gnouser123") bid = boards.CreateBoard("test_board") boards.CreateThread(bid, "First Post (title)", "Body of the first post. (body)") @@ -33,12 +35,12 @@ func main() { // # Second Post (title) // // Body of the second post. (body) -// \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/2) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&threadid=2&postid=2)] \[[repost](/r/demo/boards$help&func=CreateRepost&bid=1&postid=2)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&threadid=2&postid=2)] +// \- [@gnouser123](/r/gnoland/users/v1:gnouser123), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/2) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&threadid=2&postid=2)] \[[repost](/r/demo/boards$help&func=CreateRepost&bid=1&postid=2)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&threadid=2&postid=2)] // // > Reply of the second post -// > \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/2/3) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&threadid=2&postid=3)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&threadid=2&postid=3)] +// > \- [@gnouser123](/r/gnoland/users/v1:gnouser123), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/2/3) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&threadid=2&postid=3)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&threadid=2&postid=3)] // // > Second reply of the second post // > -// > \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/2/4) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&threadid=2&postid=4)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&threadid=2&postid=4)] +// > \- [@gnouser123](/r/gnoland/users/v1:gnouser123), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/2/4) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&threadid=2&postid=4)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&threadid=2&postid=4)] // diff --git a/examples/gno.land/r/demo/boards/z_6_filetest.gno b/examples/gno.land/r/demo/boards/z_6_filetest.gno index ec40cf5f8e9..a41c2cbad97 100644 --- a/examples/gno.land/r/demo/boards/z_6_filetest.gno +++ b/examples/gno.land/r/demo/boards/z_6_filetest.gno @@ -4,10 +4,11 @@ package boards_test // SEND: 200000000ugnot import ( + "std" "strconv" "gno.land/r/demo/boards" - "gno.land/r/demo/users" + users "gno.land/r/gnoland/users/v1" ) var ( @@ -17,7 +18,8 @@ var ( ) func init() { - users.Register("", "gnouser", "my profile") + std.TestSetRealm(std.NewUserRealm(std.Address("g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm"))) // so that CurrentRealm.Addr() matches OrigCaller + users.Register("gnouser123") bid = boards.CreateBoard("test_board") boards.CreateThread(bid, "First Post (title)", "Body of the first post. (body)") @@ -35,16 +37,16 @@ func main() { // # Second Post (title) // // Body of the second post. (body) -// \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/2) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&threadid=2&postid=2)] \[[repost](/r/demo/boards$help&func=CreateRepost&bid=1&postid=2)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&threadid=2&postid=2)] +// \- [@gnouser123](/r/gnoland/users/v1:gnouser123), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/2) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&threadid=2&postid=2)] \[[repost](/r/demo/boards$help&func=CreateRepost&bid=1&postid=2)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&threadid=2&postid=2)] // // > Reply of the second post -// > \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/2/3) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&threadid=2&postid=3)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&threadid=2&postid=3)] +// > \- [@gnouser123](/r/gnoland/users/v1:gnouser123), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/2/3) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&threadid=2&postid=3)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&threadid=2&postid=3)] // > // > > First reply of the first reply // > > -// > > \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/2/5) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&threadid=2&postid=5)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&threadid=2&postid=5)] +// > > \- [@gnouser123](/r/gnoland/users/v1:gnouser123), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/2/5) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&threadid=2&postid=5)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&threadid=2&postid=5)] // // > Second reply of the second post // > -// > \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/2/4) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&threadid=2&postid=4)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&threadid=2&postid=4)] +// > \- [@gnouser123](/r/gnoland/users/v1:gnouser123), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/2/4) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&threadid=2&postid=4)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&threadid=2&postid=4)] // diff --git a/examples/gno.land/r/demo/boards/z_7_filetest.gno b/examples/gno.land/r/demo/boards/z_7_filetest.gno index 353b84f6d87..43136353e5c 100644 --- a/examples/gno.land/r/demo/boards/z_7_filetest.gno +++ b/examples/gno.land/r/demo/boards/z_7_filetest.gno @@ -4,13 +4,16 @@ package boards_test // SEND: 200000000ugnot import ( + "std" + "gno.land/r/demo/boards" - "gno.land/r/demo/users" + users "gno.land/r/gnoland/users/v1" ) func init() { // register - users.Register("", "gnouser", "my profile") + std.TestSetRealm(std.NewUserRealm(std.Address("g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm"))) // so that CurrentRealm.Addr() matches OrigCaller + users.Register("gnouser123") // create board and post bid := boards.CreateBoard("test_board") @@ -28,6 +31,6 @@ func main() { // ## [First Post (title)](/r/demo/boards:test_board/1) // // Body of the first post. (body) -// \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm UTC](/r/demo/boards:test_board/1) \[[x](/r/demo/boards$help&func=DeletePost&bid=1&threadid=1&postid=1)] (0 replies) (0 reposts) +// \- [@gnouser123](/r/gnoland/users/v1:gnouser123), [2009-02-13 11:31pm UTC](/r/demo/boards:test_board/1) \[[x](/r/demo/boards$help&func=DeletePost&bid=1&threadid=1&postid=1)] (0 replies) (0 reposts) // // diff --git a/examples/gno.land/r/demo/boards/z_8_filetest.gno b/examples/gno.land/r/demo/boards/z_8_filetest.gno index 4896dfcfccf..ca179b7775e 100644 --- a/examples/gno.land/r/demo/boards/z_8_filetest.gno +++ b/examples/gno.land/r/demo/boards/z_8_filetest.gno @@ -4,10 +4,11 @@ package boards_test // SEND: 200000000ugnot import ( + "std" "strconv" "gno.land/r/demo/boards" - "gno.land/r/demo/users" + users "gno.land/r/gnoland/users/v1" ) var ( @@ -17,7 +18,8 @@ var ( ) func init() { - users.Register("", "gnouser", "my profile") + std.TestSetRealm(std.NewUserRealm(std.Address("g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm"))) // so that CurrentRealm.Addr() matches OrigCaller + users.Register("gnouser123") bid = boards.CreateBoard("test_board") boards.CreateThread(bid, "First Post (title)", "Body of the first post. (body)") @@ -35,11 +37,11 @@ func main() { // _[see thread](/r/demo/boards:test_board/2)_ // // Reply of the second post -// \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/2/3) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&threadid=2&postid=3)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&threadid=2&postid=3)] +// \- [@gnouser123](/r/gnoland/users/v1:gnouser123), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/2/3) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&threadid=2&postid=3)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&threadid=2&postid=3)] // // _[see all 1 replies](/r/demo/boards:test_board/2/3)_ // // > First reply of the first reply // > -// > \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/2/5) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&threadid=2&postid=5)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&threadid=2&postid=5)] +// > \- [@gnouser123](/r/gnoland/users/v1:gnouser123), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/2/5) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&threadid=2&postid=5)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&threadid=2&postid=5)] // diff --git a/examples/gno.land/r/demo/boards/z_9_a_filetest.gno b/examples/gno.land/r/demo/boards/z_9_a_filetest.gno index 8d07ba0e710..aff09b4aa0a 100644 --- a/examples/gno.land/r/demo/boards/z_9_a_filetest.gno +++ b/examples/gno.land/r/demo/boards/z_9_a_filetest.gno @@ -4,14 +4,17 @@ package boards_test // SEND: 200000000ugnot import ( + "std" + "gno.land/r/demo/boards" - "gno.land/r/demo/users" + users "gno.land/r/gnoland/users/v1" ) var dstBoard boards.BoardID func init() { - users.Register("", "gnouser", "my profile") + std.TestSetRealm(std.NewUserRealm(std.Address("g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm"))) // so that CurrentRealm.Addr() matches OrigCaller + users.Register("gnouser123") dstBoard = boards.CreateBoard("dst_board") diff --git a/examples/gno.land/r/demo/boards/z_9_b_filetest.gno b/examples/gno.land/r/demo/boards/z_9_b_filetest.gno index 68daf770b4f..ac017b40a0f 100644 --- a/examples/gno.land/r/demo/boards/z_9_b_filetest.gno +++ b/examples/gno.land/r/demo/boards/z_9_b_filetest.gno @@ -4,8 +4,10 @@ package boards_test // SEND: 200000000ugnot import ( + "std" + "gno.land/r/demo/boards" - "gno.land/r/demo/users" + users "gno.land/r/gnoland/users/v1" ) var ( @@ -14,7 +16,8 @@ var ( ) func init() { - users.Register("", "gnouser", "my profile") + std.TestSetRealm(std.NewUserRealm(std.Address("g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm"))) // so that CurrentRealm.Addr() matches OrigCaller + users.Register("gnouser123") srcBoard = boards.CreateBoard("first_board") pid = boards.CreateThread(srcBoard, "First Post in (title)", "Body of the first post. (body)") diff --git a/examples/gno.land/r/demo/boards/z_9_filetest.gno b/examples/gno.land/r/demo/boards/z_9_filetest.gno index ca37e306bda..9beb18e9977 100644 --- a/examples/gno.land/r/demo/boards/z_9_filetest.gno +++ b/examples/gno.land/r/demo/boards/z_9_filetest.gno @@ -4,10 +4,11 @@ package boards_test // SEND: 200000000ugnot import ( + "std" "strconv" "gno.land/r/demo/boards" - "gno.land/r/demo/users" + users "gno.land/r/gnoland/users/v1" ) var ( @@ -17,7 +18,8 @@ var ( ) func init() { - users.Register("", "gnouser", "my profile") + std.TestSetRealm(std.NewUserRealm(std.Address("g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm"))) // so that CurrentRealm.Addr() matches OrigCaller + users.Register("gnouser123") firstBoard = boards.CreateBoard("first_board") secondBoard = boards.CreateBoard("second_board") @@ -34,5 +36,5 @@ func main() { // # First Post in (title) // // Body of the first post. (body) -// \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:second_board/1/1) \[[reply](/r/demo/boards$help&func=CreateReply&bid=2&threadid=1&postid=1)] \[[x](/r/demo/boards$help&func=DeletePost&bid=2&threadid=1&postid=1)] +// \- [@gnouser123](/r/gnoland/users/v1:gnouser123), [2009-02-13 11:31pm (UTC)](/r/demo/boards:second_board/1/1) \[[reply](/r/demo/boards$help&func=CreateReply&bid=2&threadid=1&postid=1)] \[[x](/r/demo/boards$help&func=DeletePost&bid=2&threadid=1&postid=1)] // diff --git a/examples/gno.land/r/demo/foo1155/foo1155.gno b/examples/gno.land/r/demo/foo1155/foo1155.gno index 2bd3b7a84c0..76dd5ed8a94 100644 --- a/examples/gno.land/r/demo/foo1155/foo1155.gno +++ b/examples/gno.land/r/demo/foo1155/foo1155.gno @@ -5,9 +5,6 @@ import ( "gno.land/p/demo/grc/grc1155" "gno.land/p/demo/ufmt" - "gno.land/r/demo/users" - - pusers "gno.land/p/demo/users" ) var ( @@ -29,8 +26,8 @@ func mintGRC1155Token(owner std.Address) { // Getters -func BalanceOf(user pusers.AddressOrName, tid grc1155.TokenID) uint64 { - balance, err := foo.BalanceOf(users.Resolve(user), tid) +func BalanceOf(user std.Address, tid grc1155.TokenID) uint64 { + balance, err := foo.BalanceOf(user, tid) if err != nil { panic(err) } @@ -38,11 +35,11 @@ func BalanceOf(user pusers.AddressOrName, tid grc1155.TokenID) uint64 { return balance } -func BalanceOfBatch(ul []pusers.AddressOrName, batch []grc1155.TokenID) []uint64 { +func BalanceOfBatch(ul []std.Address, batch []grc1155.TokenID) []uint64 { var usersResolved []std.Address for i := 0; i < len(ul); i++ { - usersResolved[i] = users.Resolve(ul[i]) + usersResolved[i] = ul[i] } balanceBatch, err := foo.BalanceOfBatch(usersResolved, batch) if err != nil { @@ -52,28 +49,28 @@ func BalanceOfBatch(ul []pusers.AddressOrName, batch []grc1155.TokenID) []uint64 return balanceBatch } -func IsApprovedForAll(owner, user pusers.AddressOrName) bool { - return foo.IsApprovedForAll(users.Resolve(owner), users.Resolve(user)) +func IsApprovedForAll(owner, user std.Address) bool { + return foo.IsApprovedForAll(owner, user) } // Setters -func SetApprovalForAll(user pusers.AddressOrName, approved bool) { - err := foo.SetApprovalForAll(users.Resolve(user), approved) +func SetApprovalForAll(user std.Address, approved bool) { + err := foo.SetApprovalForAll(user, approved) if err != nil { panic(err) } } -func TransferFrom(from, to pusers.AddressOrName, tid grc1155.TokenID, amount uint64) { - err := foo.SafeTransferFrom(users.Resolve(from), users.Resolve(to), tid, amount) +func TransferFrom(from, to std.Address, tid grc1155.TokenID, amount uint64) { + err := foo.SafeTransferFrom(from, to, tid, amount) if err != nil { panic(err) } } -func BatchTransferFrom(from, to pusers.AddressOrName, batch []grc1155.TokenID, amounts []uint64) { - err := foo.SafeBatchTransferFrom(users.Resolve(from), users.Resolve(to), batch, amounts) +func BatchTransferFrom(from, to std.Address, batch []grc1155.TokenID, amounts []uint64) { + err := foo.SafeBatchTransferFrom(from, to, batch, amounts) if err != nil { panic(err) } @@ -81,37 +78,37 @@ func BatchTransferFrom(from, to pusers.AddressOrName, batch []grc1155.TokenID, a // Admin -func Mint(to pusers.AddressOrName, tid grc1155.TokenID, amount uint64) { +func Mint(to std.Address, tid grc1155.TokenID, amount uint64) { caller := std.GetOrigCaller() assertIsAdmin(caller) - err := foo.SafeMint(users.Resolve(to), tid, amount) + err := foo.SafeMint(to, tid, amount) if err != nil { panic(err) } } -func MintBatch(to pusers.AddressOrName, batch []grc1155.TokenID, amounts []uint64) { +func MintBatch(to std.Address, batch []grc1155.TokenID, amounts []uint64) { caller := std.GetOrigCaller() assertIsAdmin(caller) - err := foo.SafeBatchMint(users.Resolve(to), batch, amounts) + err := foo.SafeBatchMint(to, batch, amounts) if err != nil { panic(err) } } -func Burn(from pusers.AddressOrName, tid grc1155.TokenID, amount uint64) { +func Burn(from std.Address, tid grc1155.TokenID, amount uint64) { caller := std.GetOrigCaller() assertIsAdmin(caller) - err := foo.Burn(users.Resolve(from), tid, amount) + err := foo.Burn(from, tid, amount) if err != nil { panic(err) } } -func BurnBatch(from pusers.AddressOrName, batch []grc1155.TokenID, amounts []uint64) { +func BurnBatch(from std.Address, batch []grc1155.TokenID, amounts []uint64) { caller := std.GetOrigCaller() assertIsAdmin(caller) - err := foo.BatchBurn(users.Resolve(from), batch, amounts) + err := foo.BatchBurn(from, batch, amounts) if err != nil { panic(err) } diff --git a/examples/gno.land/r/demo/foo1155/foo1155_test.gno b/examples/gno.land/r/demo/foo1155/foo1155_test.gno index 8ea722939f2..503052c5174 100644 --- a/examples/gno.land/r/demo/foo1155/foo1155_test.gno +++ b/examples/gno.land/r/demo/foo1155/foo1155_test.gno @@ -1,15 +1,15 @@ package foo1155 import ( + "std" "testing" "gno.land/p/demo/grc/grc1155" - "gno.land/p/demo/users" ) func TestFoo721(t *testing.T) { - admin := users.AddressOrName("g10x5phu0k6p64cwrhfpsc8tk43st9kug6wft530") - bob := users.AddressOrName("g1ze6et22ces5atv79y4xh38s4kuraey4y2fr6tw") + admin := std.Address("g10x5phu0k6p64cwrhfpsc8tk43st9kug6wft530") + bob := std.Address("g1ze6et22ces5atv79y4xh38s4kuraey4y2fr6tw") tid1 := grc1155.TokenID("1") for _, tc := range []struct { diff --git a/examples/gno.land/r/demo/foo721/foo721.gno b/examples/gno.land/r/demo/foo721/foo721.gno index f7364d4185f..8f18bb70f15 100644 --- a/examples/gno.land/r/demo/foo721/foo721.gno +++ b/examples/gno.land/r/demo/foo721/foo721.gno @@ -5,9 +5,6 @@ import ( "gno.land/p/demo/grc/grc721" "gno.land/p/demo/ufmt" - "gno.land/r/demo/users" - - pusers "gno.land/p/demo/users" ) var ( @@ -30,8 +27,8 @@ func mintNNFT(owner std.Address, n uint64) { // Getters -func BalanceOf(user pusers.AddressOrName) uint64 { - balance, err := foo.BalanceOf(users.Resolve(user)) +func BalanceOf(addr std.Address) uint64 { + balance, err := foo.BalanceOf(addr) if err != nil { panic(err) } @@ -48,8 +45,8 @@ func OwnerOf(tid grc721.TokenID) std.Address { return owner } -func IsApprovedForAll(owner, user pusers.AddressOrName) bool { - return foo.IsApprovedForAll(users.Resolve(owner), users.Resolve(user)) +func IsApprovedForAll(owner, user std.Address) bool { + return foo.IsApprovedForAll(owner, user) } func GetApproved(tid grc721.TokenID) std.Address { @@ -63,22 +60,22 @@ func GetApproved(tid grc721.TokenID) std.Address { // Setters -func Approve(user pusers.AddressOrName, tid grc721.TokenID) { - err := foo.Approve(users.Resolve(user), tid) +func Approve(user std.Address, tid grc721.TokenID) { + err := foo.Approve(user, tid) if err != nil { panic(err) } } -func SetApprovalForAll(user pusers.AddressOrName, approved bool) { - err := foo.SetApprovalForAll(users.Resolve(user), approved) +func SetApprovalForAll(user std.Address, approved bool) { + err := foo.SetApprovalForAll(user, approved) if err != nil { panic(err) } } -func TransferFrom(from, to pusers.AddressOrName, tid grc721.TokenID) { - err := foo.TransferFrom(users.Resolve(from), users.Resolve(to), tid) +func TransferFrom(from, to std.Address, tid grc721.TokenID) { + err := foo.TransferFrom(from, to, tid) if err != nil { panic(err) } @@ -86,10 +83,10 @@ func TransferFrom(from, to pusers.AddressOrName, tid grc721.TokenID) { // Admin -func Mint(to pusers.AddressOrName, tid grc721.TokenID) { +func Mint(to std.Address, tid grc721.TokenID) { caller := std.PrevRealm().Addr() assertIsAdmin(caller) - err := foo.Mint(users.Resolve(to), tid) + err := foo.Mint(to, tid) if err != nil { panic(err) } diff --git a/examples/gno.land/r/demo/foo721/foo721_test.gno b/examples/gno.land/r/demo/foo721/foo721_test.gno index fab39e561d1..7ee524ba9a6 100644 --- a/examples/gno.land/r/demo/foo721/foo721_test.gno +++ b/examples/gno.land/r/demo/foo721/foo721_test.gno @@ -1,17 +1,15 @@ package foo721 import ( + "std" "testing" "gno.land/p/demo/grc/grc721" - "gno.land/r/demo/users" - - pusers "gno.land/p/demo/users" ) func TestFoo721(t *testing.T) { - admin := pusers.AddressOrName("g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj") - hariom := pusers.AddressOrName("g1var589z07ppjsjd24ukm4uguzwdt0tw7g47cgm") + admin := std.Address("g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj") + hariom := std.Address("g1var589z07ppjsjd24ukm4uguzwdt0tw7g47cgm") for _, tc := range []struct { name string @@ -20,7 +18,7 @@ func TestFoo721(t *testing.T) { }{ {"BalanceOf(admin)", uint64(10), func() interface{} { return BalanceOf(admin) }}, {"BalanceOf(hariom)", uint64(5), func() interface{} { return BalanceOf(hariom) }}, - {"OwnerOf(0)", users.Resolve(admin), func() interface{} { return OwnerOf(grc721.TokenID("0")) }}, + {"OwnerOf(0)", admin, func() interface{} { return OwnerOf(grc721.TokenID("0")) }}, {"IsApprovedForAll(admin, hariom)", false, func() interface{} { return IsApprovedForAll(admin, hariom) }}, } { t.Run(tc.name, func(t *testing.T) { diff --git a/examples/gno.land/r/demo/games/dice_roller/dice_roller.gno b/examples/gno.land/r/demo/games/dice_roller/dice_roller.gno index 4dbbd6c7682..3b48e7dc97a 100644 --- a/examples/gno.land/r/demo/games/dice_roller/dice_roller.gno +++ b/examples/gno.land/r/demo/games/dice_roller/dice_roller.gno @@ -12,7 +12,8 @@ import ( "gno.land/p/demo/entropy" "gno.land/p/demo/seqid" "gno.land/p/demo/ufmt" - "gno.land/r/demo/users" + + "gno.land/r/sys/users" ) type ( @@ -222,9 +223,9 @@ The top players are ranked by performance. Games played against oneself are not // shortName returns a shortened name for the given address func shortName(addr std.Address) string { - user := users.GetUserByAddress(addr) + user := users.ResolveAddress(addr) if user != nil { - return user.Name + return user.Name() } if len(addr) < 10 { return string(addr) diff --git a/examples/gno.land/r/demo/games/shifumi/shifumi.gno b/examples/gno.land/r/demo/games/shifumi/shifumi.gno index 3de09196da1..dd0848c4abe 100644 --- a/examples/gno.land/r/demo/games/shifumi/shifumi.gno +++ b/examples/gno.land/r/demo/games/shifumi/shifumi.gno @@ -8,7 +8,7 @@ import ( "gno.land/p/demo/avl" "gno.land/p/demo/seqid" - "gno.land/r/demo/users" + "gno.land/r/sys/users" ) const ( @@ -109,9 +109,9 @@ Actions: } func shortName(addr std.Address) string { - user := users.GetUserByAddress(addr) + user := users.ResolveAddress(addr) if user != nil { - return user.Name + return user.Name() } if len(addr) < 10 { return string(addr) diff --git a/examples/gno.land/r/demo/groups/README.md b/examples/gno.land/r/demo/groups/README.md deleted file mode 100644 index ecdd5065903..00000000000 --- a/examples/gno.land/r/demo/groups/README.md +++ /dev/null @@ -1,24 +0,0 @@ -### - test package - - ./build/gno test examples/gno.land/r/demo/groups/ - -### - add pkg - - ./build/gnokey maketx addpkg -pkgdir "examples/gno.land/r/demo/groups" -deposit 100000000ugnot -gas-fee 1000000ugnot -gas-wanted 10000000 -broadcast -chainid dev -remote 0.0.0.0:26657 -pkgpath "gno.land/r/demo/groups" test1 - -### - create group - - ./build/gnokey maketx call -func "CreateGroup" -args "dao_trinity_ngo" -gas-fee "1000000ugnot" -gas-wanted 4000000 -broadcast -chainid dev -remote 0.0.0.0:26657 -pkgpath "gno.land/r/demo/groups" test1 - -### - add member - - ./build/gnokey maketx call -func "AddMember" -args "1" -args "g1hd3gwzevxlqmd3jsf64mpfczag8a8e5j2wdn3c" -args 12 -args "i am new user" -gas-fee "1000000ugnot" -gas-wanted "4000000" -broadcast -chainid dev -remote 0.0.0.0:26657 -pkgpath "gno.land/r/demo/groups" test1 - -### - delete member - - ./build/gnokey maketx call -func "DeleteMember" -args "1" -args "0" -gas-fee "1000000ugnot" -gas-wanted "4000000" -broadcast -chainid dev -remote 0.0.0.0:26657 -pkgpath "gno.land/r/demo/groups" test1 - -### - delete group - - ./build/gnokey maketx call -func "DeleteGroup" -args "1" -gas-fee "1000000ugnot" -gas-wanted "4000000" -broadcast -chainid dev -remote 0.0.0.0:26657 -pkgpath "gno.land/r/demo/groups" test1 - diff --git a/examples/gno.land/r/demo/groups/misc.gno b/examples/gno.land/r/demo/groups/misc.gno index 24834b7b60c..d842f9b552e 100644 --- a/examples/gno.land/r/demo/groups/misc.gno +++ b/examples/gno.land/r/demo/groups/misc.gno @@ -5,7 +5,7 @@ import ( "strconv" "strings" - "gno.land/r/demo/users" + "gno.land/r/sys/users" ) //---------------------------------------- @@ -76,19 +76,19 @@ func summaryOf(str string, length int) string { } func displayAddressMD(addr std.Address) string { - user := users.GetUserByAddress(addr) + user := users.ResolveAddress(addr) if user == nil { - return "[" + addr.String() + "](/r/demo/users:" + addr.String() + ")" + return "[" + addr.String() + "](/r/gnoland/users/v1:" + addr.String() + ")" } - return "[@" + user.Name + "](/r/demo/users:" + user.Name + ")" + return "[@" + user.Name() + "](/r/gnoland/users/v1:" + user.Name() + ")" } func usernameOf(addr std.Address) string { - user := users.GetUserByAddress(addr) + user := users.ResolveAddress(addr) if user == nil { panic("user not found") } - return user.Name + return user.Name() } func isValidPermission(perm Permission) bool { diff --git a/examples/gno.land/r/demo/groups/public.gno b/examples/gno.land/r/demo/groups/public.gno index 33e7dbdcf35..443d59ac0cc 100644 --- a/examples/gno.land/r/demo/groups/public.gno +++ b/examples/gno.land/r/demo/groups/public.gno @@ -3,7 +3,7 @@ package groups import ( "std" - "gno.land/r/demo/users" + "gno.land/r/sys/users" ) //---------------------------------------- @@ -37,7 +37,7 @@ func AddMember(gid GroupID, address string, weight int, metadata string) MemberI if !group.HasPermission(caller, EditPermission) { panic("unauthorized to edit group") } - user := users.GetUserByAddress(std.Address(address)) + user := users.ResolveAddress(std.Address(address)) if user == nil { panic("unknown address " + address) } diff --git a/examples/gno.land/r/demo/groups/z_0_b_filetest.gno b/examples/gno.land/r/demo/groups/z_0_b_filetest.gno index 6d328825dd6..9198a923cbd 100644 --- a/examples/gno.land/r/demo/groups/z_0_b_filetest.gno +++ b/examples/gno.land/r/demo/groups/z_0_b_filetest.gno @@ -3,17 +3,17 @@ package groups_test import ( "gno.land/r/demo/groups" - "gno.land/r/demo/users" + users "gno.land/r/gnoland/users/v1" ) var gid groups.GroupID func main() { - users.Register("", "gnouser", "my profile") + users.Register("gnouser123") gid = groups.CreateGroup("test_group") println(gid) println(groups.Render("")) } // Error: -// payment must not be less than 20000000 +// user not found diff --git a/examples/gno.land/r/demo/groups/z_0_c_filetest.gno b/examples/gno.land/r/demo/groups/z_0_c_filetest.gno index 60600e38b78..32abb99a2c4 100644 --- a/examples/gno.land/r/demo/groups/z_0_c_filetest.gno +++ b/examples/gno.land/r/demo/groups/z_0_c_filetest.gno @@ -1,17 +1,18 @@ // PKGPATH: gno.land/r/demo/groups_test package groups_test -// SEND: 200000000ugnot - import ( + "std" + "gno.land/r/demo/groups" - "gno.land/r/demo/users" + users "gno.land/r/gnoland/users/v1" ) var gid groups.GroupID func main() { - users.Register("", "gnouser", "my profile") + std.TestSetRealm(std.NewUserRealm(std.Address("g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm"))) // so that CurrentRealm.Addr() matches OrigCaller + users.Register("gnouser123") gid = groups.CreateGroup("test_group") println(gid) println(groups.Render("")) diff --git a/examples/gno.land/r/demo/groups/z_1_a_filetest.gno b/examples/gno.land/r/demo/groups/z_1_a_filetest.gno index 71da1b966ec..cc48b7203b9 100644 --- a/examples/gno.land/r/demo/groups/z_1_a_filetest.gno +++ b/examples/gno.land/r/demo/groups/z_1_a_filetest.gno @@ -1,14 +1,12 @@ // PKGPATH: gno.land/r/demo/groups_test package groups_test -// SEND: 200000000ugnot - import ( "std" "gno.land/p/demo/testutils" "gno.land/r/demo/groups" - "gno.land/r/demo/users" + users "gno.land/r/gnoland/users/v1" ) var gid groups.GroupID @@ -17,42 +15,26 @@ const admin = std.Address("g1manfred47kzduec920z88wfr64ylksmdcedlf5") func main() { caller := std.GetOrigCaller() // main - users.Register("", "gnouser0", "my profile 1") + std.TestSetRealm(std.NewUserRealm(caller)) + users.Register("main123") - std.TestSetOrigCaller(admin) - users.GrantInvites(caller.String() + ":1") - // switch back to caller - std.TestSetOrigCaller(caller) - // invite another addr test1 := testutils.TestAddress("gnouser1") - users.Invite(test1.String()) - // switch to test1 std.TestSetOrigCaller(test1) - users.Register(caller, "gnouser1", "my other profile 1") + std.TestSetRealm(std.NewUserRealm(test1)) + users.Register("test123") - std.TestSetOrigCaller(admin) - users.GrantInvites(caller.String() + ":1") - // switch back to caller - std.TestSetOrigCaller(caller) - // invite another addr test2 := testutils.TestAddress("gnouser2") - users.Invite(test2.String()) - // switch to test1 std.TestSetOrigCaller(test2) - users.Register(caller, "gnouser2", "my other profile 2") + std.TestSetRealm(std.NewUserRealm(test2)) + users.Register("test223") - std.TestSetOrigCaller(admin) - users.GrantInvites(caller.String() + ":1") - // switch back to caller - std.TestSetOrigCaller(caller) - // invite another addr test3 := testutils.TestAddress("gnouser3") - users.Invite(test3.String()) - // switch to test1 std.TestSetOrigCaller(test3) - users.Register(caller, "gnouser3", "my other profile 3") + std.TestSetRealm(std.NewUserRealm(test3)) + users.Register("test323") std.TestSetOrigCaller(caller) + std.TestSetRealm(std.NewUserRealm(caller)) gid = groups.CreateGroup("test_group") println(gid) @@ -67,7 +49,7 @@ func main() { // // Group Name: test_group // -// Group Creator: gnouser0 +// Group Creator: main123 // // Group createdAt: 2009-02-13 23:31:30 +0000 UTC m=+1234567890.000000001 // diff --git a/examples/gno.land/r/demo/groups/z_1_b_filetest.gno b/examples/gno.land/r/demo/groups/z_1_b_filetest.gno index 31a036d4e41..2a931909e53 100644 --- a/examples/gno.land/r/demo/groups/z_1_b_filetest.gno +++ b/examples/gno.land/r/demo/groups/z_1_b_filetest.gno @@ -5,13 +5,13 @@ package groups_test import ( "gno.land/r/demo/groups" - "gno.land/r/demo/users" + users "gno.land/r/gnoland/users/v1" ) var gid groups.GroupID func main() { - users.Register("", "gnouser", "my profile") + users.Register("gnouser123") gid = groups.CreateGroup("test_group") println(gid) groups.AddMember(2, "g1vahx7atnv4erxh6lta047h6lta047h6ll85gpy", 55, "metadata3") @@ -19,4 +19,4 @@ func main() { } // Error: -// group id (2) does not exists +// user not found diff --git a/examples/gno.land/r/demo/groups/z_2_a_filetest.gno b/examples/gno.land/r/demo/groups/z_2_a_filetest.gno index 0c482e1b52f..38f4e081ae1 100644 --- a/examples/gno.land/r/demo/groups/z_2_a_filetest.gno +++ b/examples/gno.land/r/demo/groups/z_2_a_filetest.gno @@ -8,7 +8,7 @@ import ( "gno.land/p/demo/testutils" "gno.land/r/demo/groups" - "gno.land/r/demo/users" + users "gno.land/r/gnoland/users/v1" ) var gid groups.GroupID @@ -17,45 +17,29 @@ const admin = std.Address("g1manfred47kzduec920z88wfr64ylksmdcedlf5") func main() { caller := std.GetOrigCaller() // main - users.Register("", "gnouser0", "my profile 1") + std.TestSetRealm(std.NewUserRealm(caller)) + users.Register("main123") - std.TestSetOrigCaller(admin) - users.GrantInvites(caller.String() + ":1") - // switch back to caller - std.TestSetOrigCaller(caller) - // invite another addr test1 := testutils.TestAddress("gnouser1") - users.Invite(test1.String()) - // switch to test1 std.TestSetOrigCaller(test1) - users.Register(caller, "gnouser1", "my other profile 1") + std.TestSetRealm(std.NewUserRealm(test1)) + users.Register("test123") - std.TestSetOrigCaller(admin) - users.GrantInvites(caller.String() + ":1") - // switch back to caller - std.TestSetOrigCaller(caller) - // invite another addr test2 := testutils.TestAddress("gnouser2") - users.Invite(test2.String()) - // switch to test1 std.TestSetOrigCaller(test2) - users.Register(caller, "gnouser2", "my other profile 2") + std.TestSetRealm(std.NewUserRealm(test2)) + users.Register("test223") - std.TestSetOrigCaller(admin) - users.GrantInvites(caller.String() + ":1") - // switch back to caller - std.TestSetOrigCaller(caller) - // invite another addr test3 := testutils.TestAddress("gnouser3") - users.Invite(test3.String()) - // switch to test1 std.TestSetOrigCaller(test3) - users.Register(caller, "gnouser3", "my other profile 3") + std.TestSetRealm(std.NewUserRealm(test3)) + users.Register("test323") std.TestSetOrigCaller(caller) + std.TestSetRealm(std.NewUserRealm(caller)) gid = groups.CreateGroup("test_group") - println(gid) + println(groups.Render("test_group")) groups.AddMember(gid, test2.String(), 42, "metadata3") @@ -64,12 +48,24 @@ func main() { } // Output: -// 1 // Group ID: 0000000001 // // Group Name: test_group // -// Group Creator: gnouser0 +// Group Creator: main123 +// +// Group createdAt: 2009-02-13 23:31:30 +0000 UTC m=+1234567890.000000001 +// +// Group Last MemberID: 0000000000 +// +// Group Members: +// +// +// Group ID: 0000000001 +// +// Group Name: test_group +// +// Group Creator: main123 // // Group createdAt: 2009-02-13 23:31:30 +0000 UTC m=+1234567890.000000001 // diff --git a/examples/gno.land/r/demo/groups/z_2_b_filetest.gno b/examples/gno.land/r/demo/groups/z_2_b_filetest.gno index fd8e485f16f..1049382a94a 100644 --- a/examples/gno.land/r/demo/groups/z_2_b_filetest.gno +++ b/examples/gno.land/r/demo/groups/z_2_b_filetest.gno @@ -5,13 +5,13 @@ package groups_test import ( "gno.land/r/demo/groups" - "gno.land/r/demo/users" + users "gno.land/r/gnoland/users/v1" ) var gid groups.GroupID func main() { - users.Register("", "gnouser", "my profile") + users.Register("gnouser123") gid = groups.CreateGroup("test_group") println(gid) groups.DeleteMember(2, 0) @@ -19,4 +19,4 @@ func main() { } // Error: -// group id (2) does not exists +// user not found diff --git a/examples/gno.land/r/demo/groups/z_2_d_filetest.gno b/examples/gno.land/r/demo/groups/z_2_d_filetest.gno index 3caa726cbd3..6c13f3be783 100644 --- a/examples/gno.land/r/demo/groups/z_2_d_filetest.gno +++ b/examples/gno.land/r/demo/groups/z_2_d_filetest.gno @@ -8,13 +8,13 @@ import ( "gno.land/p/demo/testutils" "gno.land/r/demo/groups" - "gno.land/r/demo/users" + users "gno.land/r/gnoland/users/v1" ) var gid groups.GroupID func main() { - users.Register("", "gnouser", "my profile") + users.Register("gnouser123") gid = groups.CreateGroup("test_group") println(gid) @@ -28,4 +28,4 @@ func main() { } // Error: -// unauthorized to delete member +// user not found diff --git a/examples/gno.land/r/demo/groups/z_2_e_filetest.gno b/examples/gno.land/r/demo/groups/z_2_e_filetest.gno index ff38acf45a4..93ecc8eb5ea 100644 --- a/examples/gno.land/r/demo/groups/z_2_e_filetest.gno +++ b/examples/gno.land/r/demo/groups/z_2_e_filetest.gno @@ -4,14 +4,17 @@ package groups_test // SEND: 200000000ugnot import ( + "std" + "gno.land/r/demo/groups" - "gno.land/r/demo/users" + users "gno.land/r/gnoland/users/v1" ) var gid groups.GroupID func main() { - users.Register("", "gnouser", "my profile") + std.TestSetRealm(std.NewUserRealm(std.Address("g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm"))) // so that CurrentRealm.Addr() matches OrigCaller + users.Register("gnouser123") gid = groups.CreateGroup("test_group") println(gid) groups.DeleteGroup(gid) diff --git a/examples/gno.land/r/demo/groups/z_2_f_filetest.gno b/examples/gno.land/r/demo/groups/z_2_f_filetest.gno index 4fddb768e08..27c0a7a6e7b 100644 --- a/examples/gno.land/r/demo/groups/z_2_f_filetest.gno +++ b/examples/gno.land/r/demo/groups/z_2_f_filetest.gno @@ -5,13 +5,13 @@ package groups_test import ( "gno.land/r/demo/groups" - "gno.land/r/demo/users" + users "gno.land/r/gnoland/users/v1" ) var gid groups.GroupID func main() { - users.Register("", "gnouser", "my profile") + users.Register("gnouser123") gid = groups.CreateGroup("test_group") println(gid) groups.DeleteGroup(20) @@ -19,4 +19,4 @@ func main() { } // Error: -// group id (20) does not exists +// user not found diff --git a/examples/gno.land/r/demo/groups/z_2_g_filetest.gno b/examples/gno.land/r/demo/groups/z_2_g_filetest.gno index 6230b110c74..03278ee8870 100644 --- a/examples/gno.land/r/demo/groups/z_2_g_filetest.gno +++ b/examples/gno.land/r/demo/groups/z_2_g_filetest.gno @@ -8,13 +8,13 @@ import ( "gno.land/p/demo/testutils" "gno.land/r/demo/groups" - "gno.land/r/demo/users" + users "gno.land/r/gnoland/users/v1" ) var gid groups.GroupID func main() { - users.Register("", "gnouser", "my profile") + users.Register("gnouser123") gid = groups.CreateGroup("test_group") println(gid) @@ -28,4 +28,4 @@ func main() { } // Error: -// unauthorized to delete group +// user not found diff --git a/examples/gno.land/r/demo/microblog/microblog.gno b/examples/gno.land/r/demo/microblog/microblog.gno index 1c3cd5e7d68..739229ec6b4 100644 --- a/examples/gno.land/r/demo/microblog/microblog.gno +++ b/examples/gno.land/r/demo/microblog/microblog.gno @@ -6,12 +6,12 @@ package microblog import ( - "std" "strings" "gno.land/p/demo/microblog" "gno.land/p/demo/ufmt" - "gno.land/r/demo/users" + + "gno.land/r/sys/users" ) var ( @@ -29,8 +29,8 @@ func renderHome() string { output += "# pages\n\n" for _, page := range m.GetPages() { - if u := users.GetUserByAddress(page.Author); u != nil { - output += ufmt.Sprintf("- [%s (%s)](%s%s)\n", u.Name, page.Author.String(), m.Prefix, page.Author.String()) + if u := users.ResolveAddress(page.Author); u != nil { + output += ufmt.Sprintf("- [%s (%s)](%s%s)\n", u.Name(), page.Author.String(), m.Prefix, page.Author.String()) } else { output += ufmt.Sprintf("- [%s](%s%s)\n", page.Author.String(), m.Prefix, page.Author.String()) } @@ -67,9 +67,8 @@ func Render(path string) string { func PageToString(p *microblog.Page) string { o := "" - if u := users.GetUserByAddress(p.Author); u != nil { - o += ufmt.Sprintf("# [%s](/r/demo/users:%s)\n\n", u, u) - o += ufmt.Sprintf("%s\n\n", u.Profile) + if u := users.ResolveAddress(p.Author); u != nil { + o += ufmt.Sprintf("# [%s](/r/gnoland/users/v1:%s)\n\n", u, u) } o += ufmt.Sprintf("## [%s](/r/demo/microblog:%s)\n\n", p.Author, p.Author) @@ -89,9 +88,3 @@ func NewPost(text string) string { } return "added new post" } - -func Register(name, profile string) string { - caller := std.GetOrigCaller() // main - users.Register(caller, name, profile) - return "OK" -} diff --git a/examples/gno.land/r/demo/userbook/render.gno b/examples/gno.land/r/demo/userbook/render.gno index 94f7567cbf4..035dd060185 100644 --- a/examples/gno.land/r/demo/userbook/render.gno +++ b/examples/gno.land/r/demo/userbook/render.gno @@ -4,14 +4,14 @@ package userbook import ( "strconv" - "gno.land/r/demo/users" + "gno.land/r/sys/users" "gno.land/p/demo/avl/pager" "gno.land/p/demo/ufmt" "gno.land/p/moul/txlink" ) -const usersLink = "/r/demo/users" +const usersLink = "/r/gnoland/users/v1" func Render(path string) string { p := pager.NewPager(signupsTree, 20, true) @@ -26,8 +26,8 @@ func Render(path string) string { signup := item.Value.(*Signup) user := signup.address.String() - if data := users.GetUserByAddress(signup.address); data != nil { - user = ufmt.Sprintf("[%s](%s:%s)", data.Name, usersLink, data.Name) + if data := users.ResolveAddress(signup.address); data != nil { + user = ufmt.Sprintf("[%s](%s:%s)", data.Name(), usersLink, data.Name()) } out += ufmt.Sprintf("- **User #%d - %s - signed up on %s**\n\n", signup.ordinal, user, signup.timestamp.Format("January 2 2006, 03:04:04 PM")) diff --git a/examples/gno.land/r/demo/users/gno.mod b/examples/gno.land/r/demo/users/gno.mod deleted file mode 100644 index 4d7fd15d1cd..00000000000 --- a/examples/gno.land/r/demo/users/gno.mod +++ /dev/null @@ -1 +0,0 @@ -module gno.land/r/demo/users diff --git a/examples/gno.land/r/demo/users/users.gno b/examples/gno.land/r/demo/users/users.gno deleted file mode 100644 index 451afc7bf96..00000000000 --- a/examples/gno.land/r/demo/users/users.gno +++ /dev/null @@ -1,350 +0,0 @@ -package users - -import ( - "regexp" - "std" - "strconv" - "strings" - - "gno.land/p/demo/avl" - "gno.land/p/demo/avl/pager" - "gno.land/p/demo/avlhelpers" - "gno.land/p/demo/users" -) - -//---------------------------------------- -// State - -var ( - admin std.Address = "g1manfred47kzduec920z88wfr64ylksmdcedlf5" // @moul - - restricted avl.Tree // Name -> true - restricted name - name2User avl.Tree // Name -> *users.User - addr2User avl.Tree // std.Address -> *users.User - invites avl.Tree // string(inviter+":"+invited) -> true - counter int // user id counter - minFee int64 = 20 * 1_000_000 // minimum gnot must be paid to register. - maxFeeMult int64 = 10 // maximum multiples of minFee accepted. -) - -//---------------------------------------- -// Top-level functions - -func Register(inviter std.Address, name string, profile string) { - // assert CallTx call. - std.AssertOriginCall() - // assert invited or paid. - caller := std.GetCallerAt(2) - if caller != std.GetOrigCaller() { - panic("should not happen") // because std.AssertOrigCall(). - } - - sentCoins := std.GetOrigSend() - minCoin := std.NewCoin("ugnot", minFee) - - if inviter == "" { - // banker := std.GetBanker(std.BankerTypeOrigSend) - if len(sentCoins) == 1 && sentCoins[0].IsGTE(minCoin) { - if sentCoins[0].Amount > minFee*maxFeeMult { - panic("payment must not be greater than " + strconv.Itoa(int(minFee*maxFeeMult))) - } else { - // ok - } - } else { - panic("payment must not be less than " + strconv.Itoa(int(minFee))) - } - } else { - invitekey := inviter.String() + ":" + caller.String() - _, ok := invites.Get(invitekey) - if !ok { - panic("invalid invitation") - } - invites.Remove(invitekey) - } - - // assert not already registered. - _, ok := name2User.Get(name) - if ok { - panic("name already registered: " + name) - } - _, ok = addr2User.Get(caller.String()) - if ok { - panic("address already registered: " + caller.String()) - } - - isInviterAdmin := inviter == admin - - // check for restricted name - if _, isRestricted := restricted.Get(name); isRestricted { - // only address invite by the admin can register restricted name - if !isInviterAdmin { - panic("restricted name: " + name) - } - - restricted.Remove(name) - } - - // assert name is valid. - // admin inviter can bypass name restriction - if !isInviterAdmin && !reName.MatchString(name) { - panic("invalid name: " + name + " (must be at least 6 characters, lowercase alphanumeric with underscore)") - } - - // remainder of fees go toward invites. - invites := int(0) - if len(sentCoins) == 1 { - if sentCoins[0].Denom == "ugnot" && sentCoins[0].Amount >= minFee { - invites = int(sentCoins[0].Amount / minFee) - if inviter == "" && invites > 0 { - invites -= 1 - } - } - } - // register. - counter++ - user := &users.User{ - Address: caller, - Name: name, - Profile: profile, - Number: counter, - Invites: invites, - Inviter: inviter, - } - name2User.Set(name, user) - addr2User.Set(caller.String(), user) -} - -func Invite(invitee string) { - // assert CallTx call. - std.AssertOriginCall() - // get caller/inviter. - caller := std.GetCallerAt(2) - if caller != std.GetOrigCaller() { - panic("should not happen") // because std.AssertOrigCall(). - } - lines := strings.Split(invitee, "\n") - if caller == admin { - // nothing to do, all good - } else { - // ensure has invites. - userI, ok := addr2User.Get(caller.String()) - if !ok { - panic("user unknown") - } - user := userI.(*users.User) - if user.Invites <= 0 { - panic("user has no invite tokens") - } - user.Invites -= len(lines) - if user.Invites < 0 { - panic("user has insufficient invite tokens") - } - } - // for each line... - for _, line := range lines { - if line == "" { - continue // file bodies have a trailing newline. - } else if strings.HasPrefix(line, `//`) { - continue // comment - } - // record invite. - invitekey := string(caller) + ":" + string(line) - invites.Set(invitekey, true) - } -} - -func GrantInvites(invites string) { - // assert CallTx call. - std.AssertOriginCall() - // assert admin. - caller := std.GetCallerAt(2) - if caller != std.GetOrigCaller() { - panic("should not happen") // because std.AssertOrigCall(). - } - if caller != admin { - panic("unauthorized") - } - // for each line... - lines := strings.Split(invites, "\n") - for _, line := range lines { - if line == "" { - continue // file bodies have a trailing newline. - } else if strings.HasPrefix(line, `//`) { - continue // comment - } - // parse name and invites. - var name string - var invites int - parts := strings.Split(line, ":") - if len(parts) == 1 { // short for :1. - name = parts[0] - invites = 1 - } else if len(parts) == 2 { - name = parts[0] - invites_, err := strconv.Atoi(parts[1]) - if err != nil { - panic(err) - } - invites = int(invites_) - } else { - panic("should not happen") - } - // give invites. - userI, ok := name2User.Get(name) - if !ok { - // maybe address. - userI, ok = addr2User.Get(name) - if !ok { - panic("invalid user " + name) - } - } - user := userI.(*users.User) - user.Invites += invites - } -} - -// Any leftover fees go toward invitations. -func SetMinFee(newMinFee int64) { - // assert CallTx call. - std.AssertOriginCall() - // assert admin caller. - caller := std.GetCallerAt(2) - if caller != admin { - panic("unauthorized") - } - // update global variables. - minFee = newMinFee -} - -// This helps prevent fat finger accidents. -func SetMaxFeeMultiple(newMaxFeeMult int64) { - // assert CallTx call. - std.AssertOriginCall() - // assert admin caller. - caller := std.GetCallerAt(2) - if caller != admin { - panic("unauthorized") - } - // update global variables. - maxFeeMult = newMaxFeeMult -} - -//---------------------------------------- -// Exposed public functions - -func GetUserByName(name string) *users.User { - userI, ok := name2User.Get(name) - if !ok { - return nil - } - return userI.(*users.User) -} - -func GetUserByAddress(addr std.Address) *users.User { - userI, ok := addr2User.Get(addr.String()) - if !ok { - return nil - } - return userI.(*users.User) -} - -// unlike GetUserByName, input must be "@" prefixed for names. -func GetUserByAddressOrName(input users.AddressOrName) *users.User { - name, isName := input.GetName() - if isName { - return GetUserByName(name) - } - return GetUserByAddress(std.Address(input)) -} - -// Get a list of user names starting from the given prefix. Limit the -// number of results to maxResults. (This can be used for a name search tool.) -func ListUsersByPrefix(prefix string, maxResults int) []string { - return avlhelpers.ListByteStringKeysByPrefix(&name2User, prefix, maxResults) -} - -func Resolve(input users.AddressOrName) std.Address { - name, isName := input.GetName() - if !isName { - return std.Address(input) // TODO check validity - } - - user := GetUserByName(name) - return user.Address -} - -// Add restricted name to the list -func AdminAddRestrictedName(name string) { - // assert CallTx call. - std.AssertOriginCall() - // get caller - caller := std.GetOrigCaller() - // assert admin - if caller != admin { - panic("unauthorized") - } - - if user := GetUserByName(name); user != nil { - panic("already registered name") - } - - // register restricted name - - restricted.Set(name, true) -} - -//---------------------------------------- -// Constants - -// NOTE: name length must be clearly distinguishable from a bech32 address. -var reName = regexp.MustCompile(`^[a-z]+[_a-z0-9]{5,16}$`) - -//---------------------------------------- -// Render main page - -func Render(fullPath string) string { - path, _ := splitPathAndQuery(fullPath) - if path == "" { - return renderHome(fullPath) - } else if len(path) >= 38 { // 39? 40? - if path[:2] != "g1" { - return "invalid address " + path - } - user := GetUserByAddress(std.Address(path)) - if user == nil { - // TODO: display basic information about account. - return "unknown address " + path - } - return user.Render() - } else { - user := GetUserByName(path) - if user == nil { - return "unknown username " + path - } - return user.Render() - } -} - -func renderHome(path string) string { - doc := "" - - page := pager.NewPager(&name2User, 50, false).MustGetPageByPath(path) - - for _, item := range page.Items { - user := item.Value.(*users.User) - doc += " * [" + user.Name + "](/r/demo/users:" + user.Name + ")\n" - } - doc += "\n" - doc += page.Picker() - return doc -} - -func splitPathAndQuery(fullPath string) (string, string) { - parts := strings.SplitN(fullPath, "?", 2) - path := parts[0] - queryString := "" - if len(parts) > 1 { - queryString = "?" + parts[1] - } - return path, queryString -} diff --git a/examples/gno.land/r/demo/users/users_test.gno b/examples/gno.land/r/demo/users/users_test.gno deleted file mode 100644 index 864793dc514..00000000000 --- a/examples/gno.land/r/demo/users/users_test.gno +++ /dev/null @@ -1,13 +0,0 @@ -package users - -import ( - "testing" - - "gno.land/p/demo/uassert" -) - -func TestPreRegisteredTest1(t *testing.T) { - names := ListUsersByPrefix("test1", 1) - uassert.Equal(t, len(names), 1) - uassert.Equal(t, names[0], "test1") -} diff --git a/examples/gno.land/r/demo/users/z_0_b_filetest.gno b/examples/gno.land/r/demo/users/z_0_b_filetest.gno deleted file mode 100644 index c33edc32985..00000000000 --- a/examples/gno.land/r/demo/users/z_0_b_filetest.gno +++ /dev/null @@ -1,15 +0,0 @@ -package main - -// SEND: 19900000ugnot - -import ( - "gno.land/r/demo/users" -) - -func main() { - users.Register("", "gnouser", "my profile") - println("done") -} - -// Error: -// payment must not be less than 20000000 diff --git a/examples/gno.land/r/demo/users/z_0_filetest.gno b/examples/gno.land/r/demo/users/z_0_filetest.gno deleted file mode 100644 index cbb2e9209f4..00000000000 --- a/examples/gno.land/r/demo/users/z_0_filetest.gno +++ /dev/null @@ -1,16 +0,0 @@ -package main - -import ( - "std" - - "gno.land/r/demo/users" -) - -func main() { - std.TestSetOrigSend(std.Coins{std.NewCoin("dontcare", 1)}, nil) - users.Register("", "gnouser", "my profile") - println("done") -} - -// Error: -// incompatible coin denominations: dontcare, ugnot diff --git a/examples/gno.land/r/demo/users/z_10_filetest.gno b/examples/gno.land/r/demo/users/z_10_filetest.gno deleted file mode 100644 index afeecffcc42..00000000000 --- a/examples/gno.land/r/demo/users/z_10_filetest.gno +++ /dev/null @@ -1,33 +0,0 @@ -// PKGPATH: gno.land/r/demo/users_test -package users_test - -import ( - "std" - - "gno.land/p/demo/testutils" - "gno.land/r/demo/users" -) - -const admin = std.Address("g1manfred47kzduec920z88wfr64ylksmdcedlf5") - -func init() { - caller := std.GetOrigCaller() // main - test2 := testutils.TestAddress("test2") - // as admin, invite gnouser and test2 - std.TestSetOrigCaller(admin) - users.Invite(caller.String() + "\n" + test2.String()) - // register as caller - std.TestSetOrigCaller(caller) - users.Register(admin, "gnouser", "my profile") -} - -func main() { - // register as test2 - test2 := testutils.TestAddress("test2") - std.TestSetOrigCaller(test2) - users.Register(admin, "test222", "my profile 2") - println("done") -} - -// Output: -// done diff --git a/examples/gno.land/r/demo/users/z_11_filetest.gno b/examples/gno.land/r/demo/users/z_11_filetest.gno deleted file mode 100644 index 27c7e9813da..00000000000 --- a/examples/gno.land/r/demo/users/z_11_filetest.gno +++ /dev/null @@ -1,25 +0,0 @@ -package main - -// SEND: 200000000ugnot - -import ( - "std" - - "gno.land/r/demo/users" -) - -const admin = std.Address("g1manfred47kzduec920z88wfr64ylksmdcedlf5") - -func main() { - caller := std.GetOrigCaller() // main - std.TestSetOrigCaller(admin) - users.AdminAddRestrictedName("superrestricted") - - // test restricted name - std.TestSetOrigCaller(caller) - users.Register("", "superrestricted", "my profile") - println("done") -} - -// Error: -// restricted name: superrestricted diff --git a/examples/gno.land/r/demo/users/z_11b_filetest.gno b/examples/gno.land/r/demo/users/z_11b_filetest.gno deleted file mode 100644 index be508963911..00000000000 --- a/examples/gno.land/r/demo/users/z_11b_filetest.gno +++ /dev/null @@ -1,28 +0,0 @@ -package main - -// SEND: 200000000ugnot - -import ( - "std" - - "gno.land/r/demo/users" -) - -const admin = std.Address("g1manfred47kzduec920z88wfr64ylksmdcedlf5") - -func main() { - caller := std.GetOrigCaller() // main - std.TestSetOrigCaller(admin) - // add restricted name - users.AdminAddRestrictedName("superrestricted") - // grant invite to caller - users.Invite(caller.String()) - // set back caller - std.TestSetOrigCaller(caller) - // register restricted name with admin invite - users.Register(admin, "superrestricted", "my profile") - println("done") -} - -// Output: -// done diff --git a/examples/gno.land/r/demo/users/z_12_filetest.gno b/examples/gno.land/r/demo/users/z_12_filetest.gno deleted file mode 100644 index 0fb7d27bd34..00000000000 --- a/examples/gno.land/r/demo/users/z_12_filetest.gno +++ /dev/null @@ -1,49 +0,0 @@ -package main - -// SEND: 200000000ugnot - -import ( - "strconv" - - "gno.land/r/demo/users" -) - -func main() { - users.Register("", "alicia", "my profile") - - { - // Normal usage - names := users.ListUsersByPrefix("a", 1) - println("# names: " + strconv.Itoa(len(names))) - println("name: " + names[0]) - } - - { - // Empty prefix: match all - names := users.ListUsersByPrefix("", 1) - println("# names: " + strconv.Itoa(len(names))) - println("name: " + names[0]) - } - - { - // The prefix is before "alicia" - names := users.ListUsersByPrefix("alich", 1) - println("# names: " + strconv.Itoa(len(names))) - } - - { - // The prefix is after the last name - names := users.ListUsersByPrefix("y", 10) - println("# names: " + strconv.Itoa(len(names))) - } - - // More tests are in p/demo/avlhelpers -} - -// Output: -// # names: 1 -// name: alicia -// # names: 1 -// name: alicia -// # names: 0 -// # names: 0 diff --git a/examples/gno.land/r/demo/users/z_13_filetest.gno b/examples/gno.land/r/demo/users/z_13_filetest.gno deleted file mode 100644 index 6ef312dc41c..00000000000 --- a/examples/gno.land/r/demo/users/z_13_filetest.gno +++ /dev/null @@ -1,22 +0,0 @@ -package main - -// SEND: 200000000ugnot - -import ( - "strconv" - - "gno.land/r/demo/users" -) - -func main() { - { - // Verify pre-registered test1 user - names := users.ListUsersByPrefix("test1", 1) - println("# names: " + strconv.Itoa(len(names))) - println("name: " + names[0]) - } -} - -// Output: -// # names: 1 -// name: test1 diff --git a/examples/gno.land/r/demo/users/z_1_filetest.gno b/examples/gno.land/r/demo/users/z_1_filetest.gno deleted file mode 100644 index 504a0c7c3f9..00000000000 --- a/examples/gno.land/r/demo/users/z_1_filetest.gno +++ /dev/null @@ -1,15 +0,0 @@ -package main - -// SEND: 200000000ugnot - -import ( - "gno.land/r/demo/users" -) - -func main() { - users.Register("", "gnouser", "my profile") - println("done") -} - -// Output: -// done diff --git a/examples/gno.land/r/demo/users/z_2_filetest.gno b/examples/gno.land/r/demo/users/z_2_filetest.gno deleted file mode 100644 index c1b92790f8b..00000000000 --- a/examples/gno.land/r/demo/users/z_2_filetest.gno +++ /dev/null @@ -1,32 +0,0 @@ -package main - -// SEND: 200000000ugnot - -import ( - "std" - - "gno.land/p/demo/testutils" - "gno.land/r/demo/users" -) - -const admin = std.Address("g1manfred47kzduec920z88wfr64ylksmdcedlf5") - -func main() { - caller := std.GetOrigCaller() // main - users.Register("", "gnouser", "my profile") - // as admin, grant invites to gnouser - std.TestSetOrigCaller(admin) - users.GrantInvites(caller.String() + ":1") - // switch back to caller - std.TestSetOrigCaller(caller) - // invite another addr - test1 := testutils.TestAddress("test1") - users.Invite(test1.String()) - // switch to test1 - std.TestSetOrigCaller(test1) - users.Register(caller, "satoshi", "my other profile") - println("done") -} - -// Output: -// done diff --git a/examples/gno.land/r/demo/users/z_3_filetest.gno b/examples/gno.land/r/demo/users/z_3_filetest.gno deleted file mode 100644 index 5402235e03d..00000000000 --- a/examples/gno.land/r/demo/users/z_3_filetest.gno +++ /dev/null @@ -1,33 +0,0 @@ -package main - -// SEND: 200000000ugnot - -import ( - "std" - - "gno.land/p/demo/testutils" - "gno.land/r/demo/users" -) - -const admin = std.Address("g1manfred47kzduec920z88wfr64ylksmdcedlf5") - -func main() { - caller := std.GetOrigCaller() // main - users.Register("", "gnouser", "my profile") - // as admin, grant invites to gnouser - std.TestSetOrigCaller(admin) - users.GrantInvites(caller.String() + ":1") - // switch back to caller - std.TestSetOrigCaller(caller) - // invite another addr - test1 := testutils.TestAddress("test1") - users.Invite(test1.String()) - // switch to test1 - std.TestSetOrigCaller(test1) - std.TestSetOrigSend(std.Coins{{"dontcare", 1}}, nil) - users.Register(caller, "satoshi", "my other profile") - println("done") -} - -// Output: -// done diff --git a/examples/gno.land/r/demo/users/z_4_filetest.gno b/examples/gno.land/r/demo/users/z_4_filetest.gno deleted file mode 100644 index 613fadf9625..00000000000 --- a/examples/gno.land/r/demo/users/z_4_filetest.gno +++ /dev/null @@ -1,34 +0,0 @@ -package main - -// SEND: 200000000ugnot - -import ( - "std" - - "gno.land/p/demo/testutils" - "gno.land/r/demo/users" -) - -const admin = std.Address("g1manfred47kzduec920z88wfr64ylksmdcedlf5") - -func main() { - caller := std.GetOrigCaller() // main - users.Register("", "gnouser", "my profile") - // as admin, grant invites to gnouser - std.TestSetOrigCaller(admin) - users.GrantInvites(caller.String() + ":1") - // switch back to caller - std.TestSetOrigCaller(caller) - // invite another addr - test1 := testutils.TestAddress("test1") - test2 := testutils.TestAddress("test2") - users.Invite(test1.String()) - // switch to test2 (not test1) - std.TestSetOrigCaller(test2) - std.TestSetOrigSend(std.Coins{{"dontcare", 1}}, nil) - users.Register(caller, "satoshi", "my other profile") - println("done") -} - -// Error: -// invalid invitation diff --git a/examples/gno.land/r/demo/users/z_5_filetest.gno b/examples/gno.land/r/demo/users/z_5_filetest.gno deleted file mode 100644 index 6465cc9c378..00000000000 --- a/examples/gno.land/r/demo/users/z_5_filetest.gno +++ /dev/null @@ -1,76 +0,0 @@ -package main - -// SEND: 200000000ugnot - -import ( - "std" - - "gno.land/p/demo/testutils" - "gno.land/r/demo/users" -) - -const admin = std.Address("g1manfred47kzduec920z88wfr64ylksmdcedlf5") - -func main() { - caller := std.GetOrigCaller() // main - users.Register("", "gnouser", "my profile") - // as admin, grant invites to gnouser - std.TestSetOrigCaller(admin) - users.GrantInvites(caller.String() + ":1") - // switch back to caller - std.TestSetOrigCaller(caller) - // invite another addr - test1 := testutils.TestAddress("test1") - users.Invite(test1.String()) - // switch to test1 - std.TestSetOrigCaller(test1) - std.TestSetOrigSend(std.Coins{{"dontcare", 1}}, nil) - users.Register(caller, "satoshi", "my other profile") - println(users.Render("")) - println("========================================") - println(users.Render("?page=2")) - println("========================================") - println(users.Render("gnouser")) - println("========================================") - println(users.Render("satoshi")) - println("========================================") - println(users.Render("badname")) -} - -// Output: -// * [archives](/r/demo/users:archives) -// * [demo](/r/demo/users:demo) -// * [gno](/r/demo/users:gno) -// * [gnoland](/r/demo/users:gnoland) -// * [gnolang](/r/demo/users:gnolang) -// * [gnouser](/r/demo/users:gnouser) -// * [gov](/r/demo/users:gov) -// * [nt](/r/demo/users:nt) -// * [satoshi](/r/demo/users:satoshi) -// * [sys](/r/demo/users:sys) -// * [test1](/r/demo/users:test1) -// * [x](/r/demo/users:x) -// -// -// ======================================== -// -// -// ======================================== -// ## user gnouser -// -// * address = g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm -// * 9 invites -// -// my profile -// -// ======================================== -// ## user satoshi -// -// * address = g1w3jhxap3ta047h6lta047h6lta047h6l4mfnm7 -// * 0 invites -// * invited by g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm -// -// my other profile -// -// ======================================== -// unknown username badname diff --git a/examples/gno.land/r/demo/users/z_6_filetest.gno b/examples/gno.land/r/demo/users/z_6_filetest.gno deleted file mode 100644 index 919088088a2..00000000000 --- a/examples/gno.land/r/demo/users/z_6_filetest.gno +++ /dev/null @@ -1,20 +0,0 @@ -package main - -import ( - "std" - - "gno.land/r/demo/users" -) - -const admin = std.Address("g1manfred47kzduec920z88wfr64ylksmdcedlf5") - -func main() { - caller := std.GetOrigCaller() - // as admin, grant invites to unregistered user. - std.TestSetOrigCaller(admin) - users.GrantInvites(caller.String() + ":1") - println("done") -} - -// Error: -// invalid user g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm diff --git a/examples/gno.land/r/demo/users/z_7_filetest.gno b/examples/gno.land/r/demo/users/z_7_filetest.gno deleted file mode 100644 index 1d3c9e3a917..00000000000 --- a/examples/gno.land/r/demo/users/z_7_filetest.gno +++ /dev/null @@ -1,36 +0,0 @@ -package main - -// SEND: 200000000ugnot - -import ( - "std" - - "gno.land/p/demo/testutils" - "gno.land/r/demo/users" -) - -const admin = std.Address("g1manfred47kzduec920z88wfr64ylksmdcedlf5") - -func main() { - caller := std.GetOrigCaller() // main - users.Register("", "gnouser", "my profile") - // as admin, grant invites to gnouser - std.TestSetOrigCaller(admin) - users.GrantInvites(caller.String() + ":1") - // switch back to caller - std.TestSetOrigCaller(caller) - // invite another addr - test1 := testutils.TestAddress("test1") - users.Invite(test1.String()) - // switch to test1 - std.TestSetOrigCaller(test1) - std.TestSetOrigSend(std.Coins{{"dontcare", 1}}, nil) - users.Register(caller, "satoshi", "my other profile") - // as admin, grant invites to gnouser(again) and satoshi. - std.TestSetOrigCaller(admin) - users.GrantInvites(caller.String() + ":1\n" + test1.String() + ":1") - println("done") -} - -// Output: -// done diff --git a/examples/gno.land/r/demo/users/z_7b_filetest.gno b/examples/gno.land/r/demo/users/z_7b_filetest.gno deleted file mode 100644 index 09c15bb135d..00000000000 --- a/examples/gno.land/r/demo/users/z_7b_filetest.gno +++ /dev/null @@ -1,36 +0,0 @@ -package main - -// SEND: 200000000ugnot - -import ( - "std" - - "gno.land/p/demo/testutils" - "gno.land/r/demo/users" -) - -const admin = std.Address("g1manfred47kzduec920z88wfr64ylksmdcedlf5") - -func main() { - caller := std.GetOrigCaller() // main - users.Register("", "gnouser", "my profile") - // as admin, grant invites to gnouser - std.TestSetOrigCaller(admin) - users.GrantInvites(caller.String() + ":1\n") - // switch back to caller - std.TestSetOrigCaller(caller) - // invite another addr - test1 := testutils.TestAddress("test1") - users.Invite(test1.String()) - // switch to test1 - std.TestSetOrigCaller(test1) - std.TestSetOrigSend(std.Coins{{"dontcare", 1}}, nil) - users.Register(caller, "satoshi", "my other profile") - // as admin, grant invites to gnouser(again) and satoshi. - std.TestSetOrigCaller(admin) - users.GrantInvites(caller.String() + ":1\n" + test1.String() + ":1") - println("done") -} - -// Output: -// done diff --git a/examples/gno.land/r/demo/users/z_8_filetest.gno b/examples/gno.land/r/demo/users/z_8_filetest.gno deleted file mode 100644 index 78fada74a71..00000000000 --- a/examples/gno.land/r/demo/users/z_8_filetest.gno +++ /dev/null @@ -1,37 +0,0 @@ -package main - -// SEND: 200000000ugnot - -import ( - "std" - - "gno.land/p/demo/testutils" - "gno.land/r/demo/users" -) - -const admin = std.Address("g1manfred47kzduec920z88wfr64ylksmdcedlf5") - -func main() { - caller := std.GetOrigCaller() // main - users.Register("", "gnouser", "my profile") - // as admin, grant invites to gnouser - std.TestSetOrigCaller(admin) - users.GrantInvites(caller.String() + ":1") - // switch back to caller - std.TestSetOrigCaller(caller) - // invite another addr - test1 := testutils.TestAddress("test1") - users.Invite(test1.String()) - // switch to test1 - std.TestSetOrigCaller(test1) - std.TestSetOrigSend(std.Coins{{"dontcare", 1}}, nil) - users.Register(caller, "satoshi", "my other profile") - // as admin, grant invites to gnouser(again) and nonexistent user. - std.TestSetOrigCaller(admin) - test2 := testutils.TestAddress("test2") - users.GrantInvites(caller.String() + ":1\n" + test2.String() + ":1") - println("done") -} - -// Error: -// invalid user g1w3jhxapjta047h6lta047h6lta047h6laqcyu4 diff --git a/examples/gno.land/r/demo/users/z_9_filetest.gno b/examples/gno.land/r/demo/users/z_9_filetest.gno deleted file mode 100644 index c73c685aebd..00000000000 --- a/examples/gno.land/r/demo/users/z_9_filetest.gno +++ /dev/null @@ -1,28 +0,0 @@ -package main - -import ( - "std" - - "gno.land/p/demo/testutils" - "gno.land/r/demo/users" -) - -const admin = std.Address("g1manfred47kzduec920z88wfr64ylksmdcedlf5") - -func main() { - caller := std.GetOrigCaller() // main - test2 := testutils.TestAddress("test2") - // as admin, invite gnouser and test2 - std.TestSetOrigCaller(admin) - users.Invite(caller.String() + "\n" + test2.String()) - // register as caller - std.TestSetOrigCaller(caller) - users.Register(admin, "gnouser", "my profile") - // register as test2 - std.TestSetOrigCaller(test2) - users.Register(admin, "test222", "my profile 2") - println("done") -} - -// Output: -// done diff --git a/examples/gno.land/r/demo/wugnot/wugnot.gno b/examples/gno.land/r/demo/wugnot/wugnot.gno index d0395fe7e8c..2f1e210811a 100644 --- a/examples/gno.land/r/demo/wugnot/wugnot.gno +++ b/examples/gno.land/r/demo/wugnot/wugnot.gno @@ -6,7 +6,6 @@ import ( "gno.land/p/demo/grc/grc20" "gno.land/p/demo/ufmt" - "gno.land/r/demo/grc20reg" ) diff --git a/examples/gno.land/r/gnoland/home/home.gno b/examples/gno.land/r/gnoland/home/home.gno index 2d1aad8a1a0..6c53d81b815 100644 --- a/examples/gno.land/r/gnoland/home/home.gno +++ b/examples/gno.land/r/gnoland/home/home.gno @@ -202,6 +202,7 @@ func packageStaffPicks() ui.Element { ui.H3("[r/sys](https://github.com/gnolang/gno/tree/master/examples/gno.land/r/sys)"), ui.BulletList{ ui.Link{URL: "r/sys/names"}, + ui.Link{URL: "r/sys/users"}, ui.Link{URL: "r/sys/rewards"}, ui.Link{URL: "/r/sys/validators/v2"}, }, @@ -209,7 +210,7 @@ func packageStaffPicks() ui.Element { ui.H3("[r/demo](https://github.com/gnolang/gno/tree/master/examples/gno.land/r/demo)"), ui.BulletList{ ui.Link{URL: "r/demo/boards"}, - ui.Link{URL: "r/demo/users"}, + ui.Link{URL: "r/demo/banktest"}, ui.Link{URL: "r/demo/foo20"}, ui.Link{URL: "r/demo/foo721"}, diff --git a/examples/gno.land/r/gnoland/home/home_filetest.gno b/examples/gno.land/r/gnoland/home/home_filetest.gno index 5b5ff5740c3..78a53edf811 100644 --- a/examples/gno.land/r/gnoland/home/home_filetest.gno +++ b/examples/gno.land/r/gnoland/home/home_filetest.gno @@ -115,6 +115,7 @@ func main() { // ### [r/sys](https://github.com/gnolang/gno/tree/master/examples/gno.land/r/sys) // // - [r/sys/names](r/sys/names) +// - [r/sys/users](r/sys/users) // - [r/sys/rewards](r/sys/rewards) // - [/r/sys/validators/v2](/r/sys/validators/v2) // @@ -124,7 +125,6 @@ func main() { // ### [r/demo](https://github.com/gnolang/gno/tree/master/examples/gno.land/r/demo) // // - [r/demo/boards](r/demo/boards) -// - [r/demo/users](r/demo/users) // - [r/demo/banktest](r/demo/banktest) // - [r/demo/foo20](r/demo/foo20) // - [r/demo/foo721](r/demo/foo721) diff --git a/examples/gno.land/r/gnoland/users/v1/admin.gno b/examples/gno.land/r/gnoland/users/v1/admin.gno new file mode 100644 index 00000000000..cc394ef3e90 --- /dev/null +++ b/examples/gno.land/r/gnoland/users/v1/admin.gno @@ -0,0 +1,18 @@ +package v1 + +import ( + "gno.land/p/demo/dao" + "gno.land/r/gov/dao/bridge" +) + +var paused = false // until a DAO can be owner of ownable/pausable + +// NewPauseUnpauseExecutor allows GovDAO to pause or unpause this realm +func NewPauseUnpauseExecutor(newPausedValue bool) dao.Executor { + cb := func() error { + paused = newPausedValue + return nil + } + + return bridge.GovDAO().NewGovDAOExecutor(cb) +} diff --git a/examples/gno.land/r/gnoland/users/v1/errors.gno b/examples/gno.land/r/gnoland/users/v1/errors.gno new file mode 100644 index 00000000000..4a0c4750018 --- /dev/null +++ b/examples/gno.land/r/gnoland/users/v1/errors.gno @@ -0,0 +1,8 @@ +package v1 + +import "errors" + +var ( + ErrInvalidUsername = errors.New("r/gnoland/users: invalid username") + ErrPaused = errors.New("r/gnoland/users: paused") +) diff --git a/examples/gno.land/r/gnoland/users/v1/gno.mod b/examples/gno.land/r/gnoland/users/v1/gno.mod new file mode 100644 index 00000000000..669e9c66511 --- /dev/null +++ b/examples/gno.land/r/gnoland/users/v1/gno.mod @@ -0,0 +1 @@ +module gno.land/r/gnoland/users/v1 diff --git a/examples/gno.land/r/demo/users/preregister.gno b/examples/gno.land/r/gnoland/users/v1/preregister.gno similarity index 55% rename from examples/gno.land/r/demo/users/preregister.gno rename to examples/gno.land/r/gnoland/users/v1/preregister.gno index e87bb478d4e..95c0ae438b3 100644 --- a/examples/gno.land/r/demo/users/preregister.gno +++ b/examples/gno.land/r/gnoland/users/v1/preregister.gno @@ -1,16 +1,11 @@ -package users +package v1 import ( "std" - "gno.land/p/demo/users" + "gno.land/r/sys/users" ) -// pre-restricted names -var preRestrictedNames = []string{ - "bitcoin", "cosmos", "newtendermint", "ethereum", -} - // pre-registered users var preRegisteredUsers = []struct { Name string @@ -34,36 +29,8 @@ var preRegisteredUsers = []struct { func init() { // add pre-registered users for _, res := range preRegisteredUsers { - // assert not already registered. - _, ok := name2User.Get(res.Name) - if ok { - panic("name already registered") - } - - _, ok = addr2User.Get(res.Address.String()) - if ok { - panic("address already registered") - } - - counter++ - user := &users.User{ - Address: res.Address, - Name: res.Name, - Profile: "", - Number: counter, - Invites: int(0), - Inviter: admin, + if err := users.RegisterUser(res.Name, res.Address); err != nil { + panic(err) } - name2User.Set(res.Name, user) - addr2User.Set(res.Address.String(), user) - } - - // add pre-restricted names - for _, name := range preRestrictedNames { - if _, ok := name2User.Get(name); ok { - panic("name already registered") - } - - restricted.Set(name, true) } } diff --git a/examples/gno.land/r/gnoland/users/v1/render.gno b/examples/gno.land/r/gnoland/users/v1/render.gno new file mode 100644 index 00000000000..3ac510426c1 --- /dev/null +++ b/examples/gno.land/r/gnoland/users/v1/render.gno @@ -0,0 +1,143 @@ +package v1 + +import ( + "std" + "strconv" + + "gno.land/p/demo/avl/pager" + "gno.land/p/demo/ufmt" + "gno.land/p/moul/md" + "gno.land/p/moul/realmpath" + "gno.land/p/moul/txlink" + + "gno.land/r/demo/profile" + "gno.land/r/sys/users" +) + +var ( + connectUrlBase = "https://gno.studio/connect/view/gno.land/r/gnoland/users/v1?network=" + std.GetChainID() + connectRegisterUrl = connectUrlBase + "#Register" + connectUpdateNameUrl = connectUrlBase + "#UpdateName" +) + +func Render(path string) string { + req := realmpath.Parse(path) + + if req.Path == "" { + return renderHomePage(path) + } + + // Otherwise, render the user page + return renderUserPage(req.Path) +} + +func renderHomePage(path string) string { + var out string + + out += "# gno.land user registry\n" + + out += renderIntroParagraph() + + out += md.H2("User list") + + p := pager.NewPager(users.GetReadOnlyNameStore(), 20, false) + page := p.MustGetPageByPath(path) + for _, item := range page.Items { + data := item.Value.(*users.UserData) + + // Skip previous names + if item.Key != data.Name() { + continue + } + + var displayKey string + if data.IsDeleted() { + displayKey = md.Strikethrough(item.Key) + } else { + displayKey = ufmt.Sprintf("**%s**", item.Key) + } + + out += ufmt.Sprintf("- User [%s](/r/gnoland/users/v1:%s)\n", displayKey, item.Key) + } + + out += "\n" + out += page.Picker() + out += "\n\n" + out += "Page " + strconv.Itoa(page.PageNumber) + " of " + strconv.Itoa(page.TotalPages) + "\n\n" + + return out +} + +func renderIntroParagraph() string { + out := md.Paragraph("Welcome to the gno.land user registry (v1). Please register a username.") + out += md.Paragraph(`Registering a username grants the registering address the right to deploy packages and realms +under that username’s namespace. For example, if an address registers the username ` + md.InlineCode("gnome123") + `, it +will gain permission to deploy packages and realms to package paths with the pattern ` + md.InlineCode("gno.land/{p,r}/gnome123/*") + `.`) + + out += md.Paragraph("In V1, usernames must follow these rules, in order to prevent username squatting:") + items := []string{ + "Must start with 3 characters", + "Must end with 3 numbers", + "Have a maximum length of 20 characters", + "With the only special character allowed being `_`", + } + out += md.BulletList(items) + + out += "\n\n" + out += md.Paragraph("In later versions of the registry, vanity usernames will be allowed through specific mechanisms.") + + out += md.H2("Actions\n\n") + + items = []string{ + "Register: " + ufmt.Sprintf(" [[Connect]](%s) - [[gnokey]](%s)\n\n", connectRegisterUrl, txlink.Call("Register")), + "Update Name: " + ufmt.Sprintf(" [[Connect]](%s) - [[gnokey]](%s)\n\n", connectUpdateNameUrl, txlink.Call("UpdateName")), + } + + out += md.BulletList(items) + + out += md.HorizontalRule() + out += "\n\n" + + return out +} + +// resolveUser resolves the user based on the path, determining if it's a name or address +func resolveUser(path string) (*users.UserData, bool, bool) { + if std.Address(path).IsValid() { + return users.ResolveAddress(std.Address(path)), false, false + } + + data, isLatest := users.ResolveName(path) + return data, isLatest, true +} + +// renderUserPage generates the user page based on user data and path +func renderUserPage(path string) string { + var out string + + // Render single user page + data, isLatest, isName := resolveUser(path) + if data == nil { + out += md.H1("User not found.") + out += "This user does not exist or has been deleted.\n" + return out + } + + out += md.H1("User - " + md.InlineCode(data.Name())) + + if isName && !isLatest { + out += md.Paragraph(ufmt.Sprintf( + "Note: You searched for `%s`, which is a previous name of [`%s`](/r/gnoland/users/v1:%s).", + path, data.Name(), data.Name())) + } else { + out += ufmt.Sprintf("Address: %s\n\n", data.Addr().String()) + + out += md.H2("Bio") + out += profile.GetStringField(data.Addr(), "Bio", "No bio defined.") + out += "\n\n" + out += ufmt.Sprintf("[Update bio](%s)", txlink.Realm("gno.land/r/demo/profile").Call("SetStringField", "field", "Bio")) + out += "\n\n" + } + + return out +} diff --git a/examples/gno.land/r/gnoland/users/v1/users.gno b/examples/gno.land/r/gnoland/users/v1/users.gno new file mode 100644 index 00000000000..551af2e4120 --- /dev/null +++ b/examples/gno.land/r/gnoland/users/v1/users.gno @@ -0,0 +1,77 @@ +package v1 + +import ( + "regexp" + "std" + + "gno.land/r/sys/users" +) + +const ( + reValidUsername = "^[a-z]{3}[_a-z0-9]{0,14}[0-9]{3}$" +) + +var reUsername = regexp.MustCompile(reValidUsername) + +// Register registers a new username for the caller +// A valid username must start with a minimum of 3 letters, +// end with a minimum of 3 numbers, and be less than 20 chars long. +// All letters must be lowercase, and the only valid special char is `_`. +// Only calls from EOAs are supported. +func Register(username string) error { + std.AssertOriginCall() + + if paused { + return ErrPaused + } + + if matched := reUsername.MatchString(username); !matched { + return ErrInvalidUsername + } + + registrant := std.PrevRealm().Addr() + if err := users.RegisterUser(username, registrant); err != nil { + return err + } + + return nil +} + +// UpdateName allows a user to update their name. +// The associated address and all previous names of a user that changes a name +// are preserved, and all resolve to the new name. +func UpdateName(newName string) error { + std.AssertOriginCall() + + if paused { + return ErrPaused + } + + if matched := reUsername.MatchString(newName); !matched { + return ErrInvalidUsername + } + + registrant := std.PrevRealm().Addr() + if err := users.UpdateName(newName, registrant); err != nil { + return err + } + + return nil +} + +// DeleteUser makes all names associated with the `PrevRealm()` address unresolvable. +// WARN: After deletion, the same address WILL NOT be able to register a new name. +func DeleteUser() error { + std.AssertOriginCall() + + if paused { + return ErrPaused + } + + addr := std.PrevRealm().Addr() + if err := users.Delete(addr); err != nil { + return err + } + + return nil +} diff --git a/examples/gno.land/r/gnoland/users/v1/users_test.gno b/examples/gno.land/r/gnoland/users/v1/users_test.gno new file mode 100644 index 00000000000..cf556ced9fa --- /dev/null +++ b/examples/gno.land/r/gnoland/users/v1/users_test.gno @@ -0,0 +1,104 @@ +package v1 + +import ( + "std" + "testing" + + "gno.land/p/demo/testutils" + "gno.land/p/demo/uassert" + "gno.land/p/demo/urequire" + + "gno.land/r/sys/users" +) + +var ( + alice = "alice123" + bob = "bob123" + aliceAddr = testutils.TestAddress(alice) + bobAddr = testutils.TestAddress(bob) +) + +func TestRegister_Valid(t *testing.T) { + std.TestSetRealm(std.NewUserRealm(aliceAddr)) + std.TestSetOrigCaller(aliceAddr) + + uassert.NoError(t, Register(alice)) + res, latest := users.ResolveName(alice) + + uassert.NotEqual(t, nil, res) + uassert.Equal(t, alice, res.Name()) + uassert.Equal(t, aliceAddr, res.Addr()) + uassert.False(t, res.IsDeleted()) + uassert.True(t, latest) +} + +func TestRegister_Invalid(t *testing.T) { + std.TestSetRealm(std.NewUserRealm(bobAddr)) + std.TestSetOrigCaller(bobAddr) + + // Invalid usernames + uassert.Error(t, Register("alice"), ErrInvalidUsername.Error()) // vanity + uassert.Error(t, Register(""), ErrInvalidUsername.Error()) // empty + uassert.Error(t, Register(" "), ErrInvalidUsername.Error()) // empty + uassert.Error(t, Register("123"), ErrInvalidUsername.Error()) // only numbers + uassert.Error(t, Register("alice&#($)"), ErrInvalidUsername.Error()) // non-allowed chars + uassert.Error(t, Register("Alice123"), ErrInvalidUsername.Error()) // upper-case + uassert.Error(t, Register("toolongusernametoolongusernametoolongusername123"), + ErrInvalidUsername.Error()) // too long + + // Name taken + urequire.NoError(t, Register(bob)) + uassert.Error(t, Register(bob), users.ErrNameTaken.Error()) +} + +func TestUpdateName_Valid(t *testing.T) { + std.TestSetRealm(std.NewUserRealm(aliceAddr)) + std.TestSetOrigCaller(aliceAddr) + + newalice := "newalice123" + // resolve old name + urequire.NoError(t, UpdateName(newalice)) + + res, latest := users.ResolveName(alice) + uassert.NotEqual(t, nil, res) + uassert.Equal(t, newalice, res.Name()) + uassert.Equal(t, aliceAddr, res.Addr()) + uassert.False(t, res.IsDeleted()) + uassert.False(t, latest) +} + +func TestUpdateName_Invalid(t *testing.T) { + std.TestSetRealm(std.NewUserRealm(aliceAddr)) + std.TestSetOrigCaller(aliceAddr) + + // Invalid names + uassert.Error(t, UpdateName("alice"), ErrInvalidUsername.Error()) // vanity + uassert.Error(t, UpdateName(""), ErrInvalidUsername.Error()) // empty + uassert.Error(t, UpdateName(" "), ErrInvalidUsername.Error()) // empty + uassert.Error(t, UpdateName("123"), ErrInvalidUsername.Error()) // only numbers + uassert.Error(t, UpdateName("alice&#($)"), ErrInvalidUsername.Error()) // non-allowed chars + uassert.Error(t, UpdateName("Alice123"), ErrInvalidUsername.Error()) // upper-case + uassert.Error(t, UpdateName("toolongusernametoolongusernametoolongusername123"), + ErrInvalidUsername.Error()) // too long + + urequire.Error(t, UpdateName(bob), users.ErrNameTaken.Error()) +} + +func TestDeleteUser(t *testing.T) { + std.TestSetRealm(std.NewUserRealm(aliceAddr)) + std.TestSetOrigCaller(aliceAddr) + + urequire.NoError(t, DeleteUser()) + res, _ := users.ResolveName(alice) + uassert.Equal(t, nil, res) + res = users.ResolveAddress(aliceAddr) + uassert.Equal(t, nil, res) +} + +func TestDeleteUser_Invalid(t *testing.T) { + std.TestSetRealm(std.NewUserRealm(aliceAddr)) + std.TestSetOrigCaller(aliceAddr) + + // Already deleted user + urequire.Error(t, DeleteUser(), users.ErrUserNotExist.Error()) +} diff --git a/examples/gno.land/r/gov/dao/v2/render.gno b/examples/gno.land/r/gov/dao/v2/render.gno index 57b7b601523..a5f934360a6 100644 --- a/examples/gno.land/r/gov/dao/v2/render.gno +++ b/examples/gno.land/r/gov/dao/v2/render.gno @@ -7,7 +7,8 @@ import ( "gno.land/p/demo/dao" "gno.land/p/demo/ufmt" "gno.land/p/moul/txlink" - "gno.land/r/demo/users" + + "gno.land/r/sys/users" ) func Render(path string) string { @@ -41,10 +42,10 @@ func Render(path string) string { out += ufmt.Sprintf("## [Prop #%d - %s](/r/gov/dao/v2:%d)\n\n", propID, title, propID) out += ufmt.Sprintf("**Status: %s**\n\n", strings.ToUpper(prop.Status().String())) - user := users.GetUserByAddress(prop.Author()) + user := users.ResolveAddress(prop.Author()) authorDisplayText := prop.Author().String() if user != nil { - authorDisplayText = ufmt.Sprintf("[%s](/r/demo/users:%s)", user.Name, user.Name) + authorDisplayText = ufmt.Sprintf("[%s](/r/sys/users:%s)", user.Name(), user.Name()) } out += ufmt.Sprintf("**Author: %s**\n\n", authorDisplayText) @@ -91,13 +92,13 @@ func renderAuthor(p dao.Proposal) string { var out string authorUsername := "" - user := users.GetUserByAddress(p.Author()) + user := users.ResolveAddress(p.Author()) if user != nil { - authorUsername = user.Name + authorUsername = user.Name() } if authorUsername != "" { - out += ufmt.Sprintf("**Author: [%s](/r/demo/users:%s)**\n\n", authorUsername, authorUsername) + out += ufmt.Sprintf("**Author: [%s](/r/gnoland/users/v1:%s)**\n\n", authorUsername, authorUsername) } else { out += ufmt.Sprintf("**Author: %s**\n\n", p.Author().String()) } diff --git a/examples/gno.land/r/morgan/guestbook/guestbook_test.gno b/examples/gno.land/r/morgan/guestbook/guestbook_test.gno index b14fee45b42..a68aacb2e7c 100644 --- a/examples/gno.land/r/morgan/guestbook/guestbook_test.gno +++ b/examples/gno.land/r/morgan/guestbook/guestbook_test.gno @@ -33,7 +33,7 @@ func TestSign(t *testing.T) { } func TestSign_FromRealm(t *testing.T) { - std.TestSetRealm(std.NewCodeRealm("gno.land/r/demo/users")) + std.TestSetRealm(std.NewCodeRealm("gno.land/r/gnoland/users/v1")) defer func() { rec := recover() diff --git a/examples/gno.land/r/stefann/home/home.gno b/examples/gno.land/r/stefann/home/home.gno index f54721ce37c..77199caa256 100644 --- a/examples/gno.land/r/stefann/home/home.gno +++ b/examples/gno.land/r/stefann/home/home.gno @@ -8,8 +8,8 @@ import ( "gno.land/p/demo/avl" "gno.land/p/demo/ownable" "gno.land/p/demo/ufmt" - "gno.land/r/demo/users" "gno.land/r/leon/hof" + "gno.land/r/sys/users" "gno.land/r/stefann/registry" ) @@ -231,8 +231,8 @@ func formatAddress(address string) string { } func getDisplayName(addr std.Address) string { - if user := users.GetUserByAddress(addr); user != nil { - return user.Name + if user := users.ResolveAddress(addr); user != nil { + return user.Name() } return formatAddress(addr.String()) } diff --git a/examples/gno.land/r/sys/names/gno.mod b/examples/gno.land/r/sys/names/gno.mod new file mode 100644 index 00000000000..3cc9b843ed5 --- /dev/null +++ b/examples/gno.land/r/sys/names/gno.mod @@ -0,0 +1 @@ +module gno.land/r/sys/names diff --git a/examples/gno.land/r/sys/names/render.gno b/examples/gno.land/r/sys/names/render.gno new file mode 100644 index 00000000000..769435b7bb8 --- /dev/null +++ b/examples/gno.land/r/sys/names/render.gno @@ -0,0 +1,6 @@ +package names + +func Render(_ string) string { + return `# r/sys/names +System Realm for checking namespace deployment permissions.` +} diff --git a/examples/gno.land/r/sys/names/verifier.gno b/examples/gno.land/r/sys/names/verifier.gno new file mode 100644 index 00000000000..caf961ba0b3 --- /dev/null +++ b/examples/gno.land/r/sys/names/verifier.gno @@ -0,0 +1,85 @@ +// Package names provides functionality for checking of package deployments +// by users registered in r/sys/users are done to proper namespaces. +package names + +import ( + "std" + "strings" + + "gno.land/p/demo/dao" + "gno.land/p/demo/ownable" + + "gno.land/r/gov/dao/bridge" + "gno.land/r/sys/users" +) + +type verifierFunc func(address std.Address, name string) bool + +var ( + Ownable = ownable.NewWithAddress("g1manfred47kzduec920z88wfr64ylksmdcedlf5") // @moul > dropped in genesis via Enable + + checkFunc = defaultVerifier // Callback for the namespace check + enabled = false // set to true in genesis +) + +const VerifyFuncUpdatedEvent = "VerifyFuncUpdated" + +// IsAuthorizedAddressForName ensures that the given address has ownership of the given name. +func IsAuthorizedAddressForName(address std.Address, name string) bool { + return checkFunc(enabled, address, name) +} + +// Enable enables the namespace check and drops centralized ownership of this realm. +// The namespace check is disabled initially to ease txtar and other testing contexts, +// but this function is meant to be called in the genesis of a chain. +// This way, only GovDAO will be able to modify the namespace checking function. +func Enable() { + Ownable.AssertCallerIsOwner() + enabled = true + Ownable.DropOwnership() +} + +func IsEnabled() bool { + return enabled +} + +// defaultVerifyFunction checks the store to see that the +// user has properly registered the given name. +// This function considers as valid an `address` that matches the `name`. +func defaultVerifier(enabled bool, address std.Address, name string) bool { + if !enabled { + return true // only in pre-genesis cases + } + + if strings.TrimSpace(address.String()) == "" || strings.TrimSpace(name) == "" { + return false + } + + // Allow user with their own address as name + // This enables pseudo-anon namespaces + if address.String() == name { + return true + } + + // Can be a registered name or an alias + userData, _ := users.ResolveName(name) + if userData == nil || userData.IsDeleted() { + return false + } + + /// XXX: add check for r/sys/teams down the line + + return userData.Addr() == address +} + +// NewVerifyCallExecutor allows updating the verifier function via a GovDAO proposal +func NewVerifyCallExecutor(newVerifyCall func(enabled bool, address std.Address, name string) bool) dao.Executor { + callback := func() error { + checkFunc = newVerifyCall + + std.Emit(VerifyFuncUpdatedEvent) + return nil + } + + return bridge.GovDAO().NewGovDAOExecutor(callback) +} diff --git a/examples/gno.land/r/sys/names/verifier_test.gno b/examples/gno.land/r/sys/names/verifier_test.gno new file mode 100644 index 00000000000..b800c01cca8 --- /dev/null +++ b/examples/gno.land/r/sys/names/verifier_test.gno @@ -0,0 +1,54 @@ +package names + +import ( + "std" + "testing" + + "gno.land/p/demo/ownable" + "gno.land/p/demo/testutils" + "gno.land/p/demo/uassert" + "gno.land/p/demo/urequire" + + "gno.land/r/sys/users" +) + +var alice = testutils.TestAddress("alice") + +func TestDefaultVerifier(t *testing.T) { + // Check disabled, any case is true + uassert.True(t, defaultVerifier(false, alice, alice.String())) + uassert.True(t, defaultVerifier(false, "", alice.String())) + uassert.True(t, defaultVerifier(false, alice, "somerandomusername")) + + // Check enabled + // username + addr mismatch + uassert.False(t, defaultVerifier(true, alice, "notregistered")) + // PA namespace check + uassert.True(t, defaultVerifier(true, alice, alice.String())) + + // Empty name/address + uassert.False(t, defaultVerifier(true, std.Address(""), "")) + + // Register proper username + std.TestSetRealm(std.NewCodeRealm("gno.land/r/gnoland/users/v1")) // authorized write + std.TestSetOrigCaller(std.DerivePkgAddr("gno.land/r/gnoland/users/v1")) + urequire.NoError(t, users.RegisterUser("alice", alice)) + + // Proper namespace + uassert.True(t, defaultVerifier(true, alice, "alice")) +} + +func TestEnable(t *testing.T) { + std.TestSetRealm(std.NewUserRealm("g1manfred47kzduec920z88wfr64ylksmdcedlf5")) + std.TestSetOrigCaller("g1manfred47kzduec920z88wfr64ylksmdcedlf5") + + uassert.NotPanics(t, func() { + Enable() + }) + + // Confirm enable drops ownerships + uassert.Equal(t, Ownable.Owner().String(), "") + uassert.PanicsWithMessage(t, ownable.ErrUnauthorized.Error(), func() { + Enable() + }) +} diff --git a/examples/gno.land/r/sys/users/admin.gno b/examples/gno.land/r/sys/users/admin.gno new file mode 100644 index 00000000000..8766e6db18a --- /dev/null +++ b/examples/gno.land/r/sys/users/admin.gno @@ -0,0 +1,76 @@ +package users + +import ( + "std" + + "gno.land/p/demo/ownable" + "gno.land/p/demo/pausable" + "gno.land/p/moul/addrset" +) + +const ( + adminAddr = "g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq" // @moul > will be GovDAO managed soon, see #3523 & #3546 + gnolandUsers = "gno.land/r/gnoland/users/v1" // preregistered with store write perms +) + +var ( + callerWhitelist = addrset.Set{} + + // Safe objects + Ownable = ownable.NewWithAddress(adminAddr) + Pausable = pausable.NewFromOwnable(Ownable) +) + +func init() { + callerWhitelist.Add(std.DerivePkgAddr(gnolandUsers)) // initially whitelisted +} + +// AddToWhitelist adds a new address to the caller whitelist +func AddToWhitelist(addr std.Address) error { + if !Ownable.CallerIsOwner() { + return ownable.ErrUnauthorized + } + + return addToWhitelist(addr) +} + +// DelFromWhitelist removes a caller from the whitelist +func DelFromWhitelist(addr std.Address) error { + if !Ownable.CallerIsOwner() { + return ownable.ErrUnauthorized + } + + return deleteFromwhitelist(addr) +} + +// IsOnWhitelist checks if the given address has +// permission to write to the user store +func IsOnWhitelist(addr std.Address) bool { + return callerWhitelist.Has(addr) +} + +// Helpers + +func deleteFromwhitelist(addr std.Address) error { + if !callerWhitelist.Has(addr) { + return ErrNotWhitelisted + } + + if ok := callerWhitelist.Remove(addr); !ok { + panic("failed to remove address from whitelist") + } + + return nil +} + +func addToWhitelist(newCaller std.Address) error { + if !newCaller.IsValid() { + return ErrInvalidAddress + } + if callerWhitelist.Has(newCaller) { + return ErrAlreadyWhitelisted + } + + callerWhitelist.Add(newCaller) + return nil +} diff --git a/examples/gno.land/r/sys/users/admin_test.gno b/examples/gno.land/r/sys/users/admin_test.gno new file mode 100644 index 00000000000..848a184775c --- /dev/null +++ b/examples/gno.land/r/sys/users/admin_test.gno @@ -0,0 +1,48 @@ +package users + +import ( + "std" + "testing" + + "gno.land/p/demo/uassert" +) + +func TestAddToWhitelist(t *testing.T) { + t.Run("invalid new address", func(t *testing.T) { + std.TestSetRealm(adminRealm) + + uassert.Error(t, AddToWhitelist(std.Address("invalidaddress")), ErrInvalidAddress.Error()) + uassert.Error(t, AddToWhitelist(std.Address("")), ErrInvalidAddress.Error()) + uassert.Error(t, AddToWhitelist(std.Address("000000000000000000000000000000000000000000000000")), ErrInvalidAddress.Error()) + }) + + t.Run("already whitelisted", func(t *testing.T) { + std.TestSetRealm(adminRealm) + + uassert.Error(t, AddToWhitelist(whitelistedCallerAddr), ErrAlreadyWhitelisted.Error()) + }) + + t.Run("valid addition", func(t *testing.T) { + std.TestSetRealm(adminRealm) + + uassert.NoError(t, AddToWhitelist(std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5"))) + }) +} + +func TestDelFromWhitelist(t *testing.T) { + t.Run("not on whitelist", func(t *testing.T) { + std.TestSetRealm(adminRealm) + + uassert.Error(t, DelFromWhitelist(std.Address("")), ErrNotWhitelisted.Error()) + }) + + t.Run("valid deletion", func(t *testing.T) { + std.TestSetRealm(adminRealm) + + uassert.NoError(t, DelFromWhitelist(whitelistedCallerAddr)) + }) + + // FIXME: should not be needed. realm state should reset after + // each test, and init should happen before running a new one. + callerWhitelist.Add(std.DerivePkgAddr(gnolandUsers)) +} diff --git a/examples/gno.land/r/sys/users/errors.gno b/examples/gno.land/r/sys/users/errors.gno new file mode 100644 index 00000000000..41d9bd7e464 --- /dev/null +++ b/examples/gno.land/r/sys/users/errors.gno @@ -0,0 +1,23 @@ +package users + +import "errors" + +const prefix = "r/sys/users: " + +var ( + ErrNotWhitelisted = errors.New(prefix + "does not exist in whitelist") + ErrAlreadyWhitelisted = errors.New(prefix + "already whitelisted") + + ErrNameTaken = errors.New(prefix + "name/Alias already taken") + ErrInvalidAddress = errors.New(prefix + "invalid address") + + ErrEmptyUsername = errors.New(prefix + "empty username provided") + ErrNameTooLong = errors.New(prefix + "username is too long") + ErrNameLikeAddress = errors.New(prefix + "username resembles a gno.land address") + + ErrAlreadyHasName = errors.New(prefix + "username for this address already registered - try creating an Alias") + ErrDeletedUser = errors.New(prefix + "cannot register a new username after deleting") + ErrAliasBeforeName = errors.New(prefix + "cannot register Alias before a username") + + ErrUserNotExist = errors.New(prefix + "this user has not been registered before") +) diff --git a/examples/gno.land/r/sys/users/render.gno b/examples/gno.land/r/sys/users/render.gno new file mode 100644 index 00000000000..8b0d831f526 --- /dev/null +++ b/examples/gno.land/r/sys/users/render.gno @@ -0,0 +1,6 @@ +package users + +func Render(_ string) string { + return `# r/sys/users +System Realm for managing user data.` +} diff --git a/examples/gno.land/r/sys/users/store.gno b/examples/gno.land/r/sys/users/store.gno new file mode 100644 index 00000000000..dfc5ce9fa9f --- /dev/null +++ b/examples/gno.land/r/sys/users/store.gno @@ -0,0 +1,173 @@ +package users + +import ( + "regexp" + "std" + "strings" + + "gno.land/p/demo/avl" +) + +var ( + nameStore = avl.NewTree() // name/aliases > *UserData + addressStore = avl.NewTree() // address > *UserData + + reAddressLookalike = regexp.MustCompile(`^g1[a-z0-9]{20,38}$`) +) + +const ( + RegisterUserEvent = "Registered" + UpdateNameEvent = "NameUpdated" + DeleteUserEvent = "Deleted" + + maxUsernameLen = 64 +) + +type UserData struct { + addr std.Address + username string // contains the latest name of a user + deleted bool +} + +func (u UserData) Name() string { + return u.username +} + +func (u UserData) Addr() std.Address { + return u.addr +} + +func (u UserData) IsDeleted() bool { + return u.deleted +} + +// RegisterUser adds a new user to the system. +func RegisterUser(name string, address std.Address) error { + // Validate caller + if err := validateCall(); err != nil { + return err + } + + // Validate inputs + if err := validateInputs(name, address); err != nil { + return err + } + + // Check if name is taken + if nameStore.Has(name) { + return ErrNameTaken + } + + raw, ok := addressStore.Get(address.String()) + if ok { + // Cannot re-register after deletion + if raw.(*UserData).IsDeleted() { + return ErrDeletedUser + } + + // For a second name, user RegisterAlias + return ErrAlreadyHasName + } + + // Create UserData + data := &UserData{ + addr: address, + username: name, + deleted: false, + } + + // Set corresponding stores + nameStore.Set(name, data) + addressStore.Set(address.String(), data) + + std.Emit(RegisterUserEvent, "name", name, "address", address.String()) + return nil +} + +// UpdateName adds a name that is associated with a specific address +// All previous names are preserved and resolvable +func UpdateName(newName string, address std.Address) error { + // Validate caller + if err := validateCall(); err != nil { + return err + } + + // Validate inputs + if err := validateInputs(newName, address); err != nil { + return err + } + + // Check if the requested Alias is already taken + if nameStore.Has(newName) { + return ErrNameTaken + } + + // Check if user has a name before an Alias + raw, ok := addressStore.Get(address.String()) + if !ok { + return ErrAliasBeforeName + } + + userData := raw.(*UserData) + + if userData.IsDeleted() { + return ErrDeletedUser + } + + userData.username = newName + nameStore.Set(newName, userData) + + std.Emit(UpdateNameEvent, "Alias", newName, "address", address.String()) + return nil +} + +// Delete marks a user and all their aliases as deleted. +func Delete(addr std.Address) error { + if err := validateCall(); err != nil { + return err + } + + data := ResolveAddress(addr) + if data == nil { + return ErrUserNotExist + } + + data.deleted = true + + std.Emit(DeleteUserEvent, "address", data.addr.String()) + return nil +} + +func validateCall() error { + if !IsOnWhitelist(std.PrevRealm().Addr()) { + return ErrNotWhitelisted + } + + if Pausable.IsPaused() { + panic("paused") + } + + return nil +} + +// Validate validates username and address passed in +func validateInputs(username string, address std.Address) error { + if !address.IsValid() { + return ErrInvalidAddress + } + + if strings.TrimSpace(username) == "" { + return ErrEmptyUsername + } + + if len(username) > maxUsernameLen { + return ErrNameTooLong + } + + // Check if the username can be decoded or looks like a valid address + if _, _, ok := std.DecodeBech32(std.Address(username)); ok || reAddressLookalike.MatchString(username) { + return ErrNameLikeAddress + } + + return nil +} diff --git a/examples/gno.land/r/sys/users/store_test.gno b/examples/gno.land/r/sys/users/store_test.gno new file mode 100644 index 00000000000..245deb063fc --- /dev/null +++ b/examples/gno.land/r/sys/users/store_test.gno @@ -0,0 +1,193 @@ +package users + +import ( + "std" + "testing" + + "gno.land/p/demo/avl" + "gno.land/p/demo/testutils" + "gno.land/p/demo/uassert" + "gno.land/p/demo/urequire" +) + +var ( + alice = "alice" + aliceAddr = testutils.TestAddress(alice) + bob = "bob" + bobAddr = testutils.TestAddress(bob) + + whitelistedCallerAddr = std.DerivePkgAddr(gnolandUsers) + adminRealm = std.NewUserRealm(adminAddr) +) + +func TestRegister(t *testing.T) { + std.TestSetOrigCaller(whitelistedCallerAddr) + + t.Run("valid_registration", func(t *testing.T) { + urequire.NoError(t, RegisterUser(alice, aliceAddr)) + + res, isLatest := ResolveName(alice) + uassert.Equal(t, aliceAddr, res.Addr()) + uassert.True(t, isLatest) + + res = ResolveAddress(aliceAddr) + uassert.Equal(t, alice, res.Name()) + }) + + t.Run("invalid_inputs", func(t *testing.T) { + cleanStore(t) + + uassert.ErrorContains(t, RegisterUser(" ", aliceAddr), ErrEmptyUsername.Error()) + uassert.ErrorContains(t, + RegisterUser("65letterusername65letterusername65letterusername65letterusername0", + aliceAddr), + ErrNameTooLong.Error()) + + uassert.ErrorContains(t, RegisterUser(alice, ""), ErrInvalidAddress.Error()) + uassert.ErrorContains(t, RegisterUser(alice, "invalidaddress"), ErrInvalidAddress.Error()) + }) + + t.Run("addr_already_registered", func(t *testing.T) { + cleanStore(t) + + urequire.NoError(t, RegisterUser(alice, aliceAddr)) + + // Try registering again + uassert.ErrorContains(t, RegisterUser("othername", aliceAddr), ErrAlreadyHasName.Error()) + }) + + t.Run("name_taken", func(t *testing.T) { + cleanStore(t) + + urequire.NoError(t, RegisterUser(alice, aliceAddr)) + + // Try registering alice's name with bob's address + uassert.ErrorContains(t, RegisterUser(alice, bobAddr), ErrNameTaken.Error()) + }) + + t.Run("user_deleted", func(t *testing.T) { + cleanStore(t) + + urequire.NoError(t, RegisterUser(alice, aliceAddr)) + urequire.NoError(t, Delete(aliceAddr)) + + // Try re-registering after deletion + uassert.ErrorContains(t, RegisterUser("newname", aliceAddr), ErrDeletedUser.Error()) + }) + + t.Run("address_lookalike", func(t *testing.T) { + cleanStore(t) + + // Address as username + uassert.ErrorContains(t, RegisterUser("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", aliceAddr), ErrNameLikeAddress.Error()) + // Beginning of address as username + uassert.ErrorContains(t, RegisterUser("g1jg8mtutu9khhfwc4nxmu", aliceAddr), ErrNameLikeAddress.Error()) + uassert.NoError(t, RegisterUser("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5longerthananaddress", aliceAddr)) + }) +} + +func TestUpdateName(t *testing.T) { + std.TestSetOrigCaller(whitelistedCallerAddr) + t.Run("valid_direct_alias", func(t *testing.T) { + cleanStore(t) + + urequire.NoError(t, RegisterUser(alice, aliceAddr)) + uassert.NoError(t, UpdateName("alice1", aliceAddr)) + }) + + t.Run("valid_double_alias", func(t *testing.T) { + cleanStore(t) + + urequire.NoError(t, RegisterUser(alice, aliceAddr)) + uassert.NoError(t, UpdateName("alice1", aliceAddr)) + uassert.NoError(t, UpdateName("alice2", aliceAddr)) + }) + + t.Run("name_taken", func(t *testing.T) { + cleanStore(t) + + urequire.NoError(t, RegisterUser(alice, aliceAddr)) + uassert.NoError(t, UpdateName("alice1", aliceAddr)) + }) + + t.Run("alias_before_name", func(t *testing.T) { + cleanStore(t) + + uassert.ErrorContains(t, UpdateName(alice, aliceAddr), ErrAliasBeforeName.Error()) + }) + + t.Run("alias_after_delete", func(t *testing.T) { + cleanStore(t) + + urequire.NoError(t, RegisterUser(alice, aliceAddr)) + urequire.NoError(t, Delete(aliceAddr)) + + uassert.ErrorContains(t, UpdateName("newalice", aliceAddr), ErrDeletedUser.Error()) + }) + + t.Run("invalid_inputs", func(t *testing.T) { + cleanStore(t) + + urequire.NoError(t, RegisterUser(alice, aliceAddr)) + + uassert.ErrorContains(t, UpdateName(" ", aliceAddr), ErrEmptyUsername.Error()) + uassert.ErrorContains(t, + UpdateName("65letterusername65letterusername65letterusername65letterusername0", + aliceAddr), + ErrNameTooLong.Error()) + + uassert.ErrorContains(t, UpdateName(alice, ""), ErrInvalidAddress.Error()) + uassert.ErrorContains(t, UpdateName(alice, "invalidaddress"), ErrInvalidAddress.Error()) + }) + + t.Run("address_lookalike", func(t *testing.T) { + cleanStore(t) + + urequire.NoError(t, RegisterUser(alice, aliceAddr)) + + // Address as username + uassert.ErrorContains(t, UpdateName("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", aliceAddr), ErrNameLikeAddress.Error()) + // Beginning of address as username + uassert.ErrorContains(t, UpdateName("g1jg8mtutu9khhfwc4nxmu", aliceAddr), ErrNameLikeAddress.Error()) + uassert.NoError(t, UpdateName("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5longerthananaddress", aliceAddr)) + }) +} + +func TestDelete(t *testing.T) { + std.TestSetOrigCaller(whitelistedCallerAddr) + + t.Run("non_existent_user", func(t *testing.T) { + cleanStore(t) + + uassert.ErrorContains(t, Delete(testutils.TestAddress("unregistered")), ErrUserNotExist.Error()) + }) + + t.Run("double_delete", func(t *testing.T) { + cleanStore(t) + + urequire.NoError(t, RegisterUser(alice, aliceAddr)) + + urequire.NoError(t, Delete(aliceAddr)) + + uassert.ErrorContains(t, Delete(aliceAddr), ErrUserNotExist.Error()) + }) + + t.Run("valid_delete", func(t *testing.T) { + cleanStore(t) + + urequire.NoError(t, RegisterUser(alice, aliceAddr)) + uassert.NoError(t, Delete(aliceAddr)) + + resolved, _ := ResolveName(alice) + uassert.Equal(t, nil, resolved) + }) +} + +// cleanStore should not be needed, as vm store should be reset after each test. +// Reference: https://github.com/gnolang/gno/issues/1982 +func cleanStore(t *testing.T) { + t.Helper() + + nameStore = avl.NewTree() + addressStore = avl.NewTree() +} diff --git a/examples/gno.land/r/sys/users/users.gno b/examples/gno.land/r/sys/users/users.gno new file mode 100644 index 00000000000..f351404da60 --- /dev/null +++ b/examples/gno.land/r/sys/users/users.gno @@ -0,0 +1,47 @@ +package users + +import ( + "std" + + "gno.land/p/demo/avl/rotree" +) + +// ResolveName returns the latest UserData of a specific user by name or alias +func ResolveName(name string) (data *UserData, isLatest bool) { + raw, ok := nameStore.Get(name) + if !ok { + return nil, false + } + + data = raw.(*UserData) + if data.deleted { + return nil, false + } + + return data, name == data.username +} + +// ResolveAddress returns the latest UserData of a specific user by address +func ResolveAddress(addr std.Address) (data *UserData) { + raw, ok := addressStore.Get(addr.String()) + if !ok { + return nil + } + + data = raw.(*UserData) + if data.deleted { + return nil + } + + return data +} + +// GetReadonlyAddrStore exposes the address store in readonly mode +func GetReadonlyAddrStore() *rotree.ReadOnlyTree { + return rotree.Wrap(addressStore, nil) +} + +// GetReadOnlyNameStore exposes the name store in readonly mode +func GetReadOnlyNameStore() *rotree.ReadOnlyTree { + return rotree.Wrap(nameStore, nil) +} diff --git a/examples/gno.land/r/sys/users/users_test.gno b/examples/gno.land/r/sys/users/users_test.gno new file mode 100644 index 00000000000..910ba6f1067 --- /dev/null +++ b/examples/gno.land/r/sys/users/users_test.gno @@ -0,0 +1,111 @@ +package users + +import ( + "std" + "strconv" + "testing" + + "gno.land/p/demo/uassert" + "gno.land/p/demo/urequire" +) + +func TestResolveName(t *testing.T) { + std.TestSetOrigCaller(whitelistedCallerAddr) + + t.Run("single_name", func(t *testing.T) { + cleanStore(t) + + urequire.NoError(t, RegisterUser(alice, aliceAddr)) + + res, isLatest := ResolveName(alice) + uassert.Equal(t, aliceAddr, res.Addr()) + uassert.Equal(t, alice, res.Name()) + uassert.True(t, isLatest) + }) + + t.Run("name+Alias", func(t *testing.T) { + cleanStore(t) + + urequire.NoError(t, RegisterUser(alice, aliceAddr)) + urequire.NoError(t, UpdateName("alice1", aliceAddr)) + + res, isLatest := ResolveName("alice1") + urequire.NotEqual(t, nil, res) + + uassert.Equal(t, aliceAddr, res.Addr()) + uassert.Equal(t, "alice1", res.Name()) + uassert.True(t, isLatest) + }) + + t.Run("multiple_aliases", func(t *testing.T) { + cleanStore(t) + + urequire.NoError(t, RegisterUser(alice, aliceAddr)) + + // RegisterUser and check each Alias + var names []string + names = append(names, alice) + for i := 0; i < 5; i++ { + alias := "alice" + strconv.Itoa(i) + names = append(names, alias) + + urequire.NoError(t, UpdateName(alias, aliceAddr)) + } + + for _, alias := range names { + res, _ := ResolveName(alias) + urequire.NotEqual(t, nil, res) + + uassert.Equal(t, aliceAddr, res.Addr()) + uassert.Equal(t, "alice4", res.Name()) + } + }) +} + +func TestResolveAddress(t *testing.T) { + std.TestSetOrigCaller(whitelistedCallerAddr) + + t.Run("single_name", func(t *testing.T) { + cleanStore(t) + + urequire.NoError(t, RegisterUser(alice, aliceAddr)) + + res := ResolveAddress(aliceAddr) + + uassert.Equal(t, aliceAddr, res.Addr()) + uassert.Equal(t, alice, res.Name()) + }) + + t.Run("name+Alias", func(t *testing.T) { + cleanStore(t) + + urequire.NoError(t, RegisterUser(alice, aliceAddr)) + urequire.NoError(t, UpdateName("alice1", aliceAddr)) + + res := ResolveAddress(aliceAddr) + urequire.NotEqual(t, nil, res) + + uassert.Equal(t, aliceAddr, res.Addr()) + uassert.Equal(t, "alice1", res.Name()) + }) + + t.Run("multiple_aliases", func(t *testing.T) { + cleanStore(t) + + urequire.NoError(t, RegisterUser(alice, aliceAddr)) + + // RegisterUser and check each Alias + var names []string + names = append(names, alice) + + for i := 0; i < 5; i++ { + alias := "alice" + strconv.Itoa(i) + names = append(names, alias) + urequire.NoError(t, UpdateName(alias, aliceAddr)) + } + + res := ResolveAddress(aliceAddr) + uassert.Equal(t, aliceAddr, res.Addr()) + uassert.Equal(t, "alice4", res.Name()) + }) +} diff --git a/examples/gno.land/r/sys/users/verify.gno b/examples/gno.land/r/sys/users/verify.gno deleted file mode 100644 index 71869fda1a1..00000000000 --- a/examples/gno.land/r/sys/users/verify.gno +++ /dev/null @@ -1,83 +0,0 @@ -package users - -import ( - "std" - - "gno.land/p/demo/ownable" - "gno.land/r/demo/users" -) - -const admin = "g1manfred47kzduec920z88wfr64ylksmdcedlf5" // @moul - -type VerifyNameFunc func(enabled bool, address std.Address, name string) bool - -var ( - owner = ownable.NewWithAddress(admin) // Package owner - checkFunc = VerifyNameByUser // Checking namespace callback - enabled = false // For now this package is disabled by default -) - -func IsEnabled() bool { return enabled } - -// This method ensures that the given address has ownership of the given name. -func IsAuthorizedAddressForName(address std.Address, name string) bool { - return checkFunc(enabled, address, name) -} - -// VerifyNameByUser checks from the `users` package that the user has correctly -// registered the given name. -// This function considers as valid an `address` that matches the `name`. -func VerifyNameByUser(enable bool, address std.Address, name string) bool { - if !enable { - return true - } - - // Allow user with their own address as name - if address.String() == name { - return true - } - - if user := users.GetUserByName(name); user != nil { - return user.Address == address - } - - return false -} - -// Admin calls - -// Enable this package. -func AdminEnable() { - if !owner.CallerIsOwner() { - panic(ownable.ErrUnauthorized) - } - - enabled = true -} - -// Disable this package. -func AdminDisable() { - if !owner.CallerIsOwner() { - panic(ownable.ErrUnauthorized) - } - - enabled = false -} - -// AdminUpdateVerifyCall updates the method that verifies the namespace. -func AdminUpdateVerifyCall(check VerifyNameFunc) { - if !owner.CallerIsOwner() { - panic(ownable.ErrUnauthorized) - } - - checkFunc = check -} - -// AdminTransferOwnership transfers the ownership to a new owner. -func AdminTransferOwnership(newOwner std.Address) error { - if !owner.CallerIsOwner() { - panic(ownable.ErrUnauthorized) - } - - return owner.TransferOwnership(newOwner) -} diff --git a/gno.land/genesis/genesis_params.toml b/gno.land/genesis/genesis_params.toml index fb080024624..9c70a001086 100644 --- a/gno.land/genesis/genesis_params.toml +++ b/gno.land/genesis/genesis_params.toml @@ -1,7 +1,7 @@ ## gno.land ["gno.land/r/sys/params.sys"] - users_pkgpath.string = "gno.land/r/sys/users" # if empty, no namespace support. + names_pkgpath.string = "gno.land/r/sys/names" # if empty, no namespace support. # TODO: validators_pkgpath.string = "gno.land/r/sys/validators" # TODO: rewards_pkgpath.string = "gno.land/r/sys/rewards" # TODO: token_lock.bool = true diff --git a/gno.land/genesis/genesis_txs.jsonl b/gno.land/genesis/genesis_txs.jsonl index 9027d51c0ac..5e852d9a1cf 100644 --- a/gno.land/genesis/genesis_txs.jsonl +++ b/gno.land/genesis/genesis_txs.jsonl @@ -1,17 +1,14 @@ -{"tx": {"msg":[{"@type":"/vm.m_call","caller":"g1manfred47kzduec920z88wfr64ylksmdcedlf5","send":"","pkg_path":"gno.land/r/demo/users","func":"Invite","args":["g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj:10\ng1589c8cekvmjfmy0qrd4f3z52r7fn7rgk02667s:1\ng13sm84nuqed3fuank8huh7x9mupgw22uft3lcl8:1\ng1m6732pkrngu9vrt0g7056lvr9kcqc4mv83xl5q:1\ng1wg88rhzlwxjd2z4j5de5v5xq30dcf6rjq3dhsj:1\ng18pmaskasz7mxj6rmgrl3al58xu45a7w0l5nmc0:1\ng19wwhkmqlns70604ksp6rkuuu42qhtvyh05lffz:1\ng187982000zsc493znqt828s90cmp6hcp2erhu6m:1\ng1ndpsnrspdnauckytvkfv8s823t3gmpqmtky8pl:1\ng16ja66d65emkr0zxd2tu7xjvm7utthyhpej0037:1\ng1ds24jj9kqjcskd0gzu24r9e4n62ggye230zuv5:1\ng1trkzq75ntamsnw9xnrav2v7gy2lt5g6p29yhdr:1\ng1rrf8s5mrmu00sx04fzfsvc399fklpeg2x0a7mz:1\ng19p5ntfvpt4lwq4jqsmnxsnelhf3tff9scy3w8w:1\ng1tue8l73d6rq4vhqdsp2sr3zhuzpure3k2rnwpz:1\ng14hhsss4ngx5kq77je5g0tl4vftg8qp45ceadk3:1\ng1768hvkh7anhd40ch4h7jdh6j3mpcs7hrat4gl0:1\ng15fa8kyjhu88t9dr8zzua8fwdvkngv5n8yqsm0n:1\ng1xhccdjcscuhgmt3quww6qdy3j3czqt3urc2eac:1\ng1z629z04f85k4t5gnkk5egpxw9tqxeec435esap:1\ng1pfldkplz9puq0v82lu9vqcve9nwrxuq9qe5ttv:1\ng152pn0g5qfgxr7yx8zlwjq48hytkafd8x7egsfv:1\ng1cf2ye686ke38vjyqakreprljum4xu6rwf5jskq:1\ng1c5shztyaj4gjrc5zlwmh9xhex5w7l4asffs2w6:1\ng1lhpx2ktk0ha3qw42raxq4m24a4c4xqxyrgv54q:1\ng1026p54q0j902059sm2zsv37krf0ghcl7gmhyv7:1\ng1n4yvwnv77frq2ccuw27dmtjkd7u4p4jg0pgm7k:1\ng13m7f2e6r3lh3ykxupacdt9sem2tlvmaamwjhll:1\ng19uxluuecjlsqvwmwu8sp6pxaaqfhk972q975xd:1\ng1j80fpcsumfkxypvydvtwtz3j4sdwr8c2u0lr64:1\ng1tjdpptuk9eysq6z38nscqyycr998xjyx3w8jvw:1\ng19t3n89slfemgd3mwuat4lajwcp0yxrkadgeg7a:1\ng1yqndt8xx92l9h494jfruz2w79swzjes3n4wqjc:1\ng13278z0a5ufeg80ffqxpda9dlp599t7ekregcy6:1\ng1ht236wjd83x96uqwh9rh3fq6pylyn78mtwq9v6:1\ng1fj9jccm3zjnqspq7lp2g7lj4czyfq0s35600g9:1\ng1wwppuzdns5u6c6jqpkzua24zh6ppsus6399cea:1\ng1k8pjnguyu36pkc8hy0ufzgpzfmj2jl78la7ek3:1\ng1e8umkzumtxgs8399lw0us4rclea3xl5gxy9spp:1\ng14qekdkj2nmmwea4ufg9n002a3pud23y8k7ugs5:1\ng19w2488ntfgpduzqq3sk4j5x387zynwknqdvjqf:1\ng1495y3z7zrej4rendysnw5kaeu4g3d7x7w0734g:1\ng1hygx8ga9qakhkczyrzs9drm8j8tu4qds9y5e3r:1\ng1f977l6wxdh3qu60kzl75vx2wmzswu68l03r8su:1\ng1644qje5rx6jsdqfkzmgnfcegx4dxkjh6rwqd69:1\ng1mzjajymvmtksdwh3wkrndwj6zls2awl9q83dh6:1\ng14da4n9hcynyzz83q607uu8keuh9hwlv42ra6fa:10\ng14vhcdsyf83ngsrrqc92kmw8q9xakqjm0v8448t:5\n"]}],"fee":{"gas_wanted":"2000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AmG6kzznyo1uNqWPAYU6wDpsmzQKDaEOrVRaZ08vOyX0"},"signature":"S8iMMzlOMK8dmox78R9Z8+pSsS8YaTCXrIcaHDpiOgkOy7gqoQJ0oftM0zf8zAz4xpezK8Lzg8Q0fCdXJxV76w=="}],"memo":""}} -{"tx": {"msg":[{"@type":"/vm.m_call","caller":"g1manfred47kzduec920z88wfr64ylksmdcedlf5","send":"","pkg_path":"gno.land/r/demo/users","func":"Invite","args":["g1thlf3yct7n7ex70k0p62user0kn6mj6d3s0cg3\ng1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\ng1manfred47kzduec920z88wfr64ylksmdcedlf5\n"]}],"fee":{"gas_wanted":"2000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AmG6kzznyo1uNqWPAYU6wDpsmzQKDaEOrVRaZ08vOyX0"},"signature":"njczE6xYdp01+CaUU/8/v0YC/NuZD06+qLind+ZZEEMNaRe/4Ln+4z7dG6HYlaWUMsyI1KCoB6NIehoE0PZ44Q=="}],"memo":""}} -{"tx": {"msg":[{"@type":"/vm.m_call","caller":"g1manfred47kzduec920z88wfr64ylksmdcedlf5","send":"","pkg_path":"gno.land/r/demo/users","func":"Invite","args":["g1589c8cekvmjfmy0qrd4f3z52r7fn7rgk02667s\ng13sm84nuqed3fuank8huh7x9mupgw22uft3lcl8\ng1m6732pkrngu9vrt0g7056lvr9kcqc4mv83xl5q\ng1wg88rhzlwxjd2z4j5de5v5xq30dcf6rjq3dhsj\ng18pmaskasz7mxj6rmgrl3al58xu45a7w0l5nmc0\ng19wwhkmqlns70604ksp6rkuuu42qhtvyh05lffz\ng187982000zsc493znqt828s90cmp6hcp2erhu6m\ng1ndpsnrspdnauckytvkfv8s823t3gmpqmtky8pl\ng16ja66d65emkr0zxd2tu7xjvm7utthyhpej0037\ng1ds24jj9kqjcskd0gzu24r9e4n62ggye230zuv5\ng1trkzq75ntamsnw9xnrav2v7gy2lt5g6p29yhdr\ng1rrf8s5mrmu00sx04fzfsvc399fklpeg2x0a7mz\ng19p5ntfvpt4lwq4jqsmnxsnelhf3tff9scy3w8w\ng1tue8l73d6rq4vhqdsp2sr3zhuzpure3k2rnwpz\ng14hhsss4ngx5kq77je5g0tl4vftg8qp45ceadk3\ng1768hvkh7anhd40ch4h7jdh6j3mpcs7hrat4gl0\ng15fa8kyjhu88t9dr8zzua8fwdvkngv5n8yqsm0n\ng1xhccdjcscuhgmt3quww6qdy3j3czqt3urc2eac\ng1z629z04f85k4t5gnkk5egpxw9tqxeec435esap\ng1pfldkplz9puq0v82lu9vqcve9nwrxuq9qe5ttv\ng152pn0g5qfgxr7yx8zlwjq48hytkafd8x7egsfv\ng1cf2ye686ke38vjyqakreprljum4xu6rwf5jskq\ng1c5shztyaj4gjrc5zlwmh9xhex5w7l4asffs2w6\ng1lhpx2ktk0ha3qw42raxq4m24a4c4xqxyrgv54q\ng1026p54q0j902059sm2zsv37krf0ghcl7gmhyv7\ng1n4yvwnv77frq2ccuw27dmtjkd7u4p4jg0pgm7k\ng13m7f2e6r3lh3ykxupacdt9sem2tlvmaamwjhll\ng19uxluuecjlsqvwmwu8sp6pxaaqfhk972q975xd\ng1j80fpcsumfkxypvydvtwtz3j4sdwr8c2u0lr64\ng1tjdpptuk9eysq6z38nscqyycr998xjyx3w8jvw\ng19t3n89slfemgd3mwuat4lajwcp0yxrkadgeg7a\ng1yqndt8xx92l9h494jfruz2w79swzjes3n4wqjc\ng13278z0a5ufeg80ffqxpda9dlp599t7ekregcy6\ng1ht236wjd83x96uqwh9rh3fq6pylyn78mtwq9v6\ng1fj9jccm3zjnqspq7lp2g7lj4czyfq0s35600g9\ng1wwppuzdns5u6c6jqpkzua24zh6ppsus6399cea\ng1k8pjnguyu36pkc8hy0ufzgpzfmj2jl78la7ek3\ng1e8umkzumtxgs8399lw0us4rclea3xl5gxy9spp\ng14qekdkj2nmmwea4ufg9n002a3pud23y8k7ugs5\ng19w2488ntfgpduzqq3sk4j5x387zynwknqdvjqf\ng1495y3z7zrej4rendysnw5kaeu4g3d7x7w0734g\ng1hygx8ga9qakhkczyrzs9drm8j8tu4qds9y5e3r\ng1f977l6wxdh3qu60kzl75vx2wmzswu68l03r8su\ng1644qje5rx6jsdqfkzmgnfcegx4dxkjh6rwqd69\ng1mzjajymvmtksdwh3wkrndwj6zls2awl9q83dh6\ng1manfred47kzduec920z88wfr64ylksmdcedlf5\ng14da4n9hcynyzz83q607uu8keuh9hwlv42ra6fa\ng14vhcdsyf83ngsrrqc92kmw8q9xakqjm0v8448t\n"]}],"fee":{"gas_wanted":"4000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AmG6kzznyo1uNqWPAYU6wDpsmzQKDaEOrVRaZ08vOyX0"},"signature":"7AmlhZhsVkxCUl0bbpvpPMnIKihwtG7A5IFR6Tg4xStWLgaUr05XmWRKlO2xjstTtwbVKQT5mFL4h5wyX4SQzw=="}],"memo":""}} -{"tx": {"msg":[{"@type":"/vm.m_call","caller":"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj","send":"200000000ugnot","pkg_path":"gno.land/r/demo/users","func":"Register","args":["","administrator","g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj"]}],"fee":{"gas_wanted":"2000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AmG6kzznyo1uNqWPAYU6wDpsmzQKDaEOrVRaZ08vOyX0"},"signature":"AqCqe0cS55Ym7/BvPDoCDyPP5q8284gecVQ2PMOlq/4lJpO9Q18SOWKI15dMEBY1pT0AYyhCeTirlsM1I3Y4Cg=="}],"memo":""}} -{"tx": {"msg":[{"@type":"/vm.m_call","caller":"g1qpymzwx4l4cy6cerdyajp9ksvjsf20rk5y9rtt","send":"200000000ugnot","pkg_path":"gno.land/r/demo/users","func":"Register","args":["","zo_oma","Love is the encryption key\u003c3"]}],"fee":{"gas_wanted":"2000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6yg5/iiktruezVw5vZJwLlGwyrvw8RlqOToTRMWXkE2"},"signature":"GGp+bVL2eEvKecPqgcULSABYOSnSMnJzfIsR8ZIRER1GGX/fOiCReX4WKMrGLVROJVfbLQkDRwvhS4TLHlSoSQ=="}],"memo":""}} -{"tx": {"msg":[{"@type":"/vm.m_call","caller":"g1manfred47kzduec920z88wfr64ylksmdcedlf5","send":"200000000ugnot","pkg_path":"gno.land/r/demo/users","func":"Register","args":["g1manfred47kzduec920z88wfr64ylksmdcedlf5","moul","https://github.com/moul"]}],"fee":{"gas_wanted":"2000000","gas_fee":"200000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AnK+a6mcFDjY6b/v6p7r8QFW1M1PgIoQxBgrwOoyY7v3"},"signature":"9CWeNbKx+hEL+RdHplAVAFntcrAVx5mK9tMqoywuHVoreH844n3yOxddQrGfBk6T2tMBmNWakERRqWZfS+bYAQ=="}],"memo":""}} -{"tx": {"msg":[{"@type":"/vm.m_call","caller":"g1fj9jccm3zjnqspq7lp2g7lj4czyfq0s35600g9","send":"200000000ugnot","pkg_path":"gno.land/r/demo/users","func":"Register","args":["","piupiu","@piux2"]}],"fee":{"gas_wanted":"2000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"Ar68lqbU2YC63fbMcYUtJhYO3/66APM/EqF7m0nUjGyz"},"signature":"pTUpP0d/XlfVe3TH1hlaoLhKadzIKG1gtQ/Ueuat72p+659RWRea58Z0mk6GgPE/EeTbhMEY45zufevBdGJVoQ=="}],"memo":""}} -{"tx": {"msg":[{"@type":"/vm.m_call","caller":"g1ds24jj9kqjcskd0gzu24r9e4n62ggye230zuv5","send":"","pkg_path":"gno.land/r/demo/users","func":"Register","args":["g1manfred47kzduec920z88wfr64ylksmdcedlf5","anarcher","https://twitter.com/anarcher"]}],"fee":{"gas_wanted":"2000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AjpLbKdQeH+yB/1OCB148l5GlRRrXma71hdA8EES3H7f"},"signature":"pf5xm8oWIQIOEwSGw4icPmynLXb1P1HxKfjeh8UStU1mlIBPKa7yppeIMPpAflC0o2zjFR7Axe7CimAebm3BHg=="}],"memo":""}} -{"tx": {"msg":[{"@type":"/vm.m_call","caller":"g15gdm49ktawvkrl88jadqpucng37yxutucuwaef","send":"200000000ugnot","pkg_path":"gno.land/r/demo/users","func":"Register","args":["","ideamour","\u003c3"]}],"fee":{"gas_wanted":"2000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AhClx4AsDuX3DNCPxhDwWnrfd4MIZmxJE4vt47ClVvT2"},"signature":"IQe64af878k6HjLDqIJeg27GXAVF6xS+96cDe2jMlxNV6+8sOcuUctp0GiWVnYfN4tpthC6d4WhBo+VlpHqkbg=="}],"memo":""}} -{"tx": {"msg":[{"@type":"/vm.m_call","caller":"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj","send":"","pkg_path":"gno.land/r/demo/boards","func":"CreateBoard","args":["testboard"]}],"fee":{"gas_wanted":"2000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A+FhNtsXHjLfSJk1lB8FbiL4mGPjc50Kt81J7EKDnJ2y"},"signature":"vzlSxEFh5jOkaSdv3rsV91v/OJKEF2qSuoCpri1u5tRWq62T7xr3KHRCF5qFnn4aQX/yE8g8f/Y//WPOCUGhJw=="}],"memo":""}} -{"tx": {"msg":[{"@type":"/vm.m_call","caller":"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj","send":"","pkg_path":"gno.land/r/demo/boards","func":"CreateThread","args":["1","Hello World","This is a demo of Gno smart contract programming. This document was\nconstructed by Gno onto a smart contract hosted on the data Realm \nname [\"gno.land/r/demo/boards\"](https://gno.land/r/demo/boards/)\n([github](https://github.com/gnolang/gno/tree/master/examples/gno.land/r/demo/boards)).\n\n## Starting the `gnoland` node node/validator.\n\nNOTE: Where you see `--remote %%REMOTE%%` here, that flag can be replaced\nwith `--remote localhost:26657` for local testnets.\n\n### build gnoland.\n\n```bash\ngit clone git@github.com:gnolang/gno.git\ncd ./gno\nmake \n```\n\n### add test account.\n\n```bash\n./build/gnokey add test1 --recover\n```\n\nUse this mnemonic:\n\u003e source bonus chronic canvas draft south burst lottery vacant surface solve popular case indicate oppose farm nothing bullet exhibit title speed wink action roast\n\n### start gnoland validator node.\n\n```bash\n./build/gnoland\n```\n\n(This can be reset with `make reset`).\n\n### start gnoland web server (optional).\n\n```bash\ngo run ./gnoland/website\n```\n\n## Signing and broadcasting transactions.\n\n### publish the \"gno.land/p/demo/avl\" package.\n\n```bash\n./build/gnokey maketx addpkg test1 --pkgpath \"gno.land/p/demo/avl\" --pkgdir \"examples/gno.land/p/demo/avl\" --deposit 100ugnot --gas-fee 1ugnot --gas-wanted 2000000 \u003e addpkg.avl.unsigned.txt\n./build/gnokey query \"auth/accounts/g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\"\n./build/gnokey sign test1 --txpath addpkg.avl.unsigned.txt --chainid \"%%CHAINID%%\" --number 0 --sequence 0 \u003e addpkg.avl.signed.txt\n./build/gnokey broadcast addpkg.avl.signed.txt --remote %%REMOTE%%\n```\n\n### publish the \"gno.land/r/demo/boards\" realm package.\n\n```bash\n./build/gnokey maketx addpkg test1 --pkgpath \"gno.land/r/demo/boards\" --pkgdir \"examples/gno.land/r/demo/boards\" --deposit 100ugnot --gas-fee 1ugnot --gas-wanted 300000000 \u003e addpkg.boards.unsigned.txt\n./build/gnokey sign test1 --txpath addpkg.boards.unsigned.txt --chainid \"%%CHAINID%%\" --number 0 --sequence 1 \u003e addpkg.boards.signed.txt\n./build/gnokey broadcast addpkg.boards.signed.txt --remote %%REMOTE%%\n```\n\n### create a board with a smart contract call.\n\n```bash\n./build/gnokey maketx call test1 --pkgpath \"gno.land/r/demo/boards\" --func CreateBoard --args \"testboard\" --gas-fee 1ugnot --gas-wanted 2000000 \u003e createboard.unsigned.txt\n./build/gnokey sign test1 --txpath createboard.unsigned.txt --chainid \"%%CHAINID%%\" --number 0 --sequence 2 \u003e createboard.signed.txt\n./build/gnokey broadcast createboard.signed.txt --remote %%REMOTE%%\n```\nNext, query for the permanent board ID by querying (you need this to create a new post):\n\n```bash\n./build/gnokey query \"vm/qeval\" --data \"gno.land/r/demo/boards.GetBoardIDFromName(\\\"testboard\\\")\"\n```\n\n### create a post of a board with a smart contract call.\n\n```bash\n./build/gnokey maketx call test1 --pkgpath \"gno.land/r/demo/boards\" --func CreatePost --args 1 --args \"Hello World\" --args#file \"./examples/gno.land/r/demo/boards/README.md\" --gas-fee 1ugnot --gas-wanted 2000000 \u003e createpost.unsigned.txt\n./build/gnokey sign test1 --txpath createpost.unsigned.txt --chainid \"%%CHAINID%%\" --number 0 --sequence 3 \u003e createpost.signed.txt\n./build/gnokey broadcast createpost.signed.txt --remote %%REMOTE%%\n```\n\n### create a comment to a post.\n\n```bash\n./build/gnokey maketx call test1 --pkgpath \"gno.land/r/demo/boards\" --func CreateReply --args 1 --args 1 --args \"A comment\" --gas-fee 1ugnot --gas-wanted 2000000 \u003e createcomment.unsigned.txt\n./build/gnokey sign test1 --txpath createcomment.unsigned.txt --chainid \"%%CHAINID%%\" --number 0 --sequence 4 \u003e createcomment.signed.txt\n./build/gnokey broadcast createcomment.signed.txt --remote %%REMOTE%%\n```\n\n```bash\n./build/gnokey query \"vm/qrender\" --data \"gno.land/r/demo/boards:testboard/1\"\n```\n\n### render page with optional path expression.\n\nThe contents of `https://gno.land/r/demo/boards:` and `https://gno.land/r/demo/boards:testboard` are rendered by calling\nthe `Render(path string)` function like so:\n\n```bash\n./build/gnokey query \"vm/qrender\" --data \"gno.land/r/demo/boards:testboard\"\n```\n"]}],"fee":{"gas_wanted":"2000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A+FhNtsXHjLfSJk1lB8FbiL4mGPjc50Kt81J7EKDnJ2y"},"signature":"V43B1waFxhzheW9TfmCpjLdrC4dC1yjUGES5y3J6QsNar6hRpNz4G1thzWmWK7xXhg8u1PCIpxLxGczKQYhuPw=="}],"memo":""}} -{"tx": {"msg":[{"@type":"/vm.m_call","caller":"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj","send":"","pkg_path":"gno.land/r/demo/boards","func":"CreateThread","args":["1","NFT example","NFT's are all the rage these days, for various reasons.\n\nI read over EIP-721 which appears to be the de-facto NFT standard on Ethereum. Then, made a sample implementation of EIP-721 (let's here called GRC-721). The implementation isn't complete, but it demonstrates the main functionality.\n\n - [EIP-721](https://eips.ethereum.org/EIPS/eip-721)\n - [gno.land/r/demo/nft/nft.gno](https://gno.land/r/demo/nft/nft.gno)\n - [zrealm_nft3.gno test](https://github.com/gnolang/gno/blob/master/examples/gno.land/r/demo/nft/z_3_filetest.gno)\n\nIn short, this demonstrates how to implement Ethereum contract interfaces in gno.land; by using only standard Go language features.\n\nPlease leave a comment ([guide](https://gno.land/r/demo/boards:testboard/1)).\n"]}],"fee":{"gas_wanted":"2000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A+FhNtsXHjLfSJk1lB8FbiL4mGPjc50Kt81J7EKDnJ2y"},"signature":"ZXfrTiHxPFQL8uSm+Tv7WXIHPMca9okhm94RAlC6YgNbB1VHQYYpoP4w+cnL3YskVzGrOZxensXa9CAZ+cNNeg=="}],"memo":""}} -{"tx": {"msg":[{"@type":"/vm.m_call","caller":"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj","send":"","pkg_path":"gno.land/r/demo/boards","func":"CreateThread","args":["1","Simple echo example with coins","This is a simple test realm contract that demonstrates how to use the banker.\n\nSee [gno.land/r/demo/banktest/banktest.gno](/r/demo/banktest/banktest.gno) to see the original contract code.\n\nThis article will go through each line to explain how it works.\n\n```go\npackage banktest\n```\n\nThis package is locally named \"banktest\" (could be anything).\n\n```go\nimport (\n\t\"std\"\n)\n```\n\nThe \"std\" package is defined by the gno code in stdlibs/std/. \u003c/br\u003e\nSelf explanatory; and you'll see more usage from std later.\n\n```go\ntype activity struct {\n\tcaller std.Address\n\tsent std.Coins\n\treturned std.Coins\n\ttime std.Time\n}\n\nfunc (act *activity) String() string {\n\treturn act.caller.String() + \" \" +\n\t\tact.sent.String() + \" sent, \" +\n\t\tact.returned.String() + \" returned, at \" +\n\t\tstd.FormatTimestamp(act.time, \"2006-01-02 3:04pm MST\")\n}\n\nvar latest [10]*activity\n```\n\nThis is just maintaining a list of recent activity to this contract.\nNotice that the \"latest\" variable is defined \"globally\" within\nthe context of the realm with path \"gno.land/r/demo/banktest\".\n\nThis means that calls to functions defined within this package\nare encapsulated within this \"data realm\", where the data is \nmutated based on transactions that can potentially cross many\nrealm and non-realm packge boundaries (in the call stack).\n\n```go\n// Deposit will take the coins (to the realm's pkgaddr) or return them to user.\nfunc Deposit(returnDenom string, returnAmount int64) string {\n\tstd.AssertOriginCall()\n\tcaller := std.GetOrigCaller()\n\tsend := std.Coins{{returnDenom, returnAmount}}\n```\n\nThis is the beginning of the definition of the contract function named\n\"Deposit\". `std.AssertOriginCall() asserts that this function was called by a\ngno transactional Message. The caller is the user who signed off on this\ntransactional message. Send is the amount of deposit sent along with this\nmessage.\n\n```go\n\t// record activity\n\tact := \u0026activity{\n\t\tcaller: caller,\n\t\tsent: std.GetOrigSend(),\n\t\treturned: send,\n\t\ttime: std.GetTimestamp(),\n\t}\n\tfor i := len(latest) - 2; i \u003e= 0; i-- {\n\t\tlatest[i+1] = latest[i] // shift by +1.\n\t}\n\tlatest[0] = act\n```\n\nUpdating the \"latest\" array for viewing at gno.land/r/demo/banktest: (w/ trailing colon).\n\n```go\n\t// return if any.\n\tif returnAmount \u003e 0 {\n```\n\nIf the user requested the return of coins...\n\n```go\n\t\tbanker := std.GetBanker(std.BankerTypeOrigSend)\n```\n\nuse a std.Banker instance to return any deposited coins to the original sender.\n\n```go\n\t\tpkgaddr := std.GetOrigPkgAddr()\n\t\t// TODO: use std.Coins constructors, this isn't generally safe.\n\t\tbanker.SendCoins(pkgaddr, caller, send)\n\t\treturn \"returned!\"\n```\n\nNotice that each realm package has an associated Cosmos address.\n\n\nFinally, the results are rendered via an ABCI query call when you visit [/r/demo/banktest:](/r/demo/banktest:).\n\n```go\nfunc Render(path string) string {\n\t// get realm coins.\n\tbanker := std.GetBanker(std.BankerTypeReadonly)\n\tcoins := banker.GetCoins(std.GetOrigPkgAddr())\n\n\t// render\n\tres := \"\"\n\tres += \"## recent activity\\n\"\n\tres += \"\\n\"\n\tfor _, act := range latest {\n\t\tif act == nil {\n\t\t\tbreak\n\t\t}\n\t\tres += \" * \" + act.String() + \"\\n\"\n\t}\n\tres += \"\\n\"\n\tres += \"## total deposits\\n\"\n\tres += coins.String()\n\treturn res\n}\n```\n\nYou can call this contract yourself, by vistiing [/r/demo/banktest](/r/demo/banktest) and the [quickstart guide](/r/demo/boards:testboard/4).\n"]}],"fee":{"gas_wanted":"2000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A+FhNtsXHjLfSJk1lB8FbiL4mGPjc50Kt81J7EKDnJ2y"},"signature":"iZX/llZlNTdZMLv1goCTgK2bWqzT8enlTq56wMTCpVxJGA0BTvuEM5Nnt9vrnlG6Taqj2GuTrmEnJBkDFTmt9g=="}],"memo":""}} -{"tx": {"msg":[{"@type":"/vm.m_call","caller":"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj","send":"","pkg_path":"gno.land/r/demo/boards","func":"CreateThread","args":["1","TASK: Describe in your words","Describe in an essay (250+ words), on your favorite medium, why you are interested in gno.land and gnolang.\n\nReply here with a URL link to your written piece as a comment, for rewards.\n"]}],"fee":{"gas_wanted":"2000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AmG6kzznyo1uNqWPAYU6wDpsmzQKDaEOrVRaZ08vOyX0"},"signature":"4HBNtrta8HdeHj4JTN56PBTRK8GOe31NMRRXDiyYtjozuyRdWfOGEsGjGgHWcoBUJq6DepBgD4FetdqfhZ6TNQ=="}],"memo":""}} -{"tx": {"msg":[{"@type":"/vm.m_call","caller":"g1manfred47kzduec920z88wfr64ylksmdcedlf5","send":"","pkg_path":"gno.land/r/demo/boards","func":"CreateThread","args":["1","Getting Started","This is a demo of Gno smart contract programming. This document was\nconstructed by Gno onto a smart contract hosted on the data Realm\nname [\"gno.land/r/demo/boards\"](https://gno.land/r/demo/boards/)\n([github](https://github.com/gnolang/gno/tree/master/examples/gno.land/r/demo/boards)).\n\n\n\n## Build `gnokey`, create your account, and interact with Gno.\n\nNOTE: Where you see `--remote %%REMOTE%%` here, that flag can be replaced\nwith `--remote localhost:26657` for local testnets.\n\n### Build `gnokey`.\n\n```bash\ngit clone git@github.com:gnolang/gno.git\ncd ./gno\nmake\n```\n\n### Generate a seed/mnemonic code.\n\n```bash\n./build/gnokey generate\n```\n\nNOTE: You can generate 24 words with any good bip39 generator.\n\n### Create a new account using your mnemonic.\n\n```bash\n./build/gnokey add KEYNAME --recover\n```\n\nNOTE: `KEYNAME` is your key identifier, and should be changed.\n\n### Verify that you can see your account locally.\n\n```bash\n./build/gnokey list\n```\n\n## Interact with the blockchain:\n\n### Get your current balance, account number, and sequence number.\n\n```bash\n./build/gnokey query auth/accounts/ACCOUNT_ADDR --remote %%REMOTE%%\n```\n\nNOTE: you can retrieve your `ACCOUNT_ADDR` with `./build/gnokey list`.\n\n### Acquire testnet tokens using the official faucet.\n\nGo to https://gno.land/faucet\n\n### Create a board with a smart contract call.\n\nNOTE: `BOARDNAME` will be the slug of the board, and should be changed.\n\n```bash\n./build/gnokey maketx call KEYNAME --pkgpath \"gno.land/r/demo/boards\" --func \"CreateBoard\" --args \"BOARDNAME\" --gas-fee \"1000000ugnot\" --gas-wanted \"2000000\" --broadcast=true --chainid %%CHAINID%% --remote %%REMOTE%%\n```\n\nInteractive documentation: https://gno.land/r/demo/boards$help\u0026func=CreateBoard\n\nNext, query for the permanent board ID by querying (you need this to create a new post):\n\n```bash\n./build/gnokey query \"vm/qeval\" --data \"gno.land/r/demo/boards.GetBoardIDFromName(\\\"BOARDNAME\\\")\" --remote %%REMOTE%%\n```\n\n### Create a post of a board with a smart contract call.\n\nNOTE: If a board was created successfully, your SEQUENCE_NUMBER would have increased.\n\n```bash\n./build/gnokey maketx call KEYNAME --pkgpath \"gno.land/r/demo/boards\" --func \"CreateThread\" --args BOARD_ID --args \"Hello gno.land\" --args\\#file \"./examples/gno.land/r/demo/boards/example_post.md\" --gas-fee 1000000ugnot --gas-wanted 2000000 --broadcast=true --chainid %%CHAINID%% --remote %%REMOTE%%\n```\n\nInteractive documentation: https://gno.land/r/demo/boards$help\u0026func=CreateThread\n\n### Create a comment to a post.\n\n```bash\n./build/gnokey maketx call KEYNAME --pkgpath \"gno.land/r/demo/boards\" --func \"CreateReply\" --args \"BOARD_ID\" --args \"1\" --args \"1\" --args \"Nice to meet you too.\" --gas-fee 1000000ugnot --gas-wanted 2000000 --broadcast=true --chainid %%CHAINID%% --remote %%REMOTE%%\n```\n\nInteractive documentation: https://gno.land/r/demo/boards$help\u0026func=CreateReply\n\n```bash\n./build/gnokey query \"vm/qrender\" --data \"gno.land/r/demo/boards:BOARDNAME/1\" --remote %%REMOTE%%\n```\n\n### Render page with optional path expression.\n\nThe contents of `https://gno.land/r/demo/boards:` and `https://gno.land/r/demo/boards:gnolang` are rendered by calling\nthe `Render(path string)` function like so:\n\n```bash\n./build/gnokey query \"vm/qrender\" --data \"gno.land/r/demo/boards:gnolang\"\n```\n\n## Starting a local `gnoland` node:\n\n### Add test account.\n\n```bash\n./build/gnokey add test1 --recover\n```\n\nUse this mneonic:\n\u003e source bonus chronic canvas draft south burst lottery vacant surface solve popular case indicate oppose farm nothing bullet exhibit title speed wink action roast\n\n### Start `gnoland` node.\n\n```bash\n./build/gnoland\n```\n\nNOTE: This can be reset with `make reset`\n\n### Publish the \"gno.land/p/demo/avl\" package.\n\n```bash\n./build/gnokey maketx addpkg test1 --pkgpath \"gno.land/p/demo/avl\" --pkgdir \"examples/gno.land/p/demo/avl\" --deposit 100000000ugnot --gas-fee 1000000ugnot --gas-wanted 2000000 --broadcast=true --chainid %%CHAINID%% --remote localhost:26657\n```\n\n### Publish the \"gno.land/r/demo/boards\" realm package.\n\n```bash\n./build/gnokey maketx addpkg test1 --pkgpath \"gno.land/r/demo/boards\" --pkgdir \"examples/gno.land/r/demo/boards\" --deposit 100000000ugnot --gas-fee 1000000ugnot --gas-wanted 300000000 --broadcast=true --chainid %%CHAINID%% --remote localhost:26657\n```\n"]}],"fee":{"gas_wanted":"2000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AnK+a6mcFDjY6b/v6p7r8QFW1M1PgIoQxBgrwOoyY7v3"},"signature":"sHjOGXZEi9wt2FSXFHmkDDoVQyepvFHKRDDU0zgedHYnCYPx5/YndyihsDD5Y2Z7/RgNYBh4JlJwDMGFNStzBQ=="}],"memo":""}} +{"tx": {"msg":[{"@type":"/vm.m_call","caller":"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj","pkg_path":"gno.land/r/gnoland/users/v1","func":"Register","args":["administrator123"]}],"fee":{"gas_wanted":"2000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AmG6kzznyo1uNqWPAYU6wDpsmzQKDaEOrVRaZ08vOyX0"},"signature":""}],"memo":""}} +{"tx": {"msg":[{"@type":"/vm.m_call","caller":"g1qpymzwx4l4cy6cerdyajp9ksvjsf20rk5y9rtt","pkg_path":"gno.land/r/gnoland/users/v1","func":"Register","args":["zo_oma123"]}],"fee":{"gas_wanted":"2000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6yg5/iiktruezVw5vZJwLlGwyrvw8RlqOToTRMWXkE2"},"signature":""}],"memo":""}} +{"tx": {"msg":[{"@type":"/vm.m_call","caller":"g1manfred47kzduec920z88wfr64ylksmdcedlf5","pkg_path":"gno.land/r/gnoland/users/v1","func":"Register","args":["moul123"]}],"fee":{"gas_wanted":"2000000","gas_fee":"200000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AnK+a6mcFDjY6b/v6p7r8QFW1M1PgIoQxBgrwOoyY7v3"},"signature":""}],"memo":""}} +{"tx": {"msg":[{"@type":"/vm.m_call","caller":"g1fj9jccm3zjnqspq7lp2g7lj4czyfq0s35600g9","pkg_path":"gno.land/r/gnoland/users/v1","func":"Register","args":["piupiu123"]}],"fee":{"gas_wanted":"2000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"Ar68lqbU2YC63fbMcYUtJhYO3/66APM/EqF7m0nUjGyz"},"signature":""}],"memo":""}} +{"tx": {"msg":[{"@type":"/vm.m_call","caller":"g1ds24jj9kqjcskd0gzu24r9e4n62ggye230zuv5","pkg_path":"gno.land/r/gnoland/users/v1","func":"Register","args":["anarcher123"]}],"fee":{"gas_wanted":"2000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AjpLbKdQeH+yB/1OCB148l5GlRRrXma71hdA8EES3H7f"},"signature":""}],"memo":""}} +{"tx": {"msg":[{"@type":"/vm.m_call","caller":"g15gdm49ktawvkrl88jadqpucng37yxutucuwaef","pkg_path":"gno.land/r/gnoland/users/v1","func":"Register","args":["ideamour123"]}],"fee":{"gas_wanted":"2000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AhClx4AsDuX3DNCPxhDwWnrfd4MIZmxJE4vt47ClVvT2"},"signature":""}],"memo":""}} +{"tx": {"msg":[{"@type":"/vm.m_call","caller":"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj","send":"","pkg_path":"gno.land/r/demo/boards","func":"CreateBoard","args":["testboard"]}],"fee":{"gas_wanted":"2000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A+FhNtsXHjLfSJk1lB8FbiL4mGPjc50Kt81J7EKDnJ2y"},"signature":""}],"memo":""}} +{"tx": {"msg":[{"@type":"/vm.m_call","caller":"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj","send":"","pkg_path":"gno.land/r/demo/boards","func":"CreateThread","args":["1","Hello World","This is a demo of Gno smart contract programming. This document was\nconstructed by Gno onto a smart contract hosted on the data Realm \nname [\"gno.land/r/demo/boards\"](https://gno.land/r/demo/boards/)\n([github](https://github.com/gnolang/gno/tree/master/examples/gno.land/r/demo/boards)).\n\n## Starting the `gnoland` node node/validator.\n\nNOTE: Where you see `--remote %%REMOTE%%` here, that flag can be replaced\nwith `--remote localhost:26657` for local testnets.\n\n### build gnoland.\n\n```bash\ngit clone git@github.com:gnolang/gno.git\ncd ./gno\nmake \n```\n\n### add test account.\n\n```bash\n./build/gnokey add test1 --recover\n```\n\nUse this mnemonic:\n\u003e source bonus chronic canvas draft south burst lottery vacant surface solve popular case indicate oppose farm nothing bullet exhibit title speed wink action roast\n\n### start gnoland validator node.\n\n```bash\n./build/gnoland\n```\n\n(This can be reset with `make reset`).\n\n### start gnoland web server (optional).\n\n```bash\ngo run ./gnoland/website\n```\n\n## Signing and broadcasting transactions.\n\n### publish the \"gno.land/p/demo/avl\" package.\n\n```bash\n./build/gnokey maketx addpkg test1 --pkgpath \"gno.land/p/demo/avl\" --pkgdir \"examples/gno.land/p/demo/avl\" --deposit 100ugnot --gas-fee 1ugnot --gas-wanted 2000000 \u003e addpkg.avl.unsigned.txt\n./build/gnokey query \"auth/accounts/g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\"\n./build/gnokey sign test1 --txpath addpkg.avl.unsigned.txt --chainid \"%%CHAINID%%\" --number 0 --sequence 0 \u003e addpkg.avl.signed.txt\n./build/gnokey broadcast addpkg.avl.signed.txt --remote %%REMOTE%%\n```\n\n### publish the \"gno.land/r/demo/boards\" realm package.\n\n```bash\n./build/gnokey maketx addpkg test1 --pkgpath \"gno.land/r/demo/boards\" --pkgdir \"examples/gno.land/r/demo/boards\" --deposit 100ugnot --gas-fee 1ugnot --gas-wanted 300000000 \u003e addpkg.boards.unsigned.txt\n./build/gnokey sign test1 --txpath addpkg.boards.unsigned.txt --chainid \"%%CHAINID%%\" --number 0 --sequence 1 \u003e addpkg.boards.signed.txt\n./build/gnokey broadcast addpkg.boards.signed.txt --remote %%REMOTE%%\n```\n\n### create a board with a smart contract call.\n\n```bash\n./build/gnokey maketx call test1 --pkgpath \"gno.land/r/demo/boards\" --func CreateBoard --args \"testboard\" --gas-fee 1ugnot --gas-wanted 2000000 \u003e createboard.unsigned.txt\n./build/gnokey sign test1 --txpath createboard.unsigned.txt --chainid \"%%CHAINID%%\" --number 0 --sequence 2 \u003e createboard.signed.txt\n./build/gnokey broadcast createboard.signed.txt --remote %%REMOTE%%\n```\nNext, query for the permanent board ID by querying (you need this to create a new post):\n\n```bash\n./build/gnokey query \"vm/qeval\" --data \"gno.land/r/demo/boards.GetBoardIDFromName(\\\"testboard\\\")\"\n```\n\n### create a post of a board with a smart contract call.\n\n```bash\n./build/gnokey maketx call test1 --pkgpath \"gno.land/r/demo/boards\" --func CreatePost --args 1 --args \"Hello World\" --args#file \"./examples/gno.land/r/demo/boards/README.md\" --gas-fee 1ugnot --gas-wanted 2000000 \u003e createpost.unsigned.txt\n./build/gnokey sign test1 --txpath createpost.unsigned.txt --chainid \"%%CHAINID%%\" --number 0 --sequence 3 \u003e createpost.signed.txt\n./build/gnokey broadcast createpost.signed.txt --remote %%REMOTE%%\n```\n\n### create a comment to a post.\n\n```bash\n./build/gnokey maketx call test1 --pkgpath \"gno.land/r/demo/boards\" --func CreateReply --args 1 --args 1 --args \"A comment\" --gas-fee 1ugnot --gas-wanted 2000000 \u003e createcomment.unsigned.txt\n./build/gnokey sign test1 --txpath createcomment.unsigned.txt --chainid \"%%CHAINID%%\" --number 0 --sequence 4 \u003e createcomment.signed.txt\n./build/gnokey broadcast createcomment.signed.txt --remote %%REMOTE%%\n```\n\n```bash\n./build/gnokey query \"vm/qrender\" --data \"gno.land/r/demo/boards:testboard/1\"\n```\n\n### render page with optional path expression.\n\nThe contents of `https://gno.land/r/demo/boards:` and `https://gno.land/r/demo/boards:testboard` are rendered by calling\nthe `Render(path string)` function like so:\n\n```bash\n./build/gnokey query \"vm/qrender\" --data \"gno.land/r/demo/boards:testboard\"\n```\n"]}],"fee":{"gas_wanted":"2000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A+FhNtsXHjLfSJk1lB8FbiL4mGPjc50Kt81J7EKDnJ2y"},"signature":""}],"memo":""}} +{"tx": {"msg":[{"@type":"/vm.m_call","caller":"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj","send":"","pkg_path":"gno.land/r/demo/boards","func":"CreateThread","args":["1","NFT example","NFT's are all the rage these days, for various reasons.\n\nI read over EIP-721 which appears to be the de-facto NFT standard on Ethereum. Then, made a sample implementation of EIP-721 (let's here called GRC-721). The implementation isn't complete, but it demonstrates the main functionality.\n\n - [EIP-721](https://eips.ethereum.org/EIPS/eip-721)\n - [gno.land/r/demo/nft/nft.gno](https://gno.land/r/demo/nft/nft.gno)\n - [zrealm_nft3.gno test](https://github.com/gnolang/gno/blob/master/examples/gno.land/r/demo/nft/z_3_filetest.gno)\n\nIn short, this demonstrates how to implement Ethereum contract interfaces in gno.land; by using only standard Go language features.\n\nPlease leave a comment ([guide](https://gno.land/r/demo/boards:testboard/1)).\n"]}],"fee":{"gas_wanted":"2000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A+FhNtsXHjLfSJk1lB8FbiL4mGPjc50Kt81J7EKDnJ2y"},"signature":""}],"memo":""}} +{"tx": {"msg":[{"@type":"/vm.m_call","caller":"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj","send":"","pkg_path":"gno.land/r/demo/boards","func":"CreateThread","args":["1","Simple echo example with coins","This is a simple test realm contract that demonstrates how to use the banker.\n\nSee [gno.land/r/demo/banktest/banktest.gno](/r/demo/banktest/banktest.gno) to see the original contract code.\n\nThis article will go through each line to explain how it works.\n\n```go\npackage banktest\n```\n\nThis package is locally named \"banktest\" (could be anything).\n\n```go\nimport (\n\t\"std\"\n)\n```\n\nThe \"std\" package is defined by the gno code in stdlibs/std/. \u003c/br\u003e\nSelf explanatory; and you'll see more usage from std later.\n\n```go\ntype activity struct {\n\tcaller std.Address\n\tsent std.Coins\n\treturned std.Coins\n\ttime std.Time\n}\n\nfunc (act *activity) String() string {\n\treturn act.caller.String() + \" \" +\n\t\tact.sent.String() + \" sent, \" +\n\t\tact.returned.String() + \" returned, at \" +\n\t\tstd.FormatTimestamp(act.time, \"2006-01-02 3:04pm MST\")\n}\n\nvar latest [10]*activity\n```\n\nThis is just maintaining a list of recent activity to this contract.\nNotice that the \"latest\" variable is defined \"globally\" within\nthe context of the realm with path \"gno.land/r/demo/banktest\".\n\nThis means that calls to functions defined within this package\nare encapsulated within this \"data realm\", where the data is \nmutated based on transactions that can potentially cross many\nrealm and non-realm packge boundaries (in the call stack).\n\n```go\n// Deposit will take the coins (to the realm's pkgaddr) or return them to user.\nfunc Deposit(returnDenom string, returnAmount int64) string {\n\tstd.AssertOriginCall()\n\tcaller := std.GetOrigCaller()\n\tsend := std.Coins{{returnDenom, returnAmount}}\n```\n\nThis is the beginning of the definition of the contract function named\n\"Deposit\". `std.AssertOriginCall() asserts that this function was called by a\ngno transactional Message. The caller is the user who signed off on this\ntransactional message. Send is the amount of deposit sent along with this\nmessage.\n\n```go\n\t// record activity\n\tact := \u0026activity{\n\t\tcaller: caller,\n\t\tsent: std.GetOrigSend(),\n\t\treturned: send,\n\t\ttime: std.GetTimestamp(),\n\t}\n\tfor i := len(latest) - 2; i \u003e= 0; i-- {\n\t\tlatest[i+1] = latest[i] // shift by +1.\n\t}\n\tlatest[0] = act\n```\n\nUpdating the \"latest\" array for viewing at gno.land/r/demo/banktest: (w/ trailing colon).\n\n```go\n\t// return if any.\n\tif returnAmount \u003e 0 {\n```\n\nIf the user requested the return of coins...\n\n```go\n\t\tbanker := std.GetBanker(std.BankerTypeOrigSend)\n```\n\nuse a std.Banker instance to return any deposited coins to the original sender.\n\n```go\n\t\tpkgaddr := std.GetOrigPkgAddr()\n\t\t// TODO: use std.Coins constructors, this isn't generally safe.\n\t\tbanker.SendCoins(pkgaddr, caller, send)\n\t\treturn \"returned!\"\n```\n\nNotice that each realm package has an associated Cosmos address.\n\n\nFinally, the results are rendered via an ABCI query call when you visit [/r/demo/banktest:](/r/demo/banktest:).\n\n```go\nfunc Render(path string) string {\n\t// get realm coins.\n\tbanker := std.GetBanker(std.BankerTypeReadonly)\n\tcoins := banker.GetCoins(std.GetOrigPkgAddr())\n\n\t// render\n\tres := \"\"\n\tres += \"## recent activity\\n\"\n\tres += \"\\n\"\n\tfor _, act := range latest {\n\t\tif act == nil {\n\t\t\tbreak\n\t\t}\n\t\tres += \" * \" + act.String() + \"\\n\"\n\t}\n\tres += \"\\n\"\n\tres += \"## total deposits\\n\"\n\tres += coins.String()\n\treturn res\n}\n```\n\nYou can call this contract yourself, by vistiing [/r/demo/banktest](/r/demo/banktest) and the [quickstart guide](/r/demo/boards:testboard/4).\n"]}],"fee":{"gas_wanted":"2000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A+FhNtsXHjLfSJk1lB8FbiL4mGPjc50Kt81J7EKDnJ2y"},"signature":""}],"memo":""}} +{"tx": {"msg":[{"@type":"/vm.m_call","caller":"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj","send":"","pkg_path":"gno.land/r/demo/boards","func":"CreateThread","args":["1","TASK: Describe in your words","Describe in an essay (250+ words), on your favorite medium, why you are interested in gno.land and gnolang.\n\nReply here with a URL link to your written piece as a comment, for rewards.\n"]}],"fee":{"gas_wanted":"2000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AmG6kzznyo1uNqWPAYU6wDpsmzQKDaEOrVRaZ08vOyX0"},"signature":""}],"memo":""}} +{"tx": {"msg":[{"@type":"/vm.m_call","caller":"g1manfred47kzduec920z88wfr64ylksmdcedlf5","send":"","pkg_path":"gno.land/r/demo/boards","func":"CreateThread","args":["1","Getting Started","This is a demo of Gno smart contract programming. This document was\nconstructed by Gno onto a smart contract hosted on the data Realm\nname [\"gno.land/r/demo/boards\"](https://gno.land/r/demo/boards/)\n([github](https://github.com/gnolang/gno/tree/master/examples/gno.land/r/demo/boards)).\n\n\n\n## Build `gnokey`, create your account, and interact with Gno.\n\nNOTE: Where you see `--remote %%REMOTE%%` here, that flag can be replaced\nwith `--remote localhost:26657` for local testnets.\n\n### Build `gnokey`.\n\n```bash\ngit clone git@github.com:gnolang/gno.git\ncd ./gno\nmake\n```\n\n### Generate a seed/mnemonic code.\n\n```bash\n./build/gnokey generate\n```\n\nNOTE: You can generate 24 words with any good bip39 generator.\n\n### Create a new account using your mnemonic.\n\n```bash\n./build/gnokey add KEYNAME --recover\n```\n\nNOTE: `KEYNAME` is your key identifier, and should be changed.\n\n### Verify that you can see your account locally.\n\n```bash\n./build/gnokey list\n```\n\n## Interact with the blockchain:\n\n### Get your current balance, account number, and sequence number.\n\n```bash\n./build/gnokey query auth/accounts/ACCOUNT_ADDR --remote %%REMOTE%%\n```\n\nNOTE: you can retrieve your `ACCOUNT_ADDR` with `./build/gnokey list`.\n\n### Acquire testnet tokens using the official faucet.\n\nGo to https://gno.land/faucet\n\n### Create a board with a smart contract call.\n\nNOTE: `BOARDNAME` will be the slug of the board, and should be changed.\n\n```bash\n./build/gnokey maketx call KEYNAME --pkgpath \"gno.land/r/demo/boards\" --func \"CreateBoard\" --args \"BOARDNAME\" --gas-fee \"1000000ugnot\" --gas-wanted \"2000000\" --broadcast=true --chainid %%CHAINID%% --remote %%REMOTE%%\n```\n\nInteractive documentation: https://gno.land/r/demo/boards$help\u0026func=CreateBoard\n\nNext, query for the permanent board ID by querying (you need this to create a new post):\n\n```bash\n./build/gnokey query \"vm/qeval\" --data \"gno.land/r/demo/boards.GetBoardIDFromName(\\\"BOARDNAME\\\")\" --remote %%REMOTE%%\n```\n\n### Create a post of a board with a smart contract call.\n\nNOTE: If a board was created successfully, your SEQUENCE_NUMBER would have increased.\n\n```bash\n./build/gnokey maketx call KEYNAME --pkgpath \"gno.land/r/demo/boards\" --func \"CreateThread\" --args BOARD_ID --args \"Hello gno.land\" --args\\#file \"./examples/gno.land/r/demo/boards/example_post.md\" --gas-fee 1000000ugnot --gas-wanted 2000000 --broadcast=true --chainid %%CHAINID%% --remote %%REMOTE%%\n```\n\nInteractive documentation: https://gno.land/r/demo/boards$help\u0026func=CreateThread\n\n### Create a comment to a post.\n\n```bash\n./build/gnokey maketx call KEYNAME --pkgpath \"gno.land/r/demo/boards\" --func \"CreateReply\" --args \"BOARD_ID\" --args \"1\" --args \"1\" --args \"Nice to meet you too.\" --gas-fee 1000000ugnot --gas-wanted 2000000 --broadcast=true --chainid %%CHAINID%% --remote %%REMOTE%%\n```\n\nInteractive documentation: https://gno.land/r/demo/boards$help\u0026func=CreateReply\n\n```bash\n./build/gnokey query \"vm/qrender\" --data \"gno.land/r/demo/boards:BOARDNAME/1\" --remote %%REMOTE%%\n```\n\n### Render page with optional path expression.\n\nThe contents of `https://gno.land/r/demo/boards:` and `https://gno.land/r/demo/boards:gnolang` are rendered by calling\nthe `Render(path string)` function like so:\n\n```bash\n./build/gnokey query \"vm/qrender\" --data \"gno.land/r/demo/boards:gnolang\"\n```\n\n## Starting a local `gnoland` node:\n\n### Add test account.\n\n```bash\n./build/gnokey add test1 --recover\n```\n\nUse this mneonic:\n\u003e source bonus chronic canvas draft south burst lottery vacant surface solve popular case indicate oppose farm nothing bullet exhibit title speed wink action roast\n\n### Start `gnoland` node.\n\n```bash\n./build/gnoland\n```\n\nNOTE: This can be reset with `make reset`\n\n### Publish the \"gno.land/p/demo/avl\" package.\n\n```bash\n./build/gnokey maketx addpkg test1 --pkgpath \"gno.land/p/demo/avl\" --pkgdir \"examples/gno.land/p/demo/avl\" --deposit 100000000ugnot --gas-fee 1000000ugnot --gas-wanted 2000000 --broadcast=true --chainid %%CHAINID%% --remote localhost:26657\n```\n\n### Publish the \"gno.land/r/demo/boards\" realm package.\n\n```bash\n./build/gnokey maketx addpkg test1 --pkgpath \"gno.land/r/demo/boards\" --pkgdir \"examples/gno.land/r/demo/boards\" --deposit 100000000ugnot --gas-fee 1000000ugnot --gas-wanted 300000000 --broadcast=true --chainid %%CHAINID%% --remote localhost:26657\n```\n"]}],"fee":{"gas_wanted":"2000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AnK+a6mcFDjY6b/v6p7r8QFW1M1PgIoQxBgrwOoyY7v3"},"signature":""}],"memo":""}} {"tx": {"msg":[{"@type":"/vm.m_call","caller":"g1manfred47kzduec920z88wfr64ylksmdcedlf5","send":"","pkg_path":"gno.land/r/gnoland/blog","func":"ModAddPost","args":["post1","First post","Lorem Ipsum","2022-05-20T13:17:22Z","","tag1,tag2"]}],"fee":{"gas_wanted":"2000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AnK+a6mcFDjY6b/v6p7r8QFW1M1PgIoQxBgrwOoyY7v3"},"signature":"sHjOGXZEi9wt2FSXFHmkDDoVQyepvFHKRDDU0zgedHYnCYPx5/YndyihsDD5Y2Z7/RgNYBh4JlJwDMGFNStzBQ=="}],"memo":""}} {"tx": {"msg":[{"@type":"/vm.m_call","caller":"g1manfred47kzduec920z88wfr64ylksmdcedlf5","send":"","pkg_path":"gno.land/r/gnoland/blog","func":"ModAddPost","args":["post2","Second post","Lorem Ipsum","2022-05-20T13:17:23Z","","tag1,tag3"]}],"fee":{"gas_wanted":"2000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AnK+a6mcFDjY6b/v6p7r8QFW1M1PgIoQxBgrwOoyY7v3"},"signature":"sHjOGXZEi9wt2FSXFHmkDDoVQyepvFHKRDDU0zgedHYnCYPx5/YndyihsDD5Y2Z7/RgNYBh4JlJwDMGFNStzBQ=="}],"memo":""}} \ No newline at end of file diff --git a/gno.land/pkg/gnoweb/app_test.go b/gno.land/pkg/gnoweb/app_test.go index 6fb69c6d984..f749de68a39 100644 --- a/gno.land/pkg/gnoweb/app_test.go +++ b/gno.land/pkg/gnoweb/app_test.go @@ -33,9 +33,9 @@ func TestRoutes(t *testing.T) { {"/r/gnoland/blog$help&func=Render", ok, "Render(path)"}, {"/r/gnoland/blog$help&func=Render&path=foo/bar", ok, `value="foo/bar"`}, // {"/r/gnoland/blog$help&func=NonExisting", ok, "NonExisting not found"}, // XXX(TODO) - {"/r/demo/users:administrator", ok, "address"}, - {"/r/demo/users", ok, "moul"}, - {"/r/demo/users/users.gno", ok, "// State"}, + {"/r/gnoland/users/v1", ok, "address"}, + {"/r/gnoland/users/v1", ok, "registry"}, + {"/r/gnoland/users/v1/users.gno", ok, "reValidUsername"}, {"/r/demo/deep/very/deep", ok, "it works!"}, {"/r/demo/deep/very/deep?arg1=val1&arg2=val2", ok, "hi ?arg1=val1&arg2=val2"}, {"/r/demo/deep/very/deep:bob", ok, "hi bob"}, @@ -64,6 +64,7 @@ func TestRoutes(t *testing.T) { } rootdir := gnoenv.RootDir() + println(rootdir) genesis := integration.LoadDefaultGenesisTXsFile(t, "tendermint_test", rootdir) config, _ := integration.TestingNodeConfig(t, rootdir, genesis...) node, remoteAddr := integration.TestingInMemoryNode(t, log.NewTestingLogger(t), config) @@ -106,7 +107,7 @@ func TestAnalytics(t *testing.T) { // Realm, source, help page "/r/gnoland/blog", "/r/gnoland/blog/admin.gno", - "/r/demo/users:administrator", + "/r/gnoland/users/v1", "/r/gnoland/blog$help", // Special pages diff --git a/gno.land/pkg/gnoweb/url.go b/gno.land/pkg/gnoweb/url.go index 9127225d490..25b319150f5 100644 --- a/gno.land/pkg/gnoweb/url.go +++ b/gno.land/pkg/gnoweb/url.go @@ -21,7 +21,7 @@ type GnoURL struct { // gno.land/r/demo/users/render.gno:jae$help&a=b?c=d Domain string // gno.land - Path string // /r/demo/users + Path string // /r/gnoland/users/v1 Args string // jae WebQuery url.Values // help&a=b Query url.Values // c=d diff --git a/gno.land/pkg/integration/testdata/addpkg_namespace.txtar b/gno.land/pkg/integration/testdata/addpkg_namespace.txtar index 2cfd00acda4..11e4d8785e3 100644 --- a/gno.land/pkg/integration/testdata/addpkg_namespace.txtar +++ b/gno.land/pkg/integration/testdata/addpkg_namespace.txtar @@ -1,5 +1,5 @@ -loadpkg gno.land/r/demo/users -loadpkg gno.land/r/sys/users +loadpkg gno.land/r/sys/names +loadpkg gno.land/r/gnoland/users/v1 adduser admin adduser gui @@ -8,36 +8,14 @@ patchpkg "g1manfred47kzduec920z88wfr64ylksmdcedlf5" $admin_user_addr # use our c gnoland start -## When `sys/users` is disabled - -# Should be disabled by default, addpkg should work by default - -# Check if sys/users is disabled -# gui call -> sys/users.IsEnable -gnokey maketx call -pkgpath gno.land/r/sys/users -func IsEnabled -gas-fee 100000ugnot -gas-wanted 200000 -broadcast -chainid tendermint_test gui -stdout 'OK!' -stdout 'false' - -# Gui should be able to addpkg on test1 addr -# gui addpkg -> gno.land/r//mysuperpkg -gnokey maketx addpkg -pkgdir $WORK -pkgpath gno.land/r/$test1_user_addr/mysuperpkg -gas-fee 1000000ugnot -gas-wanted 400000 -broadcast -chainid=tendermint_test gui -stdout 'OK!' - -# Gui should be able to addpkg on random name -# gui addpkg -> gno.land/r/randomname/mysuperpkg -gnokey maketx addpkg -pkgdir $WORK -pkgpath gno.land/r/randomname/mysuperpkg -gas-fee 1000000ugnot -gas-wanted 350000 -broadcast -chainid=tendermint_test gui +# Enable `sys/names` +# admin call -> sys/names.Enable +gnokey maketx call -pkgpath gno.land/r/sys/names -func Enable -gas-fee 100000ugnot -gas-wanted 1000000 -broadcast -chainid tendermint_test admin stdout 'OK!' -## When `sys/users` is enabled - -# Enable `sys/users` -# admin call -> sys/users.AdminEnable -gnokey maketx call -pkgpath gno.land/r/sys/users -func AdminEnable -gas-fee 100000ugnot -gas-wanted 1000000 -broadcast -chainid tendermint_test admin -stdout 'OK!' - -# Check that `sys/users` has been enabled -# gui call -> sys/users.IsEnable -gnokey maketx call -pkgpath gno.land/r/sys/users -func IsEnabled -gas-fee 100000ugnot -gas-wanted 200000 -broadcast -chainid tendermint_test gui +# Check that `sys/names` has been enabled +# gui call -> sys/names.IsEnabled +gnokey maketx call -pkgpath gno.land/r/sys/names -func IsEnabled -gas-fee 100000ugnot -gas-wanted 200000 -broadcast -chainid tendermint_test gui stdout 'OK!' stdout 'true' @@ -48,7 +26,7 @@ stderr 'unauthorized user' # Try to add a pkg with an unregistered user, on their own address as namespace # gui addpkg -> gno.land/r//one -gnokey maketx addpkg -pkgdir $WORK -pkgpath gno.land/r/$gui_user_addr/one -gas-fee 1000000ugnot -gas-wanted 1000000 -broadcast -chainid=tendermint_test gui +gnokey maketx addpkg -pkgdir $WORK -pkgpath gno.land/r/$gui_user_addr/one -gas-fee 1000000ugnot -gas-wanted 10000000 -broadcast -chainid=tendermint_test gui stdout 'OK!' ## Test unregistered namespace @@ -61,30 +39,53 @@ stderr 'unauthorized user' ## Test registered namespace -# Test admin invites gui -# admin call -> demo/users.Invite -gnokey maketx call -pkgpath gno.land/r/demo/users -func Invite -gas-fee 1000000ugnot -gas-wanted 2500000 -broadcast -chainid=tendermint_test -args $gui_user_addr admin -stdout 'OK!' - # test gui register namespace -# gui call -> demo/users.Register -gnokey maketx call -pkgpath gno.land/r/demo/users -func Register -gas-fee 1000000ugnot -gas-wanted 2500000 -broadcast -chainid=tendermint_test -args $admin_user_addr -args 'guiland' -args 'im gui' gui +# gui call -> gnoland/users/v1.Register +gnokey maketx call -pkgpath gno.land/r/gnoland/users/v1 -func Register -gas-fee 1000000ugnot -gas-wanted 9500000 -broadcast -chainid=tendermint_test -args 'guigui123' gui stdout 'OK!' -# Test gui publishing on guiland/one -# gui addpkg -> gno.land/r/guiland/one -gnokey maketx addpkg -pkgdir $WORK -pkgpath gno.land/r/guiland/one -gas-fee 1000000ugnot -gas-wanted 1800000 -broadcast -chainid=tendermint_test gui +# Test gui publishing on guigui123/one +# gui addpkg -> gno.land/r/guigui123/one +gnokey maketx addpkg -pkgdir $WORK -pkgpath gno.land/r/guigui123/one -gas-fee 1000000ugnot -gas-wanted 2700000 -broadcast -chainid=tendermint_test gui stdout 'OK!' -# Test admin publishing on guiland/two -# admin addpkg -> gno.land/r/guiland/two +# Test admin publishing on guigui123/two +# admin addpkg -> gno.land/r/guigui123/two # This is expected to fail at the transaction simulation stage, which is why we set gas-wanted to 1. -! gnokey maketx addpkg -pkgdir $WORK -pkgpath gno.land/r/guiland/two -gas-fee 1000000ugnot -gas-wanted 1 -broadcast -chainid=tendermint_test admin +! gnokey maketx addpkg -pkgdir $WORK -pkgpath gno.land/r/guigui123/two -gas-fee 1000000ugnot -gas-wanted 1 -broadcast -chainid=tendermint_test admin +stderr 'unauthorized user' + +## Test gui alias + +# test gui change name +# gui call -> gnoland/users/v1.UpdateName +gnokey maketx call -pkgpath gno.land/r/gnoland/users/v1 -func UpdateName -gas-fee 1000000ugnot -gas-wanted 90000000 -broadcast -chainid=tendermint_test -args 'newguigui123' gui +stdout 'OK!' + +## Old name should still work +gnokey maketx addpkg -pkgdir $WORK -pkgpath gno.land/r/guigui123/new/one -gas-fee 1000000ugnot -gas-wanted 27000000 -broadcast -chainid=tendermint_test gui +stdout 'OK!' + +## New name should work +gnokey maketx addpkg -pkgdir $WORK -pkgpath gno.land/r/newguigui123/newnew/one -gas-fee 1000000ugnot -gas-wanted 27000000 -broadcast -chainid=tendermint_test gui +stdout 'OK!' + +## Test deleted gui +## Delete guigui123 +gnokey maketx call -pkgpath gno.land/r/gnoland/users/v1 -func DeleteUser -gas-fee 1000000ugnot -gas-wanted 90000000 -broadcast -chainid=tendermint_test gui +stdout 'OK!' + +## Latest name should fail +! gnokey maketx addpkg -pkgdir $WORK -pkgpath gno.land/r/newguigui123/deleted/one -gas-fee 1000000ugnot -gas-wanted 27000000 -broadcast -chainid=tendermint_test gui +stderr 'unauthorized user' + +## Old name should fail +! gnokey maketx addpkg -pkgdir $WORK -pkgpath gno.land/r/newguigui123/deleted/one -gas-fee 1000000ugnot -gas-wanted 27000000 -broadcast -chainid=tendermint_test gui stderr 'unauthorized user' -- one.gno -- package one func Render(path string) string { - return "# Hello One" -} + return "# Hello One" +} \ No newline at end of file diff --git a/gno.land/pkg/integration/testdata/grc721_emit.txtar b/gno.land/pkg/integration/testdata/grc721_emit.txtar index 45101b74634..cbd7da4039c 100644 --- a/gno.land/pkg/integration/testdata/grc721_emit.txtar +++ b/gno.land/pkg/integration/testdata/grc721_emit.txtar @@ -1,6 +1,5 @@ # Test for https://github.com/gnolang/gno/pull/3102 loadpkg gno.land/p/demo/grc/grc721 -loadpkg gno.land/r/demo/users loadpkg gno.land/r/foo721 $WORK/foo721 gnoland start @@ -33,9 +32,6 @@ import ( "std" "gno.land/p/demo/grc/grc721" - "gno.land/r/demo/users" - - pusers "gno.land/p/demo/users" ) var ( @@ -45,22 +41,22 @@ var ( // Setters -func Approve(user pusers.AddressOrName, tid grc721.TokenID) { - err := foo.Approve(users.Resolve(user), tid) +func Approve(user std.Address, tid grc721.TokenID) { + err := foo.Approve(user, tid) if err != nil { panic(err) } } -func SetApprovalForAll(user pusers.AddressOrName, approved bool) { - err := foo.SetApprovalForAll(users.Resolve(user), approved) +func SetApprovalForAll(user std.Address, approved bool) { + err := foo.SetApprovalForAll(user, approved) if err != nil { panic(err) } } -func TransferFrom(from, to pusers.AddressOrName, tid grc721.TokenID) { - err := foo.TransferFrom(users.Resolve(from), users.Resolve(to), tid) +func TransferFrom(from, to std.Address, tid grc721.TokenID) { + err := foo.TransferFrom(from, to, tid) if err != nil { panic(err) } @@ -68,10 +64,10 @@ func TransferFrom(from, to pusers.AddressOrName, tid grc721.TokenID) { // Admin -func Mint(to pusers.AddressOrName, tid grc721.TokenID) { +func Mint(to std.Address, tid grc721.TokenID) { caller := std.PrevRealm().Addr() assertIsAdmin(caller) - err := foo.Mint(users.Resolve(to), tid) + err := foo.Mint(to, tid) if err != nil { panic(err) } diff --git a/gno.land/pkg/integration/testdata/issue_1786.txtar b/gno.land/pkg/integration/testdata/issue_1786.txtar index 71cd19e7ed7..fc7df374eda 100644 --- a/gno.land/pkg/integration/testdata/issue_1786.txtar +++ b/gno.land/pkg/integration/testdata/issue_1786.txtar @@ -81,3 +81,4 @@ func ProxyUnwrap(wugnotAmount uint64) { banker := std.GetBanker(std.BankerTypeRealmSend) banker.SendCoins(std.CurrentRealm().Addr(), std.GetOrigCaller(), std.Coins{{"ugnot", int64(wugnotAmount)}}) } + diff --git a/gno.land/pkg/integration/testdata/issue_2283.txtar b/gno.land/pkg/integration/testdata/issue_2283.txtar index 653a4dd79b0..375e555e15c 100644 --- a/gno.land/pkg/integration/testdata/issue_2283.txtar +++ b/gno.land/pkg/integration/testdata/issue_2283.txtar @@ -3,8 +3,14 @@ # These are not necessary, but they "alleviate" add_feeds.tx from the # responsibility of loading standard libraries, thus not making it exceed # the --gas-wanted. -loadpkg gno.land/r/demo/users -loadpkg gno.land/r/demo/boards +loadpkg gno.land/p/demo/avl +loadpkg gno.land/p/demo/avl/pager +loadpkg gno.land/p/demo/avlhelpers +loadpkg gno.land/p/moul/txlink + +loadpkg gno.land/p/demo/users $WORK/pusers +loadpkg gno.land/r/demo/users $WORK/users +loadpkg gno.land/r/demo/boards $WORK/boards loadpkg gno.land/r/demo/imports $WORK/imports gnoland start @@ -19,7 +25,7 @@ stdout OK! package imports import ( - _ "encoding/binary" + _ "encoding/binary" ) -- add_feeds.tx -- @@ -101,11 +107,1240 @@ import ( package bye import ( - "encoding/base64" + "encoding/base64" ) func init() { - val, _ := base64.StdEncoding.DecodeString("heyhey") - println(val) - base64.StdEncoding.EncodeToString([]byte(val)) + val, _ := base64.StdEncoding.DecodeString("heyhey") + println(val) + base64.StdEncoding.EncodeToString([]byte(val)) +} + +-- pusers/gno.mod -- +module gno.land/p/demo/users + +-- pusers/users.gno -- +package users + +import ( + "std" + "strconv" +) + +//---------------------------------------- +// Types + +type User struct { + Address std.Address + Name string + Profile string + Number int + Invites int + Inviter std.Address +} + +func (u *User) Render() string { + str := "## user " + u.Name + "\n" + + "\n" + + " * address = " + string(u.Address) + "\n" + + " * " + strconv.Itoa(u.Invites) + " invites\n" + if u.Inviter != "" { + str = str + " * invited by " + string(u.Inviter) + "\n" + } + str = str + "\n" + + u.Profile + "\n" + return str +} + +type AddressOrName string + +func (aon AddressOrName) IsName() bool { + return aon != "" && aon[0] == '@' +} + +func (aon AddressOrName) GetName() (string, bool) { + if len(aon) >= 2 && aon[0] == '@' { + return string(aon[1:]), true + } + return "", false +} + +-- users/gno.mod -- +module gno.land/r/demo/users + +-- users/users.gno -- +package users + +import ( + "regexp" + "std" + "strconv" + "strings" + + "gno.land/p/demo/avl" + "gno.land/p/demo/avl/pager" + "gno.land/p/demo/avlhelpers" + "gno.land/p/demo/users" +) + +//---------------------------------------- +// State + +var ( + admin std.Address = "g1manfred47kzduec920z88wfr64ylksmdcedlf5" // @moul + + restricted avl.Tree // Name -> true - restricted name + name2User avl.Tree // Name -> *users.User + addr2User avl.Tree // std.Address -> *users.User + invites avl.Tree // string(inviter+":"+invited) -> true + counter int // user id counter + minFee int64 = 20 * 1_000_000 // minimum gnot must be paid to register. + maxFeeMult int64 = 10 // maximum multiples of minFee accepted. +) + +//---------------------------------------- +// Top-level functions + +func Register(inviter std.Address, name string, profile string) { + // assert CallTx call. + std.AssertOriginCall() + // assert invited or paid. + caller := std.GetCallerAt(2) + if caller != std.GetOrigCaller() { + panic("should not happen") // because std.AssertOrigCall(). + } + + sentCoins := std.GetOrigSend() + minCoin := std.NewCoin("ugnot", minFee) + + if inviter == "" { + // banker := std.GetBanker(std.BankerTypeOrigSend) + if len(sentCoins) == 1 && sentCoins[0].IsGTE(minCoin) { + if sentCoins[0].Amount > minFee*maxFeeMult { + panic("payment must not be greater than " + strconv.Itoa(int(minFee*maxFeeMult))) + } else { + // ok + } + } else { + panic("payment must not be less than " + strconv.Itoa(int(minFee))) + } + } else { + invitekey := inviter.String() + ":" + caller.String() + _, ok := invites.Get(invitekey) + if !ok { + panic("invalid invitation") + } + invites.Remove(invitekey) + } + + // assert not already registered. + _, ok := name2User.Get(name) + if ok { + panic("name already registered: " + name) + } + _, ok = addr2User.Get(caller.String()) + if ok { + panic("address already registered: " + caller.String()) + } + + isInviterAdmin := inviter == admin + + // check for restricted name + if _, isRestricted := restricted.Get(name); isRestricted { + // only address invite by the admin can register restricted name + if !isInviterAdmin { + panic("restricted name: " + name) + } + + restricted.Remove(name) + } + + // assert name is valid. + // admin inviter can bypass name restriction + if !isInviterAdmin && !reName.MatchString(name) { + panic("invalid name: " + name + " (must be at least 6 characters, lowercase alphanumeric with underscore)") + } + + // remainder of fees go toward invites. + invites := int(0) + if len(sentCoins) == 1 { + if sentCoins[0].Denom == "ugnot" && sentCoins[0].Amount >= minFee { + invites = int(sentCoins[0].Amount / minFee) + if inviter == "" && invites > 0 { + invites -= 1 + } + } + } + // register. + counter++ + user := &users.User{ + Address: caller, + Name: name, + Profile: profile, + Number: counter, + Invites: invites, + Inviter: inviter, + } + name2User.Set(name, user) + addr2User.Set(caller.String(), user) +} + +func Invite(invitee string) { + // assert CallTx call. + std.AssertOriginCall() + // get caller/inviter. + caller := std.GetCallerAt(2) + if caller != std.GetOrigCaller() { + panic("should not happen") // because std.AssertOrigCall(). + } + lines := strings.Split(invitee, "\n") + if caller == admin { + // nothing to do, all good + } else { + // ensure has invites. + userI, ok := addr2User.Get(caller.String()) + if !ok { + panic("user unknown") + } + user := userI.(*users.User) + if user.Invites <= 0 { + panic("user has no invite tokens") + } + user.Invites -= len(lines) + if user.Invites < 0 { + panic("user has insufficient invite tokens") + } + } + // for each line... + for _, line := range lines { + if line == "" { + continue // file bodies have a trailing newline. + } else if strings.HasPrefix(line, `//`) { + continue // comment + } + // record invite. + invitekey := string(caller) + ":" + string(line) + invites.Set(invitekey, true) + } +} + +func GrantInvites(invites string) { + // assert CallTx call. + std.AssertOriginCall() + // assert admin. + caller := std.GetCallerAt(2) + if caller != std.GetOrigCaller() { + panic("should not happen") // because std.AssertOrigCall(). + } + if caller != admin { + panic("unauthorized") + } + // for each line... + lines := strings.Split(invites, "\n") + for _, line := range lines { + if line == "" { + continue // file bodies have a trailing newline. + } else if strings.HasPrefix(line, `//`) { + continue // comment + } + // parse name and invites. + var name string + var invites int + parts := strings.Split(line, ":") + if len(parts) == 1 { // short for :1. + name = parts[0] + invites = 1 + } else if len(parts) == 2 { + name = parts[0] + invites_, err := strconv.Atoi(parts[1]) + if err != nil { + panic(err) + } + invites = int(invites_) + } else { + panic("should not happen") + } + // give invites. + userI, ok := name2User.Get(name) + if !ok { + // maybe address. + userI, ok = addr2User.Get(name) + if !ok { + panic("invalid user " + name) + } + } + user := userI.(*users.User) + user.Invites += invites + } +} + +// Any leftover fees go toward invitations. +func SetMinFee(newMinFee int64) { + // assert CallTx call. + std.AssertOriginCall() + // assert admin caller. + caller := std.GetCallerAt(2) + if caller != admin { + panic("unauthorized") + } + // update global variables. + minFee = newMinFee +} + +// This helps prevent fat finger accidents. +func SetMaxFeeMultiple(newMaxFeeMult int64) { + // assert CallTx call. + std.AssertOriginCall() + // assert admin caller. + caller := std.GetCallerAt(2) + if caller != admin { + panic("unauthorized") + } + // update global variables. + maxFeeMult = newMaxFeeMult +} + +//---------------------------------------- +// Exposed public functions + +func GetUserByName(name string) *users.User { + userI, ok := name2User.Get(name) + if !ok { + return nil + } + return userI.(*users.User) +} + +func GetUserByAddress(addr std.Address) *users.User { + userI, ok := addr2User.Get(addr.String()) + if !ok { + return nil + } + return userI.(*users.User) +} + +// unlike GetUserByName, input must be "@" prefixed for names. +func GetUserByAddressOrName(input users.AddressOrName) *users.User { + name, isName := input.GetName() + if isName { + return GetUserByName(name) + } + return GetUserByAddress(std.Address(input)) +} + +// Get a list of user names starting from the given prefix. Limit the +// number of results to maxResults. (This can be used for a name search tool.) +func ListUsersByPrefix(prefix string, maxResults int) []string { + return avlhelpers.ListByteStringKeysByPrefix(&name2User, prefix, maxResults) +} + +func Resolve(input users.AddressOrName) std.Address { + name, isName := input.GetName() + if !isName { + return std.Address(input) // TODO check validity + } + + user := GetUserByName(name) + return user.Address +} + +// Add restricted name to the list +func AdminAddRestrictedName(name string) { + // assert CallTx call. + std.AssertOriginCall() + // get caller + caller := std.GetOrigCaller() + // assert admin + if caller != admin { + panic("unauthorized") + } + + if user := GetUserByName(name); user != nil { + panic("already registered name") + } + + // register restricted name + + restricted.Set(name, true) +} + +//---------------------------------------- +// Constants + +// NOTE: name length must be clearly distinguishable from a bech32 address. +var reName = regexp.MustCompile(`^[a-z]+[_a-z0-9]{5,16}$`) + +//---------------------------------------- +// Render main page + +func Render(fullPath string) string { + path, _ := splitPathAndQuery(fullPath) + if path == "" { + return renderHome(fullPath) + } else if len(path) >= 38 { // 39? 40? + if path[:2] != "g1" { + return "invalid address " + path + } + user := GetUserByAddress(std.Address(path)) + if user == nil { + // TODO: display basic information about account. + return "unknown address " + path + } + return user.Render() + } else { + user := GetUserByName(path) + if user == nil { + return "unknown username " + path + } + return user.Render() + } +} + +func renderHome(path string) string { + doc := "" + + page := pager.NewPager(&name2User, 50, false).MustGetPageByPath(path) + + for _, item := range page.Items { + user := item.Value.(*users.User) + doc += " * [" + user.Name + "](/r/demo/users:" + user.Name + ")\n" + } + doc += "\n" + doc += page.Picker() + return doc +} + +func splitPathAndQuery(fullPath string) (string, string) { + parts := strings.SplitN(fullPath, "?", 2) + path := parts[0] + queryString := "" + if len(parts) > 1 { + queryString = "?" + parts[1] + } + return path, queryString +} + +// pre-restricted names +var preRestrictedNames = []string{ + "bitcoin", "cosmos", "newtendermint", "ethereum", +} + +// pre-registered users +var preRegisteredUsers = []struct { + Name string + Address std.Address +}{ + // system name + {"archives", "g1xlnyjrnf03ju82v0f98ruhpgnquk28knmjfe5k"}, // -> @r_archives + {"demo", "g13ek2zz9qurzynzvssyc4sthwppnruhnp0gdz8n"}, // -> @r_demo + {"gno", "g19602kd9tfxrfd60sgreadt9zvdyyuudcyxsz8a"}, // -> @r_gno + {"gnoland", "g1g3lsfxhvaqgdv4ccemwpnms4fv6t3aq3p5z6u7"}, // -> @r_gnoland + {"gnolang", "g1yjlnm3z2630gg5mryjd79907e0zx658wxs9hnd"}, // -> @r_gnolang + {"gov", "g1g73v2anukg4ej7axwqpthsatzrxjsh0wk797da"}, // -> @r_gov + {"nt", "g15ge0ae9077eh40erwrn2eq0xw6wupwqthpv34l"}, // -> @r_nt + {"sys", "g1r929wt2qplfawe4lvqv9zuwfdcz4vxdun7qh8l"}, // -> @r_sys + {"x", "g164sdpew3c2t3rvxj3kmfv7c7ujlvcw2punzzuz"}, // -> @r_x + + // test1 user + {"test1", "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5"}, // -> @test1 +} + +func init() { + // add pre-registered users + for _, res := range preRegisteredUsers { + // assert not already registered. + _, ok := name2User.Get(res.Name) + if ok { + panic("name already registered") + } + + _, ok = addr2User.Get(res.Address.String()) + if ok { + panic("address already registered") + } + + counter++ + user := &users.User{ + Address: res.Address, + Name: res.Name, + Profile: "", + Number: counter, + Invites: int(0), + Inviter: admin, + } + name2User.Set(res.Name, user) + addr2User.Set(res.Address.String(), user) + } + + // add pre-restricted names + for _, name := range preRestrictedNames { + if _, ok := name2User.Get(name); ok { + panic("name already registered") + } + + restricted.Set(name, true) + } +} + +-- boards/gno.mod -- +module gno.land/r/demo/boards + +-- boards/boards.gno -- +package boards + +import ( + "std" + "strconv" + "regexp" + "time" + "strings" + + "gno.land/p/demo/avl" + "gno.land/p/moul/txlink" + + "gno.land/r/demo/users" + +) + +//---------------------------------------- +// Board + +type BoardID uint64 + +func (bid BoardID) String() string { + return strconv.Itoa(int(bid)) +} + +type Board struct { + id BoardID // only set for public boards. + url string + name string + creator std.Address + threads avl.Tree // Post.id -> *Post + postsCtr uint64 // increments Post.id + createdAt time.Time + deleted avl.Tree // TODO reserved for fast-delete. +} + +func newBoard(id BoardID, url string, name string, creator std.Address) *Board { + if !reName.MatchString(name) { + panic("invalid name: " + name) + } + exists := gBoardsByName.Has(name) + if exists { + panic("board already exists") + } + return &Board{ + id: id, + url: url, + name: name, + creator: creator, + threads: avl.Tree{}, + createdAt: time.Now(), + deleted: avl.Tree{}, + } +} + +/* TODO support this once we figure out how to ensure URL correctness. +// A private board is not tracked by gBoards*, +// but must be persisted by the caller's realm. +// Private boards have 0 id and does not ping +// back the remote board on reposts. +func NewPrivateBoard(url string, name string, creator std.Address) *Board { + return newBoard(0, url, name, creator) +} +*/ + +func (board *Board) IsPrivate() bool { + return board.id == 0 +} + +func (board *Board) GetThread(pid PostID) *Post { + pidkey := postIDKey(pid) + postI, exists := board.threads.Get(pidkey) + if !exists { + return nil + } + return postI.(*Post) +} + +func (board *Board) AddThread(creator std.Address, title string, body string) *Post { + pid := board.incGetPostID() + pidkey := postIDKey(pid) + thread := newPost(board, pid, creator, title, body, pid, 0, 0) + board.threads.Set(pidkey, thread) + return thread +} + +// NOTE: this can be potentially very expensive for threads with many replies. +// TODO: implement optional fast-delete where thread is simply moved. +func (board *Board) DeleteThread(pid PostID) { + pidkey := postIDKey(pid) + _, removed := board.threads.Remove(pidkey) + if !removed { + panic("thread does not exist with id " + pid.String()) + } +} + +func (board *Board) HasPermission(addr std.Address, perm Permission) bool { + if board.creator == addr { + switch perm { + case EditPermission: + return true + case DeletePermission: + return true + default: + return false + } + } + return false +} + +// Renders the board for display suitable as plaintext in +// console. This is suitable for demonstration or tests, +// but not for prod. +func (board *Board) RenderBoard() string { + str := "" + str += "\\[[post](" + board.GetPostFormURL() + ")]\n\n" + if board.threads.Size() > 0 { + board.threads.Iterate("", "", func(key string, value interface{}) bool { + if str != "" { + str += "----------------------------------------\n" + } + str += value.(*Post).RenderSummary() + "\n" + return false + }) + } + return str +} + +func (board *Board) incGetPostID() PostID { + board.postsCtr++ + return PostID(board.postsCtr) +} + +func (board *Board) GetURLFromThreadAndReplyID(threadID, replyID PostID) string { + if replyID == 0 { + return board.url + "/" + threadID.String() + } else { + return board.url + "/" + threadID.String() + "/" + replyID.String() + } +} + +func (board *Board) GetPostFormURL() string { + return txlink.Call("CreateThread", "bid", board.id.String()) +} + +var ( + gBoards avl.Tree // id -> *Board + gBoardsCtr int // increments Board.id + gBoardsByName avl.Tree // name -> *Board + gDefaultAnonFee = 100000000 // minimum fee required if anonymous +) + +//---------------------------------------- +// Constants + +var reName = regexp.MustCompile(`^[a-z]+[_a-z0-9]{2,29}$`) + +//---------------------------------------- +// private utility methods +// XXX ensure these cannot be called from public. + +func getBoard(bid BoardID) *Board { + bidkey := boardIDKey(bid) + board_, exists := gBoards.Get(bidkey) + if !exists { + return nil + } + board := board_.(*Board) + return board +} + +func incGetBoardID() BoardID { + gBoardsCtr++ + return BoardID(gBoardsCtr) +} + +func padLeft(str string, length int) string { + if len(str) >= length { + return str + } else { + return strings.Repeat(" ", length-len(str)) + str + } +} + +func padZero(u64 uint64, length int) string { + str := strconv.Itoa(int(u64)) + if len(str) >= length { + return str + } else { + return strings.Repeat("0", length-len(str)) + str + } +} + +func boardIDKey(bid BoardID) string { + return padZero(uint64(bid), 10) +} + +func postIDKey(pid PostID) string { + return padZero(uint64(pid), 10) +} + +func indentBody(indent string, body string) string { + lines := strings.Split(body, "\n") + res := "" + for i, line := range lines { + if i > 0 { + res += "\n" + } + res += indent + line + } + return res +} + +// NOTE: length must be greater than 3. +func summaryOf(str string, length int) string { + lines := strings.SplitN(str, "\n", 2) + line := lines[0] + if len(line) > length { + line = line[:(length-3)] + "..." + } else if len(lines) > 1 { + // len(line) <= 80 + line = line + "..." + } + return line +} + +func displayAddressMD(addr std.Address) string { + user := users.GetUserByAddress(addr) + if user == nil { + return "[" + addr.String() + "](/r/demo/users:" + addr.String() + ")" + } else { + return "[@" + user.Name + "](/r/demo/users:" + user.Name + ")" + } +} + +func usernameOf(addr std.Address) string { + user := users.GetUserByAddress(addr) + if user == nil { + return "" + } + return user.Name +} + +//---------------------------------------- +// Post + +// NOTE: a PostID is relative to the board. +type PostID uint64 + +func (pid PostID) String() string { + return strconv.Itoa(int(pid)) +} + +// A Post is a "thread" or a "reply" depending on context. +// A thread is a Post of a Board that holds other replies. +type Post struct { + board *Board + id PostID + creator std.Address + title string // optional + body string + replies avl.Tree // Post.id -> *Post + repliesAll avl.Tree // Post.id -> *Post (all replies, for top-level posts) + reposts avl.Tree // Board.id -> Post.id + threadID PostID // original Post.id + parentID PostID // parent Post.id (if reply or repost) + repostBoard BoardID // original Board.id (if repost) + createdAt time.Time + updatedAt time.Time +} + +func newPost(board *Board, id PostID, creator std.Address, title, body string, threadID, parentID PostID, repostBoard BoardID) *Post { + return &Post{ + board: board, + id: id, + creator: creator, + title: title, + body: body, + replies: avl.Tree{}, + repliesAll: avl.Tree{}, + reposts: avl.Tree{}, + threadID: threadID, + parentID: parentID, + repostBoard: repostBoard, + createdAt: time.Now(), + } +} + +func (post *Post) IsThread() bool { + return post.parentID == 0 +} + +func (post *Post) GetPostID() PostID { + return post.id +} + +func (post *Post) AddReply(creator std.Address, body string) *Post { + board := post.board + pid := board.incGetPostID() + pidkey := postIDKey(pid) + reply := newPost(board, pid, creator, "", body, post.threadID, post.id, 0) + post.replies.Set(pidkey, reply) + if post.threadID == post.id { + post.repliesAll.Set(pidkey, reply) + } else { + thread := board.GetThread(post.threadID) + thread.repliesAll.Set(pidkey, reply) + } + return reply +} + +func (post *Post) Update(title string, body string) { + post.title = title + post.body = body + post.updatedAt = time.Now() +} + +func (thread *Post) GetReply(pid PostID) *Post { + pidkey := postIDKey(pid) + replyI, ok := thread.repliesAll.Get(pidkey) + if !ok { + return nil + } else { + return replyI.(*Post) + } +} + +func (post *Post) AddRepostTo(creator std.Address, title, body string, dst *Board) *Post { + if !post.IsThread() { + panic("cannot repost non-thread post") + } + pid := dst.incGetPostID() + pidkey := postIDKey(pid) + repost := newPost(dst, pid, creator, title, body, pid, post.id, post.board.id) + dst.threads.Set(pidkey, repost) + if !dst.IsPrivate() { + bidkey := boardIDKey(dst.id) + post.reposts.Set(bidkey, pid) + } + return repost +} + +func (thread *Post) DeletePost(pid PostID) { + if thread.id == pid { + panic("should not happen") + } + pidkey := postIDKey(pid) + postI, removed := thread.repliesAll.Remove(pidkey) + if !removed { + panic("post not found in thread") + } + post := postI.(*Post) + if post.parentID != thread.id { + parent := thread.GetReply(post.parentID) + parent.replies.Remove(pidkey) + } else { + thread.replies.Remove(pidkey) + } +} + +func (post *Post) HasPermission(addr std.Address, perm Permission) bool { + if post.creator == addr { + switch perm { + case EditPermission: + return true + case DeletePermission: + return true + default: + return false + } + } + // post notes inherit permissions of the board. + return post.board.HasPermission(addr, perm) +} + +func (post *Post) GetSummary() string { + return summaryOf(post.body, 80) } + +func (post *Post) GetURL() string { + if post.IsThread() { + return post.board.GetURLFromThreadAndReplyID( + post.id, 0) + } else { + return post.board.GetURLFromThreadAndReplyID( + post.threadID, post.id) + } +} + +func (post *Post) GetReplyFormURL() string { + return txlink.Call("CreateReply", + "bid", post.board.id.String(), + "threadid", post.threadID.String(), + "postid", post.id.String(), + ) +} + +func (post *Post) GetRepostFormURL() string { + return txlink.Call("CreateRepost", + "bid", post.board.id.String(), + "postid", post.id.String(), + ) +} + +func (post *Post) GetDeleteFormURL() string { + return txlink.Call("DeletePost", + "bid", post.board.id.String(), + "threadid", post.threadID.String(), + "postid", post.id.String(), + ) +} + +func (post *Post) RenderSummary() string { + if post.repostBoard != 0 { + dstBoard := getBoard(post.repostBoard) + if dstBoard == nil { + panic("repostBoard does not exist") + } + thread := dstBoard.GetThread(PostID(post.parentID)) + if thread == nil { + return "reposted post does not exist" + } + return "Repost: " + post.GetSummary() + "\n" + thread.RenderSummary() + } + str := "" + if post.title != "" { + str += "## [" + summaryOf(post.title, 80) + "](" + post.GetURL() + ")\n" + str += "\n" + } + str += post.GetSummary() + "\n" + str += "\\- " + displayAddressMD(post.creator) + "," + str += " [" + post.createdAt.Format("2006-01-02 3:04pm MST") + "](" + post.GetURL() + ")" + str += " \\[[x](" + post.GetDeleteFormURL() + ")]" + str += " (" + strconv.Itoa(post.replies.Size()) + " replies)" + str += " (" + strconv.Itoa(post.reposts.Size()) + " reposts)" + "\n" + return str +} + +func (post *Post) RenderPost(indent string, levels int) string { + if post == nil { + return "nil post" + } + str := "" + if post.title != "" { + str += indent + "# " + post.title + "\n" + str += indent + "\n" + } + str += indentBody(indent, post.body) + "\n" // TODO: indent body lines. + str += indent + "\\- " + displayAddressMD(post.creator) + ", " + str += "[" + post.createdAt.Format("2006-01-02 3:04pm (MST)") + "](" + post.GetURL() + ")" + str += " \\[[reply](" + post.GetReplyFormURL() + ")]" + if post.IsThread() { + str += " \\[[repost](" + post.GetRepostFormURL() + ")]" + } + str += " \\[[x](" + post.GetDeleteFormURL() + ")]\n" + if levels > 0 { + if post.replies.Size() > 0 { + post.replies.Iterate("", "", func(key string, value interface{}) bool { + str += indent + "\n" + str += value.(*Post).RenderPost(indent+"> ", levels-1) + return false + }) + } + } else { + if post.replies.Size() > 0 { + str += indent + "\n" + str += indent + "_[see all " + strconv.Itoa(post.replies.Size()) + " replies](" + post.GetURL() + ")_\n" + } + } + return str +} + +// render reply and link to context thread +func (post *Post) RenderInner() string { + if post.IsThread() { + panic("unexpected thread") + } + threadID := post.threadID + // replyID := post.id + parentID := post.parentID + str := "" + str += "_[see thread](" + post.board.GetURLFromThreadAndReplyID( + threadID, 0) + ")_\n\n" + thread := post.board.GetThread(post.threadID) + var parent *Post + if thread.id == parentID { + parent = thread + } else { + parent = thread.GetReply(parentID) + } + str += parent.RenderPost("", 0) + str += "\n" + str += post.RenderPost("> ", 5) + return str +} + +//---------------------------------------- +// Public facing functions + +func GetBoardIDFromName(name string) (BoardID, bool) { + boardI, exists := gBoardsByName.Get(name) + if !exists { + return 0, false + } + return boardI.(*Board).id, true +} + +func CreateBoard(name string) BoardID { + if !(std.IsOriginCall() || std.PrevRealm().IsUser()) { + panic("invalid non-user call") + } + bid := incGetBoardID() + caller := std.GetOrigCaller() + if usernameOf(caller) == "" { + panic("unauthorized") + } + url := "/r/demo/boards:" + name + board := newBoard(bid, url, name, caller) + bidkey := boardIDKey(bid) + gBoards.Set(bidkey, board) + gBoardsByName.Set(name, board) + return board.id +} + +func checkAnonFee() bool { + sent := std.GetOrigSend() + anonFeeCoin := std.NewCoin("ugnot", int64(gDefaultAnonFee)) + if len(sent) == 1 && sent[0].IsGTE(anonFeeCoin) { + return true + } + return false +} + +func CreateThread(bid BoardID, title string, body string) PostID { + if !(std.IsOriginCall() || std.PrevRealm().IsUser()) { + panic("invalid non-user call") + } + caller := std.GetOrigCaller() + if usernameOf(caller) == "" { + if !checkAnonFee() { + panic("please register, otherwise minimum fee " + strconv.Itoa(gDefaultAnonFee) + " is required if anonymous") + } + } + board := getBoard(bid) + if board == nil { + panic("board not exist") + } + thread := board.AddThread(caller, title, body) + return thread.id +} + +func CreateReply(bid BoardID, threadid, postid PostID, body string) PostID { + if !(std.IsOriginCall() || std.PrevRealm().IsUser()) { + panic("invalid non-user call") + } + caller := std.GetOrigCaller() + if usernameOf(caller) == "" { + if !checkAnonFee() { + panic("please register, otherwise minimum fee " + strconv.Itoa(gDefaultAnonFee) + " is required if anonymous") + } + } + board := getBoard(bid) + if board == nil { + panic("board not exist") + } + thread := board.GetThread(threadid) + if thread == nil { + panic("thread not exist") + } + if postid == threadid { + reply := thread.AddReply(caller, body) + return reply.id + } else { + post := thread.GetReply(postid) + reply := post.AddReply(caller, body) + return reply.id + } +} + +// If dstBoard is private, does not ping back. +// If board specified by bid is private, panics. +func CreateRepost(bid BoardID, postid PostID, title string, body string, dstBoardID BoardID) PostID { + if !(std.IsOriginCall() || std.PrevRealm().IsUser()) { + panic("invalid non-user call") + } + caller := std.GetOrigCaller() + if usernameOf(caller) == "" { + // TODO: allow with gDefaultAnonFee payment. + if !checkAnonFee() { + panic("please register, otherwise minimum fee " + strconv.Itoa(gDefaultAnonFee) + " is required if anonymous") + } + } + board := getBoard(bid) + if board == nil { + panic("src board not exist") + } + if board.IsPrivate() { + panic("cannot repost from a private board") + } + dst := getBoard(dstBoardID) + if dst == nil { + panic("dst board not exist") + } + thread := board.GetThread(postid) + if thread == nil { + panic("thread not exist") + } + repost := thread.AddRepostTo(caller, title, body, dst) + return repost.id +} + +func DeletePost(bid BoardID, threadid, postid PostID, reason string) { + if !(std.IsOriginCall() || std.PrevRealm().IsUser()) { + panic("invalid non-user call") + } + caller := std.GetOrigCaller() + board := getBoard(bid) + if board == nil { + panic("board not exist") + } + thread := board.GetThread(threadid) + if thread == nil { + panic("thread not exist") + } + if postid == threadid { + // delete thread + if !thread.HasPermission(caller, DeletePermission) { + panic("unauthorized") + } + board.DeleteThread(threadid) + } else { + // delete thread's post + post := thread.GetReply(postid) + if post == nil { + panic("post not exist") + } + if !post.HasPermission(caller, DeletePermission) { + panic("unauthorized") + } + thread.DeletePost(postid) + } +} + +func EditPost(bid BoardID, threadid, postid PostID, title, body string) { + if !(std.IsOriginCall() || std.PrevRealm().IsUser()) { + panic("invalid non-user call") + } + caller := std.GetOrigCaller() + board := getBoard(bid) + if board == nil { + panic("board not exist") + } + thread := board.GetThread(threadid) + if thread == nil { + panic("thread not exist") + } + if postid == threadid { + // edit thread + if !thread.HasPermission(caller, EditPermission) { + panic("unauthorized") + } + thread.Update(title, body) + } else { + // edit thread's post + post := thread.GetReply(postid) + if post == nil { + panic("post not exist") + } + if !post.HasPermission(caller, EditPermission) { + panic("unauthorized") + } + post.Update(title, body) + } +} + +//---------------------------------------- +// Render functions + +func RenderBoard(bid BoardID) string { + board := getBoard(bid) + if board == nil { + return "missing board" + } + return board.RenderBoard() +} + +func Render(path string) string { + if path == "" { + str := "These are all the boards of this realm:\n\n" + gBoards.Iterate("", "", func(key string, value interface{}) bool { + board := value.(*Board) + str += " * [" + board.url + "](" + board.url + ")\n" + return false + }) + return str + } + parts := strings.Split(path, "/") + if len(parts) == 1 { + // /r/demo/boards:BOARD_NAME + name := parts[0] + boardI, exists := gBoardsByName.Get(name) + if !exists { + return "board does not exist: " + name + } + return boardI.(*Board).RenderBoard() + } else if len(parts) == 2 { + // /r/demo/boards:BOARD_NAME/THREAD_ID + name := parts[0] + boardI, exists := gBoardsByName.Get(name) + if !exists { + return "board does not exist: " + name + } + pid, err := strconv.Atoi(parts[1]) + if err != nil { + return "invalid thread id: " + parts[1] + } + board := boardI.(*Board) + thread := board.GetThread(PostID(pid)) + if thread == nil { + return "thread does not exist with id: " + parts[1] + } + return thread.RenderPost("", 5) + } else if len(parts) == 3 { + // /r/demo/boards:BOARD_NAME/THREAD_ID/REPLY_ID + name := parts[0] + boardI, exists := gBoardsByName.Get(name) + if !exists { + return "board does not exist: " + name + } + pid, err := strconv.Atoi(parts[1]) + if err != nil { + return "invalid thread id: " + parts[1] + } + board := boardI.(*Board) + thread := board.GetThread(PostID(pid)) + if thread == nil { + return "thread does not exist with id: " + parts[1] + } + rid, err := strconv.Atoi(parts[2]) + if err != nil { + return "invalid reply id: " + parts[2] + } + reply := thread.GetReply(PostID(rid)) + if reply == nil { + return "reply does not exist with id: " + parts[2] + } + return reply.RenderInner() + } else { + return "unrecognized path " + path + } +} + +type Permission string + +const ( + DeletePermission Permission = "role:delete" + EditPermission Permission = "role:edit" +) \ No newline at end of file diff --git a/gno.land/pkg/integration/testdata/issue_2283_cacheTypes.txtar b/gno.land/pkg/integration/testdata/issue_2283_cacheTypes.txtar index 95bd48c0144..af950ea12f3 100644 --- a/gno.land/pkg/integration/testdata/issue_2283_cacheTypes.txtar +++ b/gno.land/pkg/integration/testdata/issue_2283_cacheTypes.txtar @@ -6,8 +6,14 @@ # These are not necessary, but they "alleviate" add_feeds.tx from the # responsibility of loading standard libraries, thus not making it exceed # the --gas-wanted. -loadpkg gno.land/r/demo/users -loadpkg gno.land/r/demo/boards +loadpkg gno.land/p/demo/avl +loadpkg gno.land/p/demo/avl/pager +loadpkg gno.land/p/demo/avlhelpers +loadpkg gno.land/p/moul/txlink + +loadpkg gno.land/p/demo/users $WORK/pusers +loadpkg gno.land/r/demo/users $WORK/users +loadpkg gno.land/r/demo/boards $WORK/boards gnoland start @@ -101,3 +107,1232 @@ import ( func Call(s string) { base64.StdEncoding.DecodeString("hey") } + +-- pusers/gno.mod -- +module gno.land/p/demo/users + +-- pusers/users.gno -- +package users + +import ( + "std" + "strconv" +) + +//---------------------------------------- +// Types + +type User struct { + Address std.Address + Name string + Profile string + Number int + Invites int + Inviter std.Address +} + +func (u *User) Render() string { + str := "## user " + u.Name + "\n" + + "\n" + + " * address = " + string(u.Address) + "\n" + + " * " + strconv.Itoa(u.Invites) + " invites\n" + if u.Inviter != "" { + str = str + " * invited by " + string(u.Inviter) + "\n" + } + str = str + "\n" + + u.Profile + "\n" + return str +} + +type AddressOrName string + +func (aon AddressOrName) IsName() bool { + return aon != "" && aon[0] == '@' +} + +func (aon AddressOrName) GetName() (string, bool) { + if len(aon) >= 2 && aon[0] == '@' { + return string(aon[1:]), true + } + return "", false +} + +-- users/gno.mod -- +module gno.land/r/demo/users + +-- users/users.gno -- +package users + +import ( + "regexp" + "std" + "strconv" + "strings" + + "gno.land/p/demo/avl" + "gno.land/p/demo/avl/pager" + "gno.land/p/demo/avlhelpers" + "gno.land/p/demo/users" +) + +//---------------------------------------- +// State + +var ( + admin std.Address = "g1manfred47kzduec920z88wfr64ylksmdcedlf5" // @moul + + restricted avl.Tree // Name -> true - restricted name + name2User avl.Tree // Name -> *users.User + addr2User avl.Tree // std.Address -> *users.User + invites avl.Tree // string(inviter+":"+invited) -> true + counter int // user id counter + minFee int64 = 20 * 1_000_000 // minimum gnot must be paid to register. + maxFeeMult int64 = 10 // maximum multiples of minFee accepted. +) + +//---------------------------------------- +// Top-level functions + +func Register(inviter std.Address, name string, profile string) { + // assert CallTx call. + std.AssertOriginCall() + // assert invited or paid. + caller := std.GetCallerAt(2) + if caller != std.GetOrigCaller() { + panic("should not happen") // because std.AssertOrigCall(). + } + + sentCoins := std.GetOrigSend() + minCoin := std.NewCoin("ugnot", minFee) + + if inviter == "" { + // banker := std.GetBanker(std.BankerTypeOrigSend) + if len(sentCoins) == 1 && sentCoins[0].IsGTE(minCoin) { + if sentCoins[0].Amount > minFee*maxFeeMult { + panic("payment must not be greater than " + strconv.Itoa(int(minFee*maxFeeMult))) + } else { + // ok + } + } else { + panic("payment must not be less than " + strconv.Itoa(int(minFee))) + } + } else { + invitekey := inviter.String() + ":" + caller.String() + _, ok := invites.Get(invitekey) + if !ok { + panic("invalid invitation") + } + invites.Remove(invitekey) + } + + // assert not already registered. + _, ok := name2User.Get(name) + if ok { + panic("name already registered: " + name) + } + _, ok = addr2User.Get(caller.String()) + if ok { + panic("address already registered: " + caller.String()) + } + + isInviterAdmin := inviter == admin + + // check for restricted name + if _, isRestricted := restricted.Get(name); isRestricted { + // only address invite by the admin can register restricted name + if !isInviterAdmin { + panic("restricted name: " + name) + } + + restricted.Remove(name) + } + + // assert name is valid. + // admin inviter can bypass name restriction + if !isInviterAdmin && !reName.MatchString(name) { + panic("invalid name: " + name + " (must be at least 6 characters, lowercase alphanumeric with underscore)") + } + + // remainder of fees go toward invites. + invites := int(0) + if len(sentCoins) == 1 { + if sentCoins[0].Denom == "ugnot" && sentCoins[0].Amount >= minFee { + invites = int(sentCoins[0].Amount / minFee) + if inviter == "" && invites > 0 { + invites -= 1 + } + } + } + // register. + counter++ + user := &users.User{ + Address: caller, + Name: name, + Profile: profile, + Number: counter, + Invites: invites, + Inviter: inviter, + } + name2User.Set(name, user) + addr2User.Set(caller.String(), user) +} + +func Invite(invitee string) { + // assert CallTx call. + std.AssertOriginCall() + // get caller/inviter. + caller := std.GetCallerAt(2) + if caller != std.GetOrigCaller() { + panic("should not happen") // because std.AssertOrigCall(). + } + lines := strings.Split(invitee, "\n") + if caller == admin { + // nothing to do, all good + } else { + // ensure has invites. + userI, ok := addr2User.Get(caller.String()) + if !ok { + panic("user unknown") + } + user := userI.(*users.User) + if user.Invites <= 0 { + panic("user has no invite tokens") + } + user.Invites -= len(lines) + if user.Invites < 0 { + panic("user has insufficient invite tokens") + } + } + // for each line... + for _, line := range lines { + if line == "" { + continue // file bodies have a trailing newline. + } else if strings.HasPrefix(line, `//`) { + continue // comment + } + // record invite. + invitekey := string(caller) + ":" + string(line) + invites.Set(invitekey, true) + } +} + +func GrantInvites(invites string) { + // assert CallTx call. + std.AssertOriginCall() + // assert admin. + caller := std.GetCallerAt(2) + if caller != std.GetOrigCaller() { + panic("should not happen") // because std.AssertOrigCall(). + } + if caller != admin { + panic("unauthorized") + } + // for each line... + lines := strings.Split(invites, "\n") + for _, line := range lines { + if line == "" { + continue // file bodies have a trailing newline. + } else if strings.HasPrefix(line, `//`) { + continue // comment + } + // parse name and invites. + var name string + var invites int + parts := strings.Split(line, ":") + if len(parts) == 1 { // short for :1. + name = parts[0] + invites = 1 + } else if len(parts) == 2 { + name = parts[0] + invites_, err := strconv.Atoi(parts[1]) + if err != nil { + panic(err) + } + invites = int(invites_) + } else { + panic("should not happen") + } + // give invites. + userI, ok := name2User.Get(name) + if !ok { + // maybe address. + userI, ok = addr2User.Get(name) + if !ok { + panic("invalid user " + name) + } + } + user := userI.(*users.User) + user.Invites += invites + } +} + +// Any leftover fees go toward invitations. +func SetMinFee(newMinFee int64) { + // assert CallTx call. + std.AssertOriginCall() + // assert admin caller. + caller := std.GetCallerAt(2) + if caller != admin { + panic("unauthorized") + } + // update global variables. + minFee = newMinFee +} + +// This helps prevent fat finger accidents. +func SetMaxFeeMultiple(newMaxFeeMult int64) { + // assert CallTx call. + std.AssertOriginCall() + // assert admin caller. + caller := std.GetCallerAt(2) + if caller != admin { + panic("unauthorized") + } + // update global variables. + maxFeeMult = newMaxFeeMult +} + +//---------------------------------------- +// Exposed public functions + +func GetUserByName(name string) *users.User { + userI, ok := name2User.Get(name) + if !ok { + return nil + } + return userI.(*users.User) +} + +func GetUserByAddress(addr std.Address) *users.User { + userI, ok := addr2User.Get(addr.String()) + if !ok { + return nil + } + return userI.(*users.User) +} + +// unlike GetUserByName, input must be "@" prefixed for names. +func GetUserByAddressOrName(input users.AddressOrName) *users.User { + name, isName := input.GetName() + if isName { + return GetUserByName(name) + } + return GetUserByAddress(std.Address(input)) +} + +// Get a list of user names starting from the given prefix. Limit the +// number of results to maxResults. (This can be used for a name search tool.) +func ListUsersByPrefix(prefix string, maxResults int) []string { + return avlhelpers.ListByteStringKeysByPrefix(&name2User, prefix, maxResults) +} + +func Resolve(input users.AddressOrName) std.Address { + name, isName := input.GetName() + if !isName { + return std.Address(input) // TODO check validity + } + + user := GetUserByName(name) + return user.Address +} + +// Add restricted name to the list +func AdminAddRestrictedName(name string) { + // assert CallTx call. + std.AssertOriginCall() + // get caller + caller := std.GetOrigCaller() + // assert admin + if caller != admin { + panic("unauthorized") + } + + if user := GetUserByName(name); user != nil { + panic("already registered name") + } + + // register restricted name + + restricted.Set(name, true) +} + +//---------------------------------------- +// Constants + +// NOTE: name length must be clearly distinguishable from a bech32 address. +var reName = regexp.MustCompile(`^[a-z]+[_a-z0-9]{5,16}$`) + +//---------------------------------------- +// Render main page + +func Render(fullPath string) string { + path, _ := splitPathAndQuery(fullPath) + if path == "" { + return renderHome(fullPath) + } else if len(path) >= 38 { // 39? 40? + if path[:2] != "g1" { + return "invalid address " + path + } + user := GetUserByAddress(std.Address(path)) + if user == nil { + // TODO: display basic information about account. + return "unknown address " + path + } + return user.Render() + } else { + user := GetUserByName(path) + if user == nil { + return "unknown username " + path + } + return user.Render() + } +} + +func renderHome(path string) string { + doc := "" + + page := pager.NewPager(&name2User, 50, false).MustGetPageByPath(path) + + for _, item := range page.Items { + user := item.Value.(*users.User) + doc += " * [" + user.Name + "](/r/demo/users:" + user.Name + ")\n" + } + doc += "\n" + doc += page.Picker() + return doc +} + +func splitPathAndQuery(fullPath string) (string, string) { + parts := strings.SplitN(fullPath, "?", 2) + path := parts[0] + queryString := "" + if len(parts) > 1 { + queryString = "?" + parts[1] + } + return path, queryString +} + +// pre-restricted names +var preRestrictedNames = []string{ + "bitcoin", "cosmos", "newtendermint", "ethereum", +} + +// pre-registered users +var preRegisteredUsers = []struct { + Name string + Address std.Address +}{ + // system name + {"archives", "g1xlnyjrnf03ju82v0f98ruhpgnquk28knmjfe5k"}, // -> @r_archives + {"demo", "g13ek2zz9qurzynzvssyc4sthwppnruhnp0gdz8n"}, // -> @r_demo + {"gno", "g19602kd9tfxrfd60sgreadt9zvdyyuudcyxsz8a"}, // -> @r_gno + {"gnoland", "g1g3lsfxhvaqgdv4ccemwpnms4fv6t3aq3p5z6u7"}, // -> @r_gnoland + {"gnolang", "g1yjlnm3z2630gg5mryjd79907e0zx658wxs9hnd"}, // -> @r_gnolang + {"gov", "g1g73v2anukg4ej7axwqpthsatzrxjsh0wk797da"}, // -> @r_gov + {"nt", "g15ge0ae9077eh40erwrn2eq0xw6wupwqthpv34l"}, // -> @r_nt + {"sys", "g1r929wt2qplfawe4lvqv9zuwfdcz4vxdun7qh8l"}, // -> @r_sys + {"x", "g164sdpew3c2t3rvxj3kmfv7c7ujlvcw2punzzuz"}, // -> @r_x + + // test1 user + {"test1", "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5"}, // -> @test1 +} + +func init() { + // add pre-registered users + for _, res := range preRegisteredUsers { + // assert not already registered. + _, ok := name2User.Get(res.Name) + if ok { + panic("name already registered") + } + + _, ok = addr2User.Get(res.Address.String()) + if ok { + panic("address already registered") + } + + counter++ + user := &users.User{ + Address: res.Address, + Name: res.Name, + Profile: "", + Number: counter, + Invites: int(0), + Inviter: admin, + } + name2User.Set(res.Name, user) + addr2User.Set(res.Address.String(), user) + } + + // add pre-restricted names + for _, name := range preRestrictedNames { + if _, ok := name2User.Get(name); ok { + panic("name already registered") + } + + restricted.Set(name, true) + } +} + +-- boards/gno.mod -- +module gno.land/r/demo/boards + +-- boards/boards.gno -- +package boards + +import ( + "std" + "strconv" + "regexp" + "time" + "strings" + + "gno.land/p/demo/avl" + "gno.land/p/moul/txlink" + + "gno.land/r/demo/users" + +) + +//---------------------------------------- +// Board + +type BoardID uint64 + +func (bid BoardID) String() string { + return strconv.Itoa(int(bid)) +} + +type Board struct { + id BoardID // only set for public boards. + url string + name string + creator std.Address + threads avl.Tree // Post.id -> *Post + postsCtr uint64 // increments Post.id + createdAt time.Time + deleted avl.Tree // TODO reserved for fast-delete. +} + +func newBoard(id BoardID, url string, name string, creator std.Address) *Board { + if !reName.MatchString(name) { + panic("invalid name: " + name) + } + exists := gBoardsByName.Has(name) + if exists { + panic("board already exists") + } + return &Board{ + id: id, + url: url, + name: name, + creator: creator, + threads: avl.Tree{}, + createdAt: time.Now(), + deleted: avl.Tree{}, + } +} + +/* TODO support this once we figure out how to ensure URL correctness. +// A private board is not tracked by gBoards*, +// but must be persisted by the caller's realm. +// Private boards have 0 id and does not ping +// back the remote board on reposts. +func NewPrivateBoard(url string, name string, creator std.Address) *Board { + return newBoard(0, url, name, creator) +} +*/ + +func (board *Board) IsPrivate() bool { + return board.id == 0 +} + +func (board *Board) GetThread(pid PostID) *Post { + pidkey := postIDKey(pid) + postI, exists := board.threads.Get(pidkey) + if !exists { + return nil + } + return postI.(*Post) +} + +func (board *Board) AddThread(creator std.Address, title string, body string) *Post { + pid := board.incGetPostID() + pidkey := postIDKey(pid) + thread := newPost(board, pid, creator, title, body, pid, 0, 0) + board.threads.Set(pidkey, thread) + return thread +} + +// NOTE: this can be potentially very expensive for threads with many replies. +// TODO: implement optional fast-delete where thread is simply moved. +func (board *Board) DeleteThread(pid PostID) { + pidkey := postIDKey(pid) + _, removed := board.threads.Remove(pidkey) + if !removed { + panic("thread does not exist with id " + pid.String()) + } +} + +func (board *Board) HasPermission(addr std.Address, perm Permission) bool { + if board.creator == addr { + switch perm { + case EditPermission: + return true + case DeletePermission: + return true + default: + return false + } + } + return false +} + +// Renders the board for display suitable as plaintext in +// console. This is suitable for demonstration or tests, +// but not for prod. +func (board *Board) RenderBoard() string { + str := "" + str += "\\[[post](" + board.GetPostFormURL() + ")]\n\n" + if board.threads.Size() > 0 { + board.threads.Iterate("", "", func(key string, value interface{}) bool { + if str != "" { + str += "----------------------------------------\n" + } + str += value.(*Post).RenderSummary() + "\n" + return false + }) + } + return str +} + +func (board *Board) incGetPostID() PostID { + board.postsCtr++ + return PostID(board.postsCtr) +} + +func (board *Board) GetURLFromThreadAndReplyID(threadID, replyID PostID) string { + if replyID == 0 { + return board.url + "/" + threadID.String() + } else { + return board.url + "/" + threadID.String() + "/" + replyID.String() + } +} + +func (board *Board) GetPostFormURL() string { + return txlink.Call("CreateThread", "bid", board.id.String()) +} + +var ( + gBoards avl.Tree // id -> *Board + gBoardsCtr int // increments Board.id + gBoardsByName avl.Tree // name -> *Board + gDefaultAnonFee = 100000000 // minimum fee required if anonymous +) + +//---------------------------------------- +// Constants + +var reName = regexp.MustCompile(`^[a-z]+[_a-z0-9]{2,29}$`) + +//---------------------------------------- +// private utility methods +// XXX ensure these cannot be called from public. + +func getBoard(bid BoardID) *Board { + bidkey := boardIDKey(bid) + board_, exists := gBoards.Get(bidkey) + if !exists { + return nil + } + board := board_.(*Board) + return board +} + +func incGetBoardID() BoardID { + gBoardsCtr++ + return BoardID(gBoardsCtr) +} + +func padLeft(str string, length int) string { + if len(str) >= length { + return str + } else { + return strings.Repeat(" ", length-len(str)) + str + } +} + +func padZero(u64 uint64, length int) string { + str := strconv.Itoa(int(u64)) + if len(str) >= length { + return str + } else { + return strings.Repeat("0", length-len(str)) + str + } +} + +func boardIDKey(bid BoardID) string { + return padZero(uint64(bid), 10) +} + +func postIDKey(pid PostID) string { + return padZero(uint64(pid), 10) +} + +func indentBody(indent string, body string) string { + lines := strings.Split(body, "\n") + res := "" + for i, line := range lines { + if i > 0 { + res += "\n" + } + res += indent + line + } + return res +} + +// NOTE: length must be greater than 3. +func summaryOf(str string, length int) string { + lines := strings.SplitN(str, "\n", 2) + line := lines[0] + if len(line) > length { + line = line[:(length-3)] + "..." + } else if len(lines) > 1 { + // len(line) <= 80 + line = line + "..." + } + return line +} + +func displayAddressMD(addr std.Address) string { + user := users.GetUserByAddress(addr) + if user == nil { + return "[" + addr.String() + "](/r/demo/users:" + addr.String() + ")" + } else { + return "[@" + user.Name + "](/r/demo/users:" + user.Name + ")" + } +} + +func usernameOf(addr std.Address) string { + user := users.GetUserByAddress(addr) + if user == nil { + return "" + } + return user.Name +} + +//---------------------------------------- +// Post + +// NOTE: a PostID is relative to the board. +type PostID uint64 + +func (pid PostID) String() string { + return strconv.Itoa(int(pid)) +} + +// A Post is a "thread" or a "reply" depending on context. +// A thread is a Post of a Board that holds other replies. +type Post struct { + board *Board + id PostID + creator std.Address + title string // optional + body string + replies avl.Tree // Post.id -> *Post + repliesAll avl.Tree // Post.id -> *Post (all replies, for top-level posts) + reposts avl.Tree // Board.id -> Post.id + threadID PostID // original Post.id + parentID PostID // parent Post.id (if reply or repost) + repostBoard BoardID // original Board.id (if repost) + createdAt time.Time + updatedAt time.Time +} + +func newPost(board *Board, id PostID, creator std.Address, title, body string, threadID, parentID PostID, repostBoard BoardID) *Post { + return &Post{ + board: board, + id: id, + creator: creator, + title: title, + body: body, + replies: avl.Tree{}, + repliesAll: avl.Tree{}, + reposts: avl.Tree{}, + threadID: threadID, + parentID: parentID, + repostBoard: repostBoard, + createdAt: time.Now(), + } +} + +func (post *Post) IsThread() bool { + return post.parentID == 0 +} + +func (post *Post) GetPostID() PostID { + return post.id +} + +func (post *Post) AddReply(creator std.Address, body string) *Post { + board := post.board + pid := board.incGetPostID() + pidkey := postIDKey(pid) + reply := newPost(board, pid, creator, "", body, post.threadID, post.id, 0) + post.replies.Set(pidkey, reply) + if post.threadID == post.id { + post.repliesAll.Set(pidkey, reply) + } else { + thread := board.GetThread(post.threadID) + thread.repliesAll.Set(pidkey, reply) + } + return reply +} + +func (post *Post) Update(title string, body string) { + post.title = title + post.body = body + post.updatedAt = time.Now() +} + +func (thread *Post) GetReply(pid PostID) *Post { + pidkey := postIDKey(pid) + replyI, ok := thread.repliesAll.Get(pidkey) + if !ok { + return nil + } else { + return replyI.(*Post) + } +} + +func (post *Post) AddRepostTo(creator std.Address, title, body string, dst *Board) *Post { + if !post.IsThread() { + panic("cannot repost non-thread post") + } + pid := dst.incGetPostID() + pidkey := postIDKey(pid) + repost := newPost(dst, pid, creator, title, body, pid, post.id, post.board.id) + dst.threads.Set(pidkey, repost) + if !dst.IsPrivate() { + bidkey := boardIDKey(dst.id) + post.reposts.Set(bidkey, pid) + } + return repost +} + +func (thread *Post) DeletePost(pid PostID) { + if thread.id == pid { + panic("should not happen") + } + pidkey := postIDKey(pid) + postI, removed := thread.repliesAll.Remove(pidkey) + if !removed { + panic("post not found in thread") + } + post := postI.(*Post) + if post.parentID != thread.id { + parent := thread.GetReply(post.parentID) + parent.replies.Remove(pidkey) + } else { + thread.replies.Remove(pidkey) + } +} + +func (post *Post) HasPermission(addr std.Address, perm Permission) bool { + if post.creator == addr { + switch perm { + case EditPermission: + return true + case DeletePermission: + return true + default: + return false + } + } + // post notes inherit permissions of the board. + return post.board.HasPermission(addr, perm) +} + +func (post *Post) GetSummary() string { + return summaryOf(post.body, 80) +} + +func (post *Post) GetURL() string { + if post.IsThread() { + return post.board.GetURLFromThreadAndReplyID( + post.id, 0) + } else { + return post.board.GetURLFromThreadAndReplyID( + post.threadID, post.id) + } +} + +func (post *Post) GetReplyFormURL() string { + return txlink.Call("CreateReply", + "bid", post.board.id.String(), + "threadid", post.threadID.String(), + "postid", post.id.String(), + ) +} + +func (post *Post) GetRepostFormURL() string { + return txlink.Call("CreateRepost", + "bid", post.board.id.String(), + "postid", post.id.String(), + ) +} + +func (post *Post) GetDeleteFormURL() string { + return txlink.Call("DeletePost", + "bid", post.board.id.String(), + "threadid", post.threadID.String(), + "postid", post.id.String(), + ) +} + +func (post *Post) RenderSummary() string { + if post.repostBoard != 0 { + dstBoard := getBoard(post.repostBoard) + if dstBoard == nil { + panic("repostBoard does not exist") + } + thread := dstBoard.GetThread(PostID(post.parentID)) + if thread == nil { + return "reposted post does not exist" + } + return "Repost: " + post.GetSummary() + "\n" + thread.RenderSummary() + } + str := "" + if post.title != "" { + str += "## [" + summaryOf(post.title, 80) + "](" + post.GetURL() + ")\n" + str += "\n" + } + str += post.GetSummary() + "\n" + str += "\\- " + displayAddressMD(post.creator) + "," + str += " [" + post.createdAt.Format("2006-01-02 3:04pm MST") + "](" + post.GetURL() + ")" + str += " \\[[x](" + post.GetDeleteFormURL() + ")]" + str += " (" + strconv.Itoa(post.replies.Size()) + " replies)" + str += " (" + strconv.Itoa(post.reposts.Size()) + " reposts)" + "\n" + return str +} + +func (post *Post) RenderPost(indent string, levels int) string { + if post == nil { + return "nil post" + } + str := "" + if post.title != "" { + str += indent + "# " + post.title + "\n" + str += indent + "\n" + } + str += indentBody(indent, post.body) + "\n" // TODO: indent body lines. + str += indent + "\\- " + displayAddressMD(post.creator) + ", " + str += "[" + post.createdAt.Format("2006-01-02 3:04pm (MST)") + "](" + post.GetURL() + ")" + str += " \\[[reply](" + post.GetReplyFormURL() + ")]" + if post.IsThread() { + str += " \\[[repost](" + post.GetRepostFormURL() + ")]" + } + str += " \\[[x](" + post.GetDeleteFormURL() + ")]\n" + if levels > 0 { + if post.replies.Size() > 0 { + post.replies.Iterate("", "", func(key string, value interface{}) bool { + str += indent + "\n" + str += value.(*Post).RenderPost(indent+"> ", levels-1) + return false + }) + } + } else { + if post.replies.Size() > 0 { + str += indent + "\n" + str += indent + "_[see all " + strconv.Itoa(post.replies.Size()) + " replies](" + post.GetURL() + ")_\n" + } + } + return str +} + +// render reply and link to context thread +func (post *Post) RenderInner() string { + if post.IsThread() { + panic("unexpected thread") + } + threadID := post.threadID + // replyID := post.id + parentID := post.parentID + str := "" + str += "_[see thread](" + post.board.GetURLFromThreadAndReplyID( + threadID, 0) + ")_\n\n" + thread := post.board.GetThread(post.threadID) + var parent *Post + if thread.id == parentID { + parent = thread + } else { + parent = thread.GetReply(parentID) + } + str += parent.RenderPost("", 0) + str += "\n" + str += post.RenderPost("> ", 5) + return str +} + +//---------------------------------------- +// Public facing functions + +func GetBoardIDFromName(name string) (BoardID, bool) { + boardI, exists := gBoardsByName.Get(name) + if !exists { + return 0, false + } + return boardI.(*Board).id, true +} + +func CreateBoard(name string) BoardID { + if !(std.IsOriginCall() || std.PrevRealm().IsUser()) { + panic("invalid non-user call") + } + bid := incGetBoardID() + caller := std.GetOrigCaller() + if usernameOf(caller) == "" { + panic("unauthorized") + } + url := "/r/demo/boards:" + name + board := newBoard(bid, url, name, caller) + bidkey := boardIDKey(bid) + gBoards.Set(bidkey, board) + gBoardsByName.Set(name, board) + return board.id +} + +func checkAnonFee() bool { + sent := std.GetOrigSend() + anonFeeCoin := std.NewCoin("ugnot", int64(gDefaultAnonFee)) + if len(sent) == 1 && sent[0].IsGTE(anonFeeCoin) { + return true + } + return false +} + +func CreateThread(bid BoardID, title string, body string) PostID { + if !(std.IsOriginCall() || std.PrevRealm().IsUser()) { + panic("invalid non-user call") + } + caller := std.GetOrigCaller() + if usernameOf(caller) == "" { + if !checkAnonFee() { + panic("please register, otherwise minimum fee " + strconv.Itoa(gDefaultAnonFee) + " is required if anonymous") + } + } + board := getBoard(bid) + if board == nil { + panic("board not exist") + } + thread := board.AddThread(caller, title, body) + return thread.id +} + +func CreateReply(bid BoardID, threadid, postid PostID, body string) PostID { + if !(std.IsOriginCall() || std.PrevRealm().IsUser()) { + panic("invalid non-user call") + } + caller := std.GetOrigCaller() + if usernameOf(caller) == "" { + if !checkAnonFee() { + panic("please register, otherwise minimum fee " + strconv.Itoa(gDefaultAnonFee) + " is required if anonymous") + } + } + board := getBoard(bid) + if board == nil { + panic("board not exist") + } + thread := board.GetThread(threadid) + if thread == nil { + panic("thread not exist") + } + if postid == threadid { + reply := thread.AddReply(caller, body) + return reply.id + } else { + post := thread.GetReply(postid) + reply := post.AddReply(caller, body) + return reply.id + } +} + +// If dstBoard is private, does not ping back. +// If board specified by bid is private, panics. +func CreateRepost(bid BoardID, postid PostID, title string, body string, dstBoardID BoardID) PostID { + if !(std.IsOriginCall() || std.PrevRealm().IsUser()) { + panic("invalid non-user call") + } + caller := std.GetOrigCaller() + if usernameOf(caller) == "" { + // TODO: allow with gDefaultAnonFee payment. + if !checkAnonFee() { + panic("please register, otherwise minimum fee " + strconv.Itoa(gDefaultAnonFee) + " is required if anonymous") + } + } + board := getBoard(bid) + if board == nil { + panic("src board not exist") + } + if board.IsPrivate() { + panic("cannot repost from a private board") + } + dst := getBoard(dstBoardID) + if dst == nil { + panic("dst board not exist") + } + thread := board.GetThread(postid) + if thread == nil { + panic("thread not exist") + } + repost := thread.AddRepostTo(caller, title, body, dst) + return repost.id +} + +func DeletePost(bid BoardID, threadid, postid PostID, reason string) { + if !(std.IsOriginCall() || std.PrevRealm().IsUser()) { + panic("invalid non-user call") + } + caller := std.GetOrigCaller() + board := getBoard(bid) + if board == nil { + panic("board not exist") + } + thread := board.GetThread(threadid) + if thread == nil { + panic("thread not exist") + } + if postid == threadid { + // delete thread + if !thread.HasPermission(caller, DeletePermission) { + panic("unauthorized") + } + board.DeleteThread(threadid) + } else { + // delete thread's post + post := thread.GetReply(postid) + if post == nil { + panic("post not exist") + } + if !post.HasPermission(caller, DeletePermission) { + panic("unauthorized") + } + thread.DeletePost(postid) + } +} + +func EditPost(bid BoardID, threadid, postid PostID, title, body string) { + if !(std.IsOriginCall() || std.PrevRealm().IsUser()) { + panic("invalid non-user call") + } + caller := std.GetOrigCaller() + board := getBoard(bid) + if board == nil { + panic("board not exist") + } + thread := board.GetThread(threadid) + if thread == nil { + panic("thread not exist") + } + if postid == threadid { + // edit thread + if !thread.HasPermission(caller, EditPermission) { + panic("unauthorized") + } + thread.Update(title, body) + } else { + // edit thread's post + post := thread.GetReply(postid) + if post == nil { + panic("post not exist") + } + if !post.HasPermission(caller, EditPermission) { + panic("unauthorized") + } + post.Update(title, body) + } +} + +//---------------------------------------- +// Render functions + +func RenderBoard(bid BoardID) string { + board := getBoard(bid) + if board == nil { + return "missing board" + } + return board.RenderBoard() +} + +func Render(path string) string { + if path == "" { + str := "These are all the boards of this realm:\n\n" + gBoards.Iterate("", "", func(key string, value interface{}) bool { + board := value.(*Board) + str += " * [" + board.url + "](" + board.url + ")\n" + return false + }) + return str + } + parts := strings.Split(path, "/") + if len(parts) == 1 { + // /r/demo/boards:BOARD_NAME + name := parts[0] + boardI, exists := gBoardsByName.Get(name) + if !exists { + return "board does not exist: " + name + } + return boardI.(*Board).RenderBoard() + } else if len(parts) == 2 { + // /r/demo/boards:BOARD_NAME/THREAD_ID + name := parts[0] + boardI, exists := gBoardsByName.Get(name) + if !exists { + return "board does not exist: " + name + } + pid, err := strconv.Atoi(parts[1]) + if err != nil { + return "invalid thread id: " + parts[1] + } + board := boardI.(*Board) + thread := board.GetThread(PostID(pid)) + if thread == nil { + return "thread does not exist with id: " + parts[1] + } + return thread.RenderPost("", 5) + } else if len(parts) == 3 { + // /r/demo/boards:BOARD_NAME/THREAD_ID/REPLY_ID + name := parts[0] + boardI, exists := gBoardsByName.Get(name) + if !exists { + return "board does not exist: " + name + } + pid, err := strconv.Atoi(parts[1]) + if err != nil { + return "invalid thread id: " + parts[1] + } + board := boardI.(*Board) + thread := board.GetThread(PostID(pid)) + if thread == nil { + return "thread does not exist with id: " + parts[1] + } + rid, err := strconv.Atoi(parts[2]) + if err != nil { + return "invalid reply id: " + parts[2] + } + reply := thread.GetReply(PostID(rid)) + if reply == nil { + return "reply does not exist with id: " + parts[2] + } + return reply.RenderInner() + } else { + return "unrecognized path " + path + } +} + +type Permission string + +const ( + DeletePermission Permission = "role:delete" + EditPermission Permission = "role:edit" +) diff --git a/gno.land/pkg/sdk/vm/keeper.go b/gno.land/pkg/sdk/vm/keeper.go index bf16cd44243..1b93cf61907 100644 --- a/gno.land/pkg/sdk/vm/keeper.go +++ b/gno.land/pkg/sdk/vm/keeper.go @@ -234,8 +234,8 @@ var reNamespace = regexp.MustCompile(`^[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/(?:r|p)/([\. // checkNamespacePermission check if the user as given has correct permssion to on the given pkg path func (vm *VMKeeper) checkNamespacePermission(ctx sdk.Context, creator crypto.Address, pkgPath string) error { - sysUsersPkg := vm.getSysUsersPkgParam(ctx) - if sysUsersPkg == "" { + sysNamesPkg := vm.getSysNamesPkgParam(ctx) + if sysNamesPkg == "" { return nil } chainDomain := vm.getChainDomainParam(ctx) @@ -257,7 +257,7 @@ func (vm *VMKeeper) checkNamespacePermission(ctx sdk.Context, creator crypto.Add username := match[1] // if `sysUsersPkg` does not exist -> skip validation. - usersPkg := store.GetPackage(sysUsersPkg, false) + usersPkg := store.GetPackage(sysNamesPkg, false) if usersPkg == nil { return nil } @@ -289,13 +289,13 @@ func (vm *VMKeeper) checkNamespacePermission(ctx sdk.Context, creator crypto.Add }) defer m.Release() - // call $sysUsersPkg.IsAuthorizedAddressForName("") - // We only need to check by name here, as address have already been check + // call sysNamesPkg.IsAuthorizedAddressForName("") + // We only need to check by name here, as addresses have already been checked mpv := gno.NewPackageNode("main", "main", nil).NewPackage() m.SetActivePackage(mpv) - m.RunDeclaration(gno.ImportD("users", sysUsersPkg)) + m.RunDeclaration(gno.ImportD("names", sysNamesPkg)) x := gno.Call( - gno.Sel(gno.Nx("users"), "IsAuthorizedAddressForName"), + gno.Sel(gno.Nx("names"), "IsAuthorizedAddressForName"), gno.Str(creator.String()), gno.Str(username), ) diff --git a/gno.land/pkg/sdk/vm/params.go b/gno.land/pkg/sdk/vm/params.go index 248fb8a81fb..c673f2b5b5f 100644 --- a/gno.land/pkg/sdk/vm/params.go +++ b/gno.land/pkg/sdk/vm/params.go @@ -3,7 +3,7 @@ package vm import "github.com/gnolang/gno/tm2/pkg/sdk" const ( - sysUsersPkgParamPath = "gno.land/r/sys/params.sys.users_pkgpath.string" + sysNamesPkgParamPath = "gno.land/r/sys/params.sys.names_pkgpath.string" chainDomainParamPath = "gno.land/r/sys/params.chain_domain.string" ) @@ -13,8 +13,8 @@ func (vm *VMKeeper) getChainDomainParam(ctx sdk.Context) string { return chainDomain } -func (vm *VMKeeper) getSysUsersPkgParam(ctx sdk.Context) string { - var sysUsersPkg string - vm.prmk.GetString(ctx, sysUsersPkgParamPath, &sysUsersPkg) - return sysUsersPkg +func (vm *VMKeeper) getSysNamesPkgParam(ctx sdk.Context) string { + var sysNamesPkg string + vm.prmk.GetString(ctx, sysNamesPkgParamPath, &sysNamesPkg) + return sysNamesPkg }