From 3288fe82f77afb475796a6ebbe395975ee6f9f0f Mon Sep 17 00:00:00 2001 From: Alexis Colin Date: Tue, 25 Feb 2025 17:25:07 +0900 Subject: [PATCH 1/8] fix: CSP to allow pixel gif from simple analytics (#3822) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Related to #3619 This PR updates the Content Security Policy (CSP) to allow Simple Analytics to function correctly. The img-src directive now includes https://sa.gno.services, enabling the tracking pixel to load without being blocked. The tracking pixel (a 1×1 GIF) is essential for Simple Analytics to collect pageview data without requiring JavaScript. When loaded, it sends necessary metadata (e.g., page URL, referrer, user-agent) via the request, allowing privacy-friendly analytics to work. Without this fix, Simple Analytics is unable to capture visits. --- gno.land/cmd/gnoweb/main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gno.land/cmd/gnoweb/main.go b/gno.land/cmd/gnoweb/main.go index 0bd38489ff6..8e0b64122a6 100644 --- a/gno.land/cmd/gnoweb/main.go +++ b/gno.land/cmd/gnoweb/main.go @@ -242,7 +242,7 @@ func SecureHeadersMiddleware(next http.Handler, strict bool) http.Handler { // - 'self' allows resources from the same origin. // - 'data:' allows inline images (e.g., base64-encoded images). // - 'https://gnolang.github.io' allows images from this specific domain - used by gno.land. TODO: use a proper generic whitelisted service - w.Header().Set("Content-Security-Policy", "default-src 'self'; script-src 'self' https://sa.gno.services; style-src 'self'; img-src 'self' data: https://gnolang.github.io; font-src 'self'") + w.Header().Set("Content-Security-Policy", "default-src 'self'; script-src 'self' https://sa.gno.services; style-src 'self'; img-src 'self' data: https://gnolang.github.io https://sa.gno.services; font-src 'self'") // Enforce HTTPS by telling browsers to only access the site over HTTPS // for a specified duration (1 year in this case). This also applies to From 6bf3889b69bd46139abe8a308beec3deda679d92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jer=C3=B3nimo=20Albi?= Date: Tue, 25 Feb 2025 17:35:24 +0100 Subject: [PATCH 2/8] feat(examples): add `p/jeronimoalbi/pager` package (#3806) Package implements a way to support paging in generic cases that don't use an AVL trees. --- .../gno.land/p/jeronimoalbi/pager/gno.mod | 1 + .../gno.land/p/jeronimoalbi/pager/pager.gno | 204 ++++++++++++++++++ .../p/jeronimoalbi/pager/pager_options.gno | 33 +++ .../p/jeronimoalbi/pager/pager_test.gno | 204 ++++++++++++++++++ 4 files changed, 442 insertions(+) create mode 100644 examples/gno.land/p/jeronimoalbi/pager/gno.mod create mode 100644 examples/gno.land/p/jeronimoalbi/pager/pager.gno create mode 100644 examples/gno.land/p/jeronimoalbi/pager/pager_options.gno create mode 100644 examples/gno.land/p/jeronimoalbi/pager/pager_test.gno diff --git a/examples/gno.land/p/jeronimoalbi/pager/gno.mod b/examples/gno.land/p/jeronimoalbi/pager/gno.mod new file mode 100644 index 00000000000..e775954b9fe --- /dev/null +++ b/examples/gno.land/p/jeronimoalbi/pager/gno.mod @@ -0,0 +1 @@ +module gno.land/p/jeronimoalbi/pager diff --git a/examples/gno.land/p/jeronimoalbi/pager/pager.gno b/examples/gno.land/p/jeronimoalbi/pager/pager.gno new file mode 100644 index 00000000000..7b8a1948b3b --- /dev/null +++ b/examples/gno.land/p/jeronimoalbi/pager/pager.gno @@ -0,0 +1,204 @@ +// Package pager provides pagination functionality through a generic pager implementation. +// +// Example usage: +// +// import ( +// "strconv" +// "strings" +// +// "gno.land/p/jeronimoalbi/pager" +// ) +// +// func Render(path string) string { +// // Define the items to paginate +// items := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10} +// +// // Create a pager that paginates 4 items at a time +// p, err := pager.New(path, len(items), pager.WithPageSize(4)) +// if err != nil { +// panic(err) +// } +// +// // Render items for the current page +// var output strings.Builder +// p.Iterate(func(i int) bool { +// output.WriteString("- " + strconv.Itoa(items[i]) + "\n") +// return false +// }) +// +// // Render page picker +// if p.HasPages() { +// output.WriteString("\n" + pager.Picker(p)) +// } +// +// return output.String() +// } +package pager + +import ( + "errors" + "math" + "net/url" + "strconv" + "strings" +) + +var ErrInvalidPageNumber = errors.New("invalid page number") + +// PagerIterFn defines a callback to iterate page items. +type PagerIterFn func(index int) (stop bool) + +// New creates a new pager. +func New(rawURL string, totalItems int, options ...PagerOption) (Pager, error) { + u, err := url.Parse(rawURL) + if err != nil { + return Pager{}, err + } + + p := Pager{ + query: u.RawQuery, + pageQueryParam: DefaultPageQueryParam, + pageSize: DefaultPageSize, + page: 1, + totalItems: totalItems, + } + for _, apply := range options { + apply(&p) + } + + p.pageCount = int(math.Ceil(float64(p.totalItems) / float64(p.pageSize))) + + rawPage := u.Query().Get(p.pageQueryParam) + if rawPage != "" { + p.page, _ = strconv.Atoi(rawPage) + if p.page == 0 || p.page > p.pageCount { + return Pager{}, ErrInvalidPageNumber + } + } + + return p, nil +} + +// MustNew creates a new pager or panics if there is an error. +func MustNew(rawURL string, totalItems int, options ...PagerOption) Pager { + p, err := New(rawURL, totalItems, options...) + if err != nil { + panic(err) + } + return p +} + +// Pager allows paging items. +type Pager struct { + query, pageQueryParam string + pageSize, page, pageCount, totalItems int +} + +// TotalItems returns the total number of items to paginate. +func (p Pager) TotalItems() int { + return p.totalItems +} + +// PageSize returns the size of each page. +func (p Pager) PageSize() int { + return p.pageSize +} + +// Page returns the current page number. +func (p Pager) Page() int { + return p.page +} + +// PageCount returns the number pages. +func (p Pager) PageCount() int { + return p.pageCount +} + +// Offset returns the index of the first page item. +func (p Pager) Offset() int { + return (p.page - 1) * p.pageSize +} + +// HasPages checks if pager has more than one page. +func (p Pager) HasPages() bool { + return p.pageCount > 1 +} + +// GetPageURI returns the URI for a page. +// An empty string is returned when page doesn't exist. +func (p Pager) GetPageURI(page int) string { + if page < 1 || page > p.PageCount() { + return "" + } + + values, _ := url.ParseQuery(p.query) + values.Set(p.pageQueryParam, strconv.Itoa(page)) + return "?" + values.Encode() +} + +// PrevPageURI returns the URI path to the previous page. +// An empty string is returned when current page is the first page. +func (p Pager) PrevPageURI() string { + if p.page == 1 || !p.HasPages() { + return "" + } + return p.GetPageURI(p.page - 1) +} + +// NextPageURI returns the URI path to the next page. +// An empty string is returned when current page is the last page. +func (p Pager) NextPageURI() string { + if p.page == p.pageCount { + // Current page is the last page + return "" + } + return p.GetPageURI(p.page + 1) +} + +// Iterate allows iterating page items. +func (p Pager) Iterate(fn PagerIterFn) bool { + if p.totalItems == 0 { + return true + } + + start := p.Offset() + end := start + p.PageSize() + if end > p.totalItems { + end = p.totalItems + } + + for i := start; i < end; i++ { + if fn(i) { + return true + } + } + return false +} + +// TODO: Support different types of pickers (ex. with clickable page numbers) + +// Picker returns a string with the pager as Markdown. +// An empty string is returned when the pager has no pages. +func Picker(p Pager) string { + if !p.HasPages() { + return "" + } + + var out strings.Builder + + if s := p.PrevPageURI(); s != "" { + out.WriteString("[«](" + s + ") | ") + } else { + out.WriteString("\\- | ") + } + + out.WriteString("page " + strconv.Itoa(p.Page()) + " of " + strconv.Itoa(p.PageCount())) + + if s := p.NextPageURI(); s != "" { + out.WriteString(" | [»](" + s + ")") + } else { + out.WriteString(" | \\-") + } + + return out.String() +} diff --git a/examples/gno.land/p/jeronimoalbi/pager/pager_options.gno b/examples/gno.land/p/jeronimoalbi/pager/pager_options.gno new file mode 100644 index 00000000000..3feb467682b --- /dev/null +++ b/examples/gno.land/p/jeronimoalbi/pager/pager_options.gno @@ -0,0 +1,33 @@ +package pager + +import "strings" + +const ( + DefaultPageSize = 50 + DefaultPageQueryParam = "page" +) + +// PagerOption configures the pager. +type PagerOption func(*Pager) + +// WithPageSize assigns a page size to a pager. +func WithPageSize(size int) PagerOption { + return func(p *Pager) { + if size < 1 { + p.pageSize = DefaultPageSize + } else { + p.pageSize = size + } + } +} + +// WithPageQueryParam assigns the name of the URL query param for the page value. +func WithPageQueryParam(name string) PagerOption { + return func(p *Pager) { + name = strings.TrimSpace(name) + if name == "" { + name = DefaultPageQueryParam + } + p.pageQueryParam = name + } +} diff --git a/examples/gno.land/p/jeronimoalbi/pager/pager_test.gno b/examples/gno.land/p/jeronimoalbi/pager/pager_test.gno new file mode 100644 index 00000000000..f498079cad3 --- /dev/null +++ b/examples/gno.land/p/jeronimoalbi/pager/pager_test.gno @@ -0,0 +1,204 @@ +package pager + +import ( + "testing" + + "gno.land/p/demo/uassert" + "gno.land/p/demo/urequire" +) + +func TestPager(t *testing.T) { + cases := []struct { + name, uri, prevPath, nextPath, param string + offset, pageSize, page, pageCount int + hasPages bool + items []int + err error + }{ + { + name: "page 1", + uri: "gno.land/r/demo/test:foo/bar?page=1&foo=bar", + items: []int{1, 2, 3, 4, 5, 6}, + hasPages: true, + nextPath: "?foo=bar&page=2", + pageSize: 5, + page: 1, + pageCount: 2, + }, + { + name: "page 2", + uri: "gno.land/r/demo/test:foo/bar?page=2&foo=bar", + items: []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}, + hasPages: true, + prevPath: "?foo=bar&page=1", + nextPath: "", + offset: 5, + pageSize: 5, + page: 2, + pageCount: 2, + }, + { + name: "custom query param", + uri: "gno.land/r/demo/test:foo/bar?current=2&foo=bar", + items: []int{1, 2, 3}, + param: "current", + hasPages: true, + prevPath: "?current=1&foo=bar", + nextPath: "", + offset: 2, + pageSize: 2, + page: 2, + pageCount: 2, + }, + { + name: "missing page", + uri: "gno.land/r/demo/test:foo/bar?page=3&foo=bar", + err: ErrInvalidPageNumber, + }, + { + name: "invalid page zero", + uri: "gno.land/r/demo/test:foo/bar?page=0", + err: ErrInvalidPageNumber, + }, + { + name: "invalid page number", + uri: "gno.land/r/demo/test:foo/bar?page=foo", + err: ErrInvalidPageNumber, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + // Act + p, err := New(tc.uri, len(tc.items), WithPageSize(tc.pageSize), WithPageQueryParam(tc.param)) + + // Assert + if tc.err != nil { + urequire.ErrorIs(t, err, tc.err, "expected an error") + return + } + + urequire.NoError(t, err, "expect no error") + uassert.Equal(t, len(tc.items), p.TotalItems(), "total items") + uassert.Equal(t, tc.page, p.Page(), "page number") + uassert.Equal(t, tc.pageCount, p.PageCount(), "number of pages") + uassert.Equal(t, tc.pageSize, p.PageSize(), "page size") + uassert.Equal(t, tc.prevPath, p.PrevPageURI(), "prev URL page") + uassert.Equal(t, tc.nextPath, p.NextPageURI(), "next URL page") + uassert.Equal(t, tc.hasPages, p.HasPages(), "has pages") + uassert.Equal(t, tc.offset, p.Offset(), "item offset") + }) + } +} + +func TestPagerIterate(t *testing.T) { + cases := []struct { + name, uri string + items, page []int + stop bool + }{ + { + name: "page 1", + uri: "gno.land/r/demo/test:foo/bar?page=1", + items: []int{1, 2, 3, 4, 5, 6, 7}, + page: []int{1, 2, 3}, + }, + { + name: "page 2", + uri: "gno.land/r/demo/test:foo/bar?page=2", + items: []int{1, 2, 3, 4, 5, 6, 7}, + page: []int{4, 5, 6}, + }, + { + name: "page 3", + uri: "gno.land/r/demo/test:foo/bar?page=3", + items: []int{1, 2, 3, 4, 5, 6, 7}, + page: []int{7}, + }, + { + name: "stop iteration", + uri: "gno.land/r/demo/test:foo/bar?page=1", + items: []int{1, 2, 3}, + stop: true, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + // Arrange + var ( + items []int + p = MustNew(tc.uri, len(tc.items), WithPageSize(3)) + ) + + // Act + stopped := p.Iterate(func(i int) bool { + if tc.stop { + return true + } + + items = append(items, tc.items[i]) + return false + }) + + // Assert + uassert.Equal(t, tc.stop, stopped) + urequire.Equal(t, len(tc.page), len(items), "expect iteration of the right number of items") + + for i, v := range items { + urequire.Equal(t, tc.page[i], v, "expect iterated items to match") + } + }) + } +} + +func TestPicker(t *testing.T) { + pageSize := 3 + cases := []struct { + name, uri, output string + totalItems int + }{ + { + name: "one page", + uri: "gno.land/r/demo/test:foo/bar?page=1", + totalItems: 3, + output: "", + }, + { + name: "two pages", + uri: "gno.land/r/demo/test:foo/bar?page=1", + totalItems: 4, + output: "\\- | page 1 of 2 | [»](?page=2)", + }, + { + name: "three pages", + uri: "gno.land/r/demo/test:foo/bar?page=1", + totalItems: 7, + output: "\\- | page 1 of 3 | [»](?page=2)", + }, + { + name: "three pages second page", + uri: "gno.land/r/demo/test:foo/bar?page=2", + totalItems: 7, + output: "[«](?page=1) | page 2 of 3 | [»](?page=3)", + }, + { + name: "three pages third page", + uri: "gno.land/r/demo/test:foo/bar?page=3", + totalItems: 7, + output: "[«](?page=2) | page 3 of 3 | \\-", + }, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + // Arrange + p := MustNew(tc.uri, tc.totalItems, WithPageSize(pageSize)) + + // Act + output := Picker(p) + + // Assert + uassert.Equal(t, tc.output, output) + }) + } +} From c433aa0c4049fababb0f2c95b7d94105cf81aa7a Mon Sep 17 00:00:00 2001 From: Leon Hudak <33522493+leohhhn@users.noreply.github.com> Date: Tue, 25 Feb 2025 17:56:48 +0100 Subject: [PATCH 3/8] chore(docs): rm mentions of staging testnet (#3828) ## Description Removes `staging` from the docs. --- docs/concepts/testnets.md | 16 ---------------- docs/reference/gno-js-client/gno-provider.md | 4 ++-- docs/reference/network-config.md | 1 - .../tm2-js-client/Provider/json-rpc-provider.md | 2 +- .../tm2-js-client/Provider/ws-provider.md | 10 +++++----- 5 files changed, 8 insertions(+), 25 deletions(-) diff --git a/docs/concepts/testnets.md b/docs/concepts/testnets.md index 9769c8f873f..40cf22ed7f0 100644 --- a/docs/concepts/testnets.md +++ b/docs/concepts/testnets.md @@ -63,22 +63,6 @@ Test5 was launched in November 2024. - **Versioning strategy**: - Test5 is to be release-based, following releases of the Gno tech stack. - -## Staging - -Staging is a testnet that is reset once every 60 minutes. - -- **Persistence of state:** - - State is fully discarded -- **Timeliness of code:** - - With every reset, the latest commit of the Gno tech stack is applied, including - the demo packages and realms -- **Intended purpose** - - Demoing, single-use code in a staging environment, testing automation which - uploads code to the chain, etc. -- **Versioning strategy**: - - Staging is reset every 60 minutes to match the latest monorepo commit - ## TestX These testnets are deprecated and currently serve as archives of previous progress. diff --git a/docs/reference/gno-js-client/gno-provider.md b/docs/reference/gno-js-client/gno-provider.md index c76bfebfe31..cef6e04eb4a 100644 --- a/docs/reference/gno-js-client/gno-provider.md +++ b/docs/reference/gno-js-client/gno-provider.md @@ -20,7 +20,7 @@ Same as [`tm2-js-client` `WSProvider`](../tm2-js-client/Provider/ws-provider.md) #### Usage ```ts -new GnoWSProvider('ws://staging.gno.land:26657/ws'); +new GnoWSProvider('ws://gno.land:443/ws'); // provider with WS connection is created ``` @@ -35,7 +35,7 @@ Same as [`tm2-js-client` `JSONRPCProvider`](../tm2-js-client/Provider/json-rpc-p #### Usage ```ts -new GnoJSONRPCProvider('http://staging.gno.land:36657'); +new GnoJSONRPCProvider('https://gno.land:443'); // provider is created ``` diff --git a/docs/reference/network-config.md b/docs/reference/network-config.md index 1e50864372b..025f9f3171f 100644 --- a/docs/reference/network-config.md +++ b/docs/reference/network-config.md @@ -8,7 +8,6 @@ id: network-config |-------------|----------------------------------|---------------| | Portal Loop | https://rpc.gno.land:443 | `portal-loop` | | Test5 | https://rpc.test5.gno.land:443 | `test5` | -| Staging | https://rpc.staging.gno.land:443 | `staging` | ### WebSocket endpoints All networks follow the same pattern for websocket connections: diff --git a/docs/reference/tm2-js-client/Provider/json-rpc-provider.md b/docs/reference/tm2-js-client/Provider/json-rpc-provider.md index b7700e1d97c..284b383bd1c 100644 --- a/docs/reference/tm2-js-client/Provider/json-rpc-provider.md +++ b/docs/reference/tm2-js-client/Provider/json-rpc-provider.md @@ -17,6 +17,6 @@ Creates a new instance of the JSON-RPC Provider #### Usage ```ts -new JSONRPCProvider('http://staging.gno.land:36657'); +new JSONRPCProvider('https://gno.land:443'); // provider is created ``` diff --git a/docs/reference/tm2-js-client/Provider/ws-provider.md b/docs/reference/tm2-js-client/Provider/ws-provider.md index 9d4f2390c28..a2c4fa1d0c0 100644 --- a/docs/reference/tm2-js-client/Provider/ws-provider.md +++ b/docs/reference/tm2-js-client/Provider/ws-provider.md @@ -18,7 +18,7 @@ Creates a new instance of the WebSocket Provider #### Usage ```ts -new WSProvider('ws://staging.gno.land:26657/ws'); +new WSProvider('ws://gno.land:443/ws'); // provider with WS connection is created ``` @@ -30,7 +30,7 @@ with the WS provider #### Usage ```ts -const wsProvider = new WSProvider('ws://staging.gno.land:26657/ws'); +const wsProvider = new WSProvider('ws://gno.land:443/ws'); wsProvider.closeConnection(); // WS connection is now closed @@ -52,7 +52,7 @@ Returns **Promise>** ```ts const request: RPCRequest = // ... -const wsProvider = new WSProvider('ws://staging.gno.land:26657/ws'); +const wsProvider = new WSProvider('ws://gno.land:443/ws'); wsProvider.sendRequest(request); // request is sent over the open WS connection @@ -73,7 +73,7 @@ Returns **Result** ```ts const response: RPCResponse = // ... -const wsProvider = new WSProvider('ws://staging.gno.land:26657/ws'); +const wsProvider = new WSProvider('ws://gno.land:443/ws'); wsProvider.parseResponse(response); // response is parsed @@ -88,7 +88,7 @@ Returns **Promise** #### Usage ```ts -const wsProvider = new WSProvider('ws://staging.gno.land:26657/ws'); +const wsProvider = new WSProvider('ws://gno.land:443/ws'); await wsProvider.waitForOpenConnection() // status of the connection is: CONNECTED From fddfaccf3b905244ea2251c86786dd2000b46f94 Mon Sep 17 00:00:00 2001 From: Leon Hudak <33522493+leohhhn@users.noreply.github.com> Date: Wed, 26 Feb 2025 13:36:27 +0100 Subject: [PATCH 4/8] chore(gnoweb): bump hardcoded gas value (#3833) ## Description Bumps the hardcoded gas value in gnoweb's help page as the current one is too low. Ideally we have gas estimates for this down the line. Also reorders flags to put func args after the func name. --- gno.land/pkg/gnoweb/components/ui/help_function.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gno.land/pkg/gnoweb/components/ui/help_function.html b/gno.land/pkg/gnoweb/components/ui/help_function.html index 59c5286b093..d32d76222c6 100644 --- a/gno.land/pkg/gnoweb/components/ui/help_function.html +++ b/gno.land/pkg/gnoweb/components/ui/help_function.html @@ -32,8 +32,8 @@

Command

gnokey maketx call -pkgpath "{{ $.PkgPath }}" -func "{{ .FuncName }}" -gas-fee 1000000ugnot -gas-wanted 2000000 -broadcast -chainid "{{ $.ChainId }}"{{ range .Params }} -args ""{{ end }} -remote "{{ $.Remote }}" ADDRESS
From d7ce16918d083a2260889f4fc5c2b8cacb83daa9 Mon Sep 17 00:00:00 2001 From: Leon Hudak <33522493+leohhhn@users.noreply.github.com> Date: Wed, 26 Feb 2025 17:40:29 +0100 Subject: [PATCH 5/8] feat(r/docs): add `minisocial` example (#3834) ## Description Adds Gno Threads application to the `r/docs` folder. It adds a mini version (v1), and a full version (v2). This code will be used in the official docs. Needed for https://github.com/gnolang/docs-v2/pull/72 --- examples/gno.land/r/docs/docs.gno | 1 + examples/gno.land/r/docs/minisocial/gno.mod | 1 + .../gno.land/r/docs/minisocial/minisocial.gno | 18 +++ .../gno.land/r/docs/minisocial/v1/admin.gno | 11 ++ .../gno.land/r/docs/minisocial/v1/gno.mod | 1 + .../gno.land/r/docs/minisocial/v1/posts.gno | 50 +++++++ .../r/docs/minisocial/v1/posts_test.gno | 67 +++++++++ .../gno.land/r/docs/minisocial/v1/types.gno | 27 ++++ .../gno.land/r/docs/minisocial/v2/admin.gno | 16 ++ .../gno.land/r/docs/minisocial/v2/gno.mod | 1 + .../gno.land/r/docs/minisocial/v2/posts.gno | 141 ++++++++++++++++++ .../r/docs/minisocial/v2/posts_test.gno | 69 +++++++++ .../gno.land/r/docs/minisocial/v2/types.gno | 34 +++++ 13 files changed, 437 insertions(+) create mode 100644 examples/gno.land/r/docs/minisocial/gno.mod create mode 100644 examples/gno.land/r/docs/minisocial/minisocial.gno create mode 100644 examples/gno.land/r/docs/minisocial/v1/admin.gno create mode 100644 examples/gno.land/r/docs/minisocial/v1/gno.mod create mode 100644 examples/gno.land/r/docs/minisocial/v1/posts.gno create mode 100644 examples/gno.land/r/docs/minisocial/v1/posts_test.gno create mode 100644 examples/gno.land/r/docs/minisocial/v1/types.gno create mode 100644 examples/gno.land/r/docs/minisocial/v2/admin.gno create mode 100644 examples/gno.land/r/docs/minisocial/v2/gno.mod create mode 100644 examples/gno.land/r/docs/minisocial/v2/posts.gno create mode 100644 examples/gno.land/r/docs/minisocial/v2/posts_test.gno create mode 100644 examples/gno.land/r/docs/minisocial/v2/types.gno diff --git a/examples/gno.land/r/docs/docs.gno b/examples/gno.land/r/docs/docs.gno index b4c78205c0a..61f27c2db68 100644 --- a/examples/gno.land/r/docs/docs.gno +++ b/examples/gno.land/r/docs/docs.gno @@ -16,6 +16,7 @@ Explore various examples to learn more about Gno functionality and usage. - [AVL Pager + Render paths](/r/docs/avl_pager_params) - Handle render arguments with pagination. - [Img Embed](/r/docs/img_embed) - Demonstrates how to embed an image. - [Optional Render](/r/docs/optional_render) - Render() is optional in realms. +- [MiniSocial](/r/docs/minisocial) - Minimalistic social media app for learning purposes. - ... diff --git a/examples/gno.land/r/docs/minisocial/gno.mod b/examples/gno.land/r/docs/minisocial/gno.mod new file mode 100644 index 00000000000..70b47f7a36f --- /dev/null +++ b/examples/gno.land/r/docs/minisocial/gno.mod @@ -0,0 +1 @@ +module gno.land/r/docs/minisocial diff --git a/examples/gno.land/r/docs/minisocial/minisocial.gno b/examples/gno.land/r/docs/minisocial/minisocial.gno new file mode 100644 index 00000000000..ed1c874fea4 --- /dev/null +++ b/examples/gno.land/r/docs/minisocial/minisocial.gno @@ -0,0 +1,18 @@ +package minisocial + +func Render(_ string) string { + return `# MiniSocial +MiniSocial is a minimalistic social media platform made for example purposes. + +There are two versions of this app: +- [V1](/r/docs/minisocial/v1) - handles simple post creation and stores posts in a slice +- [V2](/r/docs/minisocial/v2) - handles post creation, updating, and deletion, +and manages storage more efficiently with an AVL tree. V2 also utilizes different p/ packages to handle pagination, +easier Markdown formatting, etc.` + +} + +// Original work & inspiration here: +// https://gno.land/r/leon/fosdem25/microposts +// https://gno.land/r/moul/microposts +// Find the full tutorial on the official gno.land docs at docs.gno.land diff --git a/examples/gno.land/r/docs/minisocial/v1/admin.gno b/examples/gno.land/r/docs/minisocial/v1/admin.gno new file mode 100644 index 00000000000..05ef8b28df9 --- /dev/null +++ b/examples/gno.land/r/docs/minisocial/v1/admin.gno @@ -0,0 +1,11 @@ +package minisocial + +import "gno.land/p/demo/ownable" + +var Ownable = ownable.NewWithAddress("g125em6arxsnj49vx35f0n0z34putv5ty3376fg5") // @leohhhn + +// ResetPosts allows admin deletion of the posts +func ResetPosts() { + Ownable.AssertCallerIsOwner() + posts = nil +} diff --git a/examples/gno.land/r/docs/minisocial/v1/gno.mod b/examples/gno.land/r/docs/minisocial/v1/gno.mod new file mode 100644 index 00000000000..9e140e688f4 --- /dev/null +++ b/examples/gno.land/r/docs/minisocial/v1/gno.mod @@ -0,0 +1 @@ +module gno.land/r/docs/minisocial/v1 diff --git a/examples/gno.land/r/docs/minisocial/v1/posts.gno b/examples/gno.land/r/docs/minisocial/v1/posts.gno new file mode 100644 index 00000000000..534bbf62c9d --- /dev/null +++ b/examples/gno.land/r/docs/minisocial/v1/posts.gno @@ -0,0 +1,50 @@ +package minisocial + +import ( + "errors" + "std" + "time" + + "gno.land/p/demo/ufmt" +) + +var posts []*Post // inefficient for large amounts of posts; see v2 + +// CreatePost creates a new post +func CreatePost(text string) error { + // If the body of the post is empty, return an error + if text == "" { + return errors.New("empty post text") + } + + // Append the new post to the list + posts = append(posts, &Post{ + text: text, // Set the input text + author: std.PreviousRealm().Address(), // The author of the address is the previous realm, the realm that called this one + createdAt: time.Now(), // Capture the time of the transaction, in this case the block timestamp + }) + + return nil +} + +func Render(_ string) string { + output := "# MiniSocial\n\n" // \n is needed just like in standard Markdown + + // Handle the edge case + if len(posts) == 0 { + output += "No posts.\n" + return output + } + + // Let's append the text of each post to the output + for i, post := range posts { + // Let's append some post metadata + output += ufmt.Sprintf("#### Post #%d\n\n", i) + // Add the stringified post + output += post.String() + // Add a line break for cleaner UI + output += "---\n\n" + } + + return output +} diff --git a/examples/gno.land/r/docs/minisocial/v1/posts_test.gno b/examples/gno.land/r/docs/minisocial/v1/posts_test.gno new file mode 100644 index 00000000000..ac0125772d4 --- /dev/null +++ b/examples/gno.land/r/docs/minisocial/v1/posts_test.gno @@ -0,0 +1,67 @@ +package minisocial + +import ( + "std" + "strings" + "testing" + + "gno.land/p/demo/testutils" // Provides testing utilities +) + +func TestCreatePostSingle(t *testing.T) { + // Get a test address for alice + aliceAddr := testutils.TestAddress("alice") + // TestSetRealm sets the realm caller, in this case Alice + std.TestSetRealm(std.NewUserRealm(aliceAddr)) + + text1 := "Hello World!" + err := CreatePost(text1) + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + + // Get the rendered page + got := Render("") + + // Content should have the text and alice's address in it + if !(strings.Contains(got, text1) && strings.Contains(got, aliceAddr.String())) { + t.Fatal("expected render to contain text & alice's address") + } +} + +func TestCreatePostMultiple(t *testing.T) { + // Initialize a slice to hold the test posts and their authors + posts := []struct { + text string + author string + }{ + {"Hello World!", "alice"}, + {"This is some new text!", "bob"}, + {"Another post by alice", "alice"}, + {"A post by charlie!", "charlie"}, + } + + for _, p := range posts { + // Set the appropriate caller realm based on the author + authorAddr := testutils.TestAddress(p.author) + std.TestSetRealm(std.NewUserRealm(authorAddr)) + + // Create the post + err := CreatePost(p.text) + if err != nil { + t.Fatalf("expected no error for post '%s', got %v", p.text, err) + } + } + + // Get the rendered page + got := Render("") + + // Check that all posts and their authors are present in the rendered output + for _, p := range posts { + expectedText := p.text + expectedAuthor := testutils.TestAddress(p.author).String() // Get the address for the author + if !(strings.Contains(got, expectedText) && strings.Contains(got, expectedAuthor)) { + t.Fatalf("expected render to contain text '%s' and address '%s'", expectedText, expectedAuthor) + } + } +} diff --git a/examples/gno.land/r/docs/minisocial/v1/types.gno b/examples/gno.land/r/docs/minisocial/v1/types.gno new file mode 100644 index 00000000000..c076bb7373b --- /dev/null +++ b/examples/gno.land/r/docs/minisocial/v1/types.gno @@ -0,0 +1,27 @@ +package minisocial + +import ( + "std" // The standard Gno package + "time" // For handling time operations + + "gno.land/p/demo/ufmt" // For string formatting, like `fmt` +) + +// Post defines the main data we keep about each post +type Post struct { + text string + author std.Address + createdAt time.Time +} + +// String stringifies a Post +func (p Post) String() string { + out := "**" + p.text + "**" + out += "\n\n" + out += ufmt.Sprintf("_by %s_, ", p.author) + // We can use `ufmt` to format strings, and the built-in time library formatting function + out += ufmt.Sprintf("_on %s_", p.createdAt.Format("02 Jan 2006, 15:04")) + + out += "\n\n" + return out +} diff --git a/examples/gno.land/r/docs/minisocial/v2/admin.gno b/examples/gno.land/r/docs/minisocial/v2/admin.gno new file mode 100644 index 00000000000..40a0ac05190 --- /dev/null +++ b/examples/gno.land/r/docs/minisocial/v2/admin.gno @@ -0,0 +1,16 @@ +package minisocial + +import ( + "gno.land/p/demo/avl" + "gno.land/p/demo/ownable" + "gno.land/p/demo/seqid" +) + +var Ownable = ownable.NewWithAddress("g125em6arxsnj49vx35f0n0z34putv5ty3376fg5") // @leohhhn + +// ResetPosts allows admin deletion of the posts +func ResetPosts() { + Ownable.AssertCallerIsOwner() + posts = avl.NewTree() + postID = seqid.ID(0) +} diff --git a/examples/gno.land/r/docs/minisocial/v2/gno.mod b/examples/gno.land/r/docs/minisocial/v2/gno.mod new file mode 100644 index 00000000000..2a7e5a32b67 --- /dev/null +++ b/examples/gno.land/r/docs/minisocial/v2/gno.mod @@ -0,0 +1 @@ +module gno.land/r/docs/minisocial/v2 diff --git a/examples/gno.land/r/docs/minisocial/v2/posts.gno b/examples/gno.land/r/docs/minisocial/v2/posts.gno new file mode 100644 index 00000000000..5cad32a5f61 --- /dev/null +++ b/examples/gno.land/r/docs/minisocial/v2/posts.gno @@ -0,0 +1,141 @@ +package minisocial + +import ( + "errors" + "std" + "strconv" + "time" + + "gno.land/p/demo/avl" + "gno.land/p/demo/avl/pager" + "gno.land/p/demo/seqid" + "gno.land/p/demo/ufmt" + "gno.land/p/moul/md" + + "gno.land/r/demo/users" +) + +var ( + postID seqid.ID // counter for post IDs + posts = avl.NewTree() // seqid.ID.String() > *Post + pag = pager.NewPager(posts, 5, true) // To help with pagination in rendering + + // Errors + ErrEmptyPost = errors.New("empty post text") + ErrPostNotFound = errors.New("post not found") + ErrUpdateWindowExpired = errors.New("update window expired") + ErrUnauthorized = errors.New("you're not authorized to update this post") +) + +// CreatePost creates a new post +func CreatePost(text string) error { + if text == "" { + return ErrEmptyPost + } + + // Get the next ID + // seqid.IDs are sequentially stored in the AVL tree + // This provides chronological order when iterating + id := postID.Next() + + // Set the key:value pair into the AVL tree: + // avl.Tree.Set takes a string for a key, and anything as a value. + // Stringify the key, and set the pointer to a new Post struct + posts.Set(id.String(), &Post{ + id: id, // Set the ID, used later for editing or deletion + text: text, // Set the input text + author: std.PreviousRealm().Address(), // The author of the address is the previous realm, the realm that called this one + createdAt: time.Now(), // Capture the time of the transaction, in this case the block timestamp + updatedAt: time.Now(), + }) + + return nil +} + +// UpdatePost allows the author to update a post +// The post can only be updated up to 10 minutes after posting +func UpdatePost(id string, text string) error { + // Try to get the post + raw, ok := posts.Get(id) + if !ok { + return ErrPostNotFound + } + + // Cast post from AVL tree + post := raw.(*Post) + if std.PreviousRealm().Address() != post.author { + return ErrUnauthorized + } + + // Can only update 10 mins after it was posted + if post.updatedAt.After(post.createdAt.Add(time.Minute * 10)) { + return ErrUpdateWindowExpired + } + + post.text = text + post.updatedAt = time.Now() + + return nil +} + +// DeletePost deletes a post with a specific id +// Only the creator of a post can delete the post +func DeletePost(id string) error { + // Try to get the post + raw, ok := posts.Get(id) + if !ok { + return ErrPostNotFound + } + + // Cast post from AVL tree + post := raw.(*Post) + if std.PreviousRealm().Address() != post.author { + return ErrUnauthorized + } + + // Use avl.Tree.Remove + _, removed := posts.Remove(id) + if !removed { + // This shouldn't happen after all checks above + // If it does, discard any possible state changes + panic("failed to remove post") + } + + return nil +} + +// Render renders the main page of threads +func Render(path string) string { + out := md.H1("MiniSocial") + + if posts.Size() == 0 { + out += "No posts yet!\n\n" + return out + } + + // Get the page from the path + page := pag.MustGetPageByPath(path) + + // Iterate over items in the page + for _, item := range page.Items { + post := item.Value.(*Post) + + // Try resolving the address for a username + text := post.author.String() + user := users.GetUserByAddress(post.author) + if user != nil { + text = user.Name + } + + out += md.H4(ufmt.Sprintf("Post #%d - @%s\n\n", int(post.id), text)) + + out += post.String() + out += md.HorizontalRule() + } + + out += page.Picker() + out += "\n\n" + out += "Page " + strconv.Itoa(page.PageNumber) + " of " + strconv.Itoa(page.TotalPages) + "\n\n" + + return out +} diff --git a/examples/gno.land/r/docs/minisocial/v2/posts_test.gno b/examples/gno.land/r/docs/minisocial/v2/posts_test.gno new file mode 100644 index 00000000000..b5179e01b38 --- /dev/null +++ b/examples/gno.land/r/docs/minisocial/v2/posts_test.gno @@ -0,0 +1,69 @@ +package minisocial + +import ( + "std" + "strings" + "testing" + + "gno.land/p/demo/testutils" // Provides testing utilities +) + +func TestCreatePostSingle(t *testing.T) { + // Get a test address for alice + aliceAddr := testutils.TestAddress("alice") + // TestSetRealm sets the realm caller, in this case Alice + std.TestSetRealm(std.NewUserRealm(aliceAddr)) + + text1 := "Hello World!" + err := CreatePost(text1) + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + + // Get the rendered page + got := Render("") + + // Content should have the text and alice's address in it + if !(strings.Contains(got, text1) && strings.Contains(got, aliceAddr.String())) { + t.Fatal("expected render to contain text & alice's address") + } +} + +func TestCreatePostMultiple(t *testing.T) { + // Initialize a slice to hold the test posts and their authors + posts := []struct { + text string + author string + }{ + {"Hello World!", "alice"}, + {"This is some new text!", "bob"}, + {"Another post by alice", "alice"}, + {"A post by charlie!", "charlie"}, + } + + for _, p := range posts { + // Set the appropriate caller realm based on the author + authorAddr := testutils.TestAddress(p.author) + std.TestSetRealm(std.NewUserRealm(authorAddr)) + + // Create the post + err := CreatePost(p.text) + if err != nil { + t.Fatalf("expected no error for post '%s', got %v", p.text, err) + } + } + + // Get the rendered page + got := Render("") + + // Check that all posts and their authors are present in the rendered output + for _, p := range posts { + expectedText := p.text + expectedAuthor := testutils.TestAddress(p.author).String() // Get the address for the author + if !(strings.Contains(got, expectedText) && strings.Contains(got, expectedAuthor)) { + t.Fatalf("expected render to contain text '%s' and address '%s'", expectedText, expectedAuthor) + } + } +} + +// TODO: Add tests for Update & Delete diff --git a/examples/gno.land/r/docs/minisocial/v2/types.gno b/examples/gno.land/r/docs/minisocial/v2/types.gno new file mode 100644 index 00000000000..17e78a120a3 --- /dev/null +++ b/examples/gno.land/r/docs/minisocial/v2/types.gno @@ -0,0 +1,34 @@ +package minisocial + +import ( + "std" // The standard Gno package + "time" // For handling time operations + + "gno.land/p/demo/seqid" + "gno.land/p/demo/ufmt" +) + +// Post defines the main data we keep about each post +type Post struct { + id seqid.ID + text string + author std.Address + createdAt time.Time + updatedAt time.Time +} + +// String stringifies a Post +func (p Post) String() string { + out := "**" + p.text + "**" + out += "\n\n" + // We can use `ufmt` to format strings, and the built-in time library formatting function + out += ufmt.Sprintf("_on %s_", p.createdAt.Format("02 Jan 2006, 15:04")) + + // Let users know if the post was updated + if p.updatedAt.After(p.createdAt) { + out += ufmt.Sprintf(" - _edited on %s_\n\n", p.updatedAt.Format("02 Jan 2006, 15:04")) + } + + out += "\n\n" + return out +} From 05d8890fdf2d9166b9fda5f8c043e5a5b1b85bce Mon Sep 17 00:00:00 2001 From: Morgan Date: Thu, 27 Feb 2025 00:43:24 +0100 Subject: [PATCH 6/8] fix(gnovm): don't assert to bodyStmt when creating stacktraces (#3836) Stacktraces being generated when executing a known statement, like the initialization of an if statement, will have the panicking statement at the top of the Stmt stack, which may not be a bodyStmt. This PR fixes the incorrect assertion. --- gnovm/pkg/gnolang/machine.go | 5 +++-- gnovm/tests/files/recover20.gno | 16 ++++++++++++++++ 2 files changed, 19 insertions(+), 2 deletions(-) create mode 100644 gnovm/tests/files/recover20.gno diff --git a/gnovm/pkg/gnolang/machine.go b/gnovm/pkg/gnolang/machine.go index ac20a6540a1..49af2ce51e7 100644 --- a/gnovm/pkg/gnolang/machine.go +++ b/gnovm/pkg/gnolang/machine.go @@ -388,8 +388,9 @@ func (m *Machine) Stacktrace() (stacktrace Stacktrace) { for i := len(m.Frames) - 1; i >= 0; i-- { if m.Frames[i].IsCall() { stm := m.Stmts[nextStmtIndex] - bs := stm.(*bodyStmt) - stm = bs.Body[bs.NextBodyIndex-1] + if bs, ok := stm.(*bodyStmt); ok { + stm = bs.Body[bs.NextBodyIndex-1] + } calls = append(calls, StacktraceCall{ Stmt: stm, Frame: m.Frames[i], diff --git a/gnovm/tests/files/recover20.gno b/gnovm/tests/files/recover20.gno new file mode 100644 index 00000000000..58e5191d910 --- /dev/null +++ b/gnovm/tests/files/recover20.gno @@ -0,0 +1,16 @@ +package main + +func main() { + var p *int + if i := *p; i == 0 { + println("hey") + } +} + +// Error: +// nil pointer dereference + +// Stacktrace: +// panic: nil pointer dereference +// main() +// main/files/recover20.gno:5 From d1db75eb9250645b61344cdaa651f361e30bd60d Mon Sep 17 00:00:00 2001 From: Alexis Colin Date: Thu, 27 Feb 2025 21:33:32 +0900 Subject: [PATCH 7/8] feat(gnoweb): add metadata to md content (#3797) Fixes #3333 This PR introduces a way to customize HTML `head` metadata (`title`, `description`, and maybe soon, `canonical`) from realm markdown content. It adds a `front-matter` option that can be used at the very top of any realm markdown file. This front-matter is then read by our Markdown parser, Goldmark, to populate the `Title` and `Description` fields in gnoweb. The PR provides default content where these fields were previously blank and maintains a fallback setup using h.Static.Domain + " - " + gnourl.Path for the `Title`. Additionally, regardless of the provided Title, it will always be followed by ` - gnourl.Path` (` - gno.land` in our case). For now, only `Title` and `Description` are supported. Any other variables in the front-matter are ignored. Usage example: ``` --- Title: Mon Titre Description: Ma description --- ``` --- examples/gno.land/r/docs/docs.gno | 13 +- gno.land/pkg/gnoweb/handler.go | 45 +++- gno.land/pkg/gnoweb/markdown/meta.go | 277 ++++++++++++++++++++++ gno.land/pkg/gnoweb/markdown/meta_test.go | 147 ++++++++++++ gno.land/pkg/gnoweb/webclient.go | 9 +- gno.land/pkg/gnoweb/webclient_html.go | 25 +- go.mod | 2 +- 7 files changed, 504 insertions(+), 14 deletions(-) create mode 100644 gno.land/pkg/gnoweb/markdown/meta.go create mode 100644 gno.land/pkg/gnoweb/markdown/meta_test.go diff --git a/examples/gno.land/r/docs/docs.gno b/examples/gno.land/r/docs/docs.gno index 61f27c2db68..a64660d587a 100644 --- a/examples/gno.land/r/docs/docs.gno +++ b/examples/gno.land/r/docs/docs.gno @@ -1,7 +1,17 @@ package docs +func frontMatter() string { + return `--- +Title: Welcome to the Gno examples documentation index +Description: Welcome to the Gno examples documentation index. Explore various examples to learn more about Gno functionality and usage. +---` +} + func Render(_ string) string { - return `# Gno Examples Documentation + content := "" + content += frontMatter() + content += ` +# Gno Examples Documentation Welcome to the Gno examples documentation index. Explore various examples to learn more about Gno functionality and usage. @@ -24,4 +34,5 @@ Explore various examples to learn more about Gno functionality and usage. - [Official documentation](https://github.com/gnolang/gno/tree/master/docs) ` + return content } diff --git a/gno.land/pkg/gnoweb/handler.go b/gno.land/pkg/gnoweb/handler.go index 41a61c4cead..d06b4f369a2 100644 --- a/gno.land/pkg/gnoweb/handler.go +++ b/gno.land/pkg/gnoweb/handler.go @@ -130,7 +130,7 @@ func (h *WebHandler) prepareIndexBodyView(r *http.Request, indexData *components switch { case gnourl.IsRealm(), gnourl.IsPure(): - return h.GetPackageView(gnourl) + return h.GetPackageView(gnourl, indexData) default: h.Logger.Debug("invalid path: path is neither a pure package or a realm") return http.StatusBadRequest, components.StatusErrorComponent("invalid path") @@ -138,27 +138,27 @@ func (h *WebHandler) prepareIndexBodyView(r *http.Request, indexData *components } // GetPackageView handles package pages. -func (h *WebHandler) GetPackageView(gnourl *weburl.GnoURL) (int, *components.View) { +func (h *WebHandler) GetPackageView(gnourl *weburl.GnoURL, indexData *components.IndexData) (int, *components.View) { // Handle Help page if gnourl.WebQuery.Has("help") { - return h.GetHelpView(gnourl) + return h.GetHelpView(gnourl, indexData) } // Handle Source page if gnourl.WebQuery.Has("source") || gnourl.IsFile() { - return h.GetSourceView(gnourl) + return h.GetSourceView(gnourl, indexData) } // Handle Source page if gnourl.IsDir() || gnourl.IsPure() { - return h.GetDirectoryView(gnourl) + return h.GetDirectoryView(gnourl, indexData) } // Ultimately get realm view - return h.GetRealmView(gnourl) + return h.GetRealmView(gnourl, indexData) } -func (h *WebHandler) GetRealmView(gnourl *weburl.GnoURL) (int, *components.View) { +func (h *WebHandler) GetRealmView(gnourl *weburl.GnoURL, indexData *components.IndexData) (int, *components.View) { var content bytes.Buffer meta, err := h.Client.RenderRealm(&content, gnourl.Path, gnourl.EncodeArgs()) @@ -171,6 +171,19 @@ func (h *WebHandler) GetRealmView(gnourl *weburl.GnoURL) (int, *components.View) return GetClientErrorStatusPage(gnourl, err) } + // HTML Head metadata + if meta.Head.Title != "" { + indexData.Title = meta.Head.Title + " - " + h.Static.Domain + } else { + indexData.HeadData.Title = h.Static.Domain + " - " + gnourl.Path + } + + if meta.Head.Description != "" { + indexData.Description = meta.Head.Description + } else { + indexData.Description = "Render of the Gno Realm " + gnourl.Path + " from " + h.Static.Domain + "." + } + return http.StatusOK, components.RealmView(components.RealmData{ TocItems: &components.RealmTOCData{ Items: meta.Toc.Items, @@ -182,13 +195,17 @@ func (h *WebHandler) GetRealmView(gnourl *weburl.GnoURL) (int, *components.View) }) } -func (h *WebHandler) GetHelpView(gnourl *weburl.GnoURL) (int, *components.View) { +func (h *WebHandler) GetHelpView(gnourl *weburl.GnoURL, indexData *components.IndexData) (int, *components.View) { fsigs, err := h.Client.Functions(gnourl.Path) if err != nil { h.Logger.Error("unable to fetch path functions", "error", err) return GetClientErrorStatusPage(gnourl, err) } + // HTML Head metadata + indexData.HeadData.Title = h.Static.Domain + " - " + gnourl.Path + " docs and realm interactions" + indexData.Description = "Read the Realm " + gnourl.Path + " functions an interact with them from " + h.Static.Domain + "." + // Get selected function selArgs := make(map[string]string) selFn := gnourl.WebQuery.Get("func") @@ -220,7 +237,7 @@ func (h *WebHandler) GetHelpView(gnourl *weburl.GnoURL) (int, *components.View) }) } -func (h *WebHandler) GetSourceView(gnourl *weburl.GnoURL) (int, *components.View) { +func (h *WebHandler) GetSourceView(gnourl *weburl.GnoURL, indexData *components.IndexData) (int, *components.View) { pkgPath := gnourl.Path files, err := h.Client.Sources(pkgPath) if err != nil { @@ -251,6 +268,10 @@ func (h *WebHandler) GetSourceView(gnourl *weburl.GnoURL) (int, *components.View return GetClientErrorStatusPage(gnourl, err) } + // HTML Head metadata + indexData.HeadData.Title = h.Static.Domain + " - " + fileName + " source code from " + gnourl.Path + indexData.Description = "Check " + fileName + " source code from " + gnourl.Path + " on " + h.Static.Domain + "." + fileSizeStr := fmt.Sprintf("%.2f Kb", meta.SizeKb) return http.StatusOK, components.SourceView(components.SourceData{ PkgPath: gnourl.Path, @@ -263,7 +284,7 @@ func (h *WebHandler) GetSourceView(gnourl *weburl.GnoURL) (int, *components.View }) } -func (h *WebHandler) GetDirectoryView(gnourl *weburl.GnoURL) (int, *components.View) { +func (h *WebHandler) GetDirectoryView(gnourl *weburl.GnoURL, indexData *components.IndexData) (int, *components.View) { pkgPath := strings.TrimSuffix(gnourl.Path, "/") files, err := h.Client.Sources(pkgPath) if err != nil { @@ -276,6 +297,10 @@ func (h *WebHandler) GetDirectoryView(gnourl *weburl.GnoURL) (int, *components.V return http.StatusOK, components.StatusErrorComponent("no files available") } + // HTML Head metadata + indexData.HeadData.Title = h.Static.Domain + " - " + gnourl.Path + " files directory" + indexData.Description = "Browse the " + gnourl.Path + " directory on " + h.Static.Domain + "." + return http.StatusOK, components.DirectoryView(components.DirData{ PkgPath: gnourl.Path, Files: files, diff --git a/gno.land/pkg/gnoweb/markdown/meta.go b/gno.land/pkg/gnoweb/markdown/meta.go new file mode 100644 index 00000000000..1185d5206e6 --- /dev/null +++ b/gno.land/pkg/gnoweb/markdown/meta.go @@ -0,0 +1,277 @@ +// Updated meta Package from goldmark (http://github.com/yuin/goldmark). +// +// This extension parses YAML metadata blocks and stores metadata in a +// parser.Context. The updated version uses gopkg.in/yaml.v3 instead of +// gopkg.in/yaml.v2 and remove useless options such as table rendering. +package markdown + +import ( + "bytes" + "fmt" + + "github.com/yuin/goldmark" + gast "github.com/yuin/goldmark/ast" + "github.com/yuin/goldmark/parser" + "github.com/yuin/goldmark/text" + "github.com/yuin/goldmark/util" + + "gopkg.in/yaml.v3" +) + +type data struct { + Map map[string]interface{} + Items []yaml.Node + Error error + Node gast.Node +} + +var contextKey = parser.NewContextKey() + +// Option interface sets options for this extension. +type Option interface { + metaOption() +} + +// Get returns the YAML metadata. +func Get(pc parser.Context) map[string]interface{} { + v := pc.Get(contextKey) + if v == nil { + return nil + } + d := v.(*data) + return d.Map +} + +// TryGet tries to get YAML metadata. +// If there are YAML parsing errors, then nil and error are returned. +func TryGet(pc parser.Context) (map[string]interface{}, error) { + dtmp := pc.Get(contextKey) + if dtmp == nil { + return nil, nil + } + d := dtmp.(*data) + if d.Error != nil { + return nil, d.Error + } + return d.Map, nil +} + +// GetItems returns the YAML metadata nodes preserving defined key order. +func GetItems(pc parser.Context) []yaml.Node { + v := pc.Get(contextKey) + if v == nil { + return nil + } + d := v.(*data) + return d.Items +} + +// TryGetItems returns the YAML metadata nodes preserving defined key order. +// If there are YAML parsing errors, then nil and error are returned. +func TryGetItems(pc parser.Context) ([]yaml.Node, error) { + dtmp := pc.Get(contextKey) + if dtmp == nil { + return nil, nil + } + d := dtmp.(*data) + if d.Error != nil { + return nil, d.Error + } + return d.Items, nil +} + +type metaParser struct{} + +var defaultParser = &metaParser{} + +// NewParser returns a BlockParser that can parse YAML metadata blocks. +func NewParser() parser.BlockParser { + return defaultParser +} + +func isSeparator(line []byte) bool { + line = util.TrimRightSpace(util.TrimLeftSpace(line)) + for i := 0; i < len(line); i++ { + if line[i] != '-' { + return false + } + } + return true +} + +func (b *metaParser) Trigger() []byte { + return []byte{'-'} +} + +func (b *metaParser) Open(parent gast.Node, reader text.Reader, pc parser.Context) (gast.Node, parser.State) { + linenum, _ := reader.Position() + if linenum != 0 { + return nil, parser.NoChildren + } + line, _ := reader.PeekLine() + if isSeparator(line) { + return gast.NewTextBlock(), parser.NoChildren + } + return nil, parser.NoChildren +} + +func (b *metaParser) Continue(node gast.Node, reader text.Reader, pc parser.Context) parser.State { + line, segment := reader.PeekLine() + if isSeparator(line) && !util.IsBlank(line) { + reader.Advance(segment.Len()) + return parser.Close + } + node.Lines().Append(segment) + return parser.Continue | parser.NoChildren +} + +func (b *metaParser) Close(node gast.Node, reader text.Reader, pc parser.Context) { + lines := node.Lines() + var buf bytes.Buffer + for i := 0; i < lines.Len(); i++ { + segment := lines.At(i) + buf.Write(segment.Value(reader.Source())) + } + d := &data{} + d.Node = node + + // Unmarshal map to get d.Map. + metaMap := map[string]interface{}{} + if err := yaml.Unmarshal(buf.Bytes(), &metaMap); err != nil { + d.Error = err + } else { + d.Map = metaMap + } + + // Unmarshal yaml.Node to get full structure. + var metaRoot yaml.Node + if err := yaml.Unmarshal(buf.Bytes(), &metaRoot); err != nil { + d.Error = err + } else { + if metaRoot.Kind == yaml.DocumentNode && len(metaRoot.Content) > 0 { + if metaRoot.Content[0].Kind == yaml.MappingNode { + for _, item := range metaRoot.Content[0].Content { + d.Items = append(d.Items, *item) + } + } else { + d.Error = fmt.Errorf("YAML n'est pas une map") + } + } else { + d.Error = fmt.Errorf("structure YAML inattendue") + } + } + + pc.Set(contextKey, d) + + if d.Error == nil { + node.Parent().RemoveChild(node.Parent(), node) + } +} + +func (b *metaParser) CanInterruptParagraph() bool { + return false +} + +func (b *metaParser) CanAcceptIndentedLine() bool { + return false +} + +type astTransformer struct { + transformerConfig +} + +type transformerConfig struct { + // Stores metadata in ast.Document.Meta(). + StoresInDocument bool +} + +type transformerOption interface { + Option + // SetMetaOption sets options for the metadata parser. + SetMetaOption(*transformerConfig) +} + +var _ transformerOption = &withStoresInDocument{} + +type withStoresInDocument struct { + value bool +} + +func (o *withStoresInDocument) metaOption() {} + +func (o *withStoresInDocument) SetMetaOption(c *transformerConfig) { + c.StoresInDocument = o.value +} + +// WithStoresInDocument is a functional option that stores YAML metadata in ast.Document.Meta(). +func WithStoresInDocument() Option { + return &withStoresInDocument{ + value: true, + } +} + +func newTransformer(opts ...transformerOption) parser.ASTTransformer { + p := &astTransformer{ + transformerConfig: transformerConfig{ + StoresInDocument: false, + }, + } + for _, o := range opts { + o.SetMetaOption(&p.transformerConfig) + } + return p +} + +func (a *astTransformer) Transform(node *gast.Document, reader text.Reader, pc parser.Context) { + dtmp := pc.Get(contextKey) + if dtmp == nil { + return + } + d := dtmp.(*data) + if d.Error != nil { + msg := gast.NewString([]byte(fmt.Sprintf("", d.Error))) + msg.SetCode(true) + d.Node.AppendChild(d.Node, msg) + return + } + + if a.StoresInDocument { + for k, v := range d.Map { + node.AddMeta(k, v) + } + } +} + +type meta struct { + options []Option +} + +// Meta is an extension for goldmark. +var Meta = &meta{} + +// NewMetadata returns a new Meta extension. +func NewMetadata(opts ...Option) goldmark.Extender { + return &meta{ + options: opts, + } +} + +// Extend implements goldmark.Extender. +func (e *meta) Extend(m goldmark.Markdown) { + topts := []transformerOption{} + for _, opt := range e.options { + if topt, ok := opt.(transformerOption); ok { + topts = append(topts, topt) + } + } + m.Parser().AddOptions( + parser.WithBlockParsers( + util.Prioritized(NewParser(), 0), + ), + ) + m.Parser().AddOptions( + parser.WithASTTransformers( + util.Prioritized(newTransformer(topts...), 0), + ), + ) +} diff --git a/gno.land/pkg/gnoweb/markdown/meta_test.go b/gno.land/pkg/gnoweb/markdown/meta_test.go new file mode 100644 index 00000000000..f97795f5f4c --- /dev/null +++ b/gno.land/pkg/gnoweb/markdown/meta_test.go @@ -0,0 +1,147 @@ +package markdown + +import ( + "bytes" + "testing" + + "github.com/yuin/goldmark" + "github.com/yuin/goldmark/parser" + "github.com/yuin/goldmark/text" +) + +func TestMeta(t *testing.T) { + markdown := goldmark.New( + goldmark.WithExtensions( + Meta, + ), + ) + source := `--- +Title: goldmark-meta +Summary: Add YAML metadata to the document +Tags: + - markdown + - goldmark +--- + +# Hello goldmark-meta +` + + var buf bytes.Buffer + context := parser.NewContext() + if err := markdown.Convert([]byte(source), &buf, parser.WithContext(context)); err != nil { + panic(err) + } + metaData := Get(context) + title := metaData["Title"] + s, ok := title.(string) + if !ok { + t.Error("Title not found in meta data or is not a string") + } + if s != "goldmark-meta" { + t.Errorf("Title must be %s, but got %v", "goldmark-meta", s) + } + if buf.String() != "

Hello goldmark-meta

\n" { + t.Errorf("should render '

Hello goldmark-meta

', but '%s'", buf.String()) + } + tags, ok := metaData["Tags"].([]interface{}) + if !ok { + t.Error("Tags not found in meta data or is not a slice") + } + if len(tags) != 2 { + t.Error("Tags must be a slice that has 2 elements") + } + if tags[0] != "markdown" { + t.Errorf("Tag#1 must be 'markdown', but got %s", tags[0]) + } + if tags[1] != "goldmark" { + t.Errorf("Tag#2 must be 'goldmark', but got %s", tags[1]) + } +} + +func TestMetaError(t *testing.T) { + markdown := goldmark.New( + goldmark.WithExtensions( + Meta, + ), + ) + source := `--- +Title: goldmark-meta +Summary: Add YAML metadata to the document +Tags: + - : { + } + - markdown + - goldmark +--- + +# Hello goldmark-meta +` + + var buf bytes.Buffer + context := parser.NewContext() + if err := markdown.Convert([]byte(source), &buf, parser.WithContext(context)); err != nil { + panic(err) + } + if buf.String() != `Title: goldmark-meta +Summary: Add YAML metadata to the document +Tags: + - : { + } + - markdown + - goldmark + +

Hello goldmark-meta

+` { + t.Error("invalid error output") + } + + v, err := TryGet(context) + if err == nil { + t.Error("error should not be nil") + } + if v != nil { + t.Error("data should be nil when there are errors") + } +} + +func TestMetaStoreInDocument(t *testing.T) { + markdown := goldmark.New( + goldmark.WithExtensions( + NewMetadata( + WithStoresInDocument(), + ), + ), + ) + source := `--- +Title: goldmark-meta +Summary: Add YAML metadata to the document +Tags: + - markdown + - goldmark +--- +` + + document := markdown.Parser().Parse(text.NewReader([]byte(source))) + metaData := document.OwnerDocument().Meta() + title := metaData["Title"] + s, ok := title.(string) + if !ok { + t.Error("Title not found in meta data or is not a string") + } + if s != "goldmark-meta" { + t.Errorf("Title must be %s, but got %v", "goldmark-meta", s) + } + tags, ok := metaData["Tags"].([]interface{}) + if !ok { + t.Error("Tags not found in meta data or is not a slice") + } + if len(tags) != 2 { + t.Error("Tags must be a slice that has 2 elements") + } + if tags[0] != "markdown" { + t.Errorf("Tag#1 must be 'markdown', but got %s", tags[0]) + } + if tags[1] != "goldmark" { + t.Errorf("Tag#2 must be 'goldmark', but got %s", tags[1]) + } +} diff --git a/gno.land/pkg/gnoweb/webclient.go b/gno.land/pkg/gnoweb/webclient.go index 1def3bc3812..3fbc0aabe6d 100644 --- a/gno.land/pkg/gnoweb/webclient.go +++ b/gno.land/pkg/gnoweb/webclient.go @@ -20,8 +20,15 @@ type FileMeta struct { SizeKb float64 } +type HeadMeta struct { + Title string + Description string + Canonical string +} + type RealmMeta struct { - Toc md.Toc + Toc md.Toc + Head HeadMeta } // WebClient is an interface for interacting with package and node resources. diff --git a/gno.land/pkg/gnoweb/webclient_html.go b/gno.land/pkg/gnoweb/webclient_html.go index efbc08870de..7d6bf075645 100644 --- a/gno.land/pkg/gnoweb/webclient_html.go +++ b/gno.land/pkg/gnoweb/webclient_html.go @@ -46,6 +46,7 @@ func NewDefaultHTMLWebClientConfig(client *client.RPCClient) *HTMLWebClientConfi goldmarkOptions := []goldmark.Option{ goldmark.WithParserOptions(parser.WithAutoHeadingID()), goldmark.WithExtensions( + md.NewMetadata(), markdown.NewHighlighting( markdown.WithFormatOptions(chromaOptions...), ), @@ -170,6 +171,23 @@ func (s *HTMLWebClient) Sources(path string) ([]string, error) { return files, nil } +// extractHeadMeta extracts optional head metadata from the provided metaData map +// and returns a HeadMeta struct. All fields ("Title", "Description", "Canonical") +// are optional; if a field is not present or not a string, it will be empty. +func extractHeadMeta(metaData map[string]interface{}) HeadMeta { + hm := HeadMeta{} + if title, ok := metaData["Title"].(string); ok { + hm.Title = title + } + if desc, ok := metaData["Description"].(string); ok { + hm.Description = desc + } + if canonical, ok := metaData["Canonical"].(string); ok { + hm.Canonical = canonical + } + return hm +} + // RenderRealm renders the content of a realm from a given path // and arguments into the provided writer. It uses Goldmark for // Markdown processing to generate HTML content. @@ -185,7 +203,10 @@ func (s *HTMLWebClient) RenderRealm(w io.Writer, pkgPath string, args string) (* } // Use Goldmark for Markdown parsing - doc := s.Markdown.Parser().Parse(text.NewReader(rawres)) + context := parser.NewContext() + doc := s.Markdown.Parser().Parse(text.NewReader(rawres), parser.WithContext(context)) + metaData := md.Get(context) + if err := s.Markdown.Renderer().Render(w, rawres, doc); err != nil { return nil, fmt.Errorf("unable to render realm %q: %w", data, err) } @@ -196,6 +217,8 @@ func (s *HTMLWebClient) RenderRealm(w io.Writer, pkgPath string, args string) (* s.logger.Warn("unable to inspect for TOC elements", "error", err) } + meta.Head = extractHeadMeta(metaData) + return &meta, nil } diff --git a/go.mod b/go.mod index 026351cec77..e9d61b9fa08 100644 --- a/go.mod +++ b/go.mod @@ -48,6 +48,7 @@ require ( golang.org/x/term v0.28.0 golang.org/x/tools v0.29.0 google.golang.org/protobuf v1.36.3 + gopkg.in/yaml.v3 v3.0.1 ) require ( @@ -71,5 +72,4 @@ require ( google.golang.org/genproto/googleapis/api v0.0.0-20250115164207-1a7da9e5054f // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f // indirect google.golang.org/grpc v1.69.4 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect ) From 17f44dcc1cadb1179882525a3968cf7ad35352f7 Mon Sep 17 00:00:00 2001 From: ltzmaxwell Date: Fri, 28 Feb 2025 00:26:12 +0800 Subject: [PATCH 8/8] fix(gnovm): remove readonly flag (#3840) closes: #3800 . see comments below. --- gnovm/pkg/gnolang/machine.go | 12 ++-- gnovm/pkg/gnolang/op_assign.go | 104 --------------------------------- gnovm/pkg/gnolang/op_call.go | 4 +- gnovm/pkg/gnolang/realm.go | 16 +---- 4 files changed, 8 insertions(+), 128 deletions(-) diff --git a/gnovm/pkg/gnolang/machine.go b/gnovm/pkg/gnolang/machine.go index 49af2ce51e7..8df6e431d94 100644 --- a/gnovm/pkg/gnolang/machine.go +++ b/gnovm/pkg/gnolang/machine.go @@ -42,7 +42,6 @@ type Machine struct { // Configuration PreprocessorMode bool // this is used as a flag when const values are evaluated during preprocessing - ReadOnly bool Output io.Writer Store Store Context interface{} @@ -78,7 +77,6 @@ type MachineOptions struct { // Active package of the given machine; must be set before execution. PkgPath string PreprocessorMode bool - ReadOnly bool Debug bool Input io.Reader // used for default debugger input only Output io.Writer // default os.Stdout @@ -109,7 +107,6 @@ var machinePool = sync.Pool{ // [Machine.Release]. func NewMachineWithOptions(opts MachineOptions) *Machine { preprocessorMode := opts.PreprocessorMode - readOnly := opts.ReadOnly vmGasMeter := opts.GasMeter output := opts.Output @@ -141,7 +138,6 @@ func NewMachineWithOptions(opts MachineOptions) *Machine { mm.Package = pv mm.Alloc = alloc mm.PreprocessorMode = preprocessorMode - mm.ReadOnly = readOnly mm.Output = output mm.Store = store mm.Context = context @@ -639,13 +635,13 @@ func (m *Machine) saveNewPackageValuesAndTypes() (throwaway *Realm) { if pv.IsRealm() { rlm := pv.Realm rlm.MarkNewReal(pv) - rlm.FinalizeRealmTransaction(m.ReadOnly, m.Store) + rlm.FinalizeRealmTransaction(m.Store) // save package realm info. m.Store.SetPackageRealm(rlm) } else { // use a throwaway realm. rlm := NewRealm(pv.PkgPath) rlm.MarkNewReal(pv) - rlm.FinalizeRealmTransaction(m.ReadOnly, m.Store) + rlm.FinalizeRealmTransaction(m.Store) throwaway = rlm } // save declared types. @@ -669,11 +665,11 @@ func (m *Machine) resavePackageValues(rlm *Realm) { pv := m.Package if pv.IsRealm() { rlm = pv.Realm - rlm.FinalizeRealmTransaction(m.ReadOnly, m.Store) + rlm.FinalizeRealmTransaction(m.Store) // re-save package realm info. m.Store.SetPackageRealm(rlm) } else { // use the throwaway realm. - rlm.FinalizeRealmTransaction(m.ReadOnly, m.Store) + rlm.FinalizeRealmTransaction(m.Store) } // types were already saved, and should not change // even after running the init function. diff --git a/gnovm/pkg/gnolang/op_assign.go b/gnovm/pkg/gnolang/op_assign.go index b42bb4744c0..c101058321b 100644 --- a/gnovm/pkg/gnolang/op_assign.go +++ b/gnovm/pkg/gnolang/op_assign.go @@ -12,14 +12,6 @@ func (m *Machine) doOpDefine() { nx := s.Lhs[i].(*NameExpr) // Finally, define (or assign if loop block). ptr := lb.GetPointerToMaybeHeapDefine(m.Store, nx) - // XXX HACK (until value persistence impl'd) - if m.ReadOnly { - if oo, ok := ptr.Base.(Object); ok { - if oo.GetIsReal() { - panic("readonly violation") - } - } - } if !m.PreprocessorMode && isUntyped(rvs[i].T) && rvs[i].T.Kind() != BoolKind { panic("untyped conversion should not happen at runtime") } @@ -36,14 +28,6 @@ func (m *Machine) doOpAssign() { for i := len(s.Lhs) - 1; 0 <= i; i-- { // Pop lhs value and desired type. lv := m.PopAsPointer(s.Lhs[i]) - // XXX HACK (until value persistence impl'd) - if m.ReadOnly { - if oo, ok := lv.Base.(Object); ok { - if oo.GetIsReal() { - panic("readonly violation") - } - } - } if !m.PreprocessorMode && isUntyped(rvs[i].T) && rvs[i].T.Kind() != BoolKind { panic("untyped conversion should not happen at runtime") } @@ -59,14 +43,6 @@ func (m *Machine) doOpAddAssign() { debugAssertSameTypes(lv.TV.T, rv.T) } - // XXX HACK (until value persistence impl'd) - if m.ReadOnly { - if oo, ok := lv.Base.(Object); ok { - if oo.GetIsReal() { - panic("readonly violation") - } - } - } // add rv to lv. addAssign(m.Alloc, lv.TV, rv) if lv.Base != nil { @@ -82,14 +58,6 @@ func (m *Machine) doOpSubAssign() { debugAssertSameTypes(lv.TV.T, rv.T) } - // XXX HACK (until value persistence impl'd) - if m.ReadOnly { - if oo, ok := lv.Base.(Object); ok { - if oo.GetIsReal() { - panic("readonly violation") - } - } - } // sub rv from lv. subAssign(lv.TV, rv) if lv.Base != nil { @@ -105,14 +73,6 @@ func (m *Machine) doOpMulAssign() { debugAssertSameTypes(lv.TV.T, rv.T) } - // XXX HACK (until value persistence impl'd) - if m.ReadOnly { - if oo, ok := lv.Base.(Object); ok { - if oo.GetIsReal() { - panic("readonly violation") - } - } - } // lv *= rv mulAssign(lv.TV, rv) if lv.Base != nil { @@ -128,14 +88,6 @@ func (m *Machine) doOpQuoAssign() { debugAssertSameTypes(lv.TV.T, rv.T) } - // XXX HACK (until value persistence impl'd) - if m.ReadOnly { - if oo, ok := lv.Base.(Object); ok { - if oo.GetIsReal() { - panic("readonly violation") - } - } - } // lv /= rv err := quoAssign(lv.TV, rv) if err != nil { @@ -155,14 +107,6 @@ func (m *Machine) doOpRemAssign() { debugAssertSameTypes(lv.TV.T, rv.T) } - // XXX HACK (until value persistence impl'd) - if m.ReadOnly { - if oo, ok := lv.Base.(Object); ok { - if oo.GetIsReal() { - panic("readonly violation") - } - } - } // lv %= rv err := remAssign(lv.TV, rv) if err != nil { @@ -182,14 +126,6 @@ func (m *Machine) doOpBandAssign() { debugAssertSameTypes(lv.TV.T, rv.T) } - // XXX HACK (until value persistence impl'd) - if m.ReadOnly { - if oo, ok := lv.Base.(Object); ok { - if oo.GetIsReal() { - panic("readonly violation") - } - } - } // lv &= rv bandAssign(lv.TV, rv) if lv.Base != nil { @@ -205,14 +141,6 @@ func (m *Machine) doOpBandnAssign() { debugAssertSameTypes(lv.TV.T, rv.T) } - // XXX HACK (until value persistence impl'd) - if m.ReadOnly { - if oo, ok := lv.Base.(Object); ok { - if oo.GetIsReal() { - panic("readonly violation") - } - } - } // lv &^= rv bandnAssign(lv.TV, rv) if lv.Base != nil { @@ -228,14 +156,6 @@ func (m *Machine) doOpBorAssign() { debugAssertSameTypes(lv.TV.T, rv.T) } - // XXX HACK (until value persistence impl'd) - if m.ReadOnly { - if oo, ok := lv.Base.(Object); ok { - if oo.GetIsReal() { - panic("readonly violation") - } - } - } // lv |= rv borAssign(lv.TV, rv) if lv.Base != nil { @@ -251,14 +171,6 @@ func (m *Machine) doOpXorAssign() { debugAssertSameTypes(lv.TV.T, rv.T) } - // XXX HACK (until value persistence impl'd) - if m.ReadOnly { - if oo, ok := lv.Base.(Object); ok { - if oo.GetIsReal() { - panic("readonly violation") - } - } - } // lv ^= rv xorAssign(lv.TV, rv) if lv.Base != nil { @@ -271,14 +183,6 @@ func (m *Machine) doOpShlAssign() { rv := m.PopValue() // only one. lv := m.PopAsPointer(s.Lhs[0]) - // XXX HACK (until value persistence impl'd) - if m.ReadOnly { - if oo, ok := lv.Base.(Object); ok { - if oo.GetIsReal() { - panic("readonly violation") - } - } - } // lv <<= rv shlAssign(m, lv.TV, rv) if lv.Base != nil { @@ -291,14 +195,6 @@ func (m *Machine) doOpShrAssign() { rv := m.PopValue() // only one. lv := m.PopAsPointer(s.Lhs[0]) - // XXX HACK (until value persistence impl'd) - if m.ReadOnly { - if oo, ok := lv.Base.(Object); ok { - if oo.GetIsReal() { - panic("readonly violation") - } - } - } // lv >>= rv shrAssign(m, lv.TV, rv) if lv.Base != nil { diff --git a/gnovm/pkg/gnolang/op_call.go b/gnovm/pkg/gnolang/op_call.go index ba5b7507cff..6442b4f4523 100644 --- a/gnovm/pkg/gnolang/op_call.go +++ b/gnovm/pkg/gnolang/op_call.go @@ -219,7 +219,7 @@ func (m *Machine) doOpReturn() { if finalize { // Finalize realm updates! // NOTE: This is a resource intensive undertaking. - crlm.FinalizeRealmTransaction(m.ReadOnly, m.Store) + crlm.FinalizeRealmTransaction(m.Store) } } // finalize @@ -254,7 +254,7 @@ func (m *Machine) doOpReturnFromBlock() { if finalize { // Finalize realm updates! // NOTE: This is a resource intensive undertaking. - crlm.FinalizeRealmTransaction(m.ReadOnly, m.Store) + crlm.FinalizeRealmTransaction(m.Store) } } // finalize diff --git a/gnovm/pkg/gnolang/realm.go b/gnovm/pkg/gnolang/realm.go index 509fcd67a60..a300e3425e7 100644 --- a/gnovm/pkg/gnolang/realm.go +++ b/gnovm/pkg/gnolang/realm.go @@ -321,24 +321,12 @@ func (rlm *Realm) MarkNewEscaped(oo Object) { // transactions // OpReturn calls this when exiting a realm transaction. -func (rlm *Realm) FinalizeRealmTransaction(readonly bool, store Store) { +func (rlm *Realm) FinalizeRealmTransaction(store Store) { if bm.OpsEnabled { bm.PauseOpCode() defer bm.ResumeOpCode() } - if readonly { - if true || - len(rlm.newCreated) > 0 || - len(rlm.newEscaped) > 0 || - len(rlm.newDeleted) > 0 || - len(rlm.created) > 0 || - len(rlm.updated) > 0 || - len(rlm.deleted) > 0 || - len(rlm.escaped) > 0 { - panic("realm updates in readonly transaction") - } - return - } + if debug { // * newCreated - may become created unless ancestor is deleted // * newDeleted - may become deleted unless attached to new-real owner