diff --git a/docs/concepts/namespaces.md b/docs/concepts/namespaces.md index 74129697c88..3c8d1f797a1 100644 --- a/docs/concepts/namespaces.md +++ b/docs/concepts/namespaces.md @@ -4,13 +4,19 @@ id: namespaces # Namespaces -Namespaces provide users with the exclusive capability to publish contracts under their designated namespaces, similar to GitHub's user and organization model. +Namespaces provide users with the exclusive capability to publish contracts under their designated namespaces, +similar to GitHub's user and organization model. -This feature is currently a work in progress (WIP). To learn more about namespaces, please checkout https://github.com/gnolang/gno/issues/1107. +:::warn 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 code which helps differentiate it from others. You can use a package path to: +A package path is a unique identifier for each package/realm. It specifies the location of the package source +code which helps differentiate it from others. You can use a package path to: - Call a specific function from a package/realm. (e.g using `gnokey maketx call`) - Import it in other packages/realms. @@ -21,7 +27,9 @@ Here's a breakdown of the structure of a package path: - Type: Defines the type of package. - `p/`: [Package](packages.md) - `r/`: [Realm](realms.md) -- Namespace: A namespace can be included after the type (e.g., user or organization name). Namespaces are a way to group related packages or realms, but currently ownership cannot be claimed. (see [Issue #1107](https://github.com/gnolang/gno/issues/1107) for more info) +- Namespace: A namespace can be included after the type (e.g., user or organization name). Namespaces are a + way to group related packages or realms, but currently ownership cannot be claimed. (see + [Issue#1107](https://github.com/gnolang/gno/issues/1107) for more info) - Remaining Path: The remaining part of the path. - Can only contain alphanumeric characters (letters and numbers) and underscores. - No special characters allowed (except underscore). @@ -33,3 +41,51 @@ Examples: - `gno.land/p/demo/avl`: This signifies a package named `avl` within the `demo` namespace. - `gno.land/r/gnoland/home`: This signifies a realm named `home` within the `gnoland` namespace. + +## 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`. + +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. + +> ex: `test1` user registering as `patrick` +```bash +$ gnokey maketx call -pkgpath gno.land/r/demo/users \ + -func Register \ + -gas-fee 1000000ugnot -gas-wanted 2000000 \ + -broadcast \ + -chainid=test4 \ + -send=20000000ugnot \ + -args '' \ + -args 'patrick' \ + -args 'My Profile Quote' test1 +``` + +:::note Chain-ID + +Do not forget to update chain id, adequate to the network you're interacting with + +::: + + +After successful registration, you can add a package under the registered namespace. + +## Anonymous Namespace + +Gno.land offers the ability to add a package without having a registered namespace. +You can do this by using your own address as a namespace. This is formatted as `{p,r}/{std.Address}/**`. + +> ex: with `test1` user adding a package `microblog` using his own address as namespace +```bash +$ gnokey maketx addpkg \ + --pkgpath "gno.land/r/g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5/microblog" \ + --pkgdir "examples/gno.land/p/demo/microblog" \ + --deposit 100000000ugnot \ + --gas-fee 1000000ugnot \ + --gas-wanted 2000000 \ + --broadcast \ + --chainid test4 \ + test1 +``` 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 index 5405a2508ec..9bcbe9ffafa 100644 --- a/examples/gno.land/r/demo/boards/z_0_b_filetest.gno +++ b/examples/gno.land/r/demo/boards/z_0_b_filetest.gno @@ -1,7 +1,7 @@ // PKGPATH: gno.land/r/demo/boards_test package boards_test -// SEND: 199000000ugnot +// SEND: 19900000ugnot import ( "gno.land/r/demo/boards" @@ -20,4 +20,4 @@ func main() { } // Error: -// payment must not be less than 200000000 +// payment must not be less than 20000000 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 1debeecba04..e20964d50b7 100644 --- a/examples/gno.land/r/demo/boards/z_0_filetest.gno +++ b/examples/gno.land/r/demo/boards/z_0_filetest.gno @@ -1,7 +1,7 @@ // PKGPATH: gno.land/r/demo/boards_test package boards_test -// SEND: 200000000ugnot +// SEND: 20000000ugnot import ( "gno.land/r/demo/boards" 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 58ecbecabdf..cf8a332174f 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 @@ -1,7 +1,7 @@ // PKGPATH: gno.land/r/demo/boards_test package boards_test -// SEND: 2000000000ugnot +// SEND: 200000000ugnot import ( "strconv" 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 50bcfac355c..d7dc7b90782 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 @@ -1,7 +1,7 @@ // PKGPATH: gno.land/r/demo/boards_test package boards_test -// SEND: 2000000000ugnot +// SEND: 200000000ugnot import ( "strconv" 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 9a067f5aac5..3aa28095502 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 @@ -1,7 +1,7 @@ // PKGPATH: gno.land/r/demo/boards_test package boards_test -// SEND: 2000000000ugnot +// SEND: 200000000ugnot import ( "strconv" 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 524b626ff6d..df764303562 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 @@ -1,7 +1,7 @@ // PKGPATH: gno.land/r/demo/boards_test package boards_test -// SEND: 2000000000ugnot +// SEND: 200000000ugnot import ( "strconv" 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 33e6ca1eea9..c114e769ab1 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 @@ -1,7 +1,7 @@ // PKGPATH: gno.land/r/demo/boards_test package boards_test -// SEND: 2000000000ugnot +// SEND: 200000000ugnot import ( "strconv" 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 9e85b8ace8c..4cbdeeca4c3 100644 --- a/examples/gno.land/r/demo/boards/z_11_filetest.gno +++ b/examples/gno.land/r/demo/boards/z_11_filetest.gno @@ -1,7 +1,7 @@ // PKGPATH: gno.land/r/demo/boards_test package boards_test -// SEND: 2000000000ugnot +// SEND: 200000000ugnot import ( "strconv" 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 105c7f19ef7..e79da5c3677 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 @@ -1,6 +1,6 @@ package main -// SEND: 2000000000ugnot +// SEND: 200000000ugnot import ( "std" 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 bb9d7a57010..176b1d89015 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 @@ -1,6 +1,6 @@ package main -// SEND: 2000000000ugnot +// SEND: 200000000ugnot import ( "std" 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 e54ac578dc1..54cfe49eec6 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 @@ -1,6 +1,6 @@ package main -// SEND: 2000000000ugnot +// SEND: 200000000ugnot import ( "std" 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 7a6b4d89eec..f1d41aa1723 100644 --- a/examples/gno.land/r/demo/boards/z_7_filetest.gno +++ b/examples/gno.land/r/demo/boards/z_7_filetest.gno @@ -1,7 +1,7 @@ // PKGPATH: gno.land/r/demo/boards_test package boards_test -// SEND: 2000000000ugnot +// SEND: 200000000ugnot import ( "gno.land/r/demo/boards" 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 e7f94a78746..18ad64083f4 100644 --- a/examples/gno.land/r/demo/boards/z_8_filetest.gno +++ b/examples/gno.land/r/demo/boards/z_8_filetest.gno @@ -1,7 +1,7 @@ // PKGPATH: gno.land/r/demo/boards_test package boards_test -// SEND: 2000000000ugnot +// SEND: 200000000ugnot import ( "strconv" 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 49e4167ae1a..6d328825dd6 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 @@ -16,4 +16,4 @@ func main() { } // Error: -// payment must not be less than 200000000 +// payment must not be less than 20000000 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 95d13c6f241..aeff9ab7774 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 @@ -13,7 +13,7 @@ import ( var gid groups.GroupID -const admin = std.Address("g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj") +const admin = std.Address("g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq") func main() { caller := std.GetOrigCaller() // main 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 d9bee0e2edc..d1cc53d612f 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 @@ -13,7 +13,7 @@ import ( var gid groups.GroupID -const admin = std.Address("g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj") +const admin = std.Address("g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq") func main() { caller := std.GetOrigCaller() // main diff --git a/examples/gno.land/r/demo/users/preregister.gno b/examples/gno.land/r/demo/users/preregister.gno new file mode 100644 index 00000000000..a6377c54938 --- /dev/null +++ b/examples/gno.land/r/demo/users/preregister.gno @@ -0,0 +1,66 @@ +package users + +import ( + "std" + + "gno.land/p/demo/users" +) + +// 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 +} + +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) + } +} diff --git a/examples/gno.land/r/demo/users/users.gno b/examples/gno.land/r/demo/users/users.gno index 5b4b8ec2c14..9b8e93b579b 100644 --- a/examples/gno.land/r/demo/users/users.gno +++ b/examples/gno.land/r/demo/users/users.gno @@ -14,13 +14,15 @@ import ( // State var ( - admin std.Address = "g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj" - 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 = 200 * 1000000 // minimum gnot must be paid to register. - maxFeeMult int64 = 10 // maximum multiples of minFee accepted. + admin std.Address = "g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq" // @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. ) //---------------------------------------- @@ -34,8 +36,10 @@ func Register(inviter std.Address, name string, profile string) { 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) { @@ -55,19 +59,35 @@ func Register(inviter std.Address, name string, profile string) { } invites.Remove(invitekey) } + // assert not already registered. _, ok := name2User.Get(name) if ok { - panic("name already registered") + panic("name already registered: " + name) } _, ok = addr2User.Get(caller.String()) if ok { - panic("address already registered") + 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. - if !reName.MatchString(name) { + // 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 { @@ -240,10 +260,31 @@ func Resolve(input users.AddressOrName) std.Address { 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 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 index 9095057076c..c33edc32985 100644 --- a/examples/gno.land/r/demo/users/z_0_b_filetest.gno +++ b/examples/gno.land/r/demo/users/z_0_b_filetest.gno @@ -1,6 +1,6 @@ package main -// SEND: 199000000ugnot +// SEND: 19900000ugnot import ( "gno.land/r/demo/users" @@ -12,4 +12,4 @@ func main() { } // Error: -// payment must not be less than 200000000 +// payment must not be less than 20000000 diff --git a/examples/gno.land/r/demo/users/z_10_filetest.gno b/examples/gno.land/r/demo/users/z_10_filetest.gno index 878fa296fdd..078058c0703 100644 --- a/examples/gno.land/r/demo/users/z_10_filetest.gno +++ b/examples/gno.land/r/demo/users/z_10_filetest.gno @@ -8,7 +8,7 @@ import ( "gno.land/r/demo/users" ) -const admin = std.Address("g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj") +const admin = std.Address("g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq") func init() { caller := std.GetOrigCaller() // main diff --git a/examples/gno.land/r/demo/users/z_11_filetest.gno b/examples/gno.land/r/demo/users/z_11_filetest.gno new file mode 100644 index 00000000000..603d63f371d --- /dev/null +++ b/examples/gno.land/r/demo/users/z_11_filetest.gno @@ -0,0 +1,25 @@ +package main + +// SEND: 200000000ugnot + +import ( + "std" + + "gno.land/r/demo/users" +) + +const admin = std.Address("g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq") + +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 new file mode 100644 index 00000000000..5e661e8f8c1 --- /dev/null +++ b/examples/gno.land/r/demo/users/z_11b_filetest.gno @@ -0,0 +1,28 @@ +package main + +// SEND: 200000000ugnot + +import ( + "std" + + "gno.land/r/demo/users" +) + +const admin = std.Address("g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq") + +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_1_filetest.gno b/examples/gno.land/r/demo/users/z_1_filetest.gno index a1c7e682022..504a0c7c3f9 100644 --- a/examples/gno.land/r/demo/users/z_1_filetest.gno +++ b/examples/gno.land/r/demo/users/z_1_filetest.gno @@ -1,6 +1,6 @@ package main -// SEND: 2000000000ugnot +// SEND: 200000000ugnot import ( "gno.land/r/demo/users" diff --git a/examples/gno.land/r/demo/users/z_2_filetest.gno b/examples/gno.land/r/demo/users/z_2_filetest.gno index a99e8fd12ee..84b62a7e483 100644 --- a/examples/gno.land/r/demo/users/z_2_filetest.gno +++ b/examples/gno.land/r/demo/users/z_2_filetest.gno @@ -1,6 +1,6 @@ package main -// SEND: 2000000000ugnot +// SEND: 200000000ugnot import ( "std" @@ -9,7 +9,7 @@ import ( "gno.land/r/demo/users" ) -const admin = std.Address("g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj") +const admin = std.Address("g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq") func main() { caller := std.GetOrigCaller() // main diff --git a/examples/gno.land/r/demo/users/z_3_filetest.gno b/examples/gno.land/r/demo/users/z_3_filetest.gno index 1cd3886152a..ce34c6bba66 100644 --- a/examples/gno.land/r/demo/users/z_3_filetest.gno +++ b/examples/gno.land/r/demo/users/z_3_filetest.gno @@ -1,6 +1,6 @@ package main -// SEND: 2000000000ugnot +// SEND: 200000000ugnot import ( "std" @@ -9,7 +9,7 @@ import ( "gno.land/r/demo/users" ) -const admin = std.Address("g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj") +const admin = std.Address("g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq") func main() { caller := std.GetOrigCaller() // main diff --git a/examples/gno.land/r/demo/users/z_4_filetest.gno b/examples/gno.land/r/demo/users/z_4_filetest.gno index 51bd255e3e7..1a46d915c96 100644 --- a/examples/gno.land/r/demo/users/z_4_filetest.gno +++ b/examples/gno.land/r/demo/users/z_4_filetest.gno @@ -1,6 +1,6 @@ package main -// SEND: 2000000000ugnot +// SEND: 200000000ugnot import ( "std" @@ -9,7 +9,7 @@ import ( "gno.land/r/demo/users" ) -const admin = std.Address("g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj") +const admin = std.Address("g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq") func main() { caller := std.GetOrigCaller() // main diff --git a/examples/gno.land/r/demo/users/z_5_filetest.gno b/examples/gno.land/r/demo/users/z_5_filetest.gno index 5a96eec5322..4ab68ec0e0b 100644 --- a/examples/gno.land/r/demo/users/z_5_filetest.gno +++ b/examples/gno.land/r/demo/users/z_5_filetest.gno @@ -1,6 +1,6 @@ package main -// SEND: 2000000000ugnot +// SEND: 200000000ugnot import ( "std" @@ -9,7 +9,7 @@ import ( "gno.land/r/demo/users" ) -const admin = std.Address("g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj") +const admin = std.Address("g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq") func main() { caller := std.GetOrigCaller() // main @@ -36,8 +36,17 @@ func main() { } // Output: -// * [gnouser](/r/demo/users:gnouser) +// * [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) +// * [x](/r/demo/users:x) // // ======================================== // ## user gnouser diff --git a/examples/gno.land/r/demo/users/z_6_filetest.gno b/examples/gno.land/r/demo/users/z_6_filetest.gno index 008ba03a936..85305fff1ad 100644 --- a/examples/gno.land/r/demo/users/z_6_filetest.gno +++ b/examples/gno.land/r/demo/users/z_6_filetest.gno @@ -6,7 +6,7 @@ import ( "gno.land/r/demo/users" ) -const admin = std.Address("g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj") +const admin = std.Address("g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq") func main() { caller := std.GetOrigCaller() diff --git a/examples/gno.land/r/demo/users/z_7_filetest.gno b/examples/gno.land/r/demo/users/z_7_filetest.gno index 60efc3b91ea..3332ab49af4 100644 --- a/examples/gno.land/r/demo/users/z_7_filetest.gno +++ b/examples/gno.land/r/demo/users/z_7_filetest.gno @@ -1,6 +1,6 @@ package main -// SEND: 2000000000ugnot +// SEND: 200000000ugnot import ( "std" @@ -9,7 +9,7 @@ import ( "gno.land/r/demo/users" ) -const admin = std.Address("g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj") +const admin = std.Address("g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq") func main() { caller := std.GetOrigCaller() // main diff --git a/examples/gno.land/r/demo/users/z_7b_filetest.gno b/examples/gno.land/r/demo/users/z_7b_filetest.gno index 81f1076b3d1..60a397abe79 100644 --- a/examples/gno.land/r/demo/users/z_7b_filetest.gno +++ b/examples/gno.land/r/demo/users/z_7b_filetest.gno @@ -1,6 +1,6 @@ package main -// SEND: 2000000000ugnot +// SEND: 200000000ugnot import ( "std" @@ -9,7 +9,7 @@ import ( "gno.land/r/demo/users" ) -const admin = std.Address("g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj") +const admin = std.Address("g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq") func main() { caller := std.GetOrigCaller() // main diff --git a/examples/gno.land/r/demo/users/z_8_filetest.gno b/examples/gno.land/r/demo/users/z_8_filetest.gno index 56c13862257..1eaa017b7d2 100644 --- a/examples/gno.land/r/demo/users/z_8_filetest.gno +++ b/examples/gno.land/r/demo/users/z_8_filetest.gno @@ -1,6 +1,6 @@ package main -// SEND: 2000000000ugnot +// SEND: 200000000ugnot import ( "std" @@ -9,7 +9,7 @@ import ( "gno.land/r/demo/users" ) -const admin = std.Address("g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj") +const admin = std.Address("g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq") func main() { caller := std.GetOrigCaller() // main diff --git a/examples/gno.land/r/demo/users/z_9_filetest.gno b/examples/gno.land/r/demo/users/z_9_filetest.gno index e77a68d7ff9..2bd9bf555dc 100644 --- a/examples/gno.land/r/demo/users/z_9_filetest.gno +++ b/examples/gno.land/r/demo/users/z_9_filetest.gno @@ -7,7 +7,7 @@ import ( "gno.land/r/demo/users" ) -const admin = std.Address("g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj") +const admin = std.Address("g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq") func main() { caller := std.GetOrigCaller() // main diff --git a/examples/gno.land/r/sys/names/genesis.gno b/examples/gno.land/r/sys/names/genesis.gno deleted file mode 100644 index 7e4ae51645f..00000000000 --- a/examples/gno.land/r/sys/names/genesis.gno +++ /dev/null @@ -1,30 +0,0 @@ -package names - -import "std" - -func init() { - // Please, do not edit this file to reserve your username, use a transaction instead. - var ( - jaekwon = std.Address("g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj") - manfred = std.Address("g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq") - test1 = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") - reservedAdmin = std.Address("g100000000000000000000000000000000000000") // FIXME: create a multisig. - reservedNames = []string{ - // FIXME: complete this list. - "gno", "gnolang", "tendermint", "cosmos", "hub", "admin", - "ethereum", "bitcoin", - // FIXME: reserve brands? then, require KYC to unlock? - } - ) - namespaces.Set("demo", &Space{Admins: []std.Address{jaekwon, manfred}}) - namespaces.Set("gnoland", &Space{Admins: []std.Address{jaekwon, manfred}}) - namespaces.Set("sys", &Space{Admins: []std.Address{jaekwon, manfred}}) - namespaces.Set("gov", &Space{Admins: []std.Address{jaekwon, manfred}}) - namespaces.Set("jaekwon", &Space{Admins: []std.Address{jaekwon}}) - namespaces.Set("manfred", &Space{Admins: []std.Address{manfred}}) - namespaces.Set("test1", &Space{Admins: []std.Address{test1}}) - - for _, keyword := range reservedNames { - namespaces.Set(keyword, &Space{Admins: []std.Address{reservedAdmin}}) - } -} diff --git a/examples/gno.land/r/sys/names/gno.mod b/examples/gno.land/r/sys/names/gno.mod deleted file mode 100644 index 97236a84892..00000000000 --- a/examples/gno.land/r/sys/names/gno.mod +++ /dev/null @@ -1,3 +0,0 @@ -module gno.land/r/sys/names - -require gno.land/p/demo/avl v0.0.0-latest diff --git a/examples/gno.land/r/sys/names/names.gno b/examples/gno.land/r/sys/names/names.gno deleted file mode 100644 index d73ec59b2c5..00000000000 --- a/examples/gno.land/r/sys/names/names.gno +++ /dev/null @@ -1,60 +0,0 @@ -// The realm r/sys/names is used to manage namespaces on gno.land. -package names - -import ( - "std" - - "gno.land/p/demo/avl" -) - -// "AddPkg" will check if r/sys/names exists. If yes, it will -// inspect the realm's state and use the following variable to -// determine if an address can publish a package or not. -var namespaces avl.Tree // name(string) -> Space - -type Space struct { - Admins []std.Address - Editors []std.Address - InPause bool -} - -func Register(namespace string) { - // TODO: input sanitization: - // - already exists / reserved. - // - min/max length, format. - // - fees (dynamic, based on length). - panic("not implemented") -} - -func AddAdmin(namespace string, newAdmin std.Address) { - // TODO: assertIsAdmin() - panic("not implemented") -} - -func RemoveAdmin(namespace string, newAdmin std.Address) { - // TODO: assertIsAdmin() - // TODO: check if self. - panic("not implemented") -} - -func AddEditor(namespace string, newAdmin std.Address) { - // TODO: assertIsAdmin() - panic("not implemented") -} - -func RemoveEditor(namespace string, newAdmin std.Address) { - // TODO: assertIsAdmin() - // TODO: check if self. - panic("not implemented") -} - -func SetInPause(namespace string, state bool) { - // TODO: assertIsAdmin() - panic("not implemented") -} - -func Render(path string) string { - // TODO: by namespace. - // TODO: by address. - return "not implemented" -} diff --git a/examples/gno.land/r/sys/users/gno.mod b/examples/gno.land/r/sys/users/gno.mod new file mode 100644 index 00000000000..774a364a272 --- /dev/null +++ b/examples/gno.land/r/sys/users/gno.mod @@ -0,0 +1,6 @@ +module gno.land/r/sys/users + +require ( + gno.land/p/demo/ownable v0.0.0-latest + gno.land/r/demo/users v0.0.0-latest +) diff --git a/examples/gno.land/r/sys/users/verify.gno b/examples/gno.land/r/sys/users/verify.gno new file mode 100644 index 00000000000..852626622e4 --- /dev/null +++ b/examples/gno.land/r/sys/users/verify.gno @@ -0,0 +1,83 @@ +package users + +import ( + "std" + + "gno.land/p/demo/ownable" + "gno.land/r/demo/users" +) + +const admin = "g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq" // @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 err := owner.CallerIsOwner(); err != nil { + panic(err) + } + + enabled = true +} + +// Disable this package. +func AdminDisable() { + if err := owner.CallerIsOwner(); err != nil { + panic(err) + } + + enabled = false +} + +// AdminUpdateVerifyCall updates the method that verifies the namespace. +func AdminUpdateVerifyCall(check VerifyNameFunc) { + if err := owner.CallerIsOwner(); err != nil { + panic(err) + } + + checkFunc = check +} + +// AdminTransferOwnership transfers the ownership to a new owner. +func AdminTransferOwnership(newOwner std.Address) error { + if err := owner.CallerIsOwner(); err != nil { + panic(err) + } + + return owner.TransferOwnership(newOwner) +} diff --git a/gno.land/cmd/gnoland/testdata/addpkg_namespace.txtar b/gno.land/cmd/gnoland/testdata/addpkg_namespace.txtar new file mode 100644 index 00000000000..5a88fd6d603 --- /dev/null +++ b/gno.land/cmd/gnoland/testdata/addpkg_namespace.txtar @@ -0,0 +1,88 @@ +loadpkg gno.land/r/demo/users +loadpkg gno.land/r/sys/users + +adduser admin +adduser gui + +patchpkg "g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq" $USER_ADDR_admin # use our custom admin + +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 2000000 -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/$USER_ADDR_test1/mysuperpkg -gas-fee 1000000ugnot -gas-wanted 100000000 -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 100000000 -broadcast -chainid=tendermint_test gui +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 2000000 -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 2000000 -broadcast -chainid tendermint_test gui +stdout 'OK!' +stdout 'true' + +# Try to add a pkg an with unregistered user +# gui addpkg -> gno.land/r//one +! gnokey maketx addpkg -pkgdir $WORK -pkgpath gno.land/r/$USER_ADDR_test1/one -gas-fee 1000000ugnot -gas-wanted 100000000 -broadcast -chainid=tendermint_test gui +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/$USER_ADDR_gui/one -gas-fee 1000000ugnot -gas-wanted 100000000 -broadcast -chainid=tendermint_test gui +stdout 'OK!' + +## Test unregistered namespace + +# Call addpkg with admin user on gui namespace +# admin addpkg -> gno.land/r/guiland/one +! gnokey maketx addpkg -pkgdir $WORK -pkgpath gno.land/r/guiland/one -gas-fee 1000000ugnot -gas-wanted 100000000 -broadcast -chainid=tendermint_test admin +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 2000000 -broadcast -chainid=tendermint_test -args $USER_ADDR_gui 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 2000000 -broadcast -chainid=tendermint_test -args $USER_ADDR_admin -args 'guiland' -args 'im gui' 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 100000000 -broadcast -chainid=tendermint_test gui +stdout 'OK!' + +# Test admin publishing on guiland/two +# admin addpkg -> gno.land/r/guiland/two +! gnokey maketx addpkg -pkgdir $WORK -pkgpath gno.land/r/guiland/two -gas-fee 1000000ugnot -gas-wanted 100000000 -broadcast -chainid=tendermint_test admin +stderr 'unauthorized user' + +-- one.gno -- +package one + +func Render(path string) string { + return "# Hello One" +} diff --git a/gno.land/genesis/genesis_txs.jsonl b/gno.land/genesis/genesis_txs.jsonl index 278055711b0..daf9fbdc5d4 100644 --- a/gno.land/genesis/genesis_txs.jsonl +++ b/gno.land/genesis/genesis_txs.jsonl @@ -1,12 +1,12 @@ -{"msg":[{"@type":"/vm.m_call","caller":"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj","send":"","pkg_path":"gno.land/r/demo/users","func":"Invite","args":["g1589c8cekvmjfmy0qrd4f3z52r7fn7rgk02667s: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\ng1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq:10\ng14da4n9hcynyzz83q607uu8keuh9hwlv42ra6fa:10\ng14vhcdsyf83ngsrrqc92kmw8q9xakqjm0v8448t:5\n"]}],"fee":{"gas_wanted":"2000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AmG6kzznyo1uNqWPAYU6wDpsmzQKDaEOrVRaZ08vOyX0"},"signature":"S8iMMzlOMK8dmox78R9Z8+pSsS8YaTCXrIcaHDpiOgkOy7gqoQJ0oftM0zf8zAz4xpezK8Lzg8Q0fCdXJxV76w=="}],"memo":""} -{"msg":[{"@type":"/vm.m_call","caller":"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj","send":"","pkg_path":"gno.land/r/demo/users","func":"Invite","args":["g1thlf3yct7n7ex70k0p62user0kn6mj6d3s0cg3\ng1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\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":""} -{"msg":[{"@type":"/vm.m_call","caller":"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj","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\ng1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq\ng14da4n9hcynyzz83q607uu8keuh9hwlv42ra6fa\ng14vhcdsyf83ngsrrqc92kmw8q9xakqjm0v8448t\n"]}],"fee":{"gas_wanted":"4000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AmG6kzznyo1uNqWPAYU6wDpsmzQKDaEOrVRaZ08vOyX0"},"signature":"7AmlhZhsVkxCUl0bbpvpPMnIKihwtG7A5IFR6Tg4xStWLgaUr05XmWRKlO2xjstTtwbVKQT5mFL4h5wyX4SQzw=="}],"memo":""} -{"msg":[{"@type":"/vm.m_call","caller":"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj","send":"2000000000ugnot","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":""} -{"msg":[{"@type":"/vm.m_call","caller":"g1qpymzwx4l4cy6cerdyajp9ksvjsf20rk5y9rtt","send":"2000000000ugnot","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":""} -{"msg":[{"@type":"/vm.m_call","caller":"g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq","send":"2000000000ugnot","pkg_path":"gno.land/r/demo/users","func":"Register","args":["","manfred","https://github.com/moul"]}],"fee":{"gas_wanted":"2000000","gas_fee":"2000000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AnK+a6mcFDjY6b/v6p7r8QFW1M1PgIoQxBgrwOoyY7v3"},"signature":"9CWeNbKx+hEL+RdHplAVAFntcrAVx5mK9tMqoywuHVoreH844n3yOxddQrGfBk6T2tMBmNWakERRqWZfS+bYAQ=="}],"memo":""} -{"msg":[{"@type":"/vm.m_call","caller":"g1fj9jccm3zjnqspq7lp2g7lj4czyfq0s35600g9","send":"2000000000ugnot","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":""} -{"msg":[{"@type":"/vm.m_call","caller":"g1ds24jj9kqjcskd0gzu24r9e4n62ggye230zuv5","send":"","pkg_path":"gno.land/r/demo/users","func":"Register","args":["g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj","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":""} -{"msg":[{"@type":"/vm.m_call","caller":"g15gdm49ktawvkrl88jadqpucng37yxutucuwaef","send":"2000000000ugnot","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":""} +{"msg":[{"@type":"/vm.m_call","caller":"g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq","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":""} +{"msg":[{"@type":"/vm.m_call","caller":"g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq","send":"","pkg_path":"gno.land/r/demo/users","func":"Invite","args":["g1thlf3yct7n7ex70k0p62user0kn6mj6d3s0cg3\ng1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\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":""} +{"msg":[{"@type":"/vm.m_call","caller":"g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq","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\ng1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq\ng14da4n9hcynyzz83q607uu8keuh9hwlv42ra6fa\ng14vhcdsyf83ngsrrqc92kmw8q9xakqjm0v8448t\n"]}],"fee":{"gas_wanted":"4000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AmG6kzznyo1uNqWPAYU6wDpsmzQKDaEOrVRaZ08vOyX0"},"signature":"7AmlhZhsVkxCUl0bbpvpPMnIKihwtG7A5IFR6Tg4xStWLgaUr05XmWRKlO2xjstTtwbVKQT5mFL4h5wyX4SQzw=="}],"memo":""} +{"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":""} +{"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":""} +{"msg":[{"@type":"/vm.m_call","caller":"g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq","send":"200000000ugnot","pkg_path":"gno.land/r/demo/users","func":"Register","args":["","manfred","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":""} +{"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":""} +{"msg":[{"@type":"/vm.m_call","caller":"g1ds24jj9kqjcskd0gzu24r9e4n62ggye230zuv5","send":"","pkg_path":"gno.land/r/demo/users","func":"Register","args":["g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq","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":""} +{"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":""} {"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":""} {"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":""} {"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":""} diff --git a/gno.land/pkg/gnoweb/gnoweb_test.go b/gno.land/pkg/gnoweb/gnoweb_test.go index d6b93b37d69..b266dc80a6a 100644 --- a/gno.land/pkg/gnoweb/gnoweb_test.go +++ b/gno.land/pkg/gnoweb/gnoweb_test.go @@ -49,7 +49,9 @@ func TestRoutes(t *testing.T) { {"/p/demo/flow/LICENSE", ok, "BSD 3-Clause"}, } - config, _ := integration.TestingNodeConfig(t, gnoenv.RootDir()) + rootdir := gnoenv.RootDir() + genesis := integration.LoadDefaultGenesisTXsFile(t, "tendermint_test", rootdir) + config, _ := integration.TestingNodeConfig(t, rootdir, genesis...) node, remoteAddr := integration.TestingInMemoryNode(t, log.NewTestingLogger(t), config) defer node.Stop() @@ -96,7 +98,9 @@ func TestAnalytics(t *testing.T) { "/404-not-found", } - config, _ := integration.TestingNodeConfig(t, gnoenv.RootDir()) + rootdir := gnoenv.RootDir() + genesis := integration.LoadDefaultGenesisTXsFile(t, "tendermint_test", rootdir) + config, _ := integration.TestingNodeConfig(t, rootdir, genesis...) node, remoteAddr := integration.TestingInMemoryNode(t, log.NewTestingLogger(t), config) defer node.Stop() diff --git a/gno.land/pkg/integration/doc.go b/gno.land/pkg/integration/doc.go index bea0fe78349..2b6d24c23b8 100644 --- a/gno.land/pkg/integration/doc.go +++ b/gno.land/pkg/integration/doc.go @@ -45,6 +45,12 @@ // - It's important to note that the load order is significant when using multiple `loadpkg` // command; packages should be loaded in the order they are dependent upon. // +// 6. `patchpkg`: +// - Patches any loaded files by package by replacing all occurrences of the first argument with the second. +// - This is mostly used to replace hardcoded addresses from loaded packages. +// - NOTE: this command may only be temporary, as it's not best approach to +// solve the above problem +// // Logging: // // Gnoland logs aren't forwarded to stdout to avoid overwhelming the tests with too much diff --git a/gno.land/pkg/integration/testdata/patchpkg.txtar b/gno.land/pkg/integration/testdata/patchpkg.txtar new file mode 100644 index 00000000000..c5962709625 --- /dev/null +++ b/gno.land/pkg/integration/testdata/patchpkg.txtar @@ -0,0 +1,20 @@ +loadpkg gno.land/r/dev/admin $WORK + +adduser dev + +patchpkg "g1abcde" $USER_ADDR_dev + +gnoland start + +gnokey maketx call -pkgpath gno.land/r/dev/admin -func Render -gas-fee 1000000ugnot -gas-wanted 2000000 -args '' -broadcast -chainid=tendermint_test test1 +! stdout g1abcde +stdout $USER_ADDR_dev + +-- admin.gno -- +package admin + +var admin = "g1abcde" + +func Render(path string) string { + return "# Hello "+admin +} diff --git a/gno.land/pkg/integration/testing_integration.go b/gno.land/pkg/integration/testing_integration.go index 654dda0b45e..0462b0c7639 100644 --- a/gno.land/pkg/integration/testing_integration.go +++ b/gno.land/pkg/integration/testing_integration.go @@ -15,6 +15,7 @@ import ( "github.com/gnolang/gno/gno.land/pkg/gnoland" "github.com/gnolang/gno/gno.land/pkg/keyscli" "github.com/gnolang/gno/gno.land/pkg/log" + "github.com/gnolang/gno/gno.land/pkg/sdk/vm" "github.com/gnolang/gno/gnovm/pkg/gnoenv" "github.com/gnolang/gno/gnovm/pkg/gnomod" "github.com/gnolang/gno/tm2/pkg/bft/node" @@ -259,7 +260,7 @@ func setupGnolandTestScript(t *testing.T, txtarDir string) testscript.Params { err = cmd.ParseAndRun(context.Background(), args) tsValidateError(ts, "gnokey", neg, err) }, - // adduser commands must be executed before starting the node; it errors out otherwise. + // adduser command must be executed before starting the node; it errors out otherwise. "adduser": func(ts *testscript.TestScript, neg bool, args []string) { if nodeIsRunning(nodes, getNodeSID(ts)) { tsValidateError(ts, "adduser", neg, errors.New("adduser must be used before starting node")) @@ -339,7 +340,24 @@ func setupGnolandTestScript(t *testing.T, txtarDir string) testscript.Params { fmt.Fprintf(ts.Stdout(), "Added %s(%s) to genesis", args[0], balance.Address) }, - // `loadpkg` load a specific package from the 'examples' or working directory + // `patchpkg` Patch any loaded files by packages by replacing all occurrences of the + // first argument with the second. + // This is mostly use to replace hardcoded address inside txtar file. + "patchpkg": func(ts *testscript.TestScript, neg bool, args []string) { + args, err := unquote(args) + if err != nil { + tsValidateError(ts, "patchpkg", neg, err) + } + + if len(args) != 2 { + ts.Fatalf("`patchpkg`: should have exactly 2 arguments") + } + + pkgs := ts.Value(envKeyPkgsLoader).(*pkgsLoader) + replace, with := args[0], args[1] + pkgs.SetPatch(replace, with) + }, + // `loadpkg` load a specific package from the 'examples' or working directory. "loadpkg": func(ts *testscript.TestScript, neg bool, args []string) { // special dirs workDir := ts.Getenv("WORK") @@ -575,12 +593,17 @@ func createAccountFrom(env envSetter, kb keys.Keybase, accountName, mnemonic str type pkgsLoader struct { pkgs []gnomod.Pkg visited map[string]struct{} + + // list of occurrences to patchs with the given value + // XXX: find a better way + patchs map[string]string } func newPkgsLoader() *pkgsLoader { return &pkgsLoader{ pkgs: make([]gnomod.Pkg, 0), visited: make(map[string]struct{}), + patchs: make(map[string]string), } } @@ -588,6 +611,10 @@ func (pl *pkgsLoader) List() gnomod.PkgList { return pl.pkgs } +func (pl *pkgsLoader) SetPatch(replace, with string) { + pl.patchs[replace] = with +} + func (pl *pkgsLoader) LoadPackages(creator bft.Address, fee std.Fee, deposit std.Coins) ([]std.Tx, error) { pkgslist, err := pl.List().Sort() // sorts packages by their dependencies. if err != nil { @@ -600,6 +627,27 @@ func (pl *pkgsLoader) LoadPackages(creator bft.Address, fee std.Fee, deposit std if err != nil { return nil, fmt.Errorf("unable to load pkg %q: %w", pkg.Name, err) } + + // If any replace value is specified, apply them + if len(pl.patchs) > 0 { + for _, msg := range tx.Msgs { + addpkg, ok := msg.(vm.MsgAddPackage) + if !ok { + continue + } + + if addpkg.Package == nil { + continue + } + + for _, file := range addpkg.Package.Files { + for replace, with := range pl.patchs { + file.Body = strings.ReplaceAll(file.Body, replace, with) + } + } + } + } + txs[i] = tx } diff --git a/gno.land/pkg/integration/testing_node.go b/gno.land/pkg/integration/testing_node.go index 06020d9c94b..993386f6b04 100644 --- a/gno.land/pkg/integration/testing_node.go +++ b/gno.land/pkg/integration/testing_node.go @@ -42,7 +42,7 @@ func TestingInMemoryNode(t TestingTS, logger *slog.Logger, config *gnoland.InMem // TestingNodeConfig constructs an in-memory node configuration // with default packages and genesis transactions already loaded. // It will return the default creator address of the loaded packages. -func TestingNodeConfig(t TestingTS, gnoroot string) (*gnoland.InMemoryNodeConfig, bft.Address) { +func TestingNodeConfig(t TestingTS, gnoroot string, additionalTxs ...std.Tx) (*gnoland.InMemoryNodeConfig, bft.Address) { cfg := TestingMinimalNodeConfig(t, gnoroot) creator := crypto.MustAddressFromString(DefaultAccount_Address) // test1 @@ -50,7 +50,7 @@ func TestingNodeConfig(t TestingTS, gnoroot string) (*gnoland.InMemoryNodeConfig balances := LoadDefaultGenesisBalanceFile(t, gnoroot) txs := []std.Tx{} txs = append(txs, LoadDefaultPackages(t, creator, gnoroot)...) - txs = append(txs, LoadDefaultGenesisTXsFile(t, cfg.Genesis.ChainID, gnoroot)...) + txs = append(txs, additionalTxs...) cfg.Genesis.AppState = gnoland.GnoGenesisState{ Balances: balances, diff --git a/gno.land/pkg/sdk/vm/errors.go b/gno.land/pkg/sdk/vm/errors.go index 0020e989eb6..a0e71e08d14 100644 --- a/gno.land/pkg/sdk/vm/errors.go +++ b/gno.land/pkg/sdk/vm/errors.go @@ -15,18 +15,20 @@ func (abciError) AssertABCIError() {} // declare all script errors. // NOTE: these are meant to be used in conjunction with pkgs/errors. type ( - InvalidPkgPathError struct{ abciError } - InvalidStmtError struct{ abciError } - InvalidExprError struct{ abciError } - TypeCheckError struct { + InvalidPkgPathError struct{ abciError } + InvalidStmtError struct{ abciError } + InvalidExprError struct{ abciError } + UnauthorizedUserError struct{ abciError } + TypeCheckError struct { abciError Errors []string `json:"errors"` } ) -func (e InvalidPkgPathError) Error() string { return "invalid package path" } -func (e InvalidStmtError) Error() string { return "invalid statement" } -func (e InvalidExprError) Error() string { return "invalid expression" } +func (e InvalidPkgPathError) Error() string { return "invalid package path" } +func (e InvalidStmtError) Error() string { return "invalid statement" } +func (e InvalidExprError) Error() string { return "invalid expression" } +func (e UnauthorizedUserError) Error() string { return "unauthorized user" } func (e TypeCheckError) Error() string { var bld strings.Builder bld.WriteString("invalid gno package; type check errors:\n") @@ -34,6 +36,10 @@ func (e TypeCheckError) Error() string { return bld.String() } +func ErrUnauthorizedUser(msg string) error { + return errors.Wrap(UnauthorizedUserError{}, msg) +} + func ErrInvalidPkgPath(msg string) error { return errors.Wrap(InvalidPkgPathError{}, msg) } diff --git a/gno.land/pkg/sdk/vm/gas_test.go b/gno.land/pkg/sdk/vm/gas_test.go index 35706325c20..66655994bd4 100644 --- a/gno.land/pkg/sdk/vm/gas_test.go +++ b/gno.land/pkg/sdk/vm/gas_test.go @@ -67,7 +67,9 @@ func TestAddPkgDeliverTx(t *testing.T) { gasDeliver := gctx.GasMeter().GasConsumed() assert.True(t, res.IsOK()) - assert.Equal(t, int64(91825), gasDeliver) + + // NOTE: let's try to keep this bellow 100_000 :) + assert.Equal(t, int64(92825), gasDeliver) } // Enough gas for a failed transaction. diff --git a/gno.land/pkg/sdk/vm/keeper.go b/gno.land/pkg/sdk/vm/keeper.go index 78786cf0b4d..9899afa2eac 100644 --- a/gno.land/pkg/sdk/vm/keeper.go +++ b/gno.land/pkg/sdk/vm/keeper.go @@ -9,12 +9,14 @@ import ( "log/slog" "os" "path/filepath" + "regexp" "strings" "sync" "time" gno "github.com/gnolang/gno/gnovm/pkg/gnolang" "github.com/gnolang/gno/gnovm/stdlibs" + "github.com/gnolang/gno/tm2/pkg/crypto" "github.com/gnolang/gno/tm2/pkg/db/memdb" "github.com/gnolang/gno/tm2/pkg/errors" osm "github.com/gnolang/gno/tm2/pkg/os" @@ -247,6 +249,88 @@ func (vm *VMKeeper) getGnoStore(ctx sdk.Context) gno.Store { } } +// Namespace can be either a user or crypto address. +var reNamespace = regexp.MustCompile(`^gno.land/(?:r|p)/([\.~_a-zA-Z0-9]+)`) + +// 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 { + const sysUsersPkg = "gno.land/r/sys/users" + + store := vm.getGnoStore(ctx) + + match := reNamespace.FindStringSubmatch(pkgPath) + switch len(match) { + case 0: + return ErrInvalidPkgPath(pkgPath) // no match + case 2: // ok + default: + panic("invalid pattern while matching pkgpath") + } + if len(match) != 2 { + return ErrInvalidPkgPath(pkgPath) + } + username := match[1] + + // if `sysUsersPkg` does not exist -> skip validation. + usersPkg := store.GetPackage(sysUsersPkg, false) + if usersPkg == nil { + return nil + } + + // Parse and run the files, construct *PV. + pkgAddr := gno.DerivePkgAddr(pkgPath) + msgCtx := stdlibs.ExecContext{ + ChainID: ctx.ChainID(), + Height: ctx.BlockHeight(), + Timestamp: ctx.BlockTime().Unix(), + OrigCaller: creator.Bech32(), + OrigSendSpent: new(std.Coins), + OrigPkgAddr: pkgAddr.Bech32(), + // XXX: should we remove the banker ? + Banker: NewSDKBanker(vm, ctx), + EventLogger: ctx.EventLogger(), + } + + m := gno.NewMachineWithOptions( + gno.MachineOptions{ + PkgPath: "", + Output: os.Stdout, // XXX + Store: store, + Context: msgCtx, + Alloc: store.GetAllocator(), + MaxCycles: vm.maxCycles, + GasMeter: ctx.GasMeter(), + }) + defer m.Release() + + // call $sysUsersPkg.IsAuthorizedAddressForName("") + // We only need to check by name here, as address have already been check + mpv := gno.NewPackageNode("main", "main", nil).NewPackage() + m.SetActivePackage(mpv) + m.RunDeclaration(gno.ImportD("users", sysUsersPkg)) + x := gno.Call( + gno.Sel(gno.Nx("users"), "IsAuthorizedAddressForName"), + gno.Str(creator.String()), + gno.Str(username), + ) + + ret := m.Eval(x) + if len(ret) == 0 { + panic("call: invalid response length") + } + + useraddress := ret[0] + if useraddress.T.Kind() != gno.BoolKind { + panic("call: invalid response kind") + } + + if isAuthorized := useraddress.GetBool(); !isAuthorized { + return ErrUnauthorizedUser(username) + } + + return nil +} + // AddPackage adds a package with given fileset. func (vm *VMKeeper) AddPackage(ctx sdk.Context, msg MsgAddPackage) (err error) { creator := msg.Creator @@ -284,9 +368,9 @@ func (vm *VMKeeper) AddPackage(ctx sdk.Context, msg MsgAddPackage) (err error) { // TODO: ACLs. // - if r/system/names does not exists -> skip validation. // - loads r/system/names data state. - // - lookup r/system/names.namespaces for `{r,p}/NAMES`. - // - check if caller is in Admins or Editors. - // - check if namespace is not in pause. + if err := vm.checkNamespacePermission(ctx, creator, pkgPath); err != nil { + return err + } err = vm.bank.SendCoins(ctx, creator, pkgAddr, deposit) if err != nil { diff --git a/gno.land/pkg/sdk/vm/package.go b/gno.land/pkg/sdk/vm/package.go index b2e7fbecfc4..e62a7b53928 100644 --- a/gno.land/pkg/sdk/vm/package.go +++ b/gno.land/pkg/sdk/vm/package.go @@ -21,4 +21,5 @@ var Package = amino.RegisterPackage(amino.NewPackage( InvalidStmtError{}, "InvalidStmtError", InvalidExprError{}, "InvalidExprError", TypeCheckError{}, "TypeCheckError", + UnauthorizedUserError{}, "UnauthorizedUserError", ))