Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 78 additions & 0 deletions .github/workflows/realm-tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
name: Realm tests

on:
pull_request:
branches: [main]
push:
branches: [main]

jobs:
realm-tests:
runs-on: ubuntu-latest

steps:
- name: Checkout dsocial
uses: actions/checkout@v4

- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: "1.24"

- name: Checkout gnolang/gno
uses: actions/checkout@v4
with:
repository: gnolang/gno
# Pin to the gno version this realm was developed against.
# Update this ref when bumping gno compatibility.
ref: 2efd147d9124e29a57146913d93a3e1f983e083d
path: .gno

- name: Cache Go build
uses: actions/cache@v4
with:
path: |
~/.cache/go-build
~/go/pkg/mod
key: ${{ runner.os }}-go-gno-${{ hashFiles('.gno/go.sum', '.gno/contribs/gnodev/go.sum') }}
restore-keys: ${{ runner.os }}-go-gno-

- name: Install gnokey
working-directory: .gno
run: go install ./gno.land/cmd/gnokey

- name: Install gnodev
working-directory: .gno/contribs/gnodev
run: go install .

# The test1 key has a fixed address on every gnodev instance.
# We deploy the realm under that address so no namespace registration
# is required, and gnomod.toml matches the --pkgpath exactly.
- name: Patch gnomod.toml for CI
env:
TEST1_ADDR: g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5
run: |
sed -i "s|module = \"gno.land/r/berty/social\"|module = \"gno.land/r/${TEST1_ADDR}/social\"|" realm/gnomod.toml

- name: Start gnodev
env:
GNOROOT: ${{ github.workspace }}/.gno
run: |
gnodev staging --no-web --no-watch realm/ > /tmp/gnodev.log 2>&1 &
echo $! > /tmp/gnodev.pid
# Wait up to 60s for the RPC endpoint to be ready
for i in $(seq 1 60); do
curl -sf http://localhost:26657/status > /dev/null 2>&1 && break
sleep 1
done
curl -sf http://localhost:26657/status > /dev/null 2>&1 \
|| { echo "gnodev did not start in time"; cat /tmp/gnodev.log; exit 1; }

- name: Run realm tests
env:
PKG_PATH: gno.land/r/g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5/social
run: ./realm/tests/test.sh

- name: Stop gnodev
if: always()
run: kill $(cat /tmp/gnodev.pid) 2>/dev/null || true
2 changes: 1 addition & 1 deletion .tool-versions
Original file line number Diff line number Diff line change
@@ -1 +1 @@
golang 1.22.5
golang 1.24.4
48 changes: 48 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,51 @@ To post a reply, enter the following where THREADID and POSTID are both the thre
(change the account address and jefft000 to your account):

gnokey maketx call -pkgpath "gno.land/r/berty/social" -func "PostReply" -args "g1juz2yxmdsa6audkp6ep9vfv80c8p5u76e03vvh" -args THREADID -args POSTID -args "my reply" -gas-fee "1000000ugnot" -gas-wanted "100000000" -broadcast -chainid dev -remote 127.0.0.1:26657 jefft000

## Deploy the realm (addpkg)

Before deploying, update `realm/gnomod.toml` so the `module` field matches the `--pkgpath` you intend to use:

```toml
module = "gno.land/r/<your-address>/social"
gno = "0.9"
```

Then deploy with `gnokey`:

gnokey maketx addpkg \
--pkgpath "gno.land/r/<your-address>/social" \
--pkgdir ./realm \
--gas-fee 1000000ugnot \
--gas-wanted 50000000 \
--broadcast \
--chainid <chain-id> \
--remote <rpc-endpoint> \
<your-key>

> **Important:** `gnomod.toml` `module` and `--pkgpath` must be identical. A mismatch causes a VM panic (`unexpected node with location`) when any transaction is executed against the realm.

## Integration tests

`realm/tests/test.sh` is a self-contained shell test suite that exercises the realm end-to-end using `gnokey`. It creates a temporary keybase, imports the `test1` funder key, generates fresh alice/bob keys, funds them if needed, and runs through the main flows (post, reply, reaction, follow, home feed, unfollow).

**Prerequisites:** `gnokey` must be in your `PATH` and the target chain must be running with the realm deployed.

**Run against a local gnodev instance** (default):

./realm/tests/test.sh

**Run against a remote chain** — override any of these environment variables:

| Variable | Default | Description |
| ----------- | ------------------------- | ------------------- |
| `CHAIN_ID` | `dev` | Chain ID |
| `NODE_ADDR` | `http://localhost:26657` | RPC endpoint |
| `PKG_PATH` | `gno.land/r/berty/social` | Deployed realm path |

Example for a remote chain:

CHAIN_ID=test12 \
NODE_ADDR=http://test12.example.org:26657 \
PKG_PATH=gno.land/r/<your-address>/social \
./realm/tests/test.sh
4 changes: 2 additions & 2 deletions realm/post.gno
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ import (
"time"

"gno.land/p/moul/txlink"
"gno.land/p/nt/avl"
"gno.land/p/nt/ufmt"
"gno.land/p/nt/avl/v0"
"gno.land/p/nt/ufmt/v0"
)

//----------------------------------------
Expand Down
41 changes: 6 additions & 35 deletions realm/public.gno
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,8 @@ import (
"chain/runtime"
"strconv"

"gno.land/p/jefft0/avlhelpers"
"gno.land/p/nt/avl"
"gno.land/p/nt/ufmt"
"gno.land/p/nt/avl/v0"
"gno.land/p/nt/ufmt/v0"
"gno.land/r/sys/users"
)

Expand All @@ -21,12 +20,7 @@ type UserAndPostID struct {
// (This is similar to boards.CreateThread, but no message title)
func PostMessage(_ realm, body string) PostID {
caller := runtime.OriginCaller()
name := usernameOf(caller)
if name == "" {
panic("please register")
}

userPosts := getOrCreateUserPosts(caller, name)
userPosts := getOrCreateUserPosts(caller, usernameOf(caller))
thread := userPosts.AddThread(body)
return thread.id
}
Expand All @@ -39,9 +33,6 @@ func PostMessage(_ realm, body string) PostID {
// (This is similar to boards.CreateReply.)
func PostReply(_ realm, userPostsAddr address, threadid, postid PostID, body string) PostID {
caller := runtime.OriginCaller()
if usernameOf(caller) == "" {
panic("please register")
}
userPosts := getUserPosts(userPostsAddr)
if userPosts == nil {
panic("posts for userPostsAddr do not exist")
Expand Down Expand Up @@ -73,11 +64,7 @@ func RepostThread(_ realm, userPostsAddr address, threadid PostID, comment strin
panic("Cannot repost a user's own message")
}

name := usernameOf(caller)
if name == "" {
panic("please register")
}
dstUserPosts := getOrCreateUserPosts(caller, name)
dstUserPosts := getOrCreateUserPosts(caller, usernameOf(caller))

userPosts := getUserPosts(userPostsAddr)
if userPosts == nil {
Expand Down Expand Up @@ -226,28 +213,18 @@ func GetJsonHomePosts(userPostsAddr address, startIndex int, endIndex int) strin
// Update the caller to follow the user with followedAddr. See UserPosts.Follow.
func Follow(_ realm, followedAddr address) PostID {
caller := runtime.OriginCaller()
name := usernameOf(caller)
if name == "" {
panic("please register")
}

if followedAddr == caller {
panic("you can't follow yourself")
}

// A user can follow someone before doing any posts, so create the UserPosts if needed.
userPosts := getOrCreateUserPosts(caller, name)
userPosts := getOrCreateUserPosts(caller, usernameOf(caller))
return userPosts.Follow(followedAddr)
}

// Update the caller to unfollow the user with followedAddr. See UserPosts.Unfollow.
func Unfollow(_ realm, followedAddr address) {
caller := runtime.OriginCaller()
name := usernameOf(caller)
if name == "" {
panic("please register")
}

userPosts := getUserPosts(caller)
if userPosts == nil {
// We don't expect this, but just do nothing.
Expand All @@ -265,9 +242,6 @@ func Unfollow(_ realm, followedAddr address) {
// Return a boolean indicating whether the userAddr was added. See Post.AddReaction.
func AddReaction(_ realm, userPostsAddr address, threadid, postid PostID, reaction Reaction) bool {
caller := runtime.OriginCaller()
if usernameOf(caller) == "" {
panic("please register")
}
userPosts := getUserPosts(userPostsAddr)
if userPosts == nil {
panic("posts for userPostsAddr do not exist")
Expand Down Expand Up @@ -295,9 +269,6 @@ func AddReaction(_ realm, userPostsAddr address, threadid, postid PostID, reacti
// Return a boolean indicating whether the userAddr was removed. See Post.RemoveReaction.
func RemoveReaction(_ realm, userPostsAddr address, threadid, postid PostID, reaction Reaction) bool {
caller := runtime.OriginCaller()
if usernameOf(caller) == "" {
panic("please register")
}
userPosts := getUserPosts(userPostsAddr)
if userPosts == nil {
panic("posts for userPostsAddr do not exist")
Expand Down Expand Up @@ -416,7 +387,7 @@ func GetJsonFollowing(address address, startIndex int, endIndex int) string {
// Get a list of user names starting from the given prefix. Limit the
// number of results to maxResults.
func ListUsersByPrefix(prefix string, maxResults int) []string {
return avlhelpers.ListByteStringKeysByPrefix(&gUserAddressByName, prefix, maxResults)
return listByteStringKeysByPrefix(&gUserAddressByName, prefix, maxResults)
}

// Get a list of user names starting from the given prefix. Limit the
Expand Down
53 changes: 26 additions & 27 deletions realm/render.gno
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,24 @@ import (
"gno.land/r/sys/users"
)

// resolveAddr looks up an address by registered name first, then falls back
// to treating the input as a raw address. This allows unregistered users to
// be found by their address string.
func resolveAddr(nameOrAddr string) address {
user, _ := users.ResolveName(nameOrAddr)
if user != nil {
return user.Addr()
}
return address(nameOrAddr)
}

func Render(path string) string {
if path == "" {
str := "Welcome to dSocial!\n\n"

// List the users who have posted. gUserAddressByName is already sorted by name.
gUserAddressByName.Iterate("", "", func(name string, value interface{}) bool {
str += " * [@" + name + "](/r/berty/social:" + name + ")" + "\n"
str += " * [@" + name + "](" + gRealmPath + ":" + name + ")" + "\n"
return false
})

Expand All @@ -22,40 +33,32 @@ func Render(path string) string {

parts := strings.Split(path, "/")
if len(parts) == 1 {
// /r/berty/social:USER_NAME
user, _ := users.ResolveName(path)
if user == nil {
return "Unknown user: " + path
}

userPosts := getUserPosts(user.Addr())
// /r/berty/social:USER_NAME_OR_ADDR
userAddr := resolveAddr(path)
userPosts := getUserPosts(userAddr)
if userPosts == nil {
return "No posts by: " + path
}

return userPosts.RenderUserPosts(false)
} else if len(parts) == 2 {
name := parts[0]
user, _ := users.ResolveName(name)
if user == nil {
return "Unknown user: " + name
}
userPosts := getUserPosts(user.Addr())
userAddr := resolveAddr(parts[0])
userPosts := getUserPosts(userAddr)
if userPosts == nil {
return "No posts by: " + name
return "No posts by: " + parts[0]
}

if parts[1] == "home" {
// /r/berty/social:USER_NAME/home
// /r/berty/social:USER_NAME_OR_ADDR/home
return userPosts.RenderUserPosts(true)
} else if parts[1] == "followers" {
// /r/berty/social:USER_NAME/followers
// /r/berty/social:USER_NAME_OR_ADDR/followers
return userPosts.RenderFollowers()
} else if parts[1] == "following" {
// /r/berty/social:USER_NAME/following
// /r/berty/social:USER_NAME_OR_ADDR/following
return userPosts.RenderFollowing()
} else {
// /r/berty/social:USER_NAME/THREAD_ID
// /r/berty/social:USER_NAME_OR_ADDR/THREAD_ID
pid, err := strconv.Atoi(parts[1])
if err != nil {
return "invalid thread id: " + parts[1]
Expand All @@ -67,15 +70,11 @@ func Render(path string) string {
return thread.RenderPost("", 5)
}
} else if len(parts) == 3 {
// /r/berty/social:USER_NAME/THREAD_ID/REPLY_ID
name := parts[0]
user, _ := users.ResolveName(name)
if user == nil {
return "Unknown user: " + name
}
userPosts := getUserPosts(user.Addr())
// /r/berty/social:USER_NAME_OR_ADDR/THREAD_ID/REPLY_ID
userAddr := resolveAddr(parts[0])
userPosts := getUserPosts(userAddr)
if userPosts == nil {
return "No posts by: " + name
return "No posts by: " + parts[0]
}
pid, err := strconv.Atoi(parts[1])
if err != nil {
Expand Down
19 changes: 18 additions & 1 deletion realm/social.gno
Original file line number Diff line number Diff line change
@@ -1,11 +1,28 @@
package social

import (
"gno.land/p/nt/avl"
"strings"

"chain/runtime"

"gno.land/p/nt/avl/v0"
)

var (
gUserPostsByAddress avl.Tree // user's address -> *UserPosts
gUserAddressByName avl.Tree // user's username -> address
postsCtr uint64 // increments Post.id globally

// gRealmPath is the realm's relative URL path (e.g. "/r/g1.../social"),
// derived from the deployed package path at init time.
gRealmPath string
)

func init() {
pkgPath := runtime.CurrentRealm().PkgPath()
// Strip the chain domain (everything before the first "/") to get the
// relative path suitable for markdown links: "/r/g1.../social".
if idx := strings.Index(pkgPath, "/"); idx >= 0 {
gRealmPath = pkgPath[idx:]
}
}
Loading
Loading